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