Skip to content

Commit 05d2e00

Browse files
Add support for floats (#67)
1 parent d7873b7 commit 05d2e00

1 file changed

Lines changed: 139 additions & 28 deletions

File tree

scrapscript.py

Lines changed: 139 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ class IntLit(Token):
4141
value: int
4242

4343

44+
@dataclass(eq=True)
45+
class FloatLit(Token):
46+
value: float
47+
48+
4449
@dataclass(eq=True)
4550
class StringLit(Token):
4651
value: str
@@ -174,7 +179,7 @@ def read_one(self) -> Token:
174179
return self.read_bytes()
175180
raise ParseError(f"unexpected token {c!r}")
176181
if c.isdigit():
177-
return self.read_integer(c)
182+
return self.read_number(c)
178183
if c in "()[]{}":
179184
custom = {
180185
"(": LeftParen,
@@ -205,11 +210,23 @@ def read_comment(self) -> None:
205210
while self.has_input() and self.read_char() != "\n":
206211
pass
207212

208-
def read_integer(self, first_digit: str) -> Token:
213+
def read_number(self, first_digit: str) -> Token:
214+
# TODO: Support floating point numbers with no integer part
209215
buf = first_digit
210-
while self.has_input() and (c := self.peek_char()).isdigit():
216+
has_decimal = False
217+
while self.has_input():
218+
c = self.peek_char()
219+
if c == ".":
220+
if has_decimal:
221+
raise ParseError(f"unexpected token {c!r}")
222+
has_decimal = True
223+
elif not c.isdigit():
224+
break
211225
self.read_char()
212226
buf += c
227+
228+
if has_decimal:
229+
return self.make_token(FloatLit, float(buf))
213230
return self.make_token(IntLit, int(buf))
214231

215232
def _starts_operator(self, buf: str) -> bool:
@@ -350,8 +367,9 @@ def parse(tokens: typing.List[Token], p: float = 0) -> "Object":
350367
token = tokens.pop(0)
351368
l: Object
352369
if isinstance(token, IntLit):
353-
# TODO: Handle float literals
354370
l = Int(token.value)
371+
elif isinstance(token, FloatLit):
372+
l = Float(token.value)
355373
elif isinstance(token, Name):
356374
# TODO: Handle kebab case vars
357375
l = Var(token.value)
@@ -554,6 +572,21 @@ def __str__(self) -> str:
554572
return str(self.value)
555573

556574

575+
@dataclass(eq=True, frozen=True, unsafe_hash=True)
576+
class Float(Object):
577+
value: float
578+
579+
def serialize(self) -> Dict[bytes, object]:
580+
raise NotImplementedError("serialization for Float is not supported")
581+
582+
@staticmethod
583+
def deserialize(msg: Dict[str, object]) -> "Float":
584+
raise NotImplementedError("serialization for Float is not supported")
585+
586+
def __str__(self) -> str:
587+
return str(self.value)
588+
589+
557590
@dataclass(eq=True, frozen=True, unsafe_hash=True)
558591
class String(Object):
559592
value: str
@@ -971,15 +1004,15 @@ def __str__(self) -> str:
9711004
return f"#{self.value}"
9721005

9731006

974-
def unpack_int(obj: Object) -> int:
975-
if not isinstance(obj, Int):
976-
raise TypeError(f"expected Int, got {type(obj).__name__}")
1007+
def unpack_number(obj: Object) -> Union[int, float]:
1008+
if not isinstance(obj, (Int, Float)):
1009+
raise TypeError(f"expected Int or Float, got {type(obj).__name__}")
9771010
return obj.value
9781011

9791012

980-
def eval_int(env: Env, exp: Object) -> int:
1013+
def eval_number(env: Env, exp: Object) -> Union[int, float]:
9811014
result = eval_exp(env, exp)
982-
return unpack_int(result)
1015+
return unpack_number(result)
9831016

9841017

9851018
def eval_str(env: Env, exp: Object) -> str:
@@ -1007,20 +1040,30 @@ def make_bool(x: bool) -> Object:
10071040
return Symbol("true" if x else "false")
10081041

10091042

1043+
def wrap_inferred_number_type(x: Union[int, float]) -> Object:
1044+
# TODO: Since this is intended to be a reference implementation
1045+
# we should avoid relying heavily on Python's implementation of
1046+
# arithmetic operations, type inference, and multiple dispatch.
1047+
# Update this to make the interpreter more language agnostic.
1048+
if isinstance(x, int):
1049+
return Int(x)
1050+
return Float(x)
1051+
1052+
10101053
BINOP_HANDLERS: Dict[BinopKind, Callable[[Env, Object, Object], Object]] = {
1011-
BinopKind.ADD: lambda env, x, y: Int(eval_int(env, x) + eval_int(env, y)),
1012-
BinopKind.SUB: lambda env, x, y: Int(eval_int(env, x) - eval_int(env, y)),
1013-
BinopKind.MUL: lambda env, x, y: Int(eval_int(env, x) * eval_int(env, y)),
1014-
BinopKind.DIV: lambda env, x, y: Int(eval_int(env, x) // eval_int(env, y)),
1015-
BinopKind.FLOOR_DIV: lambda env, x, y: Int(eval_int(env, x) // eval_int(env, y)),
1016-
BinopKind.EXP: lambda env, x, y: Int(eval_int(env, x) ** eval_int(env, y)),
1017-
BinopKind.MOD: lambda env, x, y: Int(eval_int(env, x) % eval_int(env, y)),
1054+
BinopKind.ADD: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) + eval_number(env, y)),
1055+
BinopKind.SUB: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) - eval_number(env, y)),
1056+
BinopKind.MUL: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) * eval_number(env, y)),
1057+
BinopKind.DIV: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) / eval_number(env, y)),
1058+
BinopKind.FLOOR_DIV: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) // eval_number(env, y)),
1059+
BinopKind.EXP: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) ** eval_number(env, y)),
1060+
BinopKind.MOD: lambda env, x, y: wrap_inferred_number_type(eval_number(env, x) % eval_number(env, y)),
10181061
BinopKind.EQUAL: lambda env, x, y: make_bool(eval_exp(env, x) == eval_exp(env, y)),
10191062
BinopKind.NOT_EQUAL: lambda env, x, y: make_bool(eval_exp(env, x) != eval_exp(env, y)),
1020-
BinopKind.LESS: lambda env, x, y: make_bool(eval_int(env, x) < eval_int(env, y)),
1021-
BinopKind.GREATER: lambda env, x, y: make_bool(eval_int(env, x) > eval_int(env, y)),
1022-
BinopKind.LESS_EQUAL: lambda env, x, y: make_bool(eval_int(env, x) <= eval_int(env, y)),
1023-
BinopKind.GREATER_EQUAL: lambda env, x, y: make_bool(eval_int(env, x) >= eval_int(env, y)),
1063+
BinopKind.LESS: lambda env, x, y: make_bool(eval_number(env, x) < eval_number(env, y)),
1064+
BinopKind.GREATER: lambda env, x, y: make_bool(eval_number(env, x) > eval_number(env, y)),
1065+
BinopKind.LESS_EQUAL: lambda env, x, y: make_bool(eval_number(env, x) <= eval_number(env, y)),
1066+
BinopKind.GREATER_EQUAL: lambda env, x, y: make_bool(eval_number(env, x) >= eval_number(env, y)),
10241067
BinopKind.BOOL_AND: lambda env, x, y: make_bool(eval_bool(env, x) and eval_bool(env, y)),
10251068
BinopKind.BOOL_OR: lambda env, x, y: make_bool(eval_bool(env, x) or eval_bool(env, y)),
10261069
BinopKind.STRING_CONCAT: lambda env, x, y: String(eval_str(env, x) + eval_str(env, y)),
@@ -1037,6 +1080,8 @@ class MatchError(Exception):
10371080
def match(obj: Object, pattern: Object) -> Optional[Env]:
10381081
if isinstance(pattern, Int):
10391082
return {} if isinstance(obj, Int) and obj.value == pattern.value else None
1083+
if isinstance(pattern, Float):
1084+
raise MatchError("pattern matching is not supported for Floats")
10401085
if isinstance(pattern, String):
10411086
return {} if isinstance(obj, String) and obj.value == pattern.value else None
10421087
if isinstance(pattern, Var):
@@ -1092,7 +1137,7 @@ def match(obj: Object, pattern: Object) -> Optional[Env]:
10921137
# pylint: disable=redefined-builtin
10931138
def eval_exp(env: Env, exp: Object) -> Object:
10941139
logger.debug(exp)
1095-
if isinstance(exp, (Int, String, Bytes, Hole, Closure, NativeFunction, Symbol)):
1140+
if isinstance(exp, (Int, Float, String, Bytes, Hole, Closure, NativeFunction, Symbol)):
10961141
return exp
10971142
if isinstance(exp, Var):
10981143
value = env.get(exp.name)
@@ -1285,6 +1330,23 @@ def test_tokenize_multiple_digits(self) -> None:
12851330
def test_tokenize_negative_int(self) -> None:
12861331
self.assertEqual(tokenize("-123"), [Operator("-"), IntLit(123)])
12871332

