Skip to content

Commit

Permalink
Change coercion behavior of arithmetic operators
Browse files Browse the repository at this point in the history
  • Loading branch information
benburrill committed Jan 10, 2024
1 parent 5b9a085 commit d854c9c
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 40 deletions.
13 changes: 11 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,15 @@ Output:
CPU time: 90 clock cycles
Emulator efficiency: 42.06%
The try block is skipped entirely!
The try block is skipped entirely, saving us 85 clock cycles!

Halting problems
----------------
If we modify the above code by putting a loop before ``!is_defeat()``,
One nice thing about try/undo is that like an if/else, try and undo are
mutually exclusive. Either the try block or the undo block will run,
but never both.

If we modify the previous code by putting a loop before ``!is_defeat()``,
the code will test if the loop will terminate, since defeat would never
occur if the loop runs forever:

Expand Down Expand Up @@ -559,6 +563,11 @@ Types and special coercion rules of literals:
can be coerced to. Preferentially ``const``, but may be coerced to
non-const.

Arithmetic operators (``+``, ``-``, ``*``, ``/``, ``%``) take operands
of either ``byte`` or ``int`` and always produce values of type ``int``.
However, if all of the operands are coercible to ``byte``, the resulting
value is also coercible to ``byte``.

Explicit type casts may be performed with ``is``, eg ``baba is byte``.

Allowed explicit type casts:
Expand Down
31 changes: 15 additions & 16 deletions hidc/ast/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,36 +311,35 @@ def at(self, span):
class IntValue(PrimitiveValue):
type = DataType.INT
data: int
literal: bool = True
shrinkable: bool = True
is_char: bool = dc.field(default=False, compare=False)

def cast(self, new_type, *, implicit=False):
if new_type == DataType.BOOL:
return BoolValue(bool(self.data), self.span)
elif new_type == DataType.BYTE:
# TODO: I feel that this should probably do self.data & 0xFF
# but I'm not totally sure of the implications or if that
# should be done elsewhere.
# TODO: we should probably do self.data & 0xFF
# Should have result that 4 / (258 is byte) produces 2, as
# it would if evaluated at runtime.
# For that matter I should probably also track word size in
# env and use it when evaluating IntValues to do a (signed)
# wraparound.
return ByteValue(self.data, self.span, implicit and self.literal, self.is_char)
# For that matter I should also track word size in env and
# use for (signed) wraparound in arithmetic evaluation.
# Also when we create IntValues to begin with... and maybe
# more places I forget.
return ByteValue(self.data, self.span, self.shrinkable, self.is_char)
elif new_type == DataType.INT:
return IntValue(self.data, self.span, implicit and self.literal, self.is_char)
# Whenever a ByteValue is implicitly coerced to an IntValue,
# it should be shrinkable back to byte.
return IntValue(self.data, self.span, implicit, self.is_char)
return super().cast(new_type)

def coercible(self, new_type):
return (
super().coercible(new_type) or
self.literal and new_type in {
DataType.INT, DataType.BYTE
}
self.shrinkable and new_type == DataType.BYTE
)

def coerce(self, new_type):
# Should (2 is int) be coercible to byte?
# (1 + 1) should be coercible to byte, but should (2 is int) be?
# I think not. So I'm awkwardly adding this "implicit" argument
# to retain literal status for implicit coersions only.
# Regardless, (2 is byte) is coercible to int.
Expand All @@ -350,10 +349,10 @@ def coerce(self, new_type):
return self.cast(new_type, implicit=True)
return super().coerce(new_type)

# Substitution makes IntValues no longer be literal and instead act
# like normal ints in type coercion.
# Substitution makes IntValues no longer be shrinkable and instead
# act like normal ints in type coercion.
def at(self, span):
return type(self)(self.data, span, literal=False, is_char=self.is_char)
return type(self)(self.data, span, shrinkable=False, is_char=self.is_char)


class ByteValue(IntValue):
Expand Down
45 changes: 25 additions & 20 deletions hidc/ast/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,10 @@ def evaluate(self, env):
).simplify()


