Merge branch 'peggy' into reducer-dev

This commit is contained in:
Umur Ozkul 2022-05-19 01:26:47 +02:00
commit 915b49bf97
22 changed files with 5103 additions and 549 deletions

View File

@ -16,33 +16,33 @@ testMacro([], exampleExpression, "Ok(1)")
describe("bindStatement", () => {
// A statement is bound by the bindings created by the previous statement
testMacro([], eBindStatement(eBindings([]), exampleStatementY), "Ok((:$setBindings {} :y 1))")
testMacro([], eBindStatement(eBindings([]), exampleStatementY), "Ok((:$setBindings {} :y 1) context: {})")
// Then it answers the bindings for the next statement when reduced
testMacroEval([], eBindStatement(eBindings([]), exampleStatementY), "Ok({y: 1})")
// Now let's feed a binding to see what happens
testMacro(
[],
eBindStatement(eBindings([("x", EvNumber(2.))]), exampleStatementX),
"Ok((:$setBindings {x: 2} :y 2))",
"Ok((:$setBindings {x: 2} :y 2) context: {x: 2})",
)
// An expression does not return a binding, thus error
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Error(Assignment expected)")
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Assignment expected")
// When bindings from previous statement are missing the context is injected. This must be the first statement of a block
testMacro(
[("z", EvNumber(99.))],
eBindStatementDefault(exampleStatementY),
"Ok((:$setBindings {z: 99} :y 1))",
"Ok((:$setBindings {z: 99} :y 1) context: {z: 99})",
)
})
describe("bindExpression", () => {
// x is simply bound in the expression
testMacro([], eBindExpression(eBindings([("x", EvNumber(2.))]), eSymbol("x")), "Ok(2)")
testMacro([], eBindExpression(eBindings([("x", EvNumber(2.))]), eSymbol("x")), "Ok(2 context: {x: 2})")
// When an let statement is the end expression then bindings are returned
testMacro(
[],
eBindExpression(eBindings([("x", EvNumber(2.))]), exampleStatementY),
"Ok((:$exportBindings (:$setBindings {x: 2} :y 1)))",
"Ok((:$exportBindings (:$setBindings {x: 2} :y 1)) context: {x: 2})",
)
// Now let's reduce that expression
testMacroEval(
@ -110,7 +110,7 @@ describe("block", () => {
}),
"Ok((:$$bindExpression (:$$block (:$let :y (:add :x 1)) :y)))",
)
MyOnly.testMacroEval(
testMacroEval(
[("x", EvNumber(1.))],
eBlock(list{
eBlock(list{
@ -125,7 +125,7 @@ describe("block", () => {
describe("lambda", () => {
// assign a lambda to a variable
let lambdaExpression = eFunction("$$lambda", list{eArrayString(["y"]), exampleExpressionY})
testMacro([], lambdaExpression, "Ok(lambda(y=>internal))")
testMacro([], lambdaExpression, "Ok(lambda(y=>internal code))")
// call a lambda
let callLambdaExpression = list{lambdaExpression, eNumber(1.)}->ExpressionT.EList
testMacro([], callLambdaExpression, "Ok(((:$$lambda [y] :y) 1))")

View File

@ -22,6 +22,6 @@ describe("builtin", () => {
describe("builtin exception", () => {
//It's a pity that MathJs does not return error position
test("MathJs Exception", () =>
expectEvalToBe("testZadanga()", "Error(JS Exception: Error: Undefined function testZadanga)")
expectEvalToBe("testZadanga(1)", "Error(JS Exception: Error: Undefined function testZadanga)")
)
})

View File

@ -1,74 +0,0 @@
module Parse = Reducer_MathJs.Parse
module Result = Belt.Result
open Jest
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))
}
module MyOnly = {
let testParse = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testDescriptionParse = (desc, expr, answer) =>
Only.test(desc, () => expectParseToBe(expr, answer))
}
describe("MathJs parse", () => {
describe("literals operators parenthesis", () => {
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", () => {
testParse("x = 1", "x = 1")
testParse("x", "x")
testParse("x = 1; x", "{x = 1; x}")
})
describe("functions", () => {
testParse("identity(x) = x", "identity = (x) => x")
testParse("identity(x)", "identity(x)")
})
describe("arrays", () => {
testDescriptionParse("empty", "[]", "[]")
testDescriptionParse("define", "[0, 1, 2]", "[0, 1, 2]")
testDescriptionParse("define with strings", "['hello', 'world']", "['hello', 'world']")
testParse("range(0, 4)", "range(0, 4)")
testDescriptionParse("index", "([0,1,2])[1]", "([0, 1, 2])[1]")
})
describe("records", () => {
testDescriptionParse("define", "{a: 1, b: 2}", "{a: 1, b: 2}")
testDescriptionParse("use", "record.property", "record['property']")
})
describe("comments", () => {
testDescriptionParse("define", "1 # This is a comment", "1")
})
describe("ternary operator", () => {
testParse("1 ? 2 : 3", "ternary(1, 2, 3)")
testParse("1 ? 2 : 3 ? 4 : 5", "ternary(1, 2, ternary(3, 4, 5))")
})
})

View File

@ -0,0 +1,142 @@
module Parse = Reducer_Peggy_Parse
module Result = Belt.Result
open Jest
open Expect
let expectParseToBe = (expr, answer) =>
Parse.parse(expr)->Parse.toStringResult->expect->toBe(answer)
let testParse = (expr, answer) => test(expr, () => 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))
}
module MyOnly = {
let testParse = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testDescriptionParse = (desc, expr, answer) =>
Only.test(desc, () => expectParseToBe(expr, answer))
}
describe("Peggy parse", () => {
describe("literals operators parenthesis", () => {
// Note that there is always an outer block. Otherwise, external bindings are ignrored at the first statement
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("unary", () => {
testParse("-1", "{(::unaryMinus 1)}")
testParse("!true", "{(::not true)}")
testParse("1 + -1", "{(::add 1 (::unaryMinus 1))}")
testParse("-a[0]", "{(::unaryMinus (::$atIndex :a 0))}")
})
describe("multi-line", () => {
testParse("x=1; 2", "{:x = {1}; 2}")
testParse("x=1; y=2", "{:x = {1}; :y = {2}}")
})
describe("variables", () => {
testParse("x = 1", "{:x = {1}}")
testParse("x", "{:x}")
testParse("x = 1; x", "{:x = {1}; :x}")
})
describe("functions", () => {
testParse("identity(x) = x", "{:identity = {|:x| {:x}}}") // Function definitions become lambda assignments
testParse("identity(x)", "{(::identity :x)}")
})
describe("arrays", () => {
testParse("[]", "{(::$constructArray ())}")
testParse("[0, 1, 2]", "{(::$constructArray (0 1 2))}")
testParse("['hello', 'world']", "{(::$constructArray ('hello' 'world'))}")
testParse("([0,1,2])[1]", "{(::$atIndex (::$constructArray (0 1 2)) 1)}")
})
describe("records", () => {
testParse("{a: 1, b: 2}", "{(::$constructRecord ('a': 1 'b': 2))}")
testParse("{1+0: 1, 2+0: 2}", "{(::$constructRecord ((::add 1 0): 1 (::add 2 0): 2))}") // key can be any expression
testParse("record.property", "{(::$atIndex :record 'property')}")
})
describe("comments", () => {
testParse("1 # This is a line comment", "{1}")
testParse("1 // This is a line comment", "{1}")
testParse("1 /* This is a multi line comment */", "{1}")
testParse("/* This is a multi line comment */ 1", "{1}")
})
describe("ternary operator", () => {
testParse("1 ? 2 : 3", "{(::$$ternary 1 2 3)}")
testParse("1 ? 2 : 3 ? 4 : 5", "{(::$$ternary 1 2 (::$$ternary 3 4 5))}") // nested ternary
})
describe("if then else", () => {
testParse("if 1 then 2 else 3", "{(::$$ternary 1 {2} {3})}")
testParse("if 1 then {2} else {3}", "{(::$$ternary 1 {2} {3})}")
testParse(
"if 1 then {2} else if 3 then {4} else {5}",
"{(::$$ternary 1 {2} (::$$ternary 3 {4} {5}))}",
) //nested if
})
describe("pipe", () => {
testParse("1 -> add(2)", "{(::add 1 2)}")
testParse("-1 -> add(2)", "{(::add (::unaryMinus 1) 2)}")
testParse("1 -> add(2) * 3", "{(::multiply (::add 1 2) 3)}")
})
describe("elixir pipe", () => {
testParse("1 |> add(2)", "{(::add 1 2)}")
})
describe("to", () => {
testParse("1 to 2", "{(::credibleIntervalToDistribution 1 2)}")
testParse("-1 to -2", "{(::credibleIntervalToDistribution (::unaryMinus 1) (::unaryMinus 2))}") // lower than unary
testParse(
"a[1] to a[2]",
"{(::credibleIntervalToDistribution (::$atIndex :a 1) (::$atIndex :a 2))}",
) // lower than post
testParse(
"a.p1 to a.p2",
"{(::credibleIntervalToDistribution (::$atIndex :a 'p1') (::$atIndex :a 'p2'))}",
) // lower than post
testParse("1 to 2 + 3", "{(::add (::credibleIntervalToDistribution 1 2) 3)}") // higher than binary operators
testParse(
"1->add(2) to 3->add(4) -> add(4)",
"{(::credibleIntervalToDistribution (::add 1 2) (::add (::add 3 4) 4))}",
) // lower than chain
})
describe("inner block", () => {
// inner blocks are 0 argument lambdas. They can be used whenever a value is required.
// Like lambdas they have a local scope.
testParse("x={y=1; y}; x", "{:x = {:y = {1}; :y}; :x}")
})
describe("lambda", () => {
testParse("{|x| x}", "{{|:x| {:x}}}")
testParse("f={|x| x}", "{:f = {{|:x| {:x}}}}")
testParse("f(x)=x", "{:f = {|:x| {:x}}}") // Function definitions are lambda assignments
testParse("f(x)=x ? 1 : 0", "{:f = {|:x| {(::$$ternary :x 1 0)}}}") // Function definitions are lambda assignments
})
describe("Using lambda as value", () => {
testParse("myadd(x,y)=x+y; z=myadd; z", "{:myadd = {|:x,:y| {(::add :x :y)}}; :z = {:myadd}; :z}")
testParse("myadd(x,y)=x+y; z=[myadd]; z", "{:myadd = {|:x,:y| {(::add :x :y)}}; :z = {(::$constructArray (:myadd))}; :z}")
testParse("myaddd(x,y)=x+y; z={x: myaddd}; z", "{:myaddd = {|:x,:y| {(::add :x :y)}}; :z = {(::$constructRecord ('x': :myaddd))}; :z}")
})
})

View File

@ -0,0 +1,133 @@
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface_ExpressionValue
module Parse = Reducer_Peggy_Parse
module ToExpression = Reducer_Peggy_ToExpression
module Result = Belt.Result
open Jest
open Expect
let expectToExpressionToBe = (expr, answer, ~v="_", ()) => {
let rExpr = Parse.parse(expr)
->Result.map(ToExpression.fromNode)
let a1 = rExpr->ExpressionT.toStringResultOkless
if (v=="_") {
a1->expect->toBe(answer)
} else {
let a2 = rExpr->Result.flatMap(
expr => Expression.reduceExpression(expr, Belt.Map.String.empty, ExpressionValue.defaultEnvironment)
)->ExpressionValue.toStringResultOkless
(a1, a2)->expect->toEqual((answer, v))
}
}
let testToExpression = (expr, answer, ~v="_", ()) => test(expr, () => expectToExpressionToBe(expr, answer, ~v=v, ()))
module MySkip = {
let testToExpression = (expr, answer, ~v="_", ()) => Skip.test(expr, () => expectToExpressionToBe(expr, answer, ~v=v, ()))
}
module MyOnly = {
let testToExpression = (expr, answer, ~v="_", ()) => Only.test(expr, () => expectToExpressionToBe(expr, answer, ~v=v, ()))
}
describe("Peggy to Expression", () => {
describe("literals operators parenthesis", () => {
// Note that there is always an outer block. Otherwise, external bindings are ignrored at the first statement
testToExpression("1", "(:$$block 1)", ~v="1", ())
testToExpression("'hello'", "(:$$block 'hello')", ~v="'hello'", ())
testToExpression("true", "(:$$block true)", ~v="true", ())
testToExpression("1+2", "(:$$block (:add 1 2))", ~v="3", ())
testToExpression("add(1,2)", "(:$$block (:add 1 2))", ~v="3", ())
testToExpression("(1)", "(:$$block 1)", ())
testToExpression("(1+2)", "(:$$block (:add 1 2))", ())
})
describe("unary", () => {
testToExpression("-1", "(:$$block (:unaryMinus 1))", ~v="-1", ())
testToExpression("!true", "(:$$block (:not true))", ~v="false", ())
testToExpression("1 + -1", "(:$$block (:add 1 (:unaryMinus 1)))", ~v="0", ())
testToExpression("-a[0]", "(:$$block (:unaryMinus (:$atIndex :a 0)))", ())
})
describe("multi-line", () => {
testToExpression("x=1; 2", "(:$$block (:$let :x (:$$block 1)) 2)", ~v="2", ())
testToExpression("x=1; y=2", "(:$$block (:$let :x (:$$block 1)) (:$let :y (:$$block 2)))", ~v="{x: 1,y: 2}", ())
})
describe("variables", () => {
testToExpression("x = 1", "(:$$block (:$let :x (:$$block 1)))", ~v="{x: 1}", ())
testToExpression("x", "(:$$block :x)", ~v=":x", ()) //TODO: value should return error
testToExpression("x = 1; x", "(:$$block (:$let :x (:$$block 1)) :x)", ~v="1", ())
})
describe("functions", () => {
testToExpression("identity(x) = x", "(:$$block (:$let :identity (:$$lambda [x] (:$$block :x))))", ~v="{identity: lambda(x=>internal code)}", ()) // Function definitions become lambda assignments
testToExpression("identity(x)", "(:$$block (:identity :x))", ()) // Note value returns error properly
})
describe("arrays", () => {
testToExpression("[]", "(:$$block (:$constructArray ()))", ~v="[]", ())
testToExpression("[0, 1, 2]", "(:$$block (:$constructArray (0 1 2)))", ~v="[0,1,2]", ())
testToExpression("['hello', 'world']", "(:$$block (:$constructArray ('hello' 'world')))", ~v="['hello','world']", ())
testToExpression("([0,1,2])[1]", "(:$$block (:$atIndex (:$constructArray (0 1 2)) 1))", ~v="1", ())
})
describe("records", () => {
testToExpression("{a: 1, b: 2}", "(:$$block (:$constructRecord (('a' 1) ('b' 2))))", ~v="{a: 1,b: 2}", ())
testToExpression("{1+0: 1, 2+0: 2}", "(:$$block (:$constructRecord (((:add 1 0) 1) ((:add 2 0) 2))))", ()) // key can be any expression
testToExpression("record.property", "(:$$block (:$atIndex :record 'property'))", ())
testToExpression("record={property: 1}; record.property", "(:$$block (:$let :record (:$$block (:$constructRecord (('property' 1))))) (:$atIndex :record 'property'))", ~v="1", ())
})
describe("comments", () => {
testToExpression("1 # This is a line comment", "(:$$block 1)", ~v="1", ())
testToExpression("1 // This is a line comment", "(:$$block 1)", ~v="1", ())
testToExpression("1 /* This is a multi line comment */", "(:$$block 1)", ~v="1", ())
testToExpression("/* This is a multi line comment */ 1", "(:$$block 1)", ~v="1", ())
})
describe("ternary operator", () => {
testToExpression("true ? 1 : 0", "(:$$block (:$$ternary true 1 0))", ~v="1", ())
testToExpression("false ? 1 : 0", "(:$$block (:$$ternary false 1 0))", ~v="0", ())
testToExpression("true ? 1 : false ? 2 : 0", "(:$$block (:$$ternary true 1 (:$$ternary false 2 0)))", ~v="1", ()) // nested ternary
testToExpression("false ? 1 : false ? 2 : 0", "(:$$block (:$$ternary false 1 (:$$ternary false 2 0)))", ~v="0", ()) // nested ternary
})
describe("if then else", () => {
testToExpression("if true then 2 else 3", "(:$$block (:$$ternary true (:$$block 2) (:$$block 3)))", ())
testToExpression("if true then {2} else {3}", "(:$$block (:$$ternary true (:$$block 2) (:$$block 3)))", ())
testToExpression(
"if false then {2} else if false then {4} else {5}",
"(:$$block (:$$ternary false (:$$block 2) (:$$ternary false (:$$block 4) (:$$block 5))))", ()
) //nested if
})
describe("pipe", () => {
testToExpression("1 -> add(2)", "(:$$block (:add 1 2))", ~v="3", ())
testToExpression("-1 -> add(2)", "(:$$block (:add (:unaryMinus 1) 2))", ~v="1", ()) // note that unary has higher priority naturally
testToExpression("1 -> add(2) * 3", "(:$$block (:multiply (:add 1 2) 3))", ~v="9", ())
})
describe("elixir pipe", () => {
testToExpression("1 |> add(2)", "(:$$block (:add 1 2))", ~v="3", ())
})
// see testParse for priorities of to and credibleIntervalToDistribution
describe("inner block", () => {
// inner blocks are 0 argument lambdas. They can be used whenever a value is required.
// Like lambdas they have a local scope.
testToExpression("y=99; x={y=1; y}", "(:$$block (:$let :y (:$$block 99)) (:$let :x (:$$block (:$let :y (:$$block 1)) :y)))", ~v="{x: 1,y: 99}", ())
})
describe("lambda", () => {
testToExpression("{|x| x}", "(:$$block (:$$lambda [x] (:$$block :x)))", ~v="lambda(x=>internal code)", ())
testToExpression("f={|x| x}", "(:$$block (:$let :f (:$$block (:$$lambda [x] (:$$block :x)))))", ~v="{f: lambda(x=>internal code)}", ())
testToExpression("f(x)=x","(:$$block (:$let :f (:$$lambda [x] (:$$block :x))))", ~v="{f: lambda(x=>internal code)}", ()) // Function definitions are lambda assignments
testToExpression("f(x)=x ? 1 : 0", "(:$$block (:$let :f (:$$lambda [x] (:$$block (:$$ternary :x 1 0)))))", ~v="{f: lambda(x=>internal code)}", ())
})
})

View File

@ -19,6 +19,9 @@ let expectParseToBe = (expr: string, answer: string) =>
let expectEvalToBe = (expr: string, answer: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer)
let expectEvalError = (expr: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toMatch("Error\(")
let expectEvalBindingsToBe = (expr: string, bindings: Reducer.externalBindings, answer: string) =>
Reducer.evaluateUsingOptions(expr, ~externalBindings=Some(bindings), ~environment=None)
->ExpressionValue.toStringResult
@ -29,6 +32,7 @@ let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, ans
let testDescriptionParseToBe = (desc, expr, answer) =>
test(desc, () => expectParseToBe(expr, answer))
let testEvalError = expr => test(expr, () => expectEvalError(expr))
let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer))
let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>

View File

@ -2,62 +2,11 @@
open Jest
open Reducer_TestHelpers
// describe("Parse for Bindings", () => {
// testParseOuterToBe("x", "Ok((:$$bindExpression (:$$bindings) :x))")
// testParseOuterToBe("x+1", "Ok((:$$bindExpression (:$$bindings) (:add :x 1)))")
// testParseOuterToBe(
// "y = x+1; y",
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) :y))",
// )
// })
// describe("Parse Partial", () => {
// testParsePartialToBe(
// "x",
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) :x) (:$exportVariablesExpression)))",
// )
// testParsePartialToBe(
// "y=x",
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y :x)) (:$exportVariablesExpression)))",
// )
// testParsePartialToBe(
// "y=x+1",
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) (:$exportVariablesExpression)))",
// )
// testParsePartialToBe(
// "y = x+1; z = y",
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) (:$let :z :y)) (:$exportVariablesExpression)))",
// )
// })
describe("Eval with Bindings", () => {
testEvalBindingsToBe("x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(1)")
testEvalBindingsToBe("x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
testParseToBe("y = x+1; y", "Ok((:$$block (:$$block (:$let :y (:add :x 1)) :y)))")
testParseToBe("y = x+1; y", "Ok((:$$block (:$let :y (:$$block (:add :x 1))) :y))")
testEvalBindingsToBe("y = x+1; y", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
testEvalBindingsToBe("y = x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 2})")
})
/*
Partial code is a partial code fragment that is cut out from a larger code.
Therefore it does not end with an expression.
*/
// describe("Eval Partial", () => {
// testEvalPartialBindingsToBe(
// // A partial cannot end with an expression
// "x",
// list{("x", ExpressionValue.EvNumber(1.))},
// "Error(Assignment expected)",
// )
// testEvalPartialBindingsToBe("y=x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 1})")
// testEvalPartialBindingsToBe(
// "y=x+1",
// list{("x", ExpressionValue.EvNumber(1.))},
// "Ok({x: 1,y: 2})",
// )
// testEvalPartialBindingsToBe(
// "y = x+1; z = y",
// list{("x", ExpressionValue.EvNumber(1.))},
// "Ok({x: 1,y: 2,z: 2})",
// )
// })

View File

@ -51,7 +51,7 @@ describe("call and bindings", () => {
)
testParseToBe(
"f=99; g(x)=f; g(2)",
"Ok((:$$block (:$$block (:$let :f 99) (:$let :g (:$$lambda [x] (:$$block :f))) (:g 2))))",
"Ok((:$$block (:$let :f (:$$block 99)) (:$let :g (:$$lambda [x] (:$$block :f))) (:g 2)))",
)
testEvalToBe("f=99; g(x)=f; g(2)", "Ok(99)")
testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)")
@ -63,15 +63,26 @@ describe("call and bindings", () => {
})
describe("function tricks", () => {
testParseToBe(
"f(x)=f(y)=2; f(2)",
"Ok((:$$block (:$$block (:$let :f (:$$lambda [x] (:$$block (:$let :f (:$$lambda [y] (:$$block 2)))))) (:f 2))))",
)
testEvalToBe("f(x)=f(y)=2; f(2)", "Ok({f: lambda(y=>internal code),x: 2})")
testEvalError("f(x)=f(y)=2; f(2)") //Error because chain assignment is not allowed
testEvalToBe("y=2;g(x)=y+1;g(2)", "Ok(3)")
testEvalToBe("y=2;g(x)=inspect(y)+1", "Ok({g: lambda(x=>internal code),y: 2})")
MySkip.testEvalToBe("f(x) = x(x); f(f)", "????") // TODO: Infinite loop. Any solution? Catching proper exception or timeout?
MySkip.testEvalToBe("f(x, x)=x+x; f(1,2)", "????") // TODO: Duplicate parameters
MySkip.testEvalToBe("myadd(x,y)=x+y; z=[add]; z[0](3,2)", "????") //TODO: to fix with new parser
MySkip.testEvalToBe("myaddd(x,y)=x+y; z={x: add}; z.x(3,2)", "????") //TODO: to fix with new parser
testEvalToBe("myadd(x,y)=x+y; z=myadd; z", "Ok(lambda(x,y=>internal code))")
testEvalToBe("myadd(x,y)=x+y; z=myadd; z(1, 1)", "Ok(2)")
})
describe("lambda in structures", () => {
testEvalToBe("myadd(x,y)=x+y; z=[myadd]", "Ok({myadd: lambda(x,y=>internal code),z: [lambda(x,y=>internal code)]})")
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0]", "Ok(lambda(x,y=>internal code))")
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0](3,2)", "Ok(5)")
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z", "Ok({x: lambda(x,y=>internal code)})")
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z.x", "Ok(lambda(x,y=>internal code))")
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z.x(3,2)", "Ok(5)")
})
describe("ternary and bindings", () => {
testEvalToBe("f(x)=x ? 1 : 0; f(true)", "Ok(1)")
testEvalToBe("f(x)=x>2 ? 1 : 0; f(3)", "Ok(1)")
})