1333+
def test_tokenize_float(self) -> None:
1334+
self.assertEqual(tokenize("3.14"), [FloatLit(3.14)])
1335+
1336+
def test_tokenize_negative_float(self) -> None:
1337+
self.assertEqual(tokenize("-3.14"), [Operator("-"), FloatLit(3.14)])
1338+
1339+
@unittest.skip("TODO: support floats with no integer part")
1340+
def test_tokenize_float_with_no_integer_part(self) -> None:
1341+
self.assertEqual(tokenize(".14"), [FloatLit(0.14)])
1342+
1343+
def test_tokenize_float_with_no_decimal_part(self) -> None:
1344+
self.assertEqual(tokenize("10."), [FloatLit(10.0)])
1345+
1346+
def test_tokenize_float_with_multiple_decimal_points_raises_parse_error(self) -> None:
1347+
with self.assertRaisesRegex(ParseError, re.escape("unexpected token '.'")):
1348+
tokenize("1.0.1")
1349+
12881350
def test_tokenize_binop(self) -> None:
12891351
self.assertEqual(tokenize("1 + 2"), [IntLit(1), Operator("+"), IntLit(2)])
12901352

@@ -1708,6 +1770,12 @@ def test_parse_negative_int_binds_tighter_than_apply(self) -> None:
17081770
Apply(Binop(BinopKind.SUB, Int(0), Var("l")), Var("r")),
17091771
)
17101772