@dc.dataclass(frozen=True)
class ArithmeticOp(Operator):
type = DataType.INT
shrinkable: bool = dc.field(kw_only=True, default=False)

@abstractmethod
def operate(self, *args):
Expand All @@ -141,45 +143,48 @@ def simplify(self):
if all(isinstance(arg, IntValue) for arg in self.args):
return IntValue(
int(self.operate(*[arg.data for arg in self.args])),
self.span, all(arg.literal for arg in self.args)
self.span, self.shrinkable
)

return self

def coercible(self, new_type):
return (
super().coercible(new_type) or
self.shrinkable and new_type == DataType.BYTE
)


@dc.dataclass(frozen=True)
class BinaryArithmeticOp(Binary, ArithmeticOp):
def evaluate(self, env):
left = self.left.evaluate(env)
right = self.right.evaluate(env)
result = type(self)(
return type(self)(
self.op_span,
left.coerce(DataType.INT),
right.coerce(DataType.INT)
right.coerce(DataType.INT),
# The result of an arithmetic operation has type int, but is
# coercible to byte if all of its operands are.
# In a (probably unnecessary) effort to permit repeated
# evaluation, we retain shrinkability if the operation has
# already been determined to be shrinkable.
shrinkable=self.shrinkable or (
left.coercible(DataType.BYTE) and
right.coercible(DataType.BYTE)
)
).simplify()

match (left, right):
case ((Expression(type=DataType.BYTE), IntValue(literal=True)) |
(IntValue(literal=True), Expression(type=DataType.BYTE))):
# goofiness to preserve literal status
if result.coercible(DataType.BYTE):
return result.coerce(DataType.BYTE)
return result.cast(DataType.BYTE)
return result


@dc.dataclass(frozen=True)
class UnaryArithmeticOp(Unary, ArithmeticOp):
def evaluate(self, env):
arg = self.arg.evaluate(env)
result = type(self)(
self.op_span, arg.coerce(DataType.INT)
return type(self)(
self.op_span, arg.coerce(DataType.INT),
shrinkable=self.shrinkable or arg.coercible(DataType.BYTE)
).simplify()

if arg.type == DataType.BYTE:
if result.coercible(DataType.BYTE):
return result.coerce(DataType.BYTE)
return result.cast(DataType.BYTE)
return result


class Add(BinaryArithmeticOp):
token = OpToken.ADD
Expand Down
64 changes: 62 additions & 2 deletions tests/test_typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ def test_coercion():
empty f() {
int x = 'a';
byte y = 10;
byte z = 1 + 1;
byte w = -1;
}
""")

Expand All @@ -146,7 +148,7 @@ def test_coercion():

assert_bad("""
empty f() {
int x = 10;
const int x = 10;
byte y = x;
}
""")
Expand All @@ -172,17 +174,75 @@ def test_coercion():
}
""")

assert_good("""
empty f() {
const byte x = 10;
byte y = x;
y = y + 1;
byte z = x + 1;
}
""")

assert_good("""
empty f() {
byte b = 1;
byte x = 0;
x += b;
}
""")

assert_bad("""
empty f() {
int i = 1;
byte x = 0;
x += i;
}
""")

assert_good("""
empty f() {
byte x = 0;
x += 1;
}
""")

assert_bad("""
empty f() {
byte x = 0;
x += 1 is int;
}
""")

assert_good("""
empty f() {
byte x = 0;
byte y = x * 3 + 2;
x += 1 is byte;
}
""")

assert_good("""
empty f() {
byte x = 0;
x += (1 is int) is byte;
}
""")

assert_good("""
empty f(byte x) {
byte y = x * 3 + 4;
byte z = x * 3 + y * 5;
}
""")

assert_good("""
empty f(byte b, int i) {
int x = b * 2 + i * 3;
}
""")

assert_bad("""
empty f(byte b, int i) {
byte x = b * 2 + i * 3;
}
""")

Expand Down

0 comments on commit d854c9c

Please sign in to comment.