Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions assets/js/interpreter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ export default class Interpreter {
// case() has no unit tests in interpreter_test.mjs, only feature tests in test/features/test/control_flow/case_test.exs
// 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) {
static case(condition, clauses, context, errorCallback) {
errorCallback = errorCallback || Interpreter.raiseCaseClauseError;
if (typeof condition === "function") {
condition = condition(context);
}
Expand All @@ -226,7 +227,7 @@ export default class Interpreter {
}
}

Interpreter.raiseCaseClauseError(condition);
errorCallback(condition);
}

static cloneContext(context) {
Expand Down Expand Up @@ -827,6 +828,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);
}
Expand Down Expand Up @@ -918,11 +925,35 @@ 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) {
for (const clause of clauses) {
const contextClone = Interpreter.cloneContext(context);
const condition = clause.body(contextClone);

if (Interpreter.isMatched(clause.match, condition, contextClone)) {
Interpreter.updateVarsToMatchedValues(contextClone);

if (Interpreter.#evaluateGuards(clause.guards, contextClone)) {
context = contextClone;
} else {
return Interpreter.case(
condition,
elseClauses,
context,
Interpreter.raiseWithClauseError,
);
}
} else {
return Interpreter.case(
condition,
elseClauses,
context,
Interpreter.raiseWithClauseError,
);
}
}

return body(context);
}

static #areBitstringsEqual(bitstring1, bitstring2) {
Expand Down
8 changes: 5 additions & 3 deletions lib/hologram/compiler/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,11 @@ 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

@doc """
Expand Down
9 changes: 6 additions & 3 deletions lib/hologram/compiler/ir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -384,13 +384,16 @@ 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

@doc """
Expand Down
67 changes: 64 additions & 3 deletions lib/hologram/compiler/transformer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---
Expand Down Expand Up @@ -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.Clause{
match: transform(match, %{context | pattern?: true}),
guards: transform_guards(guards, context),
body: transform(body, context)
}

%{acc | clauses: [clause | acc.clauses]}
end

defp transform_with_clause({:<-, _meta, [match, body]}, acc, context) do
clause = %IR.Clause{
match: transform(match, %{context | pattern?: true}),
guards: [],
body: transform(body, context)
}

%{acc | clauses: [clause | acc.clauses]}
end

defp transform_with_clause(clause, acc, context) do
clause =
%IR.Clause{
match: %IR.MatchPlaceholder{},
guards: [],
body: transform(clause, context)
}

%{acc | clauses: [clause | acc.clauses]}
end
end
28 changes: 26 additions & 2 deletions test/elixir/hologram/compiler/encoder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2146,9 +2146,33 @@ defmodule Hologram.Compiler.EncoderTest do
end
end

# TODO: finish implementing
test "with" do
assert encode_ir(%IR.With{}) == "Interpreter.with()"
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

describe "encode_as_class_name/1" do
Expand Down
20 changes: 17 additions & 3 deletions test/elixir/hologram/compiler/transformer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6452,15 +6452,29 @@ defmodule Hologram.Compiler.TransformerTest do
end
end

# TODO: finish implementing
test "with" do
ast =
ast("""
with true <- true do
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 transform(ast, %Context{}) == %Hologram.Compiler.IR.With{}
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.Clause{} = first_clause
assert %IR.Clause{} = first_else
end
end
125 changes: 118 additions & 7 deletions test/javascript/interpreter_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6843,12 +6843,123 @@ 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("successful match returns body result", () => {
// with b <- a do
// {a, b}
// end
const result = Interpreter.with(
body,
[
{
match: Type.variablePattern("b"),
guards: [],
body: (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: [],
body: (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],
body: (context) => context.vars.a,
},
],
[
{
match: Type.atom("ok"),
guards: [],
body: (_context) => expected,
},
],
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: [],
body: (context) => context.vars.a,
},
],
[
{
match: Type.atom("other"),
guards: [],
body: (_context) => null,
},
],
context,
),
"WithClauseError",
"no with clause matching: :ok",
);
});
});
});
Loading