diff --git a/assets/js/interpreter.mjs b/assets/js/interpreter.mjs index 0e2c4b630..b8b245e75 100644 --- a/assets/js/interpreter.mjs +++ b/assets/js/interpreter.mjs @@ -210,23 +210,12 @@ export default class Interpreter { // Unit test maintenance in interpreter_test.mjs would be problematic because tests would need to be updated // each time Hologram.Compiler.Encoder's implementation changes. static case(condition, clauses, context) { - if (typeof condition === "function") { - condition = condition(context); - } - - for (const clause of clauses) { - const contextClone = Interpreter.cloneContext(context); - - if (Interpreter.isMatched(clause.match, condition, contextClone)) { - Interpreter.updateVarsToMatchedValues(contextClone); - - if (Interpreter.#evaluateGuards(clause.guards, contextClone)) { - return clause.body(contextClone); - } - } - } - - Interpreter.raiseCaseClauseError(condition); + return Interpreter.#checkAllClauses( + condition, + clauses, + context, + Interpreter.raiseCaseClauseError, + ); } static cloneContext(context) { @@ -822,6 +811,12 @@ export default class Interpreter { Interpreter.raiseError("CaseClauseError", message); } + static raiseWithClauseError(arg) { + const message = "no with clause matching: " + Interpreter.inspect(arg); + + Interpreter.raiseError("WithClauseError", message); + } + static raiseCompileError(message) { Interpreter.raiseError("CompileError", message); } @@ -924,11 +919,26 @@ export default class Interpreter { return context; } - // TODO: finish implementing - static with() { - throw new HologramInterpreterError( - '"with" expression is not yet implemented in Hologram', - ); + static with(body, clauses, elseClauses, context) { + const originalContext = context; + for (const clause of clauses) { + const contextClone = Interpreter.cloneContext(context); + const condition = clause.expression(contextClone); + + if (Interpreter.isMatched(clause.match, condition, contextClone)) { + Interpreter.updateVarsToMatchedValues(contextClone); + + if (Interpreter.#evaluateGuards(clause.guards, contextClone)) { + context = contextClone; + } else { + return Interpreter.#withElse(condition, elseClauses, originalContext); + } + } else { + return Interpreter.#withElse(condition, elseClauses, originalContext); + } + } + + return body(context); } static #areBitstringsEqual(bitstring1, bitstring2) { @@ -1031,6 +1041,26 @@ export default class Interpreter { ); } + static #checkAllClauses(condition, clauses, context, errorFun) { + if (typeof condition === "function") { + condition = condition(context); + } + + for (const clause of clauses) { + const contextClone = Interpreter.cloneContext(context); + + if (Interpreter.isMatched(clause.match, condition, contextClone)) { + Interpreter.updateVarsToMatchedValues(contextClone); + + if (Interpreter.#evaluateGuards(clause.guards, contextClone)) { + return clause.body(contextClone); + } + } + } + + errorFun(condition); + } + static #comparePids(pid1, pid2) { for (let i = 2; i >= 0; --i) { if (pid1.segments[i] === pid2.segments[i]) { @@ -1532,6 +1562,18 @@ export default class Interpreter { "no cond clause evaluated to a truthy value", ); } + + static #withElse(condition, elseClauses, context) { + if (elseClauses.length === 0) { + return condition; + } + return Interpreter.#checkAllClauses( + condition, + elseClauses, + context, + Interpreter.raiseWithClauseError, + ); + } } const $ = Interpreter; diff --git a/lib/hologram/compiler/encoder.ex b/lib/hologram/compiler/encoder.ex index ed9ea04d3..c46c6120f 100644 --- a/lib/hologram/compiler/encoder.ex +++ b/lib/hologram/compiler/encoder.ex @@ -435,9 +435,19 @@ defmodule Hologram.Compiler.Encoder do encode_var(name, version) end - # TODO: finish implementing - def encode_ir(%IR.With{}, _context) do - "Interpreter.with()" + def encode_ir(%IR.With{body: body, clauses: clauses, else_clauses: else_clauses}, context) do + body_js = encode_closure(body, context) + clauses_js = encode_as_array(clauses, context) + else_clauses_js = encode_as_array(else_clauses, context) + "Interpreter.with(#{body_js}, #{clauses_js}, #{else_clauses_js}, context)" + end + + def encode_ir(%IR.WithClause{} = clause, context) do + match = encode_ir(clause.match, %{context | pattern?: true}) + guards = encode_as_array(clause.guards, context, &encode_closure/2) + expression = encode_closure(clause.expression, context) + + "{match: #{match}, guards: #{guards}, expression: #{expression}}" end @doc """ diff --git a/lib/hologram/compiler/ir.ex b/lib/hologram/compiler/ir.ex index 5ce8ff040..6c4d13204 100644 --- a/lib/hologram/compiler/ir.ex +++ b/lib/hologram/compiler/ir.ex @@ -47,6 +47,7 @@ defmodule Hologram.Compiler.IR do | IR.TupleType.t() | IR.Variable.t() | IR.With.t() + | IR.WithClause.t() defmodule AnonymousFunctionCall do @moduledoc false @@ -384,13 +385,24 @@ defmodule Hologram.Compiler.IR do @type t :: %__MODULE__{name: atom, version: integer | nil} end - # TODO: finish implementing defmodule With do @moduledoc false - defstruct [] + defstruct [:clauses, :body, :else_clauses] - @type t :: %__MODULE__{} + @type t :: %__MODULE__{ + clauses: list(IR.Clause.t()), + body: IR.t(), + else_clauses: list(IR.Clause.t()) + } + end + + defmodule WithClause do + @moduledoc false + + defstruct [:match, :guards, :expression] + + @type t :: %__MODULE__{match: IR.t(), guards: list(IR.t()), expression: IR.Block.t()} end @doc """ diff --git a/lib/hologram/compiler/transformer.ex b/lib/hologram/compiler/transformer.ex index 75c72fa0d..c35683e24 100644 --- a/lib/hologram/compiler/transformer.ex +++ b/lib/hologram/compiler/transformer.ex @@ -442,9 +442,21 @@ defmodule Hologram.Compiler.Transformer do |> build_tuple_type_ir(context) end - # TODO: finish implementing - def transform({:with, _meta, parts}, _context) when is_list(parts) do - %IR.With{} + def transform({:with, _meta, parts}, context) when is_list(parts) do + initial_acc = %IR.With{ + clauses: [], + else_clauses: [], + body: nil + } + + parts + |> Enum.reduce( + initial_acc, + &transform_with_clause(&1, &2, context) + ) + |> then(fn %{clauses: clauses} = ir -> + %{ir | clauses: Enum.reverse(clauses)} + end) end # --- PRESERVE ORDER (BEGIN) --- @@ -920,4 +932,53 @@ defmodule Hologram.Compiler.Transformer do %IR.Variable{name: name, version: version} end end + + defp transform_with_clause(do_and_else, acc, context) when is_list(do_and_else) do + do_part = + do_and_else + |> Keyword.get(:do) + |> transform(context) + + else_part = + do_and_else + |> Keyword.get(:else, []) + |> Enum.map(&transform(&1, context)) + + %{acc | body: do_part, else_clauses: else_part} + end + + defp transform_with_clause( + {:<-, _meta_1, [{:when, _meta_2, [match, guards]}, body]}, + acc, + context + ) do + clause = %IR.WithClause{ + match: transform(match, %{context | pattern?: true}), + guards: transform_guards(guards, context), + expression: transform(body, context) + } + + %{acc | clauses: [clause | acc.clauses]} + end + + defp transform_with_clause({:<-, _meta, [match, body]}, acc, context) do + clause = %IR.WithClause{ + match: transform(match, %{context | pattern?: true}), + guards: [], + expression: transform(body, context) + } + + %{acc | clauses: [clause | acc.clauses]} + end + + defp transform_with_clause(clause, acc, context) do + clause = + %IR.WithClause{ + match: %IR.MatchPlaceholder{}, + guards: [], + expression: transform(clause, context) + } + + %{acc | clauses: [clause | acc.clauses]} + end end diff --git a/test/elixir/hologram/compiler/encoder_test.exs b/test/elixir/hologram/compiler/encoder_test.exs index 2c059b35e..90a9af68d 100644 --- a/test/elixir/hologram/compiler/encoder_test.exs +++ b/test/elixir/hologram/compiler/encoder_test.exs @@ -2146,9 +2146,268 @@ defmodule Hologram.Compiler.EncoderTest do end end - # TODO: finish implementing - test "with" do - assert encode_ir(%IR.With{}) == "Interpreter.with()" + describe "with" do + test "minimal with" do + ir = %IR.With{ + body: %IR.Block{ + expressions: [] + }, + clauses: [], + else_clauses: [] + } + + expected = + normalize_newlines(""" + Interpreter.with((context) => { + return Type.atom("nil"); + }, [], [], context)\ + """) + + assert encode_ir(ir) === expected + end + + test "with body, clause, and else clause" do + ir = %IR.With{ + body: %IR.AtomType{value: :ok}, + clauses: [ + %IR.Clause{ + match: %IR.Variable{name: :x}, + body: %IR.Variable{name: :y}, + guards: [] + } + ], + else_clauses: [ + %IR.Clause{ + match: %IR.AtomType{value: :error}, + guards: [], + body: %IR.Block{expressions: [%IR.AtomType{value: :error}]} + } + ] + } + + expected = + normalize_newlines(""" + Interpreter.with((context) => Type.atom("ok"), [{match: Type.variablePattern("x"), guards: [], body: (context) => context.vars.y}], [{match: Type.atom("error"), guards: [], body: (context) => { + return Type.atom("error"); + }}], context)\ + """) + + assert encode_ir(ir) == expected + end + + test "with empty body" do + ir = %IR.With{ + body: %IR.AtomType{value: nil}, + clauses: [ + %IR.Clause{ + match: %IR.Variable{name: :x}, + body: %IR.Variable{name: :y}, + guards: [] + } + ], + else_clauses: [ + %IR.Clause{ + match: %IR.AtomType{value: :error}, + guards: [], + body: %IR.Block{expressions: [%IR.AtomType{value: :error}]} + } + ] + } + + expected = + normalize_newlines(""" + Interpreter.with((context) => Type.atom("nil"), [{match: Type.variablePattern("x"), guards: [], body: (context) => context.vars.y}], [{match: Type.atom("error"), guards: [], body: (context) => { + return Type.atom("error"); + }}], context)\ + """) + + assert encode_ir(ir) == expected + end + + test "with multi expression body" do + ir = %IR.With{ + body: %IR.Block{ + expressions: [ + %IR.MatchOperator{ + left: %IR.Variable{name: :x}, + right: %IR.IntegerType{value: 5} + }, + %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.Variable{name: :x} + ] + } + ] + }, + clauses: [ + %IR.Clause{ + match: %IR.Variable{name: :x}, + body: %IR.Variable{name: :y}, + guards: [] + } + ], + else_clauses: [ + %IR.Clause{ + match: %IR.AtomType{value: :error}, + guards: [], + body: %IR.Block{expressions: [%IR.AtomType{value: :error}]} + } + ] + } + + expected = + normalize_newlines(""" + Interpreter.with((context) => { + Interpreter.matchOperator(Type.integer(5n), Type.variablePattern("x"), context); + Interpreter.updateVarsToMatchedValues(context); + return Erlang_["foo/1"](context.vars.x); + }, [{match: Type.variablePattern("x"), guards: [], body: (context) => context.vars.y}], [{match: Type.atom("error"), guards: [], body: (context) => { + return Type.atom("error"); + }}], context)\ + """) + + assert encode_ir(ir) == expected + end + + test "without else clauses" do + ir = %IR.With{ + body: %IR.Block{ + expressions: [ + %IR.MatchOperator{ + left: %IR.Variable{name: :x}, + right: %IR.IntegerType{value: 5} + }, + %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.Variable{name: :x} + ] + } + ] + }, + clauses: [ + %IR.Clause{ + match: %IR.Variable{name: :x}, + body: %IR.Variable{name: :y}, + guards: [] + } + ], + else_clauses: [] + } + + expected = + normalize_newlines(""" + Interpreter.with((context) => { + Interpreter.matchOperator(Type.integer(5n), Type.variablePattern("x"), context); + Interpreter.updateVarsToMatchedValues(context); + return Erlang_["foo/1"](context.vars.x); + }, [{match: Type.variablePattern("x"), guards: [], body: (context) => context.vars.y}], [], context)\ + """) + + assert encode_ir(ir) == expected + end + + test "with multiple else clauses" do + ir = %IR.With{ + body: %IR.AtomType{value: :ok}, + clauses: [ + %IR.Clause{ + match: %IR.Variable{name: :x}, + body: %IR.Variable{name: :y}, + guards: [] + } + ], + else_clauses: [ + %IR.Clause{ + match: %IR.AtomType{value: :error}, + guards: [], + body: %IR.Block{expressions: [%IR.AtomType{value: :error}]} + }, + %IR.Clause{ + match: %IR.AtomType{value: :timeout}, + guards: [], + body: %IR.Block{expressions: [%IR.AtomType{value: :error}]} + } + ] + } + + expected = + normalize_newlines(""" + Interpreter.with((context) => Type.atom("ok"), [{match: Type.variablePattern("x"), guards: [], body: (context) => context.vars.y}], [{match: Type.atom("error"), guards: [], body: (context) => { + return Type.atom("error"); + }}, {match: Type.atom("timeout"), guards: [], body: (context) => { + return Type.atom("error"); + }}], context)\ + """) + + assert encode_ir(ir) == expected + end + end + + describe "WithClause" do + test "clause without guard" do + ir = %IR.WithClause{ + match: %IR.Variable{name: :x}, + guards: [], + expression: %IR.Variable{name: :y} + } + + assert encode_ir(ir) == + ~s|{match: Type.variablePattern("x"), guards: [], expression: (context) => context.vars.y}| + end + + test "clause with guard" do + ir = %IR.WithClause{ + match: %IR.Variable{ + name: :i + }, + guards: [ + %IR.LocalFunctionCall{ + args: [%IR.Variable{name: :i}], + function: :is_integer + } + ], + expression: %IR.Variable{name: :x} + } + + assert encode_ir(ir) =~ ~s|guards: [(context) => Erlang_["is_integer/1"](context.vars.i)]| + end + + test "with plain expression" do + ir = %IR.WithClause{ + match: %IR.MatchPlaceholder{}, + guards: [], + expression: %IR.MatchOperator{ + left: %IR.Variable{name: :x}, + right: %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.IntegerType{value: 5} + ] + } + } + } + + assert encode_ir(ir) == + ~s|{match: Type.matchPlaceholder(), guards: [], expression: (context) => Interpreter.matchOperator(Erlang_[\"foo/1\"](Type.integer(5n)), Type.variablePattern(\"x\"), context)}| + end + + test "with plain expression (function call)" do + ir = %IR.WithClause{ + match: %IR.MatchPlaceholder{}, + guards: [], + expression: %IR.LocalFunctionCall{ + function: :baz, + args: [ + %IR.IntegerType{value: 5} + ] + } + } + + assert encode_ir(ir) == + ~s|{match: Type.matchPlaceholder(), guards: [], expression: (context) => Erlang_["baz/1"](Type.integer(5n))}| + end end describe "encode_as_class_name/1" do diff --git a/test/elixir/hologram/compiler/transformer_test.exs b/test/elixir/hologram/compiler/transformer_test.exs index bf9d56cd6..c2e475b27 100644 --- a/test/elixir/hologram/compiler/transformer_test.exs +++ b/test/elixir/hologram/compiler/transformer_test.exs @@ -6452,15 +6452,364 @@ defmodule Hologram.Compiler.TransformerTest do end end - # TODO: finish implementing - test "with" do - ast = - ast(""" - with true <- true do - :ok - end - """) - - assert transform(ast, %Context{}) == %Hologram.Compiler.IR.With{} + describe "with" do + test "empty with" do + ast = + ast(""" + with do + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + clauses: [], + else_clauses: [], + body: %IR.Block{expressions: []} + } + end + + test "multiple clauses and else clauses" do + ast = + ast(""" + with {:ok, x} when not is_nil(x) <- {:ok, foo(y)}, + {x, bar} <- baz(x), + :test = :test do + :ok + else + :error -> + {:error, :wrong_data} + {:error, value} -> + {:error, :wrong_data, value} + end + """) + + assert %Hologram.Compiler.IR.With{ + body: body, + clauses: [first_clause, _second, _third], + else_clauses: [first_else, _second_else] + } = transform(ast, %Context{}) + + assert body == %IR.AtomType{value: :ok} + assert %IR.WithClause{} = first_clause + assert %IR.Clause{} = first_else + end + + test "clause without guards" do + ast = + ast(""" + with :ok <- y do + end + """) + + assert %IR.With{ + clauses: [clause], + else_clauses: [], + body: body + } = transform(ast, %Context{}) + + assert body == %IR.Block{expressions: []} + + assert clause == %IR.WithClause{ + match: %IR.AtomType{value: :ok}, + guards: [], + expression: %IR.Variable{name: :y} + } + end + + test "clause with guards" do + ast = + ast(""" + with i when is_integer(i) <- x, do: x + """) + + assert %{clauses: [clause], body: body} = transform(ast, %Context{}) + + assert clause == %IR.WithClause{ + match: %IR.Variable{ + name: :i + }, + guards: [ + %IR.LocalFunctionCall{ + args: [%IR.Variable{name: :i}], + function: :is_integer + } + ], + expression: %IR.Variable{name: :x} + } + + assert body == %IR.Variable{name: :x} + end + + test "with compound guard calls" do + ast = + ast(""" + with x when is_integer(x) and x > 5 <- y, do: nil + """) + + assert transform(ast, %Context{}) == %IR.With{ + body: %IR.AtomType{value: nil}, + else_clauses: [], + clauses: [ + %IR.WithClause{ + match: %IR.Variable{ + name: :x + }, + expression: %IR.Variable{name: :y}, + guards: [ + %IR.LocalFunctionCall{ + function: :and, + args: [ + %IR.LocalFunctionCall{ + function: :is_integer, + args: [ + %IR.Variable{ + name: :x + } + ] + }, + %IR.LocalFunctionCall{ + function: :>, + args: [ + %IR.Variable{ + name: :x + }, + %IR.IntegerType{ + value: 5 + } + ] + } + ] + } + ] + } + ] + } + end + + test "with plain expressions" do + ast = + ast(""" + with x = foo(5), + y <- x do + end + """) + + assert %{clauses: [plain_clause, arrow_clause]} = transform(ast, %Context{}) + + assert plain_clause == %IR.WithClause{ + match: %IR.MatchPlaceholder{}, + guards: [], + expression: %IR.MatchOperator{ + left: %IR.Variable{name: :x}, + right: %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.IntegerType{value: 5} + ] + } + } + } + + assert arrow_clause == %IR.WithClause{ + match: %IR.Variable{name: :y}, + guards: [], + expression: %IR.Variable{name: :x} + } + end + + test "with plain expression (function call)" do + ast = + ast(""" + with baz(5) do + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + body: %IR.Block{expressions: []}, + clauses: [ + %IR.WithClause{ + match: %IR.MatchPlaceholder{}, + guards: [], + expression: %IR.LocalFunctionCall{ + function: :baz, + args: [ + %IR.IntegerType{value: 5} + ] + } + } + ], + else_clauses: [] + } + end + + test "with single expression do block" do + ast = + ast(""" + with {:ok, x} <- y do + foo(x) + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + clauses: [ + %IR.WithClause{ + match: %IR.TupleType{ + data: [ + %IR.AtomType{value: :ok}, + %IR.Variable{name: :x} + ] + }, + guards: [], + expression: %IR.Variable{name: :y} + } + ], + else_clauses: [], + body: %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.Variable{name: :x} + ] + } + } + end + + test "with multi-expression do block" do + ast = + ast(""" + with x <- y do + x = 5 + foo(x) + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + clauses: [ + %IR.WithClause{ + match: %IR.Variable{name: :x}, + guards: [], + expression: %IR.Variable{name: :y} + } + ], + else_clauses: [], + body: %IR.Block{ + expressions: [ + %IR.MatchOperator{ + left: %IR.Variable{name: :x}, + right: %IR.IntegerType{value: 5} + }, + %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.Variable{name: :x} + ] + } + ] + } + } + end + + test "single clause else block" do + ast = + ast(""" + with do + else + {:error, other} -> + :error + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + body: %IR.Block{expressions: []}, + clauses: [], + else_clauses: [ + %IR.Clause{ + match: %IR.TupleType{ + data: [ + %IR.AtomType{value: :error}, + %IR.Variable{name: :other} + ] + }, + guards: [], + body: %IR.Block{expressions: [%IR.AtomType{value: :error}]} + } + ] + } + end + + test "multi clause else block" do + ast = + ast(""" + with do + else + {:error, :timeout} -> + foo(y) + {:error, other} -> + other + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + body: %IR.Block{expressions: []}, + clauses: [], + else_clauses: [ + %IR.Clause{ + match: %IR.TupleType{ + data: [ + %IR.AtomType{value: :error}, + %IR.AtomType{ + value: :timeout + } + ] + }, + guards: [], + body: %IR.Block{ + expressions: [ + %IR.LocalFunctionCall{ + function: :foo, + args: [ + %IR.Variable{name: :y} + ] + } + ] + } + }, + %IR.Clause{ + match: %IR.TupleType{ + data: [ + %IR.AtomType{value: :error}, + %IR.Variable{name: :other} + ] + }, + guards: [], + body: %IR.Block{ + expressions: [ + %IR.Variable{ + name: :other + } + ] + } + } + ] + } + end + + test "without else block" do + ast = + ast(""" + with x <- y do + end + """) + + assert transform(ast, %Context{}) == %IR.With{ + body: %IR.Block{expressions: []}, + clauses: [ + %IR.WithClause{ + match: %IR.Variable{name: :x}, + guards: [], + expression: %IR.Variable{name: :y} + } + ], + else_clauses: [] + } + end end end diff --git a/test/javascript/interpreter_test.mjs b/test/javascript/interpreter_test.mjs index 91c5110ec..063f2f46f 100644 --- a/test/javascript/interpreter_test.mjs +++ b/test/javascript/interpreter_test.mjs @@ -6855,12 +6855,300 @@ describe("Interpreter", () => { assert.deepStrictEqual(result, expected); }); - // TODO: finish implementing - it("with()", () => { - assert.throw( - () => Interpreter.with(), - Error, - '"with" expression is not yet implemented in Hologram', - ); + describe("with()", () => { + const context = contextFixture({ + vars: { + a: Type.atom("ok"), + }, + }); + const body = (context) => { + return Type.tuple([context.vars.a, context.vars.b]); + }; + + it("handles an empty with", () => { + // with do: nil + const result = Interpreter.with( + (_context) => { + return Type.atom("nil"); + }, + [], + [], + context, + ); + + const expected = Type.atom("nil"); + assert.deepStrictEqual(result, expected); + }); + + it("successful match returns body result", () => { + // with b <- a do + // {a, b} + // end + const result = Interpreter.with( + body, + [ + { + match: Type.variablePattern("b"), + guards: [], + expression: (context) => context.vars.a, + }, + ], + [], + context, + ); + + const expected = Type.tuple([Type.atom("ok"), Type.atom("ok")]); + assert.deepStrictEqual(result, expected); + }); + + it("can fail to match on match condition", () => { + // with :error <- a do + // else + // :ok -> {:error, :nomatch} + // end + const expected = Type.tuple([Type.atom("error"), Type.atom("nomatch")]); + const result = Interpreter.with( + body, + [ + { + match: Type.atom("error"), + guards: [], + expression: (context) => context.vars.a, + }, + ], + [ + { + match: Type.atom("ok"), + guards: [], + body: (_context) => expected, + }, + ], + context, + ); + + assert.deepStrictEqual(result, expected); + }); + it("can fail to match on guard", () => { + // with b when b == :no <- a do + // else + // :ok -> {:error, :nomatch} + // end + const guard = (context) => + Erlang["==/2"](context.vars.b, Type.atom("no")); + const expected = Type.tuple([Type.atom("error"), Type.atom("nomatch")]); + + const result = Interpreter.with( + body, + [ + { + match: Type.variablePattern("b"), + guards: [guard], + expression: (context) => context.vars.a, + }, + ], + [ + { + match: Type.atom("ok"), + guards: [], + body: (_context) => expected, + }, + ], + context, + ); + + assert.deepStrictEqual(result, expected); + }); + it("handle plain expressions", () => { + // with b = a do + // {a, b} + // end + const expected = Type.tuple([Type.atom("ok"), Type.atom("ok")]); + const result = Interpreter.with( + body, + [ + { + match: Type.variablePattern("b"), + guards: [], + expression: (context) => + Interpreter.matchOperator( + context.vars.a, + Type.variablePattern("a"), + context, + ), + }, + ], + [], + context, + ); + + assert.deepStrictEqual(result, expected); + }); + it("gives the original context to the else clauses", () => { + // x = :original + // with x <- a, + // :fail <- :mismatch do + // :body + // else + // _ -> + // x + // end + const contextWithX = contextFixture({ + vars: { + a: Type.atom("ok"), + x: Type.atom("original"), + }, + }); + const expected = Type.atom("original"); + const result = Interpreter.with( + body, + [ + { + match: Type.variablePattern("x"), + guards: [], + expression: (context) => context.vars.a, + }, + { + match: Type.atom("fail"), + guards: [], + expression: (_context) => Type.atom("mismatch"), + }, + ], + [ + { + match: Type.matchPlaceholder(), + guards: [], + body: (context) => context.vars.x, + }, + ], + contextWithX, + ); + + assert.deepStrictEqual(result, expected); + }); + it("gives the original context to the else clauses (guard mismatch)", () => { + // x = :original + // with x <- a, + // i when false <- :mismatch do + // :body + // else + // _ -> + // x + // end + const contextWithX = contextFixture({ + vars: { + a: Type.atom("ok"), + x: Type.atom("original"), + }, + }); + const expected = Type.atom("original"); + const result = Interpreter.with( + body, + [ + { + match: Type.variablePattern("x"), + guards: [], + expression: (context) => context.vars.a, + }, + { + match: Type.variablePattern("i"), + guards: [(_context) => Type.atom("false")], + expression: (_context) => Type.atom("mismatch"), + }, + ], + [ + { + match: Type.matchPlaceholder(), + guards: [], + body: (context) => context.vars.x, + }, + ], + contextWithX, + ); + + assert.deepStrictEqual(result, expected); + }); + it("returns the unmatched value directly if there are no else clauses", () => { + // with :error <- a do + // x + // end + const expected = Type.atom("ok"); + const result = Interpreter.with( + body, + [ + { + match: Type.atom("error"), + guards: [], + expression: (context) => context.vars.a, + }, + ], + [], + context, + ); + + assert.deepStrictEqual(result, expected); + }); + + it("handles multiple else clauses", () => { + const expected = Type.atom("second"); + const result = Interpreter.with( + body, + [ + { + match: Type.atom("error"), + guards: [], + expression: (context) => context.vars.a, + }, + ], + [ + { + match: Type.atom("a"), + guards: [], + body: (_context) => Type.atom("first"), + }, + { + match: Type.atom("ok"), + guards: [], + body: (_context) => Type.atom("second"), + }, + { + match: Type.atom("c"), + guards: [], + body: (_context) => Type.atom("third"), + }, + ], + context, + ); + + assert.deepStrictEqual(result, expected); + }); + it("throws error if no else clauses match", () => { + // with :error <- a do + // else + // :other -> {:error, :nomatch} + // end + assertBoxedError( + () => + Interpreter.with( + body, + [ + { + match: Type.atom("error"), + guards: [], + expression: (context) => context.vars.a, + }, + ], + [ + { + match: Type.atom("other"), + guards: [], + body: (_context) => null, + }, + ], + context, + ), + "WithClauseError", + "no with clause matching: :ok", + ); + }); }); });