diff --git a/scrapscript.py b/scrapscript.py
index b63d918a..e3b5c1cc 100755
--- a/scrapscript.py
+++ b/scrapscript.py
@@ -4,6 +4,7 @@
import code
import dataclasses
import enum
+import functools
import http.server
import json
import logging
@@ -1254,6 +1255,91 @@ def bencode(obj: object) -> bytes:
raise NotImplementedError(f"bencode not implemented for {type(obj)}")
+class JSCompiler:
+ def compile(self, env: Env, exp: Object) -> str:
+ if isinstance(exp, Int):
+ return str(exp.value)
+ if isinstance(exp, Binop):
+ left = self.compile(env, exp.left)
+ right = self.compile(env, exp.right)
+ return f"({left})" + BinopKind.to_str(exp.op) + f"({right})"
+ if isinstance(exp, Var):
+ # assert exp.name in env
+ return exp.name
+ if isinstance(exp, Where):
+ binding = exp.binding
+ assert isinstance(binding, Assign)
+ return self.compile_let(env, binding.name.name, binding.value, exp.body)
+ if isinstance(exp, Assign):
+ value = self.compile(env, exp.value)
+ return f"const {exp.name.name} = {value};\n"
+ if isinstance(exp, Apply):
+ func = self.compile(env, exp.func)
+ arg = self.compile(env, exp.arg)
+ return f"({func})({arg})"
+ if isinstance(exp, Function):
+ arg = self.compile(env, exp.arg)
+ body = self.compile(env, exp.body)
+ return f"({arg}) => ({body})"
+ if isinstance(exp, List):
+ items = [self.compile(env, item) for item in exp.items]
+ return "[" + ", ".join(items) + "]"
+ if isinstance(exp, MatchFunction):
+ err = "(() => {throw 'oh no'})()"
+ if not exp.cases:
+ return err
+ # TODO(max): Gensym arg name or something
+ arg = "__x"
+
+ def per_case(acc: str, case: MatchCase) -> str:
+ cond, body = self.compile_match_case(env, arg, case)
+ return f"({cond}) ? ({body}) : ({acc})"
+
+ return f"({arg}) => " + functools.reduce(
+ per_case,
+ reversed(exp.cases),
+ err,
+ )
+ if isinstance(exp, Symbol):
+ if exp.value in ("true", "false"):
+ return exp.value
+ return repr(exp.value)
+ if isinstance(exp, String):
+ return repr(exp.value)
+ if isinstance(exp, Access):
+ obj = self.compile(env, exp.obj)
+ if isinstance(exp.at, Int):
+ return f"{obj}[{exp.at}]"
+ assert isinstance(exp.at, Var)
+ return f"{obj}.{exp.at}"
+ if isinstance(exp, Record):
+ result = "{"
+ for key, rec_value in exp.data.items():
+ result += repr(key) + ":" + self.compile(env, rec_value) + ","
+ return result + "}"
+ raise NotImplementedError(type(exp), exp)
+
+ def compile_let(self, env: Env, name: str, value: Object, body: Object) -> str:
+ body_str = self.compile(env, body)
+ value_str = self.compile(env, value)
+ return f"(({name}) => ({body_str}))({value_str})"
+
+ def compile_match_case(self, env: Env, arg: str, case: MatchCase) -> Tuple[str, str]:
+ pattern = case.pattern
+ body = case.body
+ if isinstance(pattern, Int):
+ return f"{arg} === {pattern.value}", self.compile(env, body)
+ if isinstance(pattern, Var):
+ return "true", self.compile_let(env, pattern.name, Var(arg), body)
+ raise NotImplementedError(type(pattern))
+
+
+def compile_exp_js(env: Env, exp: Object) -> str:
+ compiler = JSCompiler()
+ result = compiler.compile(env, exp)
+ return result
+
+
class Bdecoder:
def __init__(self, msg: str) -> None:
self.msg: str = msg
@@ -4158,6 +4244,105 @@ def test_pretty_print_symbol(self) -> None:
self.assertEqual(str(obj), "#x")
+class JSCompilerTests(unittest.TestCase):
+ def test_compile_int(self) -> None:
+ exp = Int(123)
+ self.assertEqual(compile_exp_js({}, exp), "123")
+
+ def test_compile_binop_add(self) -> None:
+ exp = Binop(BinopKind.ADD, Int(3), Int(4))
+ self.assertEqual(compile_exp_js({}, exp), "(3)+(4)")
+
+ def test_compile_binop_rec(self) -> None:
+ exp = Binop(BinopKind.MUL, Binop(BinopKind.ADD, Int(3), Int(4)), Int(5))
+ self.assertEqual(compile_exp_js({}, exp), "((3)+(4))*(5)")
+
+ def test_compile_where(self) -> None:
+ exp = Where(Var("x"), Assign(Var("x"), Int(1)))
+ self.assertEqual(compile_exp_js({}, exp), "((x) => (x))(1)")
+
+ def test_compile_nested_where(self) -> None:
+ exp = parse(tokenize("x + y . x = 1 . y = 2"))
+ self.assertEqual(compile_exp_js({}, exp), "((y) => (((x) => ((x)+(y)))(1)))(2)")
+
+ def test_compile_apply(self) -> None:
+ exp = Apply(Var("f"), Var("x"))
+ self.assertEqual(compile_exp_js({}, exp), "(f)(x)")
+
+ def test_compile_apply_nested(self) -> None:
+ exp = Apply(Apply(Var("f"), Var("x")), Var("y"))
+ self.assertEqual(compile_exp_js({}, exp), "((f)(x))(y)")
+
+ def test_compile_function(self) -> None:
+ exp = Function(Var("x"), Binop(BinopKind.ADD, Var("x"), Int(1)))
+ self.assertEqual(compile_exp_js({}, exp), "(x) => ((x)+(1))")
+
+ def test_compile_function_nested(self) -> None:
+ exp = parse(tokenize("x -> y -> x + y"))
+ self.assertEqual(compile_exp_js({}, exp), "(x) => ((y) => ((x)+(y)))")
+
+ def test_compile_list(self) -> None:
+ exp = List([Binop(BinopKind.ADD, Int(1), Int(2)), Binop(BinopKind.MUL, Int(3), Int(4))])
+ self.assertEqual(compile_exp_js({}, exp), "[(1)+(2), (3)*(4)]")
+
+ def test_compile_match_function(self) -> None:
+ exp = parse(tokenize("| 1 -> 2 | 2 -> 3"))
+ self.assertEqual(
+ compile_exp_js({}, exp), "(__x) => (__x === 1) ? (2) : ((__x === 2) ? (3) : ((() => {throw 'oh no'})()))"
+ )
+
+ def test_compile_match_function_var(self) -> None:
+ exp = parse(tokenize("| 1 -> 2 | x -> x"))
+ self.assertEqual(
+ compile_exp_js({}, exp),
+ "(__x) => (__x === 1) ? (2) : ((true) ? (((x) => (x))(__x)) : ((() => {throw 'oh no'})()))",
+ )
+
+ def test_compile_symbol_bool_true(self) -> None:
+ exp = Symbol("true")
+ self.assertEqual(compile_exp_js({}, exp), "true")
+
+ def test_compile_symbol_bool_false(self) -> None:
+ exp = Symbol("false")
+ self.assertEqual(compile_exp_js({}, exp), "false")
+
+ def test_compile_symbol(self) -> None:
+ exp = Symbol("hello")
+ self.assertEqual(compile_exp_js({}, exp), "'hello'")
+
+ def test_compile_string(self) -> None:
+ exp = String("hello")
+ self.assertEqual(compile_exp_js({}, exp), "'hello'")
+
+ def test_compile_string_single_quotes(self) -> None:
+ exp = String("'hello'")
+ self.assertEqual(compile_exp_js({}, exp), "\"'hello'\"")
+
+ def test_compile_string_double_quotes(self) -> None:
+ exp = String('"hello"')
+ self.assertEqual(compile_exp_js({}, exp), "'\"hello\"'")
+
+ def test_compile_access_int(self) -> None:
+ exp = Access(Var("x"), Int(1))
+ self.assertEqual(compile_exp_js({}, exp), "x[1]")
+
+ def test_compile_access_field(self) -> None:
+ exp = Access(Var("x"), Var("y"))
+ self.assertEqual(compile_exp_js({}, exp), "x.y")
+
+ def test_compile_nested_access(self) -> None:
+ exp = Access(Access(Var("x"), Var("y")), Var("z"))
+ self.assertEqual(compile_exp_js({}, exp), "x.y.z")
+
+ def test_compile_empty_record(self) -> None:
+ exp = Record({})
+ self.assertEqual(compile_exp_js({}, exp), "{}")
+
+ def test_compile_record(self) -> None:
+ exp = Record({"a": Int(1), "b": Int(2)})
+ self.assertEqual(compile_exp_js({}, exp), "{'a':1,'b':2,}")
+
+
def fetch(url: Object) -> Object:
if not isinstance(url, String):
raise TypeError(f"fetch expected String, but got {type(url).__name__}")
@@ -4323,6 +4508,25 @@ def runsource(self, source: str, filename: str = "", symbol: str = "singl
return False
+class JSRepl(ScrapRepl):
+ def runsource(self, source: str, filename: str = "", symbol: str = "single") -> bool:
+ try:
+ tokens = tokenize(source)
+ logger.debug("Tokens: %s", tokens)
+ ast = parse(tokens)
+ logger.debug("AST: %s", ast)
+ result = compile_exp_js(self.env, ast)
+ print(result)
+ except UnexpectedEOFError:
+ # Need to read more text
+ return True
+ except ParseError as e:
+ print(f"Parse error: {e}", file=sys.stderr)
+ except Exception as e:
+ print(f"Error: {e}", file=sys.stderr)
+ return False
+
+
def eval_command(args: argparse.Namespace) -> None:
if args.debug:
logging.basicConfig(level=logging.DEBUG)
@@ -4352,7 +4556,7 @@ def repl_command(args: argparse.Namespace) -> None:
if args.debug:
logging.basicConfig(level=logging.DEBUG)
- repl = ScrapRepl()
+ repl = JSRepl() if args.js else ScrapRepl()
if readline:
repl.enable_readline()
repl.interact(banner="")
@@ -4390,6 +4594,7 @@ def main() -> None:
repl = subparsers.add_parser("repl")
repl.set_defaults(func=repl_command)
repl.add_argument("--debug", action="store_true")
+ repl.add_argument("--js", action="store_true")
test = subparsers.add_parser("test")
test.set_defaults(func=test_command)
@@ -4420,5 +4625,37 @@ def main() -> None:
args.func(args)
+print(
+ compile_exp_js(
+ {},
+ parse(
+ tokenize(
+ """
+rand_array (new_generator 42) 0 100 10
+
+. rand_array = gen -> min -> max -> n -> n |>
+ | 0 -> []
+ | n -> (rand_val >+ rand_array new_gen min max (n - 1)
+ . rand_val = get_int new_gen
+ . new_gen = next gen min max)
+
+-- from Java's java.util.Random
+. new_generator = seed -> ({params = params, seed = seed, state = state}
+ . params = {mod = 281474976710656, mult = 25214903917, inc = 11}
+ . state = {min = 0, max = 0})
+
+. get_int = gen -> $$floor (get gen)
+
+. get = gen -> (gen@seed / gen@params@mod) * (gen@state@max - gen@state@min)
+
+. next = gen -> min -> max -> ({params = gen@params, seed = next_seed, state = {min = min, max = max}}
+ . next_seed = gen@state@min + (gen@seed * gen@params@mult + gen@params@inc) % gen@params@mod)
+"""
+ )
+ ),
+ )
+)
+
+
if __name__ == "__main__":
main()