1773+
def test_parse_decimal_returns_float(self) -> None:
1774+
self.assertEqual(parse([FloatLit(3.14)]), Float(3.14))
1775+
1776+
def test_parse_negative_float_returns_binary_sub_float(self) -> None:
1777+
self.assertEqual(parse([Operator("-"), FloatLit(3.14)]), Binop(BinopKind.SUB, Int(0), Float(3.14)))
1778+
17111779
def test_parse_var_returns_var(self) -> None:
17121780
self.assertEqual(parse([Name("abc_123")]), Var("abc_123"))
17131781

@@ -2122,6 +2190,18 @@ def test_match_with_inequal_ints_returns_none(self) -> None:
21222190
def test_match_int_with_non_int_returns_none(self) -> None:
21232191
self.assertEqual(match(String("abc"), pattern=Int(1)), None)
21242192

2193+
def test_match_with_equal_floats_raises_match_error(self) -> None:
2194+
with self.assertRaisesRegex(MatchError, re.escape("pattern matching is not supported for Floats")):
2195+
match(Float(1), pattern=Float(1))
2196+
2197+
def test_match_with_inequal_floats_raises_match_error(self) -> None:
2198+
with self.assertRaisesRegex(MatchError, re.escape("pattern matching is not supported for Floats")):
2199+
match(Float(2), pattern=Float(1))
2200+
2201+
def test_match_float_with_non_float_raises_match_error(self) -> None:
2202+
with self.assertRaisesRegex(MatchError, re.escape("pattern matching is not supported for Floats")):
2203+
match(String("abc"), pattern=Float(1))
2204+
21252205
def test_match_with_equal_strings_returns_empty_dict(self) -> None:
21262206
self.assertEqual(match(String("a"), pattern=String("a")), {})
21272207