View File

@ -1,55 +1,9 @@
open Jest
open Reducer_TestHelpers
describe("reducer using mathjs parse", () => {
// Test the MathJs parser compatibility
// Those tests toString that there is a semantic mapping from MathJs to Expression
// Reducer.parse is called by Reducer.eval
// See https://mathjs.org/docs/expressions/syntax.html
// See https://mathjs.org/docs/reference/functions.html
// Those tests toString that we are converting mathjs parse tree to what we need
describe("expressions", () => {
testParseToBe("1", "Ok((:$$block 1))")
testParseToBe("(1)", "Ok((:$$block 1))")
testParseToBe("1+2", "Ok((:$$block (:add 1 2)))")
testParseToBe("1+2*3", "Ok((:$$block (: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)
testDescriptionParseToBe("empty", "[]", "Ok((:$$block ()))")
testParseToBe("[1, 2, 3]", "Ok((:$$block (1 2 3)))")
testParseToBe("['hello', 'world']", "Ok((:$$block ('hello' 'world')))")
testDescriptionParseToBe("index", "([0,1,2])[1]", "Ok((:$$block (:$atIndex (0 1 2) (1))))")
})
describe("records", () => {
testDescriptionParseToBe(
"define",
"{a: 1, b: 2}",
"Ok((:$$block (:$constructRecord (('a' 1) ('b' 2)))))",
)
testDescriptionParseToBe(
"use",
"{a: 1, b: 2}.a",
"Ok((:$$block (:$atIndex (:$constructRecord (('a' 1) ('b' 2))) ('a'))))",
)
})
describe("multi-line", () => {
testParseToBe("1; 2", "Ok((:$$block (:$$block 1 2)))")
testParseToBe("1+1; 2+1", "Ok((:$$block (:$$block (:add 1 1) (:add 2 1))))")
})
describe("assignment", () => {
testParseToBe("x=1; x", "Ok((:$$block (:$$block (:$let :x 1) :x)))")
testParseToBe("x=1+1; x+1", "Ok((:$$block (:$$block (:$let :x (:add 1 1)) (:add :x 1))))")
})
})
describe("eval", () => {
// All MathJs operators and functions are builtin for string, float and boolean
// .e.g + - / * > >= < <= == /= not and or
// See https://mathjs.org/docs/expressions/syntax.html
// See https://mathjs.org/docs/reference/functions.html
describe("expressions", () => {
testEvalToBe("1", "Ok(1)")
@ -70,20 +24,21 @@ describe("eval", () => {
})
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)"))
test("index", () => expectEvalToBe("r = {a: 1}; r.a", "Ok(1)"))
test("index", () => expectEvalToBe("r = {a: 1}; r.b", "Error(Record property not found: b)"))
testEvalError("{a: 1}.b") // invalid syntax
})
describe("multi-line", () => {
testEvalToBe("1; 2", "Error(Assignment expected)")
testEvalToBe("1+1; 2+1", "Error(Assignment expected)")
testEvalError("1; 2")
testEvalError("1+1; 2+1")
})
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)")
testEvalError("1; x=1")
testEvalError("1; 1")
testEvalToBe("x=1; x=1", "Ok({x: 1})")
})
})
@ -94,9 +49,9 @@ describe("test exceptions", () => {
"javascriptraise('div by 0')",
"Error(JS Exception: Error: 'div by 0')",
)
testDescriptionEvalToBe(
"rescript exception",
"rescriptraise()",
"Error(TODO: unhandled rescript exception)",
)
// testDescriptionEvalToBe(
// "rescript exception",
// "rescriptraise()",
// "Error(TODO: unhandled rescript exception)",
// )
})

