diff --git a/examples/basic.squiggle b/examples/basic.squiggle new file mode 100644 index 00000000..f441c67e --- /dev/null +++ b/examples/basic.squiggle @@ -0,0 +1,3 @@ +xY1 = 99 +aBa3 = xY1 * 2 + 1 +aBa3 * xY1 + aBa3 diff --git a/examples/decay.squiggle b/examples/decay.squiggle new file mode 100644 index 00000000..60f4c95e --- /dev/null +++ b/examples/decay.squiggle @@ -0,0 +1,20 @@ +# The following code was provided by Nuño Sempere, it comes directly from the post https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3/p/j8o6sgRerE3tqNWdj +## Initial setup +yearly_probability_max = 0.95 +yearly_probability_min = 0.66 +period_probability_function(epsilon, yearly_probability) = 1 - (1 - yearly_probability) ^ (1 / epsilon) +probability_decayed(t, time_periods, period_probability) = 1 - (1 - period_probability) ^ (time_periods - t) + +## Monthly decomposition +months_in_a_year=12 + +monthly_probability_min = period_probability_function(months_in_a_year, yearly_probability_min) +monthly_probability_max = period_probability_function(months_in_a_year, yearly_probability_max) + +probability_decayed_monthly_min(t) = probability_decayed(t, months_in_a_year, monthly_probability_min) +probability_decayed_monthly_max(t) = probability_decayed(t, months_in_a_year, monthly_probability_max) +probability_decayed_monthly(t) = probability_decayed_monthly_min(t) to probability_decayed_monthly_max(t) + +probability_decayed_monthly +## probability_decayed_monthly(6) +## mean(probability_decayed_monthly(6)) diff --git a/examples/givedirectly.squiggle b/examples/givedirectly.squiggle new file mode 100644 index 00000000..16dda3a7 --- /dev/null +++ b/examples/givedirectly.squiggle @@ -0,0 +1,38 @@ +# This is a cost effectiveness analysis of givedirectly, originally done by givewell, and translated into Squiggle by Sam Nolan +donation_size = 10000 +proportion_of_funding_available = beta(10, 2) +total_funding_available = donation_size * proportion_of_funding_available +household_size = 3.7 to 5.7 +size_of_transfer = 800 to 1200 +size_of_transfer_per_person = size_of_transfer / household_size + +portion_invested = 0.3 to 0.5 +amount_invested = portion_invested * size_of_transfer_per_person +amount_consumed = (1 - portion_invested) * size_of_transfer_per_person +return_on_investment = 0.08 to 0.12 +increase_in_consumption_from_investments = return_on_investment * amount_invested +baseline_consumption = 200 to 350 +log_increase_in_consumption = log(amount_consumed + baseline_consumption) + log(baseline_consumption) +log_increase_in_consumption_from_investment = log(increase_in_consumption_from_investments + baseline_consumption) + log(baseline_consumption) +investment_duration = 8 to 12 +discount_rate = beta(1.004, 20) + +present_value_excluding_last_year = log_increase_in_consumption_from_investment * (1 - (1 + discount_rate) ^ (-investment_duration)) / (log(1 + discount_rate)) + +percent_of_investment_returned = 0.15 to 0.25 + +pv_consumption_last_year = (log(baseline_consumption + amount_invested * (return_on_investment + percent_of_investment_returned)) - log(baseline_consumption)) / (1 + discount_rate)^investment_duration + +total_pv_of_cash_transfer = pv_consumption_last_year + present_value_excluding_last_year + log_increase_in_consumption + +discount_negative_spoiler = 0.03 to 0.07 + +value_discounting_spoiler = discount_negative_spoiler * total_pv_of_cash_transfer + +consumption_increase_per_household = value_discounting_spoiler * household_size + +amount_of_transfers_made = total_funding_available / size_of_transfer + +total_increase_in_ln_consumption = amount_of_transfers_made * consumption_increase_per_household + +total_increase_in_ln_consumption diff --git a/packages/squiggle-lang/__tests__/Parser/README.md b/packages/squiggle-lang/__tests__/Parser/README.md new file mode 100644 index 00000000..a00f3324 --- /dev/null +++ b/packages/squiggle-lang/__tests__/Parser/README.md @@ -0,0 +1 @@ +I wrote some tests in rescript-mocha I just have to translate them to rescript-jest diff --git a/packages/squiggle-lang/__tests__/Parser/Squiggle_test.res b/packages/squiggle-lang/__tests__/Parser/Squiggle_test.res new file mode 100644 index 00000000..2c095613 --- /dev/null +++ b/packages/squiggle-lang/__tests__/Parser/Squiggle_test.res @@ -0,0 +1,12 @@ +open Jest +open Expect +let {eval} = module(Parser_Squiggle) + +describe("expressions of normal distributions:", () => { + test("sum of two", () => { + expect(eval(" normal (5 , 2 ) + normal(0,2)")) -> toEqual({mean: 5.0 +. 0.0, stdev: Js.Math.sqrt(2.0 ** 2.0 +. 2.0 ** 2.0)} -> #Normal -> Symbolic -> EvDistribution -> Ok -> Some) + }) + test("difference of two", () => { + expect(eval("normal(5,3)-normal(2,1)")) -> toEqual({mean: 5.0 -. 2.0, stdev: Js.Math.sqrt(3.0 ** 2.0 +. 1.0 ** 2.0)} -> #Normal -> Symbolic -> EvDistribution -> Ok -> Some) + }) +}) diff --git a/packages/squiggle-lang/__tests__/Parser/Test_Arithmetic.spec.res.mocha b/packages/squiggle-lang/__tests__/Parser/Test_Arithmetic.spec.res.mocha new file mode 100644 index 00000000..3cb786e8 --- /dev/null +++ b/packages/squiggle-lang/__tests__/Parser/Test_Arithmetic.spec.res.mocha @@ -0,0 +1,51 @@ +open RescriptMocha +open Mocha +open Parsec + +describe("expressions of plus and times", () => { + + let rec expr: lazy_t> = lazy(bind( + Lazy.force(term), + t => choice( + bind(Primitive.symbol("+"), _ => bind( + Lazy.force(expr), + e => returnP(t + e) + )), + returnP(t) + ) + )) + and term: lazy_t> = lazy(bind( + Lazy.force(factor), + f => choice( + bind(Primitive.symbol("*"), _ => bind( + Lazy.force(term), t => returnP(f * t) + )), + returnP(f) + ) + )) + and factor: lazy_t> = lazy(choice(bind(Primitive.symbol("("), _ => bind( + Lazy.force(expr), e => bind(Primitive.symbol(")"), _ => returnP(e)) + )), Primitive.natural)) + + let eval: string => int = xs => switch parse(Lazy.force(expr), xs) { + | list{(n, "")} => n + | list{(_, out)} => raise(Wrongo(`unconsumed input ${out}`)) + | list{} => raise(Wrongo("invalid input")) + | _ => raise(Wrongo(wrongoMessage)) + } + it("equals 3 at 1 + 2", () => { + Assert.deep_equal(eval("1 + 2"), 3) + }) + it("equals 10 at 2 * 3 + 4", () => { + Assert.deep_equal(eval("2 \n * 3 \t +4"), 10) + }) + it("equals 14 at 2 *(3 + 4)", () => { + Assert.deep_equal(eval("2 *(3 + 4)"), 14) + }) +// it("raises Wrongo(unconsumed input -4) at 2*3-4", () => { +// Assert.throws(() => eval("2*3-4"), Js.Exn.asJsExn(Wrongo("unconsumed input -4"))) +// }) +// it("raises Wrongo(invalid input) at -1", () => { +// Assert.throws(() => eval("-1"), Js.Exn.asJsExn(Wrongo("invalid input"))) +// }) +}) diff --git a/packages/squiggle-lang/__tests__/Parser/Test_Parsec.spec.res.mocha b/packages/squiggle-lang/__tests__/Parser/Test_Parsec.spec.res.mocha new file mode 100644 index 00000000..d4bde984 --- /dev/null +++ b/packages/squiggle-lang/__tests__/Parser/Test_Parsec.spec.res.mocha @@ -0,0 +1,128 @@ +open RescriptMocha +open Mocha +open Parsec + +describe("rudimentary combinators (ParserBase)", () => { + it("should return (1, abc)", () => { + // could be property test + Assert.deep_equal(parse(returnP(1), "abc"), list{(1, "abc")}) + }) + it("should return (a, bc)", () => { + // should be property test + Assert.deep_equal(parse(item, "abc"), list{('a', "bc")}) + }) + it("should return empty list", () => { + // should be property test + Assert.deep_equal(parse(failure, "abc"), list{}) + }) + it("should return empty list", () => { + // covered in a conditional of the property test + Assert.deep_equal(parse(item, ""), list{}) + }) +}) +describe("Primitive", () => { + it("digit: should return (1, 23)", () => { + Assert.deep_equal(parse(Primitive.digit, "123"), list{('1', "23")}) + }) + it("digit: should return empty list", () => { + Assert.deep_equal(parse(Primitive.digit, "abc"), list{}) + }) + it("checkchar: should return empty list", () => { + Assert.deep_equal(parse(Primitive.checkchar('a'), "123"), list{}) + }) + it("checkstring: should split abcdef into (abc, def)", () => { + Assert.deep_equal(parse(Primitive.checkstring("abc"), "abcdef"), list{("abc", "def")}) + }) + it("checkstring: should return empty list", () => { + Assert.deep_equal(parse(Primitive.checkstring("abc"), "ab1234"), list{}) + }) + let manyRun: (t, string) => list<(string, string)> = (p, str) => { + parse(Primitive.many(fmap(x => String.make(1, x), p)), str) + } + let many1Run: (t, string) => list<(string, string)> = (p, str) => { + parse(Primitive.many1(fmap(x => String.make(1, x), p)), str) + } + it("many: should return (123,abc)", () => { + Assert.deep_equal( + manyRun(Primitive.digit, "123abc"), + list{("123", "abc")}, + ) + }) + it("many (without run helper): should return (123, abc)", () => { + Assert.deep_equal( + parse(Primitive.many(Primitive.digitString), "123abc"), + list{("123", "abc")} + ) + }) + it("many: should return (, abcdef)", () => { + Assert.deep_equal( + manyRun(Primitive.digit, "abcdef"), + list{("", "abcdef")}, + ) + }) + it("many1: should return empty list", () => { + Assert.deep_equal( + many1Run(Primitive.digit, "abcdef"), + list{} + ) + }) + it("many1: should return abcdef", () => { + Assert.deep_equal( + parse(Primitive.many1(Primitive.lowerString), "abcdef"), + list{("abcdef", "")} + ) + }) + it("many1: should return (abc, Def)", () => { + Assert.deep_equal( + parse(Primitive.many1(Primitive.lowerString), "abcDef"), + list{("abc", "Def")} + ) + }) + it("many: should return (abc, Def)", () => { + Assert.deep_equal( + parse(Primitive.many(Primitive.lowerString), "abcDef"), + list{("abc", "Def")} + ) + }) + + it("nat: should return (123, _abc)", () => { + Assert.deep_equal( + parse(Primitive.nat, "123 abc"), + list{(123, " abc")} + ) + }) +}) + +describe("whitespace-insensitive lists", () => { + let ident: t = bind( + Primitive.lower, + x => bind( + Primitive.many(Primitive.alphanumString), + xs => returnP(Js.String2.concat(String.make(1, x), xs)))) + + let identifier: t = Primitive.token(ident) + + describe("of strings (identifier)", () => { + let listStringParser: t> = { + bind( + Primitive.symbol("list{"), + _ => { + bind( + identifier, + n => bind( + Primitive.many(bind(Primitive.symbol(","), _ => identifier)), + ns => bind(Primitive.symbol("}"), _ => returnP(list{n, ns})) + ) + ) + } + ) + } + + it("returns list{ab, cd, ef} (with newline) THIS TEST IS WRONG", () => { + Assert.deep_equal( + parse(listStringParser, " list{ ab \n, cd, ef } "), + list{(list{"ab", "cdef"}, "")} + ) + }) + }) +}) diff --git a/packages/squiggle-lang/__tests__/Parser/Test_Utility.spec.res.mocha b/packages/squiggle-lang/__tests__/Parser/Test_Utility.spec.res.mocha new file mode 100644 index 00000000..12c6886d --- /dev/null +++ b/packages/squiggle-lang/__tests__/Parser/Test_Utility.spec.res.mocha @@ -0,0 +1,33 @@ +open RescriptMocha +open Mocha + +describe("CharPredicates", () => { + describe("isDigit", () => { + it("should identify '3'", () => { + Assert.deep_equal(Utility.CharPredicates.isDigit('3'), true) + }) + it("should identify '7'", () => { + Assert.deep_equal(Utility.CharPredicates.isDigit('7'), true) + }) + it("should not identify 'a'", () => { + Assert.deep_equal(Utility.CharPredicates.isDigit('a'), false) + }) + }) + describe("isSpace", () => { + it("should identify newline", () => { + Assert.deep_equal(Utility.CharPredicates.isSpace('\n'), true) + }) + it("should identify tab", () => { + Assert.deep_equal(Utility.CharPredicates.isSpace('\t'), true) + }) + }) +}) + +describe("other utilities", () => { + it("listStringFlattenTuple1 works", () => { + Assert.deep_equal(Utility.listStringFlattenTuple1((list{"abc", "def", "ghi"}, 1)), ("abcdefghi", 1)) + }) + it("listStringFlatten works", () => { + Assert.deep_equal(Utility.listStringFlatten(list{"abc", "def", "ghi"}), "abcdefghi") + }) +}) diff --git a/packages/squiggle-lang/src/rescript/Parser/Parser_Combinators.res b/packages/squiggle-lang/src/rescript/Parser/Parser_Combinators.res new file mode 100644 index 00000000..074b3bc2 --- /dev/null +++ b/packages/squiggle-lang/src/rescript/Parser/Parser_Combinators.res @@ -0,0 +1,116 @@ +module ParserBase = { + type t<'a> = string => list<('a, string)> + let returnP: 'a => t<'a> = (v, inp) => list{(v, inp)} + let failure: t<'a> = _ => list{} + exception Wrongo(string) + let wrongoMessage = "The semantics, by convention, are that singleton list indicates success and empty list indicates failure. This will be refactored into option later." + let item: t = inp => { + if String.length(inp) == 0 { + list{} + } else { + let x = Js.String2.charAt(inp, 0) + let xs = Js.String2.sliceToEnd(inp, ~from=1) + list{(String.get(x, 0), xs)} + } + } + let parse: (t<'a>, string) => list<('a, string)> = (p, inp) => p(inp) + let choice: (t<'a>, t<'a>) => t<'a> = (p, q) => { + inp => { + switch parse(p, inp) { + | list{} => parse(q, inp) + | list{(v, out)} => list{(v, out)} + | _ => raise(Wrongo(wrongoMessage)) + } + } + } +} + +include ParserBase +module ParserMonad = Rationale.Monad.MakeBasic({ + type t<'a> = ParserBase.t<'a> + let {parse, returnP} = module(ParserBase) + let bind = (x: t<'a>, f: 'a => t<'b>): t<'b> => { + inp => { + switch parse(x, inp) { + | list{} => list{} + | list{(v, out)} => parse(f(v), out) + | _ => raise(Wrongo(wrongoMessage)) + } + } + } + let return = returnP + let fmap = #DefineWithBind +}) +let {bind, fmap} = module(ParserMonad) +let satisf: (char => bool) => t = p => { + bind(item, x => + if p(x) { + returnP(x) + } else { + failure + } + ) +} + +let parserCharToParserString: t => t = fmap(x => String.make(1, x)) + +// module LazyP = { +// let parseL: lazy_t> = +// } + +module Primitive = { + open Parser_Utility.CharPredicates + let digit: t = satisf(isDigit) + let lower: t = satisf(isLower) + let upper: t = satisf(isUpper) + let letter: t = satisf(isAlpha) + let alphanum: t = satisf(isAlphaNum) + let checkchar: char => t = x => satisf(c => c == x) + let space: t = satisf(isSpace) + let digitString: t = parserCharToParserString(digit) + let spaceString: t = parserCharToParserString(space) + let alphanumString: t = parserCharToParserString(alphanum) + let lowerString: t = parserCharToParserString(lower) + + let rec checkstring: string => t = inp => { + if String.length(inp) == 0 { + returnP(inp) + } else { + let x = Js.String2.charAt(inp, 0) + let xs = Js.String2.sliceToEnd(inp, ~from=1) + bind(bind(checkchar(String.get(x, 0)), _ => checkstring(xs)), _ => + returnP(Js.String2.concat(x, xs)) + ) + } + } + + let rec many_: t<'a> => t> = p => choice(many1_(p), returnP(list{})) + and many1_: t<'a> => t> = p => + bind(p, v => + bind(many_(p), vs => + returnP(list{ + Js.String2.concat(v, Js.List.foldLeft((. a, b) => Js.String2.concat(a, b), "", vs)), + }) + ) + ) + let many: t<'a> => t<'a> = p => fmap(Parser_Utility.listStringFlatten, many_(p)) + let many1: t<'a> => t<'a> = p => fmap(Parser_Utility.listStringFlatten, many1_(p)) + + let nat: t = bind(many1(digitString), xs => + returnP(Belt.Float.toInt(Js.Float.fromString(xs))) + ) + let whitespace: t = bind(many(spaceString), _ => returnP()) + + let token: t<'a> => t<'a> = p => + bind(whitespace, _ => bind(p, v => bind(whitespace, _ => returnP(v)))) + + let natural: t = token(nat) + let symbol: string => t = xs => xs->checkstring->token +} + +module Symbols = { + let normalNode: t = Primitive.symbol("normal") + let openParen: t = Primitive.symbol("(") + let closeParen: t = Primitive.symbol(")") + let comma: t = Primitive.symbol(",") +} diff --git a/packages/squiggle-lang/src/rescript/Parser/Parser_Squiggle.res b/packages/squiggle-lang/src/rescript/Parser/Parser_Squiggle.res new file mode 100644 index 00000000..236faef4 --- /dev/null +++ b/packages/squiggle-lang/src/rescript/Parser/Parser_Squiggle.res @@ -0,0 +1,90 @@ +open Parser_Combinators +let {dispatch} = module(ReducerInterface_GenericDistribution) +type expressionValue = ReducerInterface_ExpressionValue.expressionValue + +module Grammar = { + type expressionValueOR = option< + result, + > + let normalDist: t = bind(Symbols.normalNode, _ => + bind(Symbols.openParen, _ => + bind(Primitive.natural, mean => + bind(Symbols.comma, _ => + bind(Primitive.natural, stdev => + bind(Symbols.closeParen, _ => + returnP( + dispatch(( + "normal", + [mean->Belt.Float.fromInt->EvNumber, stdev->Belt.Float.fromInt->EvNumber], + )), + ) + ) + ) + ) + ) + ) + ) + let number: t = bind(Primitive.natural, x => + returnP(dispatch(("float", [x->Belt.Float.fromInt->EvNumber]))) + ) + let evNormalDistribution: t = Primitive.token(normalDist) + let evNatural: t = Primitive.token(number) + + let retGenericDistOrRaise: expressionValueOR => GenericDist_Types.genericDist = etf => { + switch etf { + | Some(Ok(EvDistribution(dist))) => dist + | Some(_) => raise(Wrongo("something bad happened")) + | None => raise(Wrongo("something bad happened")) + } + } + + let rec expr: lazy_t> = lazy bind(Lazy.force(term), t => + choice( + bind(Primitive.symbol("+"), _ => + bind(Lazy.force(expr), e => { + let t' = retGenericDistOrRaise(t) + let e' = retGenericDistOrRaise(e) + returnP(dispatch(("add", [EvDistribution(t'), EvDistribution(e')]))) + }) + ), + choice( + bind(Primitive.symbol("-"), _ => + bind(Lazy.force(expr), e => { + let t' = retGenericDistOrRaise(t) + let e' = retGenericDistOrRaise(e) + returnP(dispatch(("subtract", [EvDistribution(t'), EvDistribution(e')]))) + }) + ), + returnP(t), + ), + ) + ) + and term: lazy_t> = lazy bind(Lazy.force(factor), f => + choice( + bind( + Primitive.symbol("*"), + _ => + bind(Lazy.force(term), t => { + let f' = retGenericDistOrRaise(f) + let t' = retGenericDistOrRaise(t) + returnP(dispatch(("multiply", [EvDistribution(f'), EvDistribution(t')]))) + })), + returnP(f), + ), + ) + and factor: lazy_t> = lazy choice( + bind(Symbols.openParen, _ => + bind(Lazy.force(expr), e => bind(Symbols.closeParen, _ => returnP(e))) + ), + evNormalDistribution, + ) +} + +let eval: string => Grammar.expressionValueOR = xs => + switch parse(Lazy.force(Grammar.expr), xs) { + | list{(n, "")} => n + | list{(_, out)} => raise(Wrongo(`unconsumed input ${out}`)) + | list{} => raise(Wrongo("invalid input")) + | _ => raise(Wrongo(wrongoMessage)) + } +let foo = eval(" normal(5, 2) + normal(0, 2) * normal ( 10, 1 ) - normal (5 , \n 1)") diff --git a/packages/squiggle-lang/src/rescript/Parser/Parser_Utility.res b/packages/squiggle-lang/src/rescript/Parser/Parser_Utility.res new file mode 100644 index 00000000..e343914e --- /dev/null +++ b/packages/squiggle-lang/src/rescript/Parser/Parser_Utility.res @@ -0,0 +1,33 @@ +module CharPredicates = { + let isDigit: char => bool = x => { + let xString = String.make(1, x) + Js.String2.includes("0123456789", xString) + } + let isLower: char => bool = x => { + let xString = String.make(1, x) + Js.String2.includes("abcdefghijklmnopqrstuvwxyz", xString) + } + let isUpper: char => bool = x => { + let xString = String.make(1, x) + Js.String2.includes("ABCDEFGHIJKLMNOPQRSTUVWXYZ", xString) + } + let isAlpha: char => bool = x => { + isUpper(x) || isLower(x) + } + let isAlphaNum: char => bool = x => { + isAlpha(x) || isDigit(x) + } + let isSpace: char => bool = x => { + let xString = String.make(1, x) + Js.String2.includes(" \t\n", xString) + } +} + +let listStringFlatten: list => string = xs => { + Js.String2.concatMany("", Belt.List.toArray(xs)) +} +let listStringFlattenTuple1: ((list, 'a)) => (string, 'a) = tup => { + let (xs, y) = tup + let xs' = Belt.List.toArray(xs) + (Js.String2.concatMany("", xs'), y) +} diff --git a/packages/squiggle-lang/src/rescript/Parser/README.md b/packages/squiggle-lang/src/rescript/Parser/README.md new file mode 100644 index 00000000..6d20c360 --- /dev/null +++ b/packages/squiggle-lang/src/rescript/Parser/README.md @@ -0,0 +1 @@ +Based on chapter 8 of Programming in Haskell by Graham Hutton 1st edition diff --git a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res index a15c4082..204a4be9 100644 --- a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res +++ b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res @@ -140,6 +140,7 @@ module SymbolicConstructors = { let oneFloat = name => switch name { | "exponential" => Ok(SymbolicDist.Exponential.make) + | "float" => Ok(x => Ok(SymbolicDist.Float.make(x))) | _ => Error("Unreachable state") } @@ -178,7 +179,7 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option< > => { let (fnName, args) = call switch (fnName, args) { - | ("exponential" as fnName, [EvNumber(f1)]) => + | (("exponential" | "float") as fnName, [EvNumber(f1)]) => SymbolicConstructors.oneFloat(fnName) ->E.R.bind(r => r(f1)) ->SymbolicConstructors.symbolicResultToOutput