toFunction

additive multiplicative

compact

whitespace

pow

relational equality

boolean

whitespace separator

left associative operators

expression

not

identifier

function call

array constructor

string

indexed values

ident

priority

block

outerBlock

optional final expression

statement separator

outerBlock

innerBlock

better errors

note xor

white space and record

unary minus

inner/outer block

statement

lambda

sort

lambda is a value constructor

lambdaCall

ternary

ternary

basicValue

cleanup

quotes

chained Functions

dot operators

unify unary operators

unify unary operatos

notes

notes

notes

notes

parser

priorities set

white space or newline defined

allow newlines

notes

function call has become a post operator

recordElement

recursive index

postOperatorToFunction

better integer

comments

notes

record priority

comment

atom

finalComment

generated parser

type cast

format

initiate test file

test initiated; todo nodeCall; nodeExpression

callIdentifier

recover extra

initiate testing

initial tests pass

tests pass

remove function node

ternary

test parse passed

to

tests pass

notes

sort

toExpression

format

notes

remove unused modules

remove unnecessary nodeLambdaCall

notes

note

fix construct array

comment test

todo

elixir pipe

fix toString

notes

initial to expression test

value test

parsing records

records

comments

ternary

ifthenelse

inner block passed

inner block

lambda

lambda

new parser tested

now test tricks

ternary in expression

to test lambda as argument

to test lambda in structures

Use peggy Parser

expectEvalError

macros tested

remove mathjs parse

reducer test

comparison operator
This commit is contained in:
Umur Ozkul 2022-05-05 21:45:25 +02:00
parent b29ba9162f
commit db050668d1
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)
@ -80,6 +77,12 @@ and reduceValueList = (valueList: list<expressionValue>, environment): result<
| 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)})`