View File

@ -4,6 +4,7 @@
"homepage": "https://squiggle-language.com",
"license": "MIT",
"scripts": {
"peggy": "peggy --cache ",
"build": "yarn build:rescript && yarn build:typescript",
"build:rescript": "rescript build -with-deps",
"build:typescript": "tsc",

View File

@ -13,6 +13,8 @@ open Reducer_ErrorValue
DO NOT try to add external function mapping here!
*/
//TODO: pow to xor
exception TestRescriptException
let callInternal = (call: functionCall, environment, reducer: ExpressionT.reducerFn): result<
@ -116,9 +118,9 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
}
switch call {
| ("$atIndex", [EvArray(aValueArray), EvArray([EvNumber(fIndex)])]) =>
arrayAtIndex(aValueArray, fIndex)
| ("$atIndex", [EvRecord(dict), EvArray([EvString(sIndex)])]) => recordAtIndex(dict, sIndex)
| ("$atIndex", [EvArray(aValueArray), EvNumber(fIndex)]) => arrayAtIndex(aValueArray, fIndex)
| ("$atIndex", [EvRecord(dict), EvString(sIndex)]) => recordAtIndex(dict, sIndex)
| ("$constructArray", [EvArray(aValueArray)]) => EvArray(aValueArray)->Ok
| ("$constructRecord", [EvArray(arrayOfPairs)]) => constructRecord(arrayOfPairs)
| ("$exportBindings", [EvRecord(externalBindings)]) => doExportBindings(externalBindings)
| ("$setBindings", [EvRecord(externalBindings), EvSymbol(symbol), value]) =>

View File

@ -4,6 +4,7 @@
Macros are used to define language building blocks. They are like Lisp macros.
*/
module Bindings = Reducer_Expression_Bindings
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue
module ExpressionWithContext = Reducer_ExpressionWithContext
@ -139,7 +140,8 @@ let dispatchMacroCall = (
bindings: ExpressionT.bindings,
environment,
): result<expressionWithContext, errorValue> => {
let rCondition = reduceExpression(condition, bindings, environment)
let blockCondition = ExpressionBuilder.eBlock(list{condition})
let rCondition = reduceExpression(blockCondition, bindings, environment)
rCondition->Result.flatMap(conditionValue =>
switch conditionValue {
| ExpressionValue.EvBool(false) => ExpressionWithContext.noContext(ifFalse)->Ok

View File

@ -18,13 +18,10 @@ type internalCode = ReducerInterface_ExpressionValue.internalCode
type t = expression
/*
Converts a MathJs code to expression
Converts a Squigle code to expression
*/
let parse_ = (expr: string, parser, converter): result<t, errorValue> =>
expr->parser->Result.flatMap(node => converter(node))
let parse = (mathJsCode: string): result<t, errorValue> =>
mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromNode)
let parse = (peggyCode: string): result<t, errorValue> =>
peggyCode->Reducer_Peggy_Parse.parse->Result.map(node => Reducer_Peggy_ToExpression.fromNode(node))
/*
Recursively evaluate/reduce the expression (Lisp AST)
@ -75,11 +72,17 @@ and reduceExpressionList = (
and reduceValueList = (valueList: list<expressionValue>, environment): result<
expressionValue,
'e,
> =>
> =>
switch valueList {
| list{EvCall(fName), ...args} =>
(fName, args->Belt.List.toArray)->BuiltIn.dispatch(environment, reduceExpression)
| list{EvLambda(_lamdaCall)} =>
valueList
->Lambda.checkIfReduced
->Result.flatMap(reducedValueList =>
reducedValueList->Belt.List.toArray->ExpressionValue.EvArray->Ok
)
| list{EvLambda(lamdaCall), ...args} =>
Lambda.doLambdaCall(lamdaCall, args, environment, reduceExpression)
| _ =>

View File

@ -42,6 +42,12 @@ let toStringResult = codeResult =>
| Error(m) => `Error(${Reducer_ErrorValue.errorToString(m)})`
}
let toStringResultOkless = codeResult =>
switch codeResult {
| Ok(a) => toString(a)
| Error(m) => `Error(${Reducer_ErrorValue.errorToString(m)})`
}
let inspect = (expr: expression): expression => {
Js.log(toString(expr))
expr

View File

@ -1,3 +1 @@
module Eval = Reducer_MathJs_Eval
module Parse = Reducer_MathJs_Parse
module ToExpression = Reducer_MathJs_ToExpression

View File

@ -1,182 +0,0 @@
/*
MathJs Nodes
We make MathJs Nodes strong-typed
*/
module Extra = Reducer_Extra
open Reducer_ErrorValue
type node = {"type": string, "isNode": bool, "comment": string}
type arrayNode = {...node, "items": array<node>}
type block = {"node": node}
type blockNode = {...node, "blocks": array<block>}
type conditionalNode = {...node, "condition": node, "trueExpr": node, "falseExpr": node}
type constantNode = {...node, "value": unit}
type functionAssignmentNode = {...node, "name": string, "params": array<string>, "expr": node}
type indexNode = {...node, "dimensions": array<node>}
type objectNode = {...node, "properties": Js.Dict.t<node>}
type accessorNode = {...node, "object": node, "index": indexNode, "name": string}
type parenthesisNode = {...node, "content": node}
//rangeNode
//relationalNode
type symbolNode = {...node, "name": string}
type functionNode = {...node, "fn": unit, "args": array<node>}
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<indexNode>}
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 castConditionalNode: node => conditionalNode = "%identity"
external castConstantNode: node => constantNode = "%identity"
external castFunctionAssignmentNode: node => functionAssignmentNode = "%identity"
external castFunctionNode: node => functionNode = "%identity"
external castIndexNode: node => indexNode = "%identity"
external castObjectNode: node => objectNode = "%identity"
external castOperatorNode: node => operatorNode = "%identity"
external castOperatorNodeToFunctionNode: operatorNode => functionNode = "%identity"
external castParenthesisNode: node => parenthesisNode = "%identity"
external castSymbolNode: node => symbolNode = "%identity"
/*
MathJs Parser
*/
@module("mathjs") external parse__: string => node = "parse"
let parse = (expr: string): result<node, errorValue> =>
try {
Ok(parse__(expr))
} catch {
| Js.Exn.Error(obj) => REJavaScriptExn(Js.Exn.message(obj), Js.Exn.name(obj))->Error
}
type mathJsNode =
| MjAccessorNode(accessorNode)
| MjArrayNode(arrayNode)
| MjAssignmentNode(assignmentNode)
| MjBlockNode(blockNode)
| MjConditionalNode(conditionalNode)
| MjConstantNode(constantNode)
| MjFunctionAssignmentNode(functionAssignmentNode)
| MjFunctionNode(functionNode)
| MjIndexNode(indexNode)
| MjObjectNode(objectNode)
| MjOperatorNode(operatorNode)
| MjParenthesisNode(parenthesisNode)
| MjSymbolNode(symbolNode)
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
| "ConditionalNode" => node->castConditionalNode->MjConditionalNode->Ok
| "ConstantNode" => node->castConstantNode->MjConstantNode->Ok
| "FunctionAssignmentNode" => node->castFunctionAssignmentNode->MjFunctionAssignmentNode->Ok
| "FunctionNode" => node->castFunctionNode->MjFunctionNode->Ok
| "IndexNode" => node->castIndexNode->MjIndexNode->Ok
| "ObjectNode" => node->castObjectNode->MjObjectNode->Ok
| "OperatorNode" => node->castOperatorNode->MjOperatorNode->Ok
| "ParenthesisNode" => node->castParenthesisNode->MjParenthesisNode->Ok
| "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 =>
if Js.typeof(a) == "string" {
`'${Js.String.make(a)}'`
} else {
Js.String.make(a)
}
let toStringNodeArray = (nodeArray: array<node>): string =>
nodeArray
->Belt.Array.map(a => toStringMathJsNode(a))
->Extra.Array.interperse(", ")
->Js.String.concatMany("")
let toStringFunctionAssignmentNode = (faNode: functionAssignmentNode): string => {
let paramNames = Js.Array2.toString(faNode["params"])
`${faNode["name"]} = (${paramNames}) => ${toStringMathJsNode(faNode["expr"])}`
}
let toStringFunctionNode = (fnode: functionNode): string =>
`${fnode->nameOfFunctionNode}(${fnode["args"]->toStringNodeArray})`
let toStringObjectEntry = ((key: string, value: node)): string =>
`${key}: ${value->toStringMathJsNode}`
let toStringObjectNode = (oNode: objectNode): string =>
`{${oNode["properties"]
->Js.Dict.entries
->Belt.Array.map(entry => entry->toStringObjectEntry)
->Extra.Array.interperse(", ")
->Js.String.concatMany("")}}`
let toStringIndexNode = (iNode: indexNode): string =>
iNode["dimensions"]
->Belt.Array.map(each => toStringResult(each->castNodeType))
->Js.String.concatMany("")
let toStringSymbolNode = (sNode: symbolNode): string => sNode["name"]
let toStringBlocks = (blocks: array<block>): 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}]`
| MjArrayNode(aNode) => `[${aNode["items"]->toStringNodeArray}]`
| MjAssignmentNode(aNode) =>
`${aNode["object"]->toStringSymbolNode} = ${aNode["value"]->toStringMathJsNode}`
| MjBlockNode(bNode) => `{${bNode["blocks"]->toStringBlocks}}`
| MjConditionalNode(cNode) =>
`ternary(${toStringMathJsNode(cNode["condition"])}, ${toStringMathJsNode(
cNode["trueExpr"],
)}, ${toStringMathJsNode(cNode["falseExpr"])})`
| MjConstantNode(cNode) => cNode["value"]->toStringValue
| MjFunctionAssignmentNode(faNode) => faNode->toStringFunctionAssignmentNode
| 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->toStringSymbolNode
}
}
and toStringResult = (rMathJsNode: result<mathJsNode, errorValue>): string =>
switch rMathJsNode {
| Error(e) => errorToString(e)
| Ok(mathJsNode) => toString(mathJsNode)
}
and toStringMathJsNode = node => node->castNodeType->toStringResult

View File

@ -1,154 +0,0 @@
/* * WARNING. DO NOT EDIT, BEAUTIFY, COMMENT ON OR REFACTOR THIS CODE.
We will stop using MathJs parser and
this whole file will go to trash
**/
module ErrorValue = Reducer_ErrorValue
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue
module JavaScript = Reducer_Js
module Parse = Reducer_MathJs_Parse
module Result = Belt.Result
type errorValue = ErrorValue.errorValue
type expression = ExpressionT.expression
type expressionValue = ExpressionValue.expressionValue
let blockToNode = block => block["node"]
let rec fromInnerNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => {
let fromNodeList = (nodeList: list<Parse.node>): result<list<expression>, 'e> =>
Belt.List.reduceReverse(nodeList, Ok(list{}), (racc, currNode) =>
racc->Result.flatMap(acc =>
fromInnerNode(currNode)->Result.map(currCode => list{currCode, ...acc})
)
)
let caseFunctionNode = fNode => {
let rLispArgs = fNode["args"]->Belt.List.fromArray->fromNodeList
rLispArgs->Result.map(lispArgs =>
ExpressionBuilder.eFunction(fNode->Parse.nameOfFunctionNode, lispArgs)
)
}
let caseObjectNode = oNode => {
let fromObjectEntries = entryList => {
let rargs = Belt.List.reduceReverse(entryList, Ok(list{}), (
racc,
(key: string, value: Parse.node),
) =>
racc->Result.flatMap(acc =>
fromInnerNode(value)->Result.map(valueExpression => {
let entryCode =
list{ExpressionBuilder.eString(key), valueExpression}->ExpressionT.EList
list{entryCode, ...acc}
})
)
)
rargs->Result.flatMap(args =>
ExpressionBuilder.eFunction("$constructRecord", list{ExpressionT.EList(args)})->Ok
) // $constructRecord gets a single argument: List of key-value paiers
}
oNode["properties"]->Js.Dict.entries->Belt.List.fromArray->fromObjectEntries
}
let caseIndexNode = iNode => {
let rpropertyCodeList = Belt.List.reduceReverse(
iNode["dimensions"]->Belt.List.fromArray,
Ok(list{}),
(racc, currentPropertyMathJsNode) =>
racc->Result.flatMap(acc =>
fromInnerNode(currentPropertyMathJsNode)->Result.map(propertyCode => list{
propertyCode,
...acc,
})
),
)
rpropertyCodeList->Result.map(propertyCodeList => ExpressionT.EList(propertyCodeList))
}
let caseAccessorNode = (objectNode, indexNode) => {
caseIndexNode(indexNode)->Result.flatMap(indexCode => {
fromInnerNode(objectNode)->Result.flatMap(objectCode =>
ExpressionBuilder.eFunction("$atIndex", list{objectCode, indexCode})->Ok
)
})
}
let caseBlock = (nodesArray: array<Parse.node>): result<expression, errorValue> => {
let rStatements: result<list<expression>, 'a> =
nodesArray
->Belt.List.fromArray
->Belt.List.reduceReverse(Ok(list{}), (racc, currNode) =>
racc->Result.flatMap(acc =>
fromInnerNode(currNode)->Result.map(currCode => list{currCode, ...acc})
)
)
rStatements->Result.map(statements => ExpressionBuilder.eBlock(statements))
}
let caseAssignmentNode = aNode => {
let symbolName = aNode["object"]["name"]
let rValueExpression = fromInnerNode(aNode["value"])
rValueExpression->Result.map(valueExpression =>
ExpressionBuilder.eLetStatement(symbolName, valueExpression)
)
}
let caseFunctionAssignmentNode = faNode => {
let symbol = faNode["name"]->ExpressionBuilder.eSymbol
let rValueExpression = fromInnerNode(faNode["expr"])
rValueExpression->Result.flatMap(valueExpression => {
let lispParams = ExpressionBuilder.eArrayString(faNode["params"])
let valueBlock = ExpressionBuilder.eBlock(list{valueExpression})
let lambda = ExpressionBuilder.eFunction("$$lambda", list{lispParams, valueBlock})
ExpressionBuilder.eFunction("$let", list{symbol, lambda})->Ok
})
}
let caseArrayNode = aNode => {
aNode["items"]->Belt.List.fromArray->fromNodeList->Result.map(list => ExpressionT.EList(list))
}
let caseConditionalNode = cndNode => {
let rCondition = fromInnerNode(cndNode["condition"])
let rTrueExpr = fromInnerNode(cndNode["trueExpr"])
let rFalse = fromInnerNode(cndNode["falseExpr"])
rCondition->Result.flatMap(condition =>
rTrueExpr->Result.flatMap(trueExpr =>
rFalse->Result.flatMap(falseExpr =>
ExpressionBuilder.eFunction("$$ternary", list{condition, trueExpr, falseExpr})->Ok
)
)
)
}
let rFinalExpression: result<expression, errorValue> = switch typedMathJsNode {
| MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"])
| MjArrayNode(aNode) => caseArrayNode(aNode)
| MjAssignmentNode(aNode) => caseAssignmentNode(aNode)
| MjSymbolNode(sNode) => {
let expr: expression = ExpressionBuilder.eSymbol(sNode["name"])
let rExpr: result<expression, errorValue> = expr->Ok
rExpr
}
| MjBlockNode(bNode) => bNode["blocks"]->Js.Array2.map(blockToNode)->caseBlock
| MjConditionalNode(cndNode) => caseConditionalNode(cndNode)
| MjConstantNode(cNode) =>
cNode["value"]->JavaScript.Gate.jsToEv->Result.flatMap(v => v->ExpressionT.EValue->Ok)
| MjFunctionAssignmentNode(faNode) => caseFunctionAssignmentNode(faNode)
| MjFunctionNode(fNode) => fNode->caseFunctionNode
| MjIndexNode(iNode) => caseIndexNode(iNode)
| MjObjectNode(oNode) => caseObjectNode(oNode)
| MjOperatorNode(opNode) => opNode->Parse.castOperatorNodeToFunctionNode->caseFunctionNode
| MjParenthesisNode(pNode) => pNode["content"]->fromInnerNode
}
rFinalExpression
})
let fromNode = (node: Parse.node): result<expression, errorValue> =>
fromInnerNode(node)->Result.map(expr => ExpressionBuilder.eBlock(list{expr}))

View File

@ -0,0 +1,321 @@
// Try in https://peggyjs.org/online
{{
var toFunction = {
'-': 'subtract',
'->': 'pipe',
'!=': 'unequal',
'.-': 'dotSubtract',
'.*': 'dotMultiply',
'./': 'dotDivide',
'.^': 'dotPow',
'.+': 'dotAdd',
'*': 'multiply',
'/': 'divide',
'&&': 'and',
'^': 'pow', // or xor
'+': 'add',
'<': 'smaller',
'<=': 'smallerEq',
'==': 'equal',
'>': 'larger',
'>=': 'largerEq',
'||': 'or',
'to': 'credibleIntervalToDistribution',
}
var unaryToFunction = {
'-': 'unaryMinus',
'!': 'not',
'.-': 'unaryDotMinus',
}
var postOperatorToFunction = {
'.': '$atIndex',
'()': '$$applyAll',
'[]': '$atIndex',
}
function nodeBlock(statements) {return{type: 'Block', statements: statements}}
function nodeBoolean(value) {return {type: 'Boolean', value: value}}
function nodeCallIndentifier(value) {return {type: 'CallIdentifier', value: value}}
function nodeExpression(args) {return {type: 'Expression', nodes: args}}
function nodeFloat(value) {return {type: 'Float', value: value}}
function makeFunctionCall(fn, args) {
if (fn === '$$applyAll') {
// Any list of values is applied from left to right anyway.
// Like in Haskell and Lisp.
// So we remove the redundant $$applyAll.
if (args[0].type === "Identifier") {args[0].type = "CallIdentifier"}
return nodeExpression(args)
} else {
return nodeExpression([nodeCallIndentifier(fn), ...args])
}
}
function nodeIdentifier(value) {return {type: 'Identifier', value: value}}
function nodeInteger(value) {return {type: 'Integer', value: value}}
function nodeKeyValue(key, value) {
if (key.type === 'Identifier') {key.type = 'String'}
return {type: 'KeyValue', key: key, value: value}}
function nodeLambda(args, body) {return {type: 'Lambda', args: args, body: body}}
function nodeLetStatment(variable, value) {return {type: 'LetStatement', variable: variable, value: value}}
function nodeString(value) {return {type: 'String', value: value}}
function nodeTernary(condition, trueExpression, falseExpression) {return {type: 'Ternary', condition: condition, trueExpression: trueExpression, falseExpression: falseExpression}}
}}
start
= _nl start:outerBlock _nl finalComment? {return start}
zeroOMoreArgumentsBlock = innerBlock / lambda
outerBlock
= statements:array_statements finalExpression: (statementSeparator @expression)?
{ if (finalExpression != null) { statements.push(finalExpression) }
return nodeBlock(statements) }
/ finalExpression: expression
{ return nodeBlock([finalExpression])}
innerBlock
= '{' _ statements:array_statements finalExpression: (statementSeparator @expression) _ '}'
{ statements.push(finalExpression)
return nodeBlock(statements) }
/ '{' _ finalExpression: expression _ '}'
{ return nodeBlock([finalExpression]) }
/ finalExpression: expression
{ return nodeBlock([finalExpression])}
array_statements
= head:statement tail:(statementSeparator @array_statements )
{ return [head, ...tail] }
/ head:statement
{ return [head] }
statement
= letStatement
/ defunStatement
letStatement
= variable:identifier _ '=' _nl value:zeroOMoreArgumentsBlock
{ return nodeLetStatment(variable, value) }
defunStatement
= variable:identifier '(' _nl args:array_parameters _nl ')' _ '=' _nl body:innerBlock
{ var value = nodeLambda(args, body)
return nodeLetStatment(variable, value) }
array_parameters
= head:identifier tail:(_ ',' _nl @identifier)*
{ return [head, ...tail]; }
expression = ifthenelse / ternary / logicalAdditive
ifthenelse
= 'if' __nl condition:logicalAdditive
__nl 'then' __nl trueExpression:innerBlock
__nl 'else' __nl falseExpression:(ifthenelse/innerBlock)
{ return nodeTernary(condition, trueExpression, falseExpression) }
ternary
= condition:logicalAdditive _ '?' _nl trueExpression:logicalAdditive _ ':' _nl falseExpression:(ternary/logicalAdditive)
{ return nodeTernary(condition, trueExpression, falseExpression) }
logicalAdditive
= head:logicalMultiplicative tail:(_ operator:'&&' _nl arg:logicalMultiplicative {return {operator: operator, right: arg}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(toFunction[element.operator], [result, element.right])
}, head)}
// start binary operators
logicalMultiplicative
= head:equality tail:(_ operator:'||' _nl arg:equality {return {operator: operator, right: arg}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(toFunction[element.operator], [result, element.right])
}, head)}
equality
= left:relational _ operator:('=='/'!=') _nl right:relational
{ return makeFunctionCall(toFunction[operator], [left, right])}
/ relational
relational
= left:additive _ operator:('<='/'<'/'>='/'>') _nl right:additive
{ return makeFunctionCall(toFunction[operator], [left, right])}
/ additive
additive
= head:multiplicative tail:(_ operator:('+' / '-' / '.+' / '.-') _nl arg:multiplicative {return {operator: operator, right: arg}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(toFunction[element.operator], [result, element.right])
}, head)}
multiplicative
= head:power tail:(_ operator:('*' / '/' / '.*' / './') _nl arg:power {return {operator: operator, right: arg}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(toFunction[element.operator], [result, element.right])
}, head)}
power
= head:credibleInterval tail:(_ operator:('^' / '.^') _nl arg:credibleInterval {return {operator: operator, right: arg}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(toFunction[element.operator], [result, element.right])
}, head)}
credibleInterval
= head:chainFunctionCall tail:(__ operator:('to') __nl arg:chainFunctionCall {return {operator: operator, right: arg}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(toFunction[element.operator], [result, element.right])
}, head)}
chainFunctionCall
= head:unary tail:(_ ('->'/'|>') _nl chained:chainedFunction {return chained})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(element.fnName, [result, ...element.args])
}, head)}
chainedFunction
= fn:identifier '(' _nl args:array_functionArguments _nl ')'
{ return {fnName: fn.value, args: args}}
/ fn:identifier '(' _nl ')'
{ return {fnName: fn.value, args: []}}
/ fn:identifier
{ return {fnName: fn.value, args: []}}
// end of binary operators
unary
= unaryOperator:unaryOperator _nl right:(unary/postOperator)
{ return makeFunctionCall(unaryToFunction[unaryOperator], [right])}
/ postOperator
unaryOperator
= ('-' / '.-' / '!' )
postOperator = indexedValue
indexedValue
= collectionElement
/ maybeRecordElement
collectionElement
= head:maybeRecordElement &('['/'(')
tail:(
_ '[' _nl arg:expression _nl ']' {return {fn: postOperatorToFunction['[]'], args: [arg]}}
/ _ '(' _nl args:array_functionArguments _nl ')' {return {fn: postOperatorToFunction['()'], args: args}}
)*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(element.fn, [result, ...element.args])
}, head)}
array_functionArguments
= head:expression tail:(_ ',' _nl @expression)*
{ return [head, ...tail]; }
maybeRecordElement
= recordElement
/ atom
recordElement
= head:identifier &'.'
tail:(_ '.' _nl arg:$identifier {return {fn: postOperatorToFunction['.'], args: [nodeString(arg)]}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(element.fn, [result, ...element.args])
}, head)}
atom
= '(' _nl expression:expression _nl ')' {return expression}
/ basicValue
basicValue = valueConstructor / basicLiteral
basicLiteral
= string
/ float
/ integer
/ boolean
/ identifier
identifier 'identifier'
= identifier:([_a-z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
string 'string'
= characters:("'" @([^'])* "'") {return nodeString(characters.join(''))}
/ characters:('"' @([^"])* '"') {return nodeString(characters.join(''))}
integer 'integer'
= digits:[0-9]+![.]
{ return nodeInteger(parseInt(text()))}
float 'float'
= ([0-9]+[.][0-9]*)
{ return nodeFloat(parseFloat(text()))}
boolean 'boolean'
= ('true'/'false')
{ return nodeBoolean(text() === 'true')}
valueConstructor
= recordConstructor
/ arrayConstructor
/ lambda
lambda
= '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
{ statements.push(finalExpression)
return nodeLambda(args, nodeBlock(statements)) }
/ '{' _nl '|' _nl args:array_parameters _nl '|' _nl finalExpression: expression _nl '}'
{ return nodeLambda(args, nodeBlock([finalExpression])) }
arrayConstructor 'array'
= '[' _nl ']'
{ return makeFunctionCall('$constructArray', [nodeExpression([])])}
/ '[' _nl args:array_elements _nl ']'
{ return makeFunctionCall('$constructArray', [nodeExpression(args)])}
array_elements
= head:expression tail:(_ ',' _nl @expression)*
{ return [head, ...tail]; }
recordConstructor 'record'
= '{' _nl args:array_recordArguments _nl '}'
{ return makeFunctionCall('$constructRecord', [nodeExpression(args)])}
array_recordArguments
= head:keyValuePair tail:(_ ',' _nl @keyValuePair)*
{ return [head, ...tail]; }
keyValuePair
= key:expression _ ':' _nl value:expression
{ return nodeKeyValue(key, value)}
_ 'optional whitespace'
= whiteSpaceCharactersOrComment*
_nl 'optional whitespace or newline'
= (whiteSpaceCharactersOrComment / commentOrNewLine)*
__ 'whitespace'
= whiteSpaceCharactersOrComment+
__nl 'whitespace or newline'
= (whiteSpaceCharactersOrComment / commentOrNewLine )+
statementSeparator 'statement separator'
= _ (';'/ commentOrNewLine)+ _
commentOrNewLine = finalComment? newLine
finalComment "line comment"
= _ ('//'/'#') @([^\r\n]*)
whiteSpaceCharactersOrComment = whiteSpaceCharacters / delimitedComment
delimitedComment "comment"
= '/*' @([^*]*) '*/'
whiteSpaceCharacters = [ \t]
newLine "newline"
= [\n\r]

View File

@ -0,0 +1,109 @@
module Extra = Reducer_Extra
open Reducer_ErrorValue
type node = {"type": string}
@module("./Reducer_Peggy_GeneratedParser.js") external parse__: string => node = "parse"
let parse = (expr: string): result<node, errorValue> =>
try {
Ok(parse__(expr))
} catch {
| Js.Exn.Error(obj) => REJavaScriptExn(Js.Exn.message(obj), Js.Exn.name(obj))->Error
}
type nodeBlock = {...node, "statements": array<node>}
type nodeBoolean = {...node, "value": bool}
type nodeCallIdentifier = {...node, "value": string}
type nodeExpression = {...node, "nodes": array<node>}
type nodeFloat = {...node, "value": float}
type nodeIdentifier = {...node, "value": string}
type nodeInteger = {...node, "value": int}
type nodeKeyValue = {...node, "key": node, "value": node}
type nodeLambda = {...node, "args": array<nodeIdentifier>, "body": nodeBlock}
type nodeLetStatement = {...node, "variable": nodeIdentifier, "value": node}
type nodeString = {...node, "value": string}
type nodeTernary = {...node, "condition": node, "trueExpression": node, "falseExpression": node}
type peggyNode =
| PgNodeBlock(nodeBlock)
| PgNodeBoolean(nodeBoolean)
| PgNodeCallIdentifier(nodeCallIdentifier)
| PgNodeExpression(nodeExpression)
| PgNodeFloat(nodeFloat)
| PgNodeIdentifier(nodeIdentifier)
| PgNodeInteger(nodeInteger)
| PgNodeKeyValue(nodeKeyValue)
| PgNodeLambda(nodeLambda)
| PgNodeLetStatement(nodeLetStatement)
| PgNodeString(nodeString)
| PgNodeTernary(nodeTernary)
external castNodeBlock: node => nodeBlock = "%identity"
external castNodeBoolean: node => nodeBoolean = "%identity"
external castNodeCallIdentifier: node => nodeCallIdentifier = "%identity"
external castNodeExpression: node => nodeExpression = "%identity"
external castNodeFloat: node => nodeFloat = "%identity"
external castNodeIdentifier: node => nodeIdentifier = "%identity"
external castNodeInteger: node => nodeInteger = "%identity"
external castNodeKeyValue: node => nodeKeyValue = "%identity"
external castNodeLambda: node => nodeLambda = "%identity"
external castNodeLetStatement: node => nodeLetStatement = "%identity"
external castNodeString: node => nodeString = "%identity"
external castNodeTernary: node => nodeTernary = "%identity"
exception UnsupportedPeggyNodeType(string) // This should never happen; programming error
let castNodeType = (node: node) =>
switch node["type"] {
| "Block" => node->castNodeBlock->PgNodeBlock
| "Boolean" => node->castNodeBoolean->PgNodeBoolean
| "CallIdentifier" => node->castNodeCallIdentifier->PgNodeCallIdentifier
| "Expression" => node->castNodeExpression->PgNodeExpression
| "Float" => node->castNodeFloat->PgNodeFloat
| "Identifier" => node->castNodeIdentifier->PgNodeIdentifier
| "Integer" => node->castNodeInteger->PgNodeInteger
| "KeyValue" => node->castNodeKeyValue->PgNodeKeyValue
| "Lambda" => node->castNodeLambda->PgNodeLambda
| "LetStatement" => node->castNodeLetStatement->PgNodeLetStatement
| "String" => node->castNodeString->PgNodeString
| "Ternary" => node->castNodeTernary->PgNodeTernary
| _ => raise(UnsupportedPeggyNodeType(node["type"]))
}
let rec pgToString = (peggyNode: peggyNode): string => {
let argsToString = (args: array<nodeIdentifier>): string =>
args->Js.Array2.map(arg => PgNodeIdentifier(arg)->pgToString)->Js.Array2.toString
let nodesToStringUsingSeparator = (nodes: array<node>, separator: string): string =>
nodes->Js.Array2.map(toString)->Extra.Array.interperse(separator)->Js.String.concatMany("")
switch peggyNode {
| PgNodeBlock(node) => "{" ++ node["statements"]->nodesToStringUsingSeparator("; ") ++ "}"
| PgNodeBoolean(node) => node["value"]->Js.String.make
| PgNodeCallIdentifier(node) => `::${Js.String.make(node["value"])}` // This is an identifier also but for function names
| PgNodeExpression(node) => "(" ++ node["nodes"]->nodesToStringUsingSeparator(" ") ++ ")"
| PgNodeFloat(node) => node["value"]->Js.String.make
| PgNodeIdentifier(node) => `:${node["value"]}`
| PgNodeInteger(node) => node["value"]->Js.String.make
| PgNodeKeyValue(node) => toString(node["key"]) ++ ": " ++ toString(node["value"])
| PgNodeLambda(node) =>
"{|" ++ node["args"]->argsToString ++ "| " ++ pgToString(PgNodeBlock(node["body"])) ++ "}"
| PgNodeLetStatement(node) =>
pgToString(PgNodeIdentifier(node["variable"])) ++ " = " ++ toString(node["value"])
| PgNodeString(node) => `'${node["value"]->Js.String.make}'`
| PgNodeTernary(node) =>
"(::$$ternary " ++
toString(node["condition"]) ++
" " ++
toString(node["trueExpression"]) ++
" " ++
toString(node["falseExpression"]) ++ ")"
}
}
and toString = (node: node): string => node->castNodeType->pgToString
let toStringResult = (rNode: result<node, errorValue>): string =>
switch rNode {
| Ok(node) => toString(node)
| Error(error) => `Error(${errorToString(error)})`
}

View File

@ -0,0 +1,49 @@
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
module ExpressionT = Reducer_Expression_T
module Parse = Reducer_Peggy_Parse
type expression = ExpressionT.expression
let rec fromNode = (node: Parse.node): expression => {
let caseBlock = nodeBlock =>
ExpressionBuilder.eBlock(nodeBlock["statements"]->Js.Array2.map(fromNode)->Belt.List.fromArray)
let caseLambda = (nodeLambda: Parse.nodeLambda): expression => {
let args =
nodeLambda["args"]
->Js.Array2.map((argNode: Parse.nodeIdentifier) => argNode["value"])
->ExpressionBuilder.eArrayString
let body = nodeLambda["body"]->caseBlock
ExpressionBuilder.eFunction("$$lambda", list{args, body})
}
switch Parse.castNodeType(node) {
| PgNodeBlock(nodeBlock) => caseBlock(nodeBlock)
| PgNodeBoolean(nodeBoolean) => ExpressionBuilder.eBool(nodeBoolean["value"])
| PgNodeCallIdentifier(nodeCallIdentifier) => ExpressionBuilder.eCall(nodeCallIdentifier["value"])
| PgNodeExpression(nodeExpression) =>
ExpressionT.EList(nodeExpression["nodes"]->Js.Array2.map(fromNode)->Belt.List.fromArray)
| PgNodeFloat(nodeFloat) => ExpressionBuilder.eNumber(nodeFloat["value"])
| PgNodeIdentifier(nodeIdentifier) => ExpressionBuilder.eSymbol(nodeIdentifier["value"])
| PgNodeInteger(nodeInteger) => ExpressionBuilder.eNumber(Belt.Int.toFloat(nodeInteger["value"]))
| PgNodeKeyValue(nodeKeyValue) =>
ExpressionT.EList(list{fromNode(nodeKeyValue["key"]), fromNode(nodeKeyValue["value"])})
| PgNodeLambda(nodeLambda) => caseLambda(nodeLambda)
| PgNodeLetStatement(nodeLetStatement) =>
ExpressionBuilder.eLetStatement(
nodeLetStatement["variable"]["value"],
fromNode(nodeLetStatement["value"]),
)
| PgNodeString(nodeString) => ExpressionBuilder.eString(nodeString["value"])
| PgNodeTernary(nodeTernary) =>
ExpressionBuilder.eFunction(
"$$ternary",
list{
fromNode(nodeTernary["condition"]),
fromNode(nodeTernary["trueExpression"]),
fromNode(nodeTernary["falseExpression"]),
},
)
}
}

View File

@ -87,6 +87,12 @@ let toStringResult = x =>
| Error(m) => `Error(${ErrorValue.errorToString(m)})`
}
let toStringResultOkless = codeResult =>
switch codeResult {
| Ok(a) => toString(a)
| Error(m) => `Error(${ErrorValue.errorToString(m)})`
}
let toStringResultRecord = x =>
switch x {
| Ok(a) => `Ok(${toStringRecord(a)})`