diff --git a/packages/squiggle-lang/__tests__/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn_test.res b/packages/squiggle-lang/__tests__/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn_test.res index 1bb26e28..438d78e5 100644 --- a/packages/squiggle-lang/__tests__/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn_test.res +++ b/packages/squiggle-lang/__tests__/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn_test.res @@ -6,15 +6,18 @@ open Expect let expectEvalToBe = (expr: string, answer: string) => Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer) +let testEval = (expr, answer) => + test(expr, () => expectEvalToBe(expr, answer)) + describe("builtin", () => { // All MathJs operators and functions are available for string, number and boolean // .e.g + - / * > >= < <= == /= not and or // See https://mathjs.org/docs/expressions/syntax.html // See https://mathjs.org/docs/reference/functions.html - test("-1", () => expectEvalToBe("-1", "Ok(-1)")) - test("1-1", () => expectEvalToBe("1-1", "Ok(0)")) - test("2>1", () => expectEvalToBe("2>1", "Ok(true)")) - test("concat('a','b')", () => expectEvalToBe("concat('a','b')", "Ok('ab')")) + testEval("-1", "Ok(-1)") + testEval("1-1", "Ok(0)") + testEval("2>1", "Ok(true)") + testEval("concat('a','b')", "Ok('ab')") }) describe("builtin exception", () => { diff --git a/packages/squiggle-lang/__tests__/Reducer/Reducer_MathJs/Reducer_MathJsParse_test.res b/packages/squiggle-lang/__tests__/Reducer/Reducer_MathJs/Reducer_MathJsParse_test.res index 4acfc1c1..879e20f9 100644 --- a/packages/squiggle-lang/__tests__/Reducer/Reducer_MathJs/Reducer_MathJsParse_test.res +++ b/packages/squiggle-lang/__tests__/Reducer/Reducer_MathJs/Reducer_MathJsParse_test.res @@ -7,45 +7,60 @@ open Expect let expectParseToBe = (expr, answer) => Parse.parse(expr)->Result.flatMap(Parse.castNodeType)->Parse.toStringResult->expect->toBe(answer) +let testParse = (expr, answer) => test(expr, () => expectParseToBe(expr, answer)) + +let testDescriptionParse = (desc, expr, answer) => test(desc, () => expectParseToBe(expr, answer)) + +module MySkip = { + let testParse = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer)) + + let testDescriptionParse = (desc, expr, answer) => Skip.test(desc, () => expectParseToBe(expr, answer)) +} + describe("MathJs parse", () => { describe("literals operators paranthesis", () => { - test("1", () => expectParseToBe("1", "1")) - test("'hello'", () => expectParseToBe("'hello'", "'hello'")) - test("true", () => expectParseToBe("true", "true")) - test("1+2", () => expectParseToBe("1+2", "add(1, 2)")) - test("add(1,2)", () => expectParseToBe("add(1,2)", "add(1, 2)")) - test("(1)", () => expectParseToBe("(1)", "(1)")) - test("(1+2)", () => expectParseToBe("(1+2)", "(add(1, 2))")) + testParse("1", "1") + testParse("'hello'", "'hello'") + testParse("true", "true") + testParse("1+2", "add(1, 2)") + testParse("add(1,2)", "add(1, 2)") + testParse("(1)", "(1)") + testParse("(1+2)", "(add(1, 2))") + }) + + describe("multi-line", () => { + testParse("1; 2", "{1; 2}") }) describe("variables", () => { - Skip.test("define", () => expectParseToBe("x = 1", "???")) - Skip.test("use", () => expectParseToBe("x", "???")) + testParse("x = 1", "x = 1") + testParse("x", "x") + testParse("x = 1; x", "{x = 1; x}") }) describe("functions", () => { - Skip.test("define", () => expectParseToBe("identity(x) = x", "???")) - Skip.test("use", () => expectParseToBe("identity(x)", "???")) + MySkip.testParse("identity(x) = x", "???") + MySkip.testParse("identity(x)", "???") }) describe("arrays", () => { - test("empty", () => expectParseToBe("[]", "[]")) - test("define", () => expectParseToBe("[0, 1, 2]", "[0, 1, 2]")) - test("define with strings", () => expectParseToBe("['hello', 'world']", "['hello', 'world']")) - Skip.test("range", () => expectParseToBe("range(0, 4)", "range(0, 4)")) - test("index", () => expectParseToBe("([0,1,2])[1]", "([0, 1, 2])[1]")) + testDescriptionParse("empty", "[]", "[]") + testDescriptionParse("define", "[0, 1, 2]", "[0, 1, 2]") + testDescriptionParse("define with strings", "['hello', 'world']", "['hello', 'world']") + MySkip.testParse("range(0, 4)", "range(0, 4)") + testDescriptionParse("index", "([0,1,2])[1]", "([0, 1, 2])[1]") }) describe("records", () => { - test("define", () => expectParseToBe("{a: 1, b: 2}", "{a: 1, b: 2}")) - test("use", () => expectParseToBe("record.property", "record['property']")) + testDescriptionParse("define", "{a: 1, b: 2}", "{a: 1, b: 2}") + testDescriptionParse("use", "record.property", "record['property']") }) describe("comments", () => { - Skip.test("define", () => expectParseToBe("# This is a comment", "???")) + MySkip.testDescriptionParse("define", "# This is a comment", "???") }) - describe("if statement", () => { - Skip.test("define", () => expectParseToBe("if (true) { 1 } else { 0 }", "???")) + describe("if statement", () => { // TODO Tertiary operator instead + MySkip.testDescriptionParse("define", "if (true) { 1 } else { 0 }", "???") }) }) diff --git a/packages/squiggle-lang/__tests__/Reducer/Reducer_test.res b/packages/squiggle-lang/__tests__/Reducer/Reducer_test.res index 288cbabe..8e65328b 100644 --- a/packages/squiggle-lang/__tests__/Reducer/Reducer_test.res +++ b/packages/squiggle-lang/__tests__/Reducer/Reducer_test.res @@ -1,6 +1,14 @@ open Jest open Reducer_TestHelpers +let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, answer)) + +let testDescriptionParseToBe = (desc, expr, answer) => test(desc, () => expectParseToBe(expr, answer)) + +let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer)) + +let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer)) + describe("reducer using mathjs parse", () => { // Test the MathJs parser compatibility // Those tests toString that there is a semantic mapping from MathJs to Expression @@ -10,33 +18,39 @@ describe("reducer using mathjs parse", () => { // Those tests toString that we are converting mathjs parse tree to what we need describe("expressions", () => { - test("1", () => expectParseToBe("1", "Ok(1)")) - test("(1)", () => expectParseToBe("(1)", "Ok(1)")) - test("1+2", () => expectParseToBe("1+2", "Ok((:add 1 2))")) - test("(1+2)", () => expectParseToBe("1+2", "Ok((:add 1 2))")) - test("add(1,2)", () => expectParseToBe("1+2", "Ok((:add 1 2))")) - test("1+2*3", () => expectParseToBe("1+2*3", "Ok((:add 1 (:multiply 2 3)))")) + testParseToBe("1", "Ok(1)") + testParseToBe("(1)", "Ok(1)") + testParseToBe("1+2", "Ok((:add 1 2))") + testParseToBe("1+2", "Ok((:add 1 2))") + testParseToBe("1+2", "Ok((:add 1 2))") + testParseToBe("1+2*3", "Ok((:add 1 (:multiply 2 3)))") }) describe("arrays", () => { //Note. () is a empty list in Lisp // The only builtin structure in Lisp is list. There are no arrays // [1,2,3] becomes (1 2 3) - test("empty", () => expectParseToBe("[]", "Ok(())")) - test("[1, 2, 3]", () => expectParseToBe("[1, 2, 3]", "Ok((1 2 3))")) - test("['hello', 'world']", () => expectParseToBe("['hello', 'world']", "Ok(('hello' 'world'))")) - test("index", () => expectParseToBe("([0,1,2])[1]", "Ok((:$atIndex (0 1 2) (1)))")) + testDescriptionParseToBe("empty", "[]", "Ok(())") + testParseToBe("[1, 2, 3]", "Ok((1 2 3))") + testParseToBe("['hello', 'world']", "Ok(('hello' 'world'))") + testDescriptionParseToBe("index", "([0,1,2])[1]", "Ok((:$atIndex (0 1 2) (1)))") }) describe("records", () => { - test("define", () => - expectParseToBe("{a: 1, b: 2}", "Ok((:$constructRecord (('a' 1) ('b' 2))))") - ) - test("use", () => - expectParseToBe( - "{a: 1, b: 2}.a", - "Ok((:$atIndex (:$constructRecord (('a' 1) ('b' 2))) ('a')))", - ) + testDescriptionParseToBe("define", "{a: 1, b: 2}", "Ok((:$constructRecord (('a' 1) ('b' 2))))") + testDescriptionParseToBe( + "use", + "{a: 1, b: 2}.a", + "Ok((:$atIndex (:$constructRecord (('a' 1) ('b' 2))) ('a')))", ) }) + describe("multi-line", () => { + testParseToBe("1; 2", "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) 1) 2))") + testParseToBe("1+1; 2+1", "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:add 1 1)) (:add 2 1)))") + }) + describe("assignment", () => { + testParseToBe("x=1; x", "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :x 1)) :x))") + testParseToBe("x=1+1; x+1", "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :x (:add 1 1))) (:add :x 1)))") + }) + }) describe("eval", () => { @@ -45,37 +59,47 @@ describe("eval", () => { // See https://mathjs.org/docs/expressions/syntax.html // See https://mathjs.org/docs/reference/functions.html describe("expressions", () => { - test("1", () => expectEvalToBe("1", "Ok(1)")) - test("1+2", () => expectEvalToBe("1+2", "Ok(3)")) - test("(1+2)*3", () => expectEvalToBe("(1+2)*3", "Ok(9)")) - test("2>1", () => expectEvalToBe("2>1", "Ok(true)")) - test("concat('a ', 'b')", () => expectEvalToBe("concat('a ', 'b')", "Ok('a b')")) - test("log(10)", () => expectEvalToBe("log(10)", "Ok(2.302585092994046)")) - test("cos(10)", () => expectEvalToBe("cos(10)", "Ok(-0.8390715290764524)")) + testEvalToBe("1", "Ok(1)") + testEvalToBe("1+2", "Ok(3)") + testEvalToBe("(1+2)*3", "Ok(9)") + testEvalToBe("2>1", "Ok(true)") + testEvalToBe("concat('a ', 'b')", "Ok('a b')") + testEvalToBe("log(10)", "Ok(2.302585092994046)") + testEvalToBe("cos(10)", "Ok(-0.8390715290764524)") // TODO more built ins }) describe("arrays", () => { test("empty array", () => expectEvalToBe("[]", "Ok([])")) - test("[1, 2, 3]", () => expectEvalToBe("[1, 2, 3]", "Ok([1, 2, 3])")) - test("['hello', 'world']", () => expectEvalToBe("['hello', 'world']", "Ok(['hello', 'world'])")) - test("index", () => expectEvalToBe("([0,1,2])[1]", "Ok(1)")) - test("index not found", () => - expectEvalToBe("([0,1,2])[10]", "Error(Array index not found: 10)") - ) + testEvalToBe("[1, 2, 3]", "Ok([1, 2, 3])") + testEvalToBe("['hello', 'world']", "Ok(['hello', 'world'])") + testEvalToBe("([0,1,2])[1]", "Ok(1)") + testDescriptionEvalToBe("index not found", "([0,1,2])[10]", "Error(Array index not found: 10)") }) describe("records", () => { test("define", () => expectEvalToBe("{a: 1, b: 2}", "Ok({a: 1, b: 2})")) test("index", () => expectEvalToBe("{a: 1}.a", "Ok(1)")) test("index not found", () => expectEvalToBe("{a: 1}.b", "Error(Record property not found: b)")) }) + + describe("multi-line", () => { + testEvalToBe("1; 2", "Error(Assignment expected)") + testEvalToBe("1+1; 2+1", "Error(Assignment expected)") + }) + describe("assignment", () => { + testEvalToBe("x=1; x", "Ok(1)") + testEvalToBe("x=1+1; x+1", "Ok(3)") + testEvalToBe("x=1; y=x+1; y+1", "Ok(3)") + testEvalToBe("1; x=1", "Error(Assignment expected)") + testEvalToBe("1; 1", "Error(Assignment expected)") + testEvalToBe("x=1; x=1", "Error(Expression expected)") + }) }) describe("test exceptions", () => { - test("javascript exception", () => - expectEvalToBe("jsraise('div by 0')", "Error(JS Exception: Error: 'div by 0')") - ) - - test("rescript exception", () => - expectEvalToBe("resraise()", "Error(TODO: unhandled rescript exception)") + testDescriptionEvalToBe( + "javascript exception", + "javascriptraise('div by 0')", + "Error(JS Exception: Error: 'div by 0')", ) + testDescriptionEvalToBe("rescript exception", "rescriptraise()", "Error(TODO: unhandled rescript exception)") }) diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index 8455c601..4b8cddab 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -88,6 +88,7 @@ function tag(x: a, y: b): tagged { export type squiggleExpression = | tagged<"symbol", string> | tagged<"string", string> + | tagged<"call", string> | tagged<"array", squiggleExpression[]> | tagged<"boolean", boolean> | tagged<"distribution", Distribution> @@ -117,6 +118,8 @@ function createTsExport( ); case "EvBool": return tag("boolean", x.value); + case "EvCall": + return tag("call", x.value); case "EvDistribution": return tag("distribution", new Distribution(x.value, sampEnv)); case "EvNumber": diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn.res index 824e36ef..21c91dc2 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Dispatch/Reducer_Dispatch_BuiltIn.res @@ -14,8 +14,8 @@ exception TestRescriptException let callInternal = (call: functionCall): result<'b, errorValue> => { let callMathJs = (call: functionCall): result<'b, errorValue> => switch call { - | ("jsraise", [msg]) => Js.Exn.raiseError(toString(msg)) // For Tests - | ("resraise", _) => raise(TestRescriptException) // For Tests + | ("javascriptraise", [msg]) => Js.Exn.raiseError(toString(msg)) // For Tests + | ("rescriptraise", _) => raise(TestRescriptException) // For Tests | call => call->toStringFunctionCall->MathJs.Eval.eval } @@ -58,7 +58,7 @@ let callInternal = (call: functionCall): result<'b, errorValue> => { } /* - Lisp engine uses Result monad while reducing expressions + Reducer uses Result monad while reducing expressions */ let dispatch = (call: functionCall): result => try { diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_ErrorValue.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_ErrorValue.res index d0c85d59..4ad60732 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_ErrorValue.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_ErrorValue.res @@ -1,9 +1,14 @@ @genType type errorValue = | REArrayIndexNotFound(string, int) + | REAssignmentExpected + | REExpressionExpected | REFunctionExpected(string) | REJavaScriptExn(option, option) // Javascript Exception + | REMacroNotFound(string) | RERecordPropertyNotFound(string, string) + | RESymbolNotFound(string) + | RESyntaxError(string) | RETodo(string) // To do type t = errorValue @@ -12,6 +17,8 @@ type t = errorValue let errorToString = err => switch err { | REArrayIndexNotFound(msg, index) => `${msg}: ${Js.String.make(index)}` + | REAssignmentExpected => "Assignment expected" + | REExpressionExpected => "Expression expected" | REFunctionExpected(msg) => `Function expected: ${msg}` | REJavaScriptExn(omsg, oname) => { let answer = "JS Exception:" @@ -25,6 +32,9 @@ let errorToString = err => } answer } + | REMacroNotFound(macro) => `Macro not found: ${macro}` | RERecordPropertyNotFound(msg, index) => `${msg}: ${index}` + | RESymbolNotFound(symbolName) => `${symbolName} is not defined` + | RESyntaxError(desc) => `Syntax Error: ${desc}` | RETodo(msg) => `TODO: ${msg}` } diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res index 76e75e07..8498c294 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res @@ -11,10 +11,11 @@ type expressionValue = ExpressionValue.expressionValue type t = expression /* - Shows the Lisp Code as text lisp code + Shows the expression as text of expression */ let rec toString = expression => switch expression { + | T.EBindings(bindings) => "$$bound" | T.EList(aList) => `(${Belt.List.map(aList, aValue => toString(aValue)) ->Extra.List.interperse(" ") @@ -30,7 +31,7 @@ let toStringResult = codeResult => } /* - Converts a MathJs code to Lisp Code + Converts a MathJs code to expression */ let parse_ = (expr: string, parser, converter): result => expr->parser->Result.flatMap(node => converter(node)) @@ -38,54 +39,141 @@ let parse_ = (expr: string, parser, converter): result => let parse = (mathJsCode: string): result => mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromNode) -module MapString = Belt.Map.String -type bindings = MapString.t -let defaultBindings: bindings = MapString.fromArray([]) -// TODO Define bindings for function execution context +let defaultBindings: T.bindings = Belt.Map.String.empty /* - After reducing each level of code tree, we have a value list to evaluate + Recursively evaluate/reduce the expression (Lisp AST) */ -let reduceValueList = (valueList: list): result => - switch valueList { - | list{EvSymbol(fName), ...args} => (fName, args->Belt.List.toArray)->BuiltIn.dispatch - | _ => valueList->Belt.List.toArray->ExpressionValue.EvArray->Ok - } - -/* - Recursively evaluate/reduce the code tree -*/ -let rec reduceExpression = (expression: t, bindings): result => - switch expression { - | T.EValue(value) => value->Ok - | T.EList(list) => { - let racc: result, 'e> = list->Belt.List.reduceReverse(Ok(list{}), ( - racc, - each: expression, - ) => - racc->Result.flatMap(acc => { - each - ->reduceExpression(bindings) - ->Result.flatMap(newNode => { - acc->Belt.List.add(newNode)->Ok - }) - }) - ) - racc->Result.flatMap(acc => acc->reduceValueList) +let rec reduceExpression = (expression: t, bindings: T.bindings): result => { + /* + After reducing each level of expression(Lisp AST), we have a value list to evaluate + */ + let reduceValueList = (valueList: list): result => + switch valueList { + | list{EvCall(fName), ...args} => (fName, args->Belt.List.toArray)->BuiltIn.dispatch + | _ => valueList->Belt.List.toArray->ExpressionValue.EvArray->Ok } + + /* + Macros are like functions but instead of taking values as parameters, + they take expressions as parameters and return a new expression. + Macros are used to define language building blocks. They are like Lisp macros. + */ + let doMacroCall = (list: list, bindings: T.bindings): result => { + let dispatchMacroCall = (list: list, bindings: T.bindings): result => { + let rec replaceSymbols = (expression: t, bindings: T.bindings): result => + switch expression { + | T.EValue(EvSymbol(aSymbol)) => + switch bindings->Belt.Map.String.get(aSymbol) { + | Some(boundExpression) => boundExpression->Ok + | None => RESymbolNotFound(aSymbol)->Error + } + | T.EValue(_) => expression->Ok + | T.EBindings(_) => expression->Ok + | T.EList(list) => { + let racc = list->Belt.List.reduceReverse(Ok(list{}), (racc, each: expression) => + racc->Result.flatMap(acc => { + each + ->replaceSymbols(bindings) + ->Result.flatMap(newNode => { + acc->Belt.List.add(newNode)->Ok + }) + }) + ) + racc->Result.map(acc => acc->T.EList) + } + } + + let doBindStatement = (statement: t, bindings: T.bindings) => { + switch statement { + | T.EList(list{T.EValue(EvCall("$let")), T.EValue(EvSymbol(aSymbol)), expression}) => { + let rNewExpression = replaceSymbols(expression, bindings) + rNewExpression->Result.map(newExpression => + Belt.Map.String.set(bindings, aSymbol, newExpression)->T.EBindings + ) + } + | _ => REAssignmentExpected->Error + } + } + + let doBindExpression = (expression: t, bindings: T.bindings) => { + switch expression { + | T.EList(list{T.EValue(EvCall("$let")), ..._}) => REExpressionExpected->Error + | _ => replaceSymbols(expression, bindings) + } + } + + switch list { + | list{T.EValue(EvCall("$$bindings"))} => bindings->EBindings->Ok + + | list{T.EValue(EvCall("$$bindStatement")), T.EBindings(bindings), statement} => + doBindStatement(statement, bindings) + | list{T.EValue(EvCall("$$bindExpression")), T.EBindings(bindings), expression} => + doBindExpression(expression, bindings) + | _ => list->T.EList->Ok + } + } + + list->dispatchMacroCall(bindings) } + let rec seekMacros = (expression: t, bindings: T.bindings): result => + switch expression { + | T.EValue(value) => expression->Ok + | T.EList(list) => { + let racc: result, 'e> = list->Belt.List.reduceReverse(Ok(list{}), ( + racc, + each: expression, + ) => + racc->Result.flatMap(acc => { + each + ->seekMacros(bindings) + ->Result.flatMap(newNode => { + acc->Belt.List.add(newNode)->Ok + }) + }) + ) + racc->Result.flatMap(acc => acc->doMacroCall(bindings)) + } + } + + let rec reduceExpandedExpression = (expression: t): result => + switch expression { + | T.EValue(value) => value->Ok + | T.EList(list) => { + let racc: result, 'e> = list->Belt.List.reduceReverse(Ok(list{}), ( + racc, + each: expression, + ) => + racc->Result.flatMap(acc => { + each + ->reduceExpandedExpression + ->Result.flatMap(newNode => { + acc->Belt.List.add(newNode)->Ok + }) + }) + ) + racc->Result.flatMap(acc => acc->reduceValueList) + } + } + + let rExpandedExpression: result = expression->seekMacros(bindings) + rExpandedExpression->Result.flatMap(expandedExpression => + expandedExpression->reduceExpandedExpression + ) +} + let evalWBindingsExpression = (aExpression, bindings): result => reduceExpression(aExpression, bindings) /* - Evaluates MathJs code via Lisp using bindings and answers the result + Evaluates MathJs code via Reducer using bindings and answers the result */ -let evalWBindings = (codeText: string, bindings: bindings) => { +let evalWBindings = (codeText: string, bindings: T.bindings) => { parse(codeText)->Result.flatMap(code => code->evalWBindingsExpression(bindings)) } /* - Evaluates MathJs code via Lisp and answers the result + Evaluates MathJs code via Reducer and answers the result */ let eval = (code: string) => evalWBindings(code, defaultBindings) diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.resi b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.resi deleted file mode 100644 index 2a6db9bd..00000000 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.resi +++ /dev/null @@ -1,29 +0,0 @@ -module Result = Belt.Result -module T = Reducer_Expression_T -type expression = T.expression -@genType -type expressionValue = ReducerInterface_ExpressionValue.expressionValue -type t = expression -let toString: T.expression => Js.String.t -let toStringResult: result => string -let parse: string => result -module MapString = Belt.Map.String -type bindings = MapString.t -let defaultBindings: bindings -let reduceValueList: list => result< - expressionValue, - Reducer_ErrorValue.t, -> -let reduceExpression: (expression, 'a) => result< - expressionValue, - Reducer_ErrorValue.t, -> -let evalWBindingsExpression: (expression, 'a) => result< - expressionValue, - Reducer_ErrorValue.t, -> -let evalWBindings: (string, bindings) => Result.t< - expressionValue, - Reducer_ErrorValue.t, -> -let eval: string => Result.t diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression_T.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression_T.res index 5f376050..fe938316 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression_T.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression_T.res @@ -1,5 +1,15 @@ open ReducerInterface.ExpressionValue +/* + An expression is a Lisp AST. An expression is either a primitive value or a list of expressions. + In the case of a list of expressions (e1, e2, e3, ...eN), the semantic is + apply e1, e2 -> apply e3 -> ... -> apply eN + This is Lisp semantics. It holds true in both eager and lazy evaluations. + A Lisp AST contains only expressions/primitive values to apply to their left. + The act of defining the semantics of a functional language is to write it in terms of Lisp AST. +*/ type rec expression = | EList(list) // A list to map-reduce | EValue(expressionValue) // Irreducible built-in value. Reducer should not know the internals. External libraries are responsible + | EBindings(bindings) // let/def kind of statements return bindings +and bindings = Belt.Map.String.t diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_Parse.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_Parse.res index bfd186e0..e3e2955c 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_Parse.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_Parse.res @@ -7,25 +7,31 @@ open Reducer_ErrorValue type node = {"type": string, "isNode": bool, "comment": string} type arrayNode = {...node, "items": array} -//assignmentNode -//blockNode +type block = {"node": node} +type blockNode = {...node, "blocks": array} //conditionalNode type constantNode = {...node, "value": unit} //functionAssignmentNode -type functionNode = {...node, "fn": string, "args": array} type indexNode = {...node, "dimensions": array} type objectNode = {...node, "properties": Js.Dict.t} -type accessorNode = {...node, "object": node, "index": indexNode} -type operatorNode = {...functionNode, "op": string} +type accessorNode = {...node, "object": node, "index": indexNode, "name": string} -//parenthesisNode type parenthesisNode = {...node, "content": node} //rangeNode //relationalNode type symbolNode = {...node, "name": string} +type functionNode = {...node, "fn": unit, "args": array} +type operatorNode = {...functionNode, "op": string} +type assignmentNode = {...node, "object": symbolNode, "value": node} +type assignmentNodeWAccessor = {...node, "object": accessorNode, "value": node} +type assignmentNodeWIndex = {...assignmentNodeWAccessor, "index": Js.null} external castAccessorNode: node => accessorNode = "%identity" external castArrayNode: node => arrayNode = "%identity" +external castAssignmentNode: node => assignmentNode = "%identity" +external castAssignmentNodeWAccessor: node => assignmentNodeWAccessor = "%identity" +external castAssignmentNodeWIndex: node => assignmentNodeWIndex = "%identity" +external castBlockNode: node => blockNode = "%identity" external castConstantNode: node => constantNode = "%identity" external castFunctionNode: node => functionNode = "%identity" external castIndexNode: node => indexNode = "%identity" @@ -50,6 +56,8 @@ let parse = (expr: string): result => type mathJsNode = | MjAccessorNode(accessorNode) | MjArrayNode(arrayNode) + | MjAssignmentNode(assignmentNode) + | MjBlockNode(blockNode) | MjConstantNode(constantNode) | MjFunctionNode(functionNode) | MjIndexNode(indexNode) @@ -58,10 +66,21 @@ type mathJsNode = | MjParenthesisNode(parenthesisNode) | MjSymbolNode(symbolNode) -let castNodeType = (node: node) => +let castNodeType = (node: node) => { + let decideAssignmentNode = node => { + let iNode = node->castAssignmentNodeWIndex + if Js.null == iNode["index"] && iNode["object"]["type"] == "SymbolNode" { + node->castAssignmentNode->MjAssignmentNode->Ok + } else { + RESyntaxError("Assignment to index or property not supported")->Error + } + } + switch node["type"] { | "AccessorNode" => node->castAccessorNode->MjAccessorNode->Ok | "ArrayNode" => node->castArrayNode->MjArrayNode->Ok + | "AssignmentNode" => node->decideAssignmentNode + | "BlockNode" => node->castBlockNode->MjBlockNode->Ok | "ConstantNode" => node->castConstantNode->MjConstantNode->Ok | "FunctionNode" => node->castFunctionNode->MjFunctionNode->Ok | "IndexNode" => node->castIndexNode->MjIndexNode->Ok @@ -71,6 +90,19 @@ let castNodeType = (node: node) => | "SymbolNode" => node->castSymbolNode->MjSymbolNode->Ok | _ => RETodo(`Argg, unhandled MathJsNode: ${node["type"]}`)->Error } +} + +external unitAsSymbolNode: unit => symbolNode = "%identity" +external unitAsString: unit => string = "%identity" + +let nameOfFunctionNode = (fNode: functionNode): string => { + let name = fNode["fn"] + if Js.typeof(name) == "string" { + name->unitAsString + } else { + (name->unitAsSymbolNode)["name"] + } +} let rec toString = (mathJsNode: mathJsNode): string => { let toStringValue = (a: 'a): string => @@ -87,9 +119,10 @@ let rec toString = (mathJsNode: mathJsNode): string => { ->Js.String.concatMany("") let toStringFunctionNode = (fnode: functionNode): string => - `${fnode["fn"]}(${fnode["args"]->toStringNodeArray})` + `${fnode->nameOfFunctionNode}(${fnode["args"]->toStringNodeArray})` - let toStringObjectEntry = ((key: string, value: node)): string => `${key}: ${value->toStringMathJsNode}` + let toStringObjectEntry = ((key: string, value: node)): string => + `${key}: ${value->toStringMathJsNode}` let toStringObjectNode = (oNode: objectNode): string => `{${oNode["properties"] @@ -103,16 +136,28 @@ let rec toString = (mathJsNode: mathJsNode): string => { ->Belt.Array.map(each => toStringResult(each->castNodeType)) ->Js.String.concatMany("") + let toStringSymbolNode = (sNode: symbolNode): string => sNode["name"] + + let toStringBlocks = (blocks: array): string => + blocks + ->Belt.Array.map(each => each["node"]->castNodeType->toStringResult) + ->Extra.Array.interperse("; ") + ->Js.String.concatMany("") + switch mathJsNode { - | MjAccessorNode(aNode) => `${aNode["object"]->toStringMathJsNode}[${aNode["index"]->toStringIndexNode}]` + | MjAccessorNode(aNode) => + `${aNode["object"]->toStringMathJsNode}[${aNode["index"]->toStringIndexNode}]` | MjArrayNode(aNode) => `[${aNode["items"]->toStringNodeArray}]` + | MjAssignmentNode(aNode) => + `${aNode["object"]->toStringSymbolNode} = ${aNode["value"]->toStringMathJsNode}` + | MjBlockNode(bNode) => `{${bNode["blocks"]->toStringBlocks}}` | MjConstantNode(cNode) => cNode["value"]->toStringValue | MjFunctionNode(fNode) => fNode->toStringFunctionNode | MjIndexNode(iNode) => iNode->toStringIndexNode | MjObjectNode(oNode) => oNode->toStringObjectNode | MjOperatorNode(opNode) => opNode->castOperatorNodeToFunctionNode->toStringFunctionNode | MjParenthesisNode(pNode) => `(${toStringMathJsNode(pNode["content"])})` - | MjSymbolNode(sNode) => sNode["name"] + | MjSymbolNode(sNode) => sNode->toStringSymbolNode } } and toStringResult = (rMathJsNode: result): string => diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_ToExpression.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_ToExpression.res index 5b16fb54..30c8651d 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_ToExpression.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_MathJs/Reducer_MathJs_ToExpression.res @@ -1,11 +1,11 @@ module ErrorValue = Reducer_ErrorValue module ExpressionValue = ReducerInterface.ExpressionValue -module ExtressionT = Reducer_Expression_T +module ExpressionT = Reducer_Expression_T module JavaScript = Reducer_Js module Parse = Reducer_MathJs_Parse module Result = Belt.Result -type expression = ExtressionT.expression +type expression = ExpressionT.expression type expressionValue = ExpressionValue.expressionValue type errorValue = ErrorValue.errorValue @@ -18,10 +18,19 @@ let rec fromNode = (mathJsNode: Parse.node): result => ) ) - let castFunctionNode = fNode => { - let fn = fNode["fn"]->ExpressionValue.EvSymbol->ExtressionT.EValue + let toEvCallValue = (name: string): expression => + name->ExpressionValue.EvCall->ExpressionT.EValue + let toEvSymbolValue = (name: string): expression => + name->ExpressionValue.EvSymbol->ExpressionT.EValue + + let passToFunction = (fName: string, rLispArgs): result => { + let fn = fName->toEvCallValue + rLispArgs->Result.flatMap(lispArgs => list{fn, ...lispArgs}->ExpressionT.EList->Ok) + } + + let caseFunctionNode = fNode => { let lispArgs = fNode["args"]->Belt.List.fromArray->fromNodeList - lispArgs->Result.map(argsCode => list{fn, ...argsCode}->ExtressionT.EList) + passToFunction(fNode->Parse.nameOfFunctionNode, lispArgs) } let caseObjectNode = oNode => { @@ -34,15 +43,16 @@ let rec fromNode = (mathJsNode: Parse.node): result => fromNode(value)->Result.map(valueExpression => { let entryCode = list{ - key->ExpressionValue.EvString->ExtressionT.EValue, + key->ExpressionValue.EvString->ExpressionT.EValue, valueExpression, - }->ExtressionT.EList + }->ExpressionT.EList list{entryCode, ...acc} }) ) ) - let lispName = "$constructRecord"->ExpressionValue.EvSymbol->ExtressionT.EValue - rargs->Result.map(args => list{lispName, ExtressionT.EList(args)}->ExtressionT.EList) + rargs->Result.flatMap(args => + passToFunction("$constructRecord", list{ExpressionT.EList(args)}->Ok) + ) // $consturctRecord gets a single argument: List of key-value paiers } oNode["properties"]->Js.Dict.entries->Belt.List.fromArray->fromObjectEntries @@ -54,33 +64,75 @@ let rec fromNode = (mathJsNode: Parse.node): result => Ok(list{}), (racc, currentPropertyMathJsNode) => racc->Result.flatMap(acc => - fromNode(currentPropertyMathJsNode)->Result.map(propertyCode => list{propertyCode, ...acc}) + fromNode(currentPropertyMathJsNode)->Result.map(propertyCode => list{ + propertyCode, + ...acc, + }) ), ) - rpropertyCodeList->Result.map(propertyCodeList => ExtressionT.EList(propertyCodeList)) + rpropertyCodeList->Result.map(propertyCodeList => ExpressionT.EList(propertyCodeList)) } let caseAccessorNode = (objectNode, indexNode) => { - let fn = "$atIndex"->ExpressionValue.EvSymbol->ExtressionT.EValue - caseIndexNode(indexNode)->Result.flatMap(indexCode => { - fromNode(objectNode)->Result.map(objectCode => - list{fn, objectCode, indexCode}->ExtressionT.EList + fromNode(objectNode)->Result.flatMap(objectCode => + passToFunction("$atIndex", list{objectCode, indexCode}->Ok) ) }) } - switch typedMathJsNode { - | MjArrayNode(aNode) => - aNode["items"]->Belt.List.fromArray->fromNodeList->Result.map(list => ExtressionT.EList(list)) - | MjConstantNode(cNode) => - cNode["value"]->JavaScript.Gate.jsToEv->Result.map(v => v->ExtressionT.EValue) - | MjFunctionNode(fNode) => fNode->castFunctionNode - | MjOperatorNode(opNode) => opNode->Parse.castOperatorNodeToFunctionNode->castFunctionNode - | MjParenthesisNode(pNode) => pNode["content"]->fromNode - | MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"]) - | MjObjectNode(oNode) => caseObjectNode(oNode) - | MjSymbolNode(sNode) => sNode["name"]->ExpressionValue.EvSymbol->ExtressionT.EValue->Ok - | MjIndexNode(iNode) => caseIndexNode(iNode) + let caseAssignmentNode = aNode => { + let symbol = aNode["object"]["name"]->toEvSymbolValue + let rValueExpression = fromNode(aNode["value"]) + rValueExpression->Result.flatMap(valueExpression => { + let lispArgs = list{symbol, valueExpression}->Ok + passToFunction("$let", lispArgs) + }) } + + let caseArrayNode = aNode => { + aNode["items"]->Belt.List.fromArray->fromNodeList->Result.map(list => ExpressionT.EList(list)) + } + + let caseBlockNode = (bNode): result => { + let blocks = bNode["blocks"] + let initialBindings = passToFunction("$$bindings", list{}->Ok) + let lastIndex = Belt.Array.length(blocks) - 1 + blocks->Belt.Array.reduceWithIndex(initialBindings, (rPreviousBindings, block, i) => { + rPreviousBindings->Result.flatMap(previousBindings => { + let node = block["node"] + let rStatement: result = node->fromNode + let bindName = if i == lastIndex { + "$$bindExpression" + } else { + "$$bindStatement" + } + rStatement->Result.flatMap((statement: expression) => { + let lispArgs = list{previousBindings, statement}->Ok + passToFunction(bindName, lispArgs) + }) + }) + }) + } + + let rFinalExpression: result = switch typedMathJsNode { + | MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"]) + | MjArrayNode(aNode) => caseArrayNode(aNode) + | MjAssignmentNode(aNode) => caseAssignmentNode(aNode) + | MjSymbolNode(sNode) => { + let expr: expression = toEvSymbolValue(sNode["name"]) + let rExpr: result = expr->Ok + rExpr + } + | MjBlockNode(bNode) => caseBlockNode(bNode) + // | MjBlockNode(bNode) => "statement"->toEvSymbolValue->Ok + | MjConstantNode(cNode) => + cNode["value"]->JavaScript.Gate.jsToEv->Result.flatMap(v => v->ExpressionT.EValue->Ok) + | MjFunctionNode(fNode) => fNode->caseFunctionNode + | MjIndexNode(iNode) => caseIndexNode(iNode) + | MjObjectNode(oNode) => caseObjectNode(oNode) + | MjOperatorNode(opNode) => opNode->Parse.castOperatorNodeToFunctionNode->caseFunctionNode + | MjParenthesisNode(pNode) => pNode["content"]->fromNode + } + rFinalExpression }) diff --git a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_ExpressionValue.res b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_ExpressionValue.res index 8842ce8f..2a8dc2e1 100644 --- a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_ExpressionValue.res +++ b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_ExpressionValue.res @@ -7,25 +7,30 @@ module ErrorValue = Reducer_ErrorValue @genType type rec expressionValue = + | EvArray(array) | EvBool(bool) + | EvCall(string) // External function call + | EvDistribution(GenericDist_Types.genericDist) | EvNumber(float) + | EvRecord(Js.Dict.t) | EvString(string) | EvSymbol(string) - | EvArray(array) - | EvRecord(Js.Dict.t) - | EvDistribution(GenericDist_Types.genericDist) type functionCall = (string, array) let rec toString = aValue => switch aValue { | EvBool(aBool) => Js.String.make(aBool) + | EvCall(fName) => `:${fName}` | EvNumber(aNumber) => Js.String.make(aNumber) | EvString(aString) => `'${aString}'` | EvSymbol(aString) => `:${aString}` | EvArray(anArray) => { let args = - anArray->Belt.Array.map(each => toString(each))->Extra_Array.interperse(", ")->Js.String.concatMany("") + anArray + ->Belt.Array.map(each => toString(each)) + ->Extra_Array.interperse(", ") + ->Js.String.concatMany("") `[${args}]` } | EvRecord(aRecord) => { @@ -37,12 +42,13 @@ let rec toString = aValue => ->Js.String.concatMany("") `{${pairs}}` } - | EvDistribution(dist) => `${GenericDist.toString(dist)}` + | EvDistribution(dist) => GenericDist.toString(dist) } let toStringWithType = aValue => switch aValue { | EvBool(_) => `Bool::${toString(aValue)}` + | EvCall(_) => `Call::${toString(aValue)}` | EvNumber(_) => `Number::${toString(aValue)}` | EvString(_) => `String::${toString(aValue)}` | EvSymbol(_) => `Symbol::${toString(aValue)}` diff --git a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res index ea65b963..ed40e5d7 100644 --- a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res +++ b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res @@ -128,7 +128,8 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option< | ("mean", [EvDistribution(dist)]) => Helpers.toFloatFn(#Mean, dist) | ("toString", [EvDistribution(dist)]) => Helpers.toStringFn(ToString, dist) | ("toSparkline", [EvDistribution(dist)]) => Helpers.toStringFn(ToSparkline(20), dist) - | ("toSparkline", [EvDistribution(dist), EvNumber(n)]) => Helpers.toStringFn(ToSparkline(Belt.Float.toInt(n)), dist) + | ("toSparkline", [EvDistribution(dist), EvNumber(n)]) => + Helpers.toStringFn(ToSparkline(Belt.Float.toInt(n)), dist) | ("exp", [EvDistribution(a)]) => // https://mathjs.org/docs/reference/functions/exp.html Helpers.twoDiststoDistFn(Algebraic, "pow", GenericDist.fromFloat(Math.e), a)->Some