@@ -2379,6 +2459,10 @@ def test_eval_int_returns_int(self) -> None:
23792459
exp = Int(5)
23802460
self.assertEqual(eval_exp({}, exp), Int(5))
23812461

2462+
def test_eval_float_returns_float(self) -> None:
2463+
exp = Float(3.14)
2464+
self.assertEqual(eval_exp({}, exp), Float(3.14))
2465+
23822466
def test_eval_str_returns_str(self) -> None:
23832467
exp = String("xyz")
23842468
self.assertEqual(eval_exp({}, exp), String("xyz"))
@@ -2410,7 +2494,7 @@ def test_eval_with_binop_add_with_int_string_raises_type_error(self) -> None:
24102494
exp = Binop(BinopKind.ADD, Int(1), String("hello"))
24112495
with self.assertRaises(TypeError) as ctx:
24122496
eval_exp({}, exp)
2413-
self.assertEqual(ctx.exception.args[0], "expected Int, got String")
2497+
self.assertEqual(ctx.exception.args[0], "expected Int or Float, got String")
24142498

24152499
def test_eval_with_binop_sub(self) -> None:
24162500
exp = Binop(BinopKind.SUB, Int(1), Int(2))
@@ -2421,8 +2505,8 @@ def test_eval_with_binop_mul(self) -> None:
24212505
self.assertEqual(eval_exp({}, exp), Int(6))
24222506

24232507
def test_eval_with_binop_div(self) -> None:
2424-
exp = Binop(BinopKind.DIV, Int(2), Int(3))
2425-
self.assertEqual(eval_exp({}, exp), Int(0))
2508+
exp = Binop(BinopKind.DIV, Int(3), Int(10))
2509+
self.assertEqual(eval_exp({}, exp), Float(0.3))
24262510

24272511
def test_eval_with_binop_floor_div(self) -> None:
24282512
exp = Binop(BinopKind.FLOOR_DIV, Int(2), Int(3))
@@ -2707,7 +2791,7 @@ def test_eval_less_returns_bool(self) -> None:
27072791

27082792
def test_eval_less_on_non_bool_raises_type_error(self) -> None:
27092793
ast = Binop(BinopKind.LESS, String("xyz"), Int(4))
2710-
with self.assertRaisesRegex(TypeError, re.escape("expected Int, got String")):
2794+
with self.assertRaisesRegex(TypeError, re.escape("expected Int or Float, got String")):
27112795
eval_exp({}, ast)
27122796

27132797
def test_eval_less_equal_returns_bool(self) -> None:
@@ -2716,7 +2800,7 @@ def test_eval_less_equal_returns_bool(self) -> None:
27162800

27172801
def test_eval_less_equal_on_non_bool_raises_type_error(self) -> None:
27182802
ast = Binop(BinopKind.LESS_EQUAL, String("xyz"), Int(4))
2719-
with self.assertRaisesRegex(TypeError, re.escape("expected Int, got String")):
2803+
with self.assertRaisesRegex(TypeError, re.escape("expected Int or Float, got String")):
27202804
eval_exp({}, ast)
27212805

27222806
def test_eval_greater_returns_bool(self) -> None:
@@ -2725,7 +2809,7 @@ def test_eval_greater_returns_bool(self) -> None:
27252809

27262810
def test_eval_greater_on_non_bool_raises_type_error(self) -> None:
27272811
ast = Binop(BinopKind.GREATER, String("xyz"), Int(4))
2728-
with self.assertRaisesRegex(TypeError, re.escape("expected Int, got String")):
2812+
with self.assertRaisesRegex(TypeError, re.escape("expected Int or Float, got String")):
27292813
eval_exp({}, ast)
27302814

27312815
def test_eval_greater_equal_returns_bool(self) -> None:
@@ -2734,7 +2818,7 @@ def test_eval_greater_equal_returns_bool(self) -> None:
27342818

27352819
def test_eval_greater_equal_on_non_bool_raises_type_error(self) -> None:
27362820
ast = Binop(BinopKind.GREATER_EQUAL, String("xyz"), Int(4))
2737-
with self.assertRaisesRegex(TypeError, re.escape("expected Int, got String")):
2821+
with self.assertRaisesRegex(TypeError, re.escape("expected Int or Float, got String")):
27382822
eval_exp({}, ast)
27392823

27402824
def test_boolean_and_evaluates_args(self) -> None:
@@ -2791,6 +2875,21 @@ def test_eval_record_with_spread_fails(self) -> None:
27912875
def test_eval_symbol_returns_symbol(self) -> None:
27922876
self.assertEqual(eval_exp({}, Symbol("abc")), Symbol("abc"))
27932877

2878+
def test_eval_float_and_float_addition_returns_float(self) -> None:
2879+
self.assertEqual(eval_exp({}, Binop(BinopKind.ADD, Float(1.0), Float(2.0))), Float(3.0))
2880+
2881+
def test_eval_int_and_float_addition_returns_float(self) -> None:
2882+
self.assertEqual(eval_exp({}, Binop(BinopKind.ADD, Int(1), Float(2.0))), Float(3.0))
2883+
2884+
def test_eval_int_and_float_division_returns_float(self) -> None:
2885+
self.assertEqual(eval_exp({}, Binop(BinopKind.DIV, Int(1), Float(2.0))), Float(0.5))
2886+
2887+
def test_eval_float_and_int_division_returns_float(self) -> None:
2888+
self.assertEqual(eval_exp({}, Binop(BinopKind.DIV, Float(1.0), Int(2))), Float(0.5))
2889+
2890+
def test_eval_int_and_int_division_returns_float(self) -> None:
2891+
self.assertEqual(eval_exp({}, Binop(BinopKind.DIV, Int(1), Int(2))), Float(0.5))
2892+
27942893

27952894
class EndToEndTestsBase(unittest.TestCase):
27962895
def _run(self, text: str, env: Optional[Env] = None) -> Object:
@@ -2805,6 +2904,9 @@ class EndToEndTests(EndToEndTestsBase):
28052904
def test_int_returns_int(self) -> None:
28062905
self.assertEqual(self._run("1"), Int(1))
28072906

2907+
def test_float_returns_float(self) -> None:
2908+
self.assertEqual(self._run("3.14"), Float(3.14))
2909+
28082910
def test_bytes_returns_bytes(self) -> None:
28092911
self.assertEqual(self._run("~~QUJD"), Bytes(b"ABC"))
28102912

@@ -3604,6 +3706,11 @@ def test_serialize_negative_int(self) -> None:
36043706
obj = Int(-123)
36053707
self.assertEqual(obj.serialize(), {b"type": b"Int", b"value": -123})
36063708

3709+
def test_serialize_float_raises_not_implemented_error(self) -> None:
3710+
obj = Float(3.14)
3711+
with self.assertRaisesRegex(NotImplementedError, re.escape("serialization for Float is not supported")):
3712+
obj.serialize()
3713+
36073714
def test_serialize_str(self) -> None:
36083715
obj = String("abc")
36093716
self.assertEqual(obj.serialize(), {b"type": b"String", b"value": b"abc"})
@@ -3816,6 +3923,10 @@ def test_pretty_print_int(self) -> None:
38163923
obj = Int(1)
38173924
self.assertEqual(str(obj), "1")
38183925

3926+
def test_pretty_print_float(self) -> None:
3927+
obj = Float(3.14)
3928+
self.assertEqual(str(obj), "3.14")
3929+
38193930
def test_pretty_print_string(self) -> None:
38203931
obj = String("hello")
38213932
self.assertEqual(str(obj), '"hello"')

0 commit comments

Comments
 (0)