From 28db1a6f5a5c9c61363f02d31eaf43f86498d75d Mon Sep 17 00:00:00 2001 From: vinicius pereira Date: Sun, 14 Dec 2025 23:28:43 -0300 Subject: [PATCH 01/33] feat: add binary_to_term/1 implementation and EtfDecoder class - Implemented the binary_to_term/1 function in erlang.mjs to decode binary representations of Erlang terms. - Added a new EtfDecoder class in etf_decoder.mjs to handle the decoding of the Erlang External Term Format (ETF). - Created comprehensive tests for binary_to_term/1 in both JavaScript and Elixir to ensure correct decoding of various Erlang data types, including integers, atoms, binaries, tuples, lists, and maps. - Included error handling for invalid inputs and edge cases in the tests. --- assets/js/erlang/erlang.mjs | 13 + assets/js/erlang/etf_decoder.mjs | 321 ++++++++++++++++++ .../ex_js_consistency/erlang/erlang_test.exs | 147 ++++++++ test/javascript/erlang/etf_decoder_test.mjs | 283 +++++++++++++++ 4 files changed, 764 insertions(+) create mode 100644 assets/js/erlang/etf_decoder.mjs create mode 100644 test/javascript/erlang/etf_decoder_test.mjs diff --git a/assets/js/erlang/erlang.mjs b/assets/js/erlang/erlang.mjs index 9d7ab01c49..05e88fec21 100644 --- a/assets/js/erlang/erlang.mjs +++ b/assets/js/erlang/erlang.mjs @@ -599,6 +599,19 @@ const Erlang = { // End binary_to_list/1 // Deps: [] + // Start binary_to_term/1 + "binary_to_term/1": (binary) => { + if (!Type.isBinary(binary)) { + Interpreter.raiseArgumentError( + Interpreter.buildArgumentErrorMsg(1, "not a binary"), + ); + } + + return EtfDecoder.decode(binary); + }, + // End binary_to_term/1 + // Deps: [] + // Start bit_size/1 "bit_size/1": (term) => { if (!Type.isBitstring(term)) { diff --git a/assets/js/erlang/etf_decoder.mjs b/assets/js/erlang/etf_decoder.mjs new file mode 100644 index 0000000000..0baa8b1655 --- /dev/null +++ b/assets/js/erlang/etf_decoder.mjs @@ -0,0 +1,321 @@ +"use strict"; + +import Bitstring from "../bitstring.mjs"; +import Interpreter from "../interpreter.mjs"; +import Type from "../type.mjs"; + +// Erlang External Term Format (ETF) Decoder +// See: https://www.erlang.org/doc/apps/erts/erl_ext_dist.html + +export default class EtfDecoder { + // ETF tag constants + static #SMALL_INTEGER_EXT = 97; + static #INTEGER_EXT = 98; + static #ATOM_EXT = 100; + static #SMALL_TUPLE_EXT = 104; + static #LARGE_TUPLE_EXT = 105; + static #NIL_EXT = 106; + static #STRING_EXT = 107; + static #LIST_EXT = 108; + static #BINARY_EXT = 109; + static #SMALL_BIG_EXT = 110; + static #LARGE_BIG_EXT = 111; + static #SMALL_ATOM_EXT = 115; + static #MAP_EXT = 116; + static #ATOM_UTF8_EXT = 118; + static #SMALL_ATOM_UTF8_EXT = 119; + + static decode(binary) { + Bitstring.maybeSetBytesFromText(binary); + + const bytes = binary.bytes; + const dataView = new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ); + + // Check ETF version byte (must be 131) + if (dataView.getUint8(0) !== 131) { + Interpreter.raiseArgumentError( + Interpreter.buildArgumentErrorMsg( + 1, + "invalid external representation of a term", + ), + ); + } + + const result = $.#decodeTerm(dataView, bytes, 1); + return result.term; + } + + static #decodeTerm(dataView, bytes, offset) { + if (offset >= bytes.length) { + Interpreter.raiseArgumentError( + Interpreter.buildArgumentErrorMsg( + 1, + "invalid external representation of a term", + ), + ); + } + + const tag = dataView.getUint8(offset); + + switch (tag) { + case $.#SMALL_INTEGER_EXT: + return $.#decodeSmallInteger(dataView, offset + 1); + + case $.#INTEGER_EXT: + return $.#decodeInteger(dataView, offset + 1); + + case $.#SMALL_BIG_EXT: + return $.#decodeSmallBig(dataView, bytes, offset + 1); + + case $.#LARGE_BIG_EXT: + return $.#decodeLargeBig(dataView, bytes, offset + 1); + + case $.#ATOM_EXT: + return $.#decodeAtom(dataView, bytes, offset + 1, false); + + case $.#SMALL_ATOM_EXT: + return $.#decodeSmallAtom(dataView, bytes, offset + 1, false); + + case $.#ATOM_UTF8_EXT: + return $.#decodeAtom(dataView, bytes, offset + 1, true); + + case $.#SMALL_ATOM_UTF8_EXT: + return $.#decodeSmallAtom(dataView, bytes, offset + 1, true); + + case $.#BINARY_EXT: + return $.#decodeBinary(dataView, bytes, offset + 1); + + case $.#SMALL_TUPLE_EXT: + return $.#decodeSmallTuple(dataView, bytes, offset + 1); + + case $.#LARGE_TUPLE_EXT: + return $.#decodeLargeTuple(dataView, bytes, offset + 1); + + case $.#NIL_EXT: + return {term: Type.list(), newOffset: offset + 1}; + + case $.#STRING_EXT: + return $.#decodeString(dataView, bytes, offset + 1); + + case $.#LIST_EXT: + return $.#decodeList(dataView, bytes, offset + 1); + + case $.#MAP_EXT: + return $.#decodeMap(dataView, bytes, offset + 1); + + default: + Interpreter.raiseArgumentError( + `unsupported external term format tag: ${tag}`, + ); + } + } + + // Integer decoders + + static #decodeSmallInteger(dataView, offset) { + const value = dataView.getUint8(offset); + return { + term: Type.integer(value), + newOffset: offset + 1, + }; + } + + static #decodeInteger(dataView, offset) { + const value = dataView.getInt32(offset); + return { + term: Type.integer(value), + newOffset: offset + 4, + }; + } + + static #decodeSmallBig(dataView, bytes, offset) { + const n = dataView.getUint8(offset); + const sign = dataView.getUint8(offset + 1); + + let value = 0n; + for (let i = 0; i < n; i++) { + const byte = BigInt(bytes[offset + 2 + i]); + value += byte << BigInt(i * 8); + } + + if (sign === 1) { + value = -value; + } + + return { + term: Type.integer(value), + newOffset: offset + 2 + n, + }; + } + + static #decodeLargeBig(dataView, bytes, offset) { + const n = dataView.getUint32(offset); + const sign = dataView.getUint8(offset + 4); + + let value = 0n; + for (let i = 0; i < n; i++) { + const byte = BigInt(bytes[offset + 5 + i]); + value += byte << BigInt(i * 8); + } + + if (sign === 1) { + value = -value; + } + + return { + term: Type.integer(value), + newOffset: offset + 5 + n, + }; + } + + // Atom decoders + + static #decodeAtom(dataView, bytes, offset, isUtf8) { + const length = dataView.getUint16(offset); + const atomBytes = bytes.slice(offset + 2, offset + 2 + length); + + const decoder = new TextDecoder(isUtf8 ? "utf-8" : "latin1"); + const atomString = decoder.decode(atomBytes); + + return { + term: Type.atom(atomString), + newOffset: offset + 2 + length, + }; + } + + static #decodeSmallAtom(dataView, bytes, offset, isUtf8) { + const length = dataView.getUint8(offset); + const atomBytes = bytes.slice(offset + 1, offset + 1 + length); + + const decoder = new TextDecoder(isUtf8 ? "utf-8" : "latin1"); + const atomString = decoder.decode(atomBytes); + + return { + term: Type.atom(atomString), + newOffset: offset + 1 + length, + }; + } + + // Binary decoder + + static #decodeBinary(dataView, bytes, offset) { + const length = dataView.getUint32(offset); + const binaryBytes = bytes.slice(offset + 4, offset + 4 + length); + + return { + term: Bitstring.fromBytes(new Uint8Array(binaryBytes)), + newOffset: offset + 4 + length, + }; + } + + // Tuple decoders + + static #decodeSmallTuple(dataView, bytes, offset) { + const arity = dataView.getUint8(offset); + const elements = []; + let currentOffset = offset + 1; + + for (let i = 0; i < arity; i++) { + const result = $.#decodeTerm(dataView, bytes, currentOffset); + elements.push(result.term); + currentOffset = result.newOffset; + } + + return { + term: Type.tuple(elements), + newOffset: currentOffset, + }; + } + + static #decodeLargeTuple(dataView, bytes, offset) { + const arity = dataView.getUint32(offset); + const elements = []; + let currentOffset = offset + 4; + + for (let i = 0; i < arity; i++) { + const result = $.#decodeTerm(dataView, bytes, currentOffset); + elements.push(result.term); + currentOffset = result.newOffset; + } + + return { + term: Type.tuple(elements), + newOffset: currentOffset, + }; + } + + // List decoders + + static #decodeString(dataView, bytes, offset) { + const length = dataView.getUint16(offset); + const elements = []; + + for (let i = 0; i < length; i++) { + const byte = bytes[offset + 2 + i]; + elements.push(Type.integer(byte)); + } + + return { + term: Type.list(elements), + newOffset: offset + 2 + length, + }; + } + + static #decodeList(dataView, bytes, offset) { + const length = dataView.getUint32(offset); + const elements = []; + let currentOffset = offset + 4; + + for (let i = 0; i < length; i++) { + const result = $.#decodeTerm(dataView, bytes, currentOffset); + elements.push(result.term); + currentOffset = result.newOffset; + } + + // Decode the tail + const tailResult = $.#decodeTerm(dataView, bytes, currentOffset); + currentOffset = tailResult.newOffset; + + // If tail is NIL (empty list), it's a proper list + if (Type.isList(tailResult.term) && tailResult.term.data.length === 0) { + return { + term: Type.list(elements), + newOffset: currentOffset, + }; + } + + // Otherwise, it's an improper list + elements.push(tailResult.term); + return { + term: Type.improperList(elements), + newOffset: currentOffset, + }; + } + + // Map decoder + + static #decodeMap(dataView, bytes, offset) { + const arity = dataView.getUint32(offset); + const entries = []; + let currentOffset = offset + 4; + + for (let i = 0; i < arity; i++) { + const keyResult = $.#decodeTerm(dataView, bytes, currentOffset); + const valueResult = $.#decodeTerm(dataView, bytes, keyResult.newOffset); + + entries.push([keyResult.term, valueResult.term]); + currentOffset = valueResult.newOffset; + } + + return { + term: Type.map(entries), + newOffset: currentOffset, + }; + } +} + +const $ = EtfDecoder; diff --git a/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs b/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs index 41ecdfaab1..3c73abba8c 100644 --- a/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs +++ b/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs @@ -1946,6 +1946,153 @@ defmodule Hologram.ExJsConsistency.Erlang.ErlangTest do end end + describe "binary_to_term/1" do + test "decodes small positive integer (SMALL_INTEGER_EXT)" do + binary = :erlang.term_to_binary(42) + assert :erlang.binary_to_term(binary) == 42 + end + + test "decodes small positive integer (max value 255)" do + binary = :erlang.term_to_binary(255) + assert :erlang.binary_to_term(binary) == 255 + end + + test "decodes positive integer (INTEGER_EXT)" do + binary = :erlang.term_to_binary(1000) + assert :erlang.binary_to_term(binary) == 1000 + end + + test "decodes negative integer (INTEGER_EXT)" do + binary = :erlang.term_to_binary(-100) + assert :erlang.binary_to_term(binary) == -100 + end + + test "decodes large positive integer (SMALL_BIG_EXT)" do + binary = :erlang.term_to_binary(1_000_000_000_000) + assert :erlang.binary_to_term(binary) == 1_000_000_000_000 + end + + test "decodes large negative integer (SMALL_BIG_EXT)" do + binary = :erlang.term_to_binary(-1_000_000_000_000) + assert :erlang.binary_to_term(binary) == -1_000_000_000_000 + end + + test "decodes UTF-8 atom (SMALL_ATOM_UTF8_EXT)" do + binary = :erlang.term_to_binary(:test) + assert :erlang.binary_to_term(binary) == :test + end + + test "decodes longer atom" do + binary = :erlang.term_to_binary(:test_atom) + assert :erlang.binary_to_term(binary) == :test_atom + end + + test "decodes true atom" do + binary = :erlang.term_to_binary(true) + assert :erlang.binary_to_term(binary) == true + end + + test "decodes false atom" do + binary = :erlang.term_to_binary(false) + assert :erlang.binary_to_term(binary) == false + end + + test "decodes binary string (BINARY_EXT)" do + binary = :erlang.term_to_binary("hello") + assert :erlang.binary_to_term(binary) == "hello" + end + + test "decodes empty binary" do + binary = :erlang.term_to_binary("") + assert :erlang.binary_to_term(binary) == "" + end + + test "decodes small tuple (SMALL_TUPLE_EXT)" do + binary = :erlang.term_to_binary({1, 2, 3}) + assert :erlang.binary_to_term(binary) == {1, 2, 3} + end + + test "decodes empty tuple" do + binary = :erlang.term_to_binary({}) + assert :erlang.binary_to_term(binary) == {} + end + + test "decodes nested tuple" do + binary = :erlang.term_to_binary({1, {2, 3}}) + assert :erlang.binary_to_term(binary) == {1, {2, 3}} + end + + test "decodes empty list (NIL_EXT)" do + binary = :erlang.term_to_binary([]) + assert :erlang.binary_to_term(binary) == [] + end + + test "decodes string list (STRING_EXT)" do + binary = :erlang.term_to_binary([1, 2, 3]) + assert :erlang.binary_to_term(binary) == [1, 2, 3] + end + + test "decodes proper list (LIST_EXT)" do + binary = :erlang.term_to_binary([100, 200, 300]) + assert :erlang.binary_to_term(binary) == [100, 200, 300] + end + + test "decodes empty map" do + binary = :erlang.term_to_binary(%{}) + assert :erlang.binary_to_term(binary) == %{} + end + + test "decodes map with atom keys" do + binary = :erlang.term_to_binary(%{a: 1, b: 2}) + assert :erlang.binary_to_term(binary) == %{a: 1, b: 2} + end + + test "decodes Code.fetch_docs/1 style tuple" do + term = {:docs_v1, 1, :elixir, "text/markdown", %{}, %{}, []} + binary = :erlang.term_to_binary(term) + assert :erlang.binary_to_term(binary) == term + end + + test "decodes complex nested structure" do + term = + {:docs_v1, 1, :elixir, "text/markdown", %{"en" => "Module docs"}, %{since: "1.0.0"}, + [{{:function, :my_func, 2}, 10, ["signature"], %{}, %{}}]} + + binary = :erlang.term_to_binary(term) + assert :erlang.binary_to_term(binary) == term + end + + test "raises ArgumentError if argument is not a binary" do + assert_error ArgumentError, + build_argument_error_msg(1, "not a binary"), + {:erlang, :binary_to_term, [:test]} + end + + test "raises ArgumentError if argument is a non-binary bitstring" do + assert_error ArgumentError, + build_argument_error_msg(1, "not a binary"), + {:erlang, :binary_to_term, [<<1::1, 0::1, 1::1>>]} + end + + test "raises ArgumentError if binary has invalid version byte" do + # Wrong version byte (130 instead of 131) + binary = <<130, 97, 42>> + + assert_error ArgumentError, + build_argument_error_msg(1, "invalid external representation of a term"), + {:erlang, :binary_to_term, [binary]} + end + + test "raises ArgumentError if binary is truncated" do + # Only version byte, no data + binary = <<131>> + + assert_error ArgumentError, + build_argument_error_msg(1, "invalid external representation of a term"), + {:erlang, :binary_to_term, [binary]} + end + end + describe "bit_size/1" do test "bitstring" do assert :erlang.bit_size(<<2::7>>) == 7 diff --git a/test/javascript/erlang/etf_decoder_test.mjs b/test/javascript/erlang/etf_decoder_test.mjs new file mode 100644 index 0000000000..df02f14c2a --- /dev/null +++ b/test/javascript/erlang/etf_decoder_test.mjs @@ -0,0 +1,283 @@ +"use strict"; + +import { + assert, + assertBoxedError, + assertBoxedStrictEqual, + defineGlobalErlangAndElixirModules, +} from "../support/helpers.mjs"; + +import Bitstring from "../../../assets/js/bitstring.mjs"; +import Erlang from "../../../assets/js/erlang/erlang.mjs"; +import Type from "../../../assets/js/type.mjs"; + +defineGlobalErlangAndElixirModules(); + +describe("binary_to_term/1", () => { + const testedFun = Erlang["binary_to_term/1"]; + + describe("integers", () => { + it("decodes small positive integer (SMALL_INTEGER_EXT)", () => { + // :erlang.term_to_binary(42) = <<131, 97, 42>> + const binary = Bitstring.fromBytes(new Uint8Array([131, 97, 42])); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.integer(42)); + }); + + it("decodes small positive integer (max value 255)", () => { + // :erlang.term_to_binary(255) = <<131, 97, 255>> + const binary = Bitstring.fromBytes(new Uint8Array([131, 97, 255])); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.integer(255)); + }); + + it("decodes positive integer (INTEGER_EXT)", () => { + // :erlang.term_to_binary(1000) = <<131, 98, 0, 0, 3, 232>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 98, 0, 0, 3, 232]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.integer(1000)); + }); + + it("decodes negative integer (INTEGER_EXT)", () => { + // :erlang.term_to_binary(-100) = <<131, 98, 255, 255, 255, 156>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 98, 255, 255, 255, 156]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.integer(-100)); + }); + + it("decodes large positive integer (SMALL_BIG_EXT)", () => { + // :erlang.term_to_binary(1000000000000) = <<131, 110, 5, 0, 0, 16, 165, 212, 232>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 110, 5, 0, 0, 16, 165, 212, 232]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.integer(1000000000000n)); + }); + + it("decodes large negative integer (SMALL_BIG_EXT)", () => { + // :erlang.term_to_binary(-1000000000000) = <<131, 110, 5, 1, 0, 16, 165, 212, 232>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 110, 5, 1, 0, 16, 165, 212, 232]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.integer(-1000000000000n)); + }); + }); + + describe("atoms", () => { + it("decodes UTF-8 atom (SMALL_ATOM_UTF8_EXT)", () => { + // :erlang.term_to_binary(:test) = <<131, 119, 4, 116, 101, 115, 116>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 119, 4, 116, 101, 115, 116]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.atom("test")); + }); + + it("decodes longer atom", () => { + // :erlang.term_to_binary(:test_atom) = <<131, 119, 9, 116, 101, 115, 116, 95, 97, 116, 111, 109>> + const binary = Bitstring.fromBytes( + new Uint8Array([ + 131, 119, 9, 116, 101, 115, 116, 95, 97, 116, 111, 109, + ]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.atom("test_atom")); + }); + + it("decodes boolean atoms", () => { + // :erlang.term_to_binary(true) = <<131, 119, 4, 116, 114, 117, 101>> + const trueBinary = Bitstring.fromBytes( + new Uint8Array([131, 119, 4, 116, 114, 117, 101]), + ); + const trueResult = testedFun(trueBinary); + assert.deepStrictEqual(trueResult, Type.atom("true")); + + // :erlang.term_to_binary(false) = <<131, 119, 5, 102, 97, 108, 115, 101>> + const falseBinary = Bitstring.fromBytes( + new Uint8Array([131, 119, 5, 102, 97, 108, 115, 101]), + ); + const falseResult = testedFun(falseBinary); + assert.deepStrictEqual(falseResult, Type.atom("false")); + }); + }); + + describe("binaries", () => { + it("decodes binary string (BINARY_EXT)", () => { + // :erlang.term_to_binary("hello") = <<131, 109, 0, 0, 0, 5, 104, 101, 108, 108, 111>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 109, 0, 0, 0, 5, 104, 101, 108, 108, 111]), + ); + const result = testedFun(binary); + assertBoxedStrictEqual(result, Type.bitstring("hello")); + }); + + it("decodes empty binary", () => { + // :erlang.term_to_binary("") = <<131, 109, 0, 0, 0, 0>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 109, 0, 0, 0, 0]), + ); + const result = testedFun(binary); + assertBoxedStrictEqual(result, Type.bitstring("")); + }); + }); + + describe("tuples", () => { + it("decodes small tuple (SMALL_TUPLE_EXT)", () => { + // :erlang.term_to_binary({1, 2, 3}) = <<131, 104, 3, 97, 1, 97, 2, 97, 3>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 104, 3, 97, 1, 97, 2, 97, 3]), + ); + const result = testedFun(binary); + assert.deepStrictEqual( + result, + Type.tuple([Type.integer(1), Type.integer(2), Type.integer(3)]), + ); + }); + + it("decodes empty tuple", () => { + // :erlang.term_to_binary({}) = <<131, 104, 0>> + const binary = Bitstring.fromBytes(new Uint8Array([131, 104, 0])); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.tuple([])); + }); + + it("decodes nested tuple", () => { + // :erlang.term_to_binary({1, {2, 3}}) = <<131, 104, 2, 97, 1, 104, 2, 97, 2, 97, 3>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 104, 2, 97, 1, 104, 2, 97, 2, 97, 3]), + ); + const result = testedFun(binary); + assert.deepStrictEqual( + result, + Type.tuple([ + Type.integer(1), + Type.tuple([Type.integer(2), Type.integer(3)]), + ]), + ); + }); + }); + + describe("lists", () => { + it("decodes empty list (NIL_EXT)", () => { + // :erlang.term_to_binary([]) = <<131, 106>> + const binary = Bitstring.fromBytes(new Uint8Array([131, 106])); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.list([])); + }); + + it("decodes string list (STRING_EXT)", () => { + // :erlang.term_to_binary([1, 2, 3]) = <<131, 107, 0, 3, 1, 2, 3>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 107, 0, 3, 1, 2, 3]), + ); + const result = testedFun(binary); + assert.deepStrictEqual( + result, + Type.list([Type.integer(1), Type.integer(2), Type.integer(3)]), + ); + }); + + it("decodes proper list (LIST_EXT)", () => { + // :erlang.term_to_binary([100, 200, 300]) = <<131, 108, 0, 0, 0, 3, 97, 100, 98, 0, 0, 0, 200, 98, 0, 0, 1, 44, 106>> + const binary = Bitstring.fromBytes( + new Uint8Array([ + 131, 108, 0, 0, 0, 3, 97, 100, 98, 0, 0, 0, 200, 98, 0, 0, 1, 44, 106, + ]), + ); + const result = testedFun(binary); + assert.deepStrictEqual( + result, + Type.list([Type.integer(100), Type.integer(200), Type.integer(300)]), + ); + }); + }); + + describe("maps", () => { + it("decodes empty map", () => { + // :erlang.term_to_binary(%{}) = <<131, 116, 0, 0, 0, 0>> + const binary = Bitstring.fromBytes( + new Uint8Array([131, 116, 0, 0, 0, 0]), + ); + const result = testedFun(binary); + assert.deepStrictEqual(result, Type.map([])); + }); + + it("decodes map with atom keys", () => { + // :erlang.term_to_binary(%{a: 1, b: 2}) = <<131, 116, 0, 0, 0, 2, 119, 1, 97, 97, 1, 119, 1, 98, 97, 2>> + const binary = Bitstring.fromBytes( + new Uint8Array([ + 131, 116, 0, 0, 0, 2, 119, 1, 97, 97, 1, 119, 1, 98, 97, 2, + ]), + ); + const result = testedFun(binary); + assert.deepStrictEqual( + result, + Type.map([ + [Type.atom("a"), Type.integer(1)], + [Type.atom("b"), Type.integer(2)], + ]), + ); + }); + }); + + describe("complex nested structures", () => { + it("decodes Code.fetch_docs/1 style tuple", () => { + // :erlang.term_to_binary({:docs_v1, 1, :elixir, "text/markdown", %{}, %{}, []}) + const binary = Bitstring.fromBytes( + new Uint8Array([ + 131, 104, 7, 119, 7, 100, 111, 99, 115, 95, 118, 49, 97, 1, 119, 6, + 101, 108, 105, 120, 105, 114, 109, 0, 0, 0, 13, 116, 101, 120, 116, + 47, 109, 97, 114, 107, 100, 111, 119, 110, 116, 0, 0, 0, 0, 116, 0, 0, + 0, 0, 106, + ]), + ); + const result = testedFun(binary); + + assertBoxedStrictEqual( + result, + Type.tuple([ + Type.atom("docs_v1"), + Type.integer(1), + Type.atom("elixir"), + Type.bitstring("text/markdown"), + Type.map([]), + Type.map([]), + Type.list([]), + ]), + ); + }); + }); + + describe("error handling", () => { + it("raises ArgumentError if argument is not a binary", () => { + assertBoxedError( + () => testedFun(Type.atom("test")), + "ArgumentError", + "errors were found at the given arguments:\n\n * 1st argument: not a binary\n", + ); + }); + + it("raises ArgumentError if binary has invalid version byte", () => { + const binary = Bitstring.fromBytes(new Uint8Array([130, 97, 42])); // Wrong version + assertBoxedError( + () => testedFun(binary), + "ArgumentError", + "errors were found at the given arguments:\n\n * 1st argument: invalid external representation of a term\n", + ); + }); + + it("raises ArgumentError if binary is truncated", () => { + const binary = Bitstring.fromBytes(new Uint8Array([131])); // Only version byte + assertBoxedError( + () => testedFun(binary), + "ArgumentError", + "errors were found at the given arguments:\n\n * 1st argument: invalid external representation of a term\n", + ); + }); + }); +}); From d538490e838eea20f1e157e269b76f8179066fc5 Mon Sep 17 00:00:00 2001 From: vinicius pereira Date: Mon, 15 Dec 2025 00:03:33 -0300 Subject: [PATCH 02/33] Enhance Erlang tests with describetags for binary functions - Added @describetag :binary to the test cases for binary_to_list/1 and binary_to_term/1 in erlang_test.exs to improve test organization and clarity. --- test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs b/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs index 3c73abba8c..c6b3a1ca76 100644 --- a/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs +++ b/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs @@ -1917,6 +1917,7 @@ defmodule Hologram.ExJsConsistency.Erlang.ErlangTest do end describe "binary_to_list/1" do + @describetag :binary test "converts a bytes-based binary to a list of integers" do assert :erlang.binary_to_list(<<1, 2, 3>>) == [1, 2, 3] end @@ -1947,6 +1948,7 @@ defmodule Hologram.ExJsConsistency.Erlang.ErlangTest do end describe "binary_to_term/1" do + @describetag :binary test "decodes small positive integer (SMALL_INTEGER_EXT)" do binary = :erlang.term_to_binary(42) assert :erlang.binary_to_term(binary) == 42 From dd7846b2533ff3c658f8377d1be1a3611a1f6cb8 Mon Sep 17 00:00:00 2001 From: vinicius pereira Date: Mon, 15 Dec 2025 00:22:28 -0300 Subject: [PATCH 03/33] chore: removed describe tag, out of scope of this pr --- test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs b/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs index c6b3a1ca76..d8e171cc7d 100644 --- a/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs +++ b/test/elixir/hologram/ex_js_consistency/erlang/erlang_test.exs @@ -1917,7 +1917,6 @@ defmodule Hologram.ExJsConsistency.Erlang.ErlangTest do end describe "binary_to_list/1" do - @describetag :binary test "converts a bytes-based binary to a list of integers" do assert :erlang.binary_to_list(<<1, 2, 3>>) == [1, 2, 3] end From 2e68feef4720c9ec8673dc55d18b6c3ed3885d00 Mon Sep 17 00:00:00 2001 From: vinicius Date: Fri, 16 Jan 2026 23:36:01 -0300 Subject: [PATCH 04/33] wip: migrating etf decoder logic to erlang.mjs [skip ci] --- assets/js/erlang/erlang.mjs | 285 +++++++++++++++++++++++++++++++ assets/js/erlang/etf_decoder.mjs | 1 + 2 files changed, 286 insertions(+) diff --git a/assets/js/erlang/erlang.mjs b/assets/js/erlang/erlang.mjs index 05e88fec21..992037676a 100644 --- a/assets/js/erlang/erlang.mjs +++ b/assets/js/erlang/erlang.mjs @@ -607,6 +607,291 @@ const Erlang = { ); } + // ETF tag constants + const SMALL_INTEGER_EXT = 97; + const INTEGER_EXT = 98; + const ATOM_EXT = 100; + const SMALL_TUPLE_EXT = 104; + const LARGE_TUPLE_EXT = 105; + const NIL_EXT = 106; + const STRING_EXT = 107; + const LIST_EXT = 108; + const BINARY_EXT = 109; + const SMALL_BIG_EXT = 110; + const LARGE_BIG_EXT = 111; + const SMALL_ATOM_EXT = 115; + const MAP_EXT = 116; + const ATOM_UTF8_EXT = 118; + const SMALL_ATOM_UTF8_EXT = 119; + + const decodeTerm = (dataView, bytes, offset) => { + if (offset >= bytes.length) { + Interpreter.raiseArgumentError( + Interpreter.buildArgumentErrorMsg( + 1, + "invalid external representation of a term", + ), + ); + } + + const tag = dataView.getUint8(offset); + + switch (tag) { + case SMALL_INTEGER_EXT: + return decodeSmallInteger(dataView, offset + 1); + + case INTEGER_EXT: + return decodeInteger(dataView, offset + 1); + + case SMALL_BIG_EXT: + return decodeSmallBig(dataView, bytes, offset + 1); + + case LARGE_BIG_EXT: + return decodeLargeBig(dataView, bytes, offset + 1); + + case ATOM_EXT: + return decodeAtom(dataView, bytes, offset + 1, false); + + case SMALL_ATOM_EXT: + return decodeSmallAtom(dataView, bytes, offset + 1, false); + + case ATOM_UTF8_EXT: + return decodeAtom(dataView, bytes, offset + 1, true); + + case SMALL_ATOM_UTF8_EXT: + return decodeSmallAtom(dataView, bytes, offset + 1, true); + + case BINARY_EXT: + return decodeBinary(dataView, bytes, offset + 1); + + case SMALL_TUPLE_EXT: + return decodeSmallTuple(dataView, bytes, offset + 1); + + case LARGE_TUPLE_EXT: + return decodeLargeTuple(dataView, bytes, offset + 1); + + case NIL_EXT: + return {term: Type.list(), newOffset: offset + 1}; + + case STRING_EXT: + return decodeString(dataView, bytes, offset + 1); + + case LIST_EXT: + return decodeList(dataView, bytes, offset + 1); + + case MAP_EXT: + return decodeMap(dataView, bytes, offset + 1); + + default: + Interpreter.raiseArgumentError( + `unsupported external term format tag: ${tag}`, + ); + } + }; + + // Integer decoders + + const decodeSmallInteger = (dataView, offset) => { + const value = dataView.getUint8(offset); + return { + term: Type.integer(value), + newOffset: offset + 1, + }; + }; + + const decodeInteger = (dataView, offset) => { + const value = dataView.getInt32(offset); + return { + term: Type.integer(value), + newOffset: offset + 4, + }; + }; + + const decodeSmallBig = (dataView, bytes, offset) => { + const n = dataView.getUint8(offset); + const sign = dataView.getUint8(offset + 1); + + let value = 0n; + for (let i = 0; i < n; i++) { + const byte = BigInt(bytes[offset + 2 + i]); + value += byte << BigInt(i * 8); + } + + if (sign === 1) { + value = -value; + } + + return { + term: Type.integer(value), + newOffset: offset + 2 + n, + }; + }; + + const decodeLargeBig = (dataView, bytes, offset) => { + const n = dataView.getUint32(offset); + const sign = dataView.getUint8(offset + 4); + + let value = 0n; + for (let i = 0; i < n; i++) { + const byte = BigInt(bytes[offset + 5 + i]); + value += byte << BigInt(i * 8); + } + + if (sign === 1) { + value = -value; + } + + return { + term: Type.integer(value), + newOffset: offset + 5 + n, + }; + }; + + // Atom decoders + + const decodeAtom = (dataView, bytes, offset, isUtf8) => { + const length = dataView.getUint16(offset); + const atomBytes = bytes.slice(offset + 2, offset + 2 + length); + + const decoder = new TextDecoder(isUtf8 ? "utf-8" : "latin1"); + const atomString = decoder.decode(atomBytes); + + return { + term: Type.atom(atomString), + newOffset: offset + 2 + length, + }; + }; + + const decodeSmallAtom = (dataView, bytes, offset, isUtf8) => { + const length = dataView.getUint8(offset); + const atomBytes = bytes.slice(offset + 1, offset + 1 + length); + + const decoder = new TextDecoder(isUtf8 ? "utf-8" : "latin1"); + const atomString = decoder.decode(atomBytes); + + return { + term: Type.atom(atomString), + newOffset: offset + 1 + length, + }; + }; + + // Binary decoder + + const decodeBinary = (dataView, bytes, offset) => { + const length = dataView.getUint32(offset); + const binaryBytes = bytes.slice(offset + 4, offset + 4 + length); + + return { + term: Bitstring.fromBytes(new Uint8Array(binaryBytes)), + newOffset: offset + 4 + length, + }; + }; + + // Tuple decoders + + const decodeSmallTuple = (dataView, bytes, offset) => { + const arity = dataView.getUint8(offset); + const elements = []; + let currentOffset = offset + 1; + + for (let i = 0; i < arity; i++) { + const result = decodeTerm(dataView, bytes, currentOffset); + elements.push(result.term); + currentOffset = result.newOffset; + } + + return { + term: Type.tuple(elements), + newOffset: currentOffset, + }; + }; + + const decodeLargeTuple = (dataView, bytes, offset) => { + const arity = dataView.getUint32(offset); + const elements = []; + let currentOffset = offset + 4; + + for (let i = 0; i < arity; i++) { + const result = decodeTerm(dataView, bytes, currentOffset); + elements.push(result.term); + currentOffset = result.newOffset; + } + + return { + term: Type.tuple(elements), + newOffset: currentOffset, + }; + }; + + // List decoders + + const decodeString = (dataView, bytes, offset) => { + const length = dataView.getUint16(offset); + const elements = []; + + for (let i = 0; i < length; i++) { + const byte = bytes[offset + 2 + i]; + elements.push(Type.integer(byte)); + } + + return { + term: Type.list(elements), + newOffset: offset + 2 + length, + }; + }; + + const decodeList = (dataView, bytes, offset) => { + const length = dataView.getUint32(offset); + const elements = []; + let currentOffset = offset + 4; + + for (let i = 0; i < length; i++) { + const result = decodeTerm(dataView, bytes, currentOffset); + elements.push(result.term); + currentOffset = result.newOffset; + } + + // Decode the tail + const tailResult = decodeTerm(dataView, bytes, currentOffset); + currentOffset = tailResult.newOffset; + + // If tail is NIL (empty list), it's a proper list + if (Type.isList(tailResult.term) && tailResult.term.data.length === 0) { + return { + term: Type.list(elements), + newOffset: currentOffset, + }; + } + + // Otherwise, it's an improper list + elements.push(tailResult.term); + return { + term: Type.improperList(elements), + newOffset: currentOffset, + }; + }; + + // Map decoder + + const decodeMap = (dataView, bytes, offset) => { + const arity = dataView.getUint32(offset); + const entries = []; + let currentOffset = offset + 4; + + for (let i = 0; i < arity; i++) { + const keyResult = decodeTerm(dataView, bytes, currentOffset); + const valueResult = decodeTerm(dataView, bytes, keyResult.newOffset); + + entries.push([keyResult.term, valueResult.term]); + currentOffset = valueResult.newOffset; + } + + return { + term: Type.map(entries), + newOffset: currentOffset, + }; + }; + return EtfDecoder.decode(binary); }, // End binary_to_term/1 diff --git a/assets/js/erlang/etf_decoder.mjs b/assets/js/erlang/etf_decoder.mjs index 0baa8b1655..2017ecdf7b 100644 --- a/assets/js/erlang/etf_decoder.mjs +++ b/assets/js/erlang/etf_decoder.mjs @@ -25,6 +25,7 @@ export default class EtfDecoder { static #ATOM_UTF8_EXT = 118; static #SMALL_ATOM_UTF8_EXT = 119; + // TODO: migrate main logic static decode(binary) { Bitstring.maybeSetBytesFromText(binary); From 426a2bcd22c2eb7c5c60a951057919af414f7e20 Mon Sep 17 00:00:00 2001 From: vinicius Date: Sun, 18 Jan 2026 22:22:19 -0300 Subject: [PATCH 05/33] chore: added main logic - removed old file --- assets/js/erlang/erlang.mjs | 21 +- assets/js/erlang/etf_decoder.mjs | 322 ------------------------------- 2 files changed, 20 insertions(+), 323 deletions(-) delete mode 100644 assets/js/erlang/etf_decoder.mjs diff --git a/assets/js/erlang/erlang.mjs b/assets/js/erlang/erlang.mjs index 992037676a..d57b94b54f 100644 --- a/assets/js/erlang/erlang.mjs +++ b/assets/js/erlang/erlang.mjs @@ -892,7 +892,26 @@ const Erlang = { }; }; - return EtfDecoder.decode(binary); + Bitstring.maybeSetBytesFromText(binary); + + const bytes = binary.bytes; + const dataView = new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ); + + // Check ETF version byte (must be 131) + if (dataView.getUint8(0) !== 131) { + Interpreter.raiseArgumentError( + Interpreter.buildArgumentErrorMsg( + 1, + "invalid external representation of a term", + ), + ); + } + + return decodeTerm(dataView, bytes, 1); }, // End binary_to_term/1 // Deps: [] diff --git a/assets/js/erlang/etf_decoder.mjs b/assets/js/erlang/etf_decoder.mjs deleted file mode 100644 index 2017ecdf7b..0000000000 --- a/assets/js/erlang/etf_decoder.mjs +++ /dev/null @@ -1,322 +0,0 @@ -"use strict"; - -import Bitstring from "../bitstring.mjs"; -import Interpreter from "../interpreter.mjs"; -import Type from "../type.mjs"; - -// Erlang External Term Format (ETF) Decoder -// See: https://www.erlang.org/doc/apps/erts/erl_ext_dist.html - -export default class EtfDecoder { - // ETF tag constants - static #SMALL_INTEGER_EXT = 97; - static #INTEGER_EXT = 98; - static #ATOM_EXT = 100; - static #SMALL_TUPLE_EXT = 104; - static #LARGE_TUPLE_EXT = 105; - static #NIL_EXT = 106; - static #STRING_EXT = 107; - static #LIST_EXT = 108; - static #BINARY_EXT = 109; - static #SMALL_BIG_EXT = 110; - static #LARGE_BIG_EXT = 111; - static #SMALL_ATOM_EXT = 115; - static #MAP_EXT = 116; - static #ATOM_UTF8_EXT = 118; - static #SMALL_ATOM_UTF8_EXT = 119; - - // TODO: migrate main logic - static decode(binary) { - Bitstring.maybeSetBytesFromText(binary); - - const bytes = binary.bytes; - const dataView = new DataView( - bytes.buffer, - bytes.byteOffset, - bytes.byteLength, - ); - - // Check ETF version byte (must be 131) - if (dataView.getUint8(0) !== 131) { - Interpreter.raiseArgumentError( - Interpreter.buildArgumentErrorMsg( - 1, - "invalid external representation of a term", - ), - ); - } - - const result = $.#decodeTerm(dataView, bytes, 1); - return result.term; - } - - static #decodeTerm(dataView, bytes, offset) { - if (offset >= bytes.length) { - Interpreter.raiseArgumentError( - Interpreter.buildArgumentErrorMsg( - 1, - "invalid external representation of a term", - ), - ); - } - - const tag = dataView.getUint8(offset); - - switch (tag) { - case $.#SMALL_INTEGER_EXT: - return $.#decodeSmallInteger(dataView, offset + 1); - - case $.#INTEGER_EXT: - return $.#decodeInteger(dataView, offset + 1); - - case $.#SMALL_BIG_EXT: - return $.#decodeSmallBig(dataView, bytes, offset + 1); - - case $.#LARGE_BIG_EXT: - return $.#decodeLargeBig(dataView, bytes, offset + 1); - - case $.#ATOM_EXT: - return $.#decodeAtom(dataView, bytes, offset + 1, false); - - case $.#SMALL_ATOM_EXT: - return $.#decodeSmallAtom(dataView, bytes, offset + 1, false); - - case $.#ATOM_UTF8_EXT: - return $.#decodeAtom(dataView, bytes, offset + 1, true); - - case $.#SMALL_ATOM_UTF8_EXT: - return $.#decodeSmallAtom(dataView, bytes, offset + 1, true); - - case $.#BINARY_EXT: - return $.#decodeBinary(dataView, bytes, offset + 1); - - case $.#SMALL_TUPLE_EXT: - return $.#decodeSmallTuple(dataView, bytes, offset + 1); - - case $.#LARGE_TUPLE_EXT: - return $.#decodeLargeTuple(dataView, bytes, offset + 1); - - case $.#NIL_EXT: - return {term: Type.list(), newOffset: offset + 1}; - - case $.#STRING_EXT: - return $.#decodeString(dataView, bytes, offset + 1); - - case $.#LIST_EXT: - return $.#decodeList(dataView, bytes, offset + 1); - - case $.#MAP_EXT: - return $.#decodeMap(dataView, bytes, offset + 1); - - default: - Interpreter.raiseArgumentError( - `unsupported external term format tag: ${tag}`, - ); - } - } - - // Integer decoders - - static #decodeSmallInteger(dataView, offset) { - const value = dataView.getUint8(offset); - return { - term: Type.integer(value), - newOffset: offset + 1, - }; - } - - static #decodeInteger(dataView, offset) { - const value = dataView.getInt32(offset); - return { - term: Type.integer(value), - newOffset: offset + 4, - }; - } - - static #decodeSmallBig(dataView, bytes, offset) { - const n = dataView.getUint8(offset); - const sign = dataView.getUint8(offset + 1); - - let value = 0n; - for (let i = 0; i < n; i++) { - const byte = BigInt(bytes[offset + 2 + i]); - value += byte << BigInt(i * 8); - } - - if (sign === 1) { - value = -value; - } - - return { - term: Type.integer(value), - newOffset: offset + 2 + n, - }; - } - - static #decodeLargeBig(dataView, bytes, offset) { - const n = dataView.getUint32(offset); - const sign = dataView.getUint8(offset + 4); - - let value = 0n; - for (let i = 0; i < n; i++) { - const byte = BigInt(bytes[offset + 5 + i]); - value += byte << BigInt(i * 8); - } - - if (sign === 1) { - value = -value; - } - - return { - term: Type.integer(value), - newOffset: offset + 5 + n, - }; - } - - // Atom decoders - - static #decodeAtom(dataView, bytes, offset, isUtf8) { - const length = dataView.getUint16(offset); - const atomBytes = bytes.slice(offset + 2, offset + 2 + length); - - const decoder = new TextDecoder(isUtf8 ? "utf-8" : "latin1"); - const atomString = decoder.decode(atomBytes); - - return { - term: Type.atom(atomString), - newOffset: offset + 2 + length, - }; - } - - static #decodeSmallAtom(dataView, bytes, offset, isUtf8) { - const length = dataView.getUint8(offset); - const atomBytes = bytes.slice(offset + 1, offset + 1 + length); - - const decoder = new TextDecoder(isUtf8 ? "utf-8" : "latin1"); - const atomString = decoder.decode(atomBytes); - - return { - term: Type.atom(atomString), - newOffset: offset + 1 + length, - }; - } - - // Binary decoder - - static #decodeBinary(dataView, bytes, offset) { - const length = dataView.getUint32(offset); - const binaryBytes = bytes.slice(offset + 4, offset + 4 + length); - - return { - term: Bitstring.fromBytes(new Uint8Array(binaryBytes)), - newOffset: offset + 4 + length, - }; - } - - // Tuple decoders - - static #decodeSmallTuple(dataView, bytes, offset) { - const arity = dataView.getUint8(offset); - const elements = []; - let currentOffset = offset + 1; - - for (let i = 0; i < arity; i++) { - const result = $.#decodeTerm(dataView, bytes, currentOffset); - elements.push(result.term); - currentOffset = result.newOffset; - } - - return { - term: Type.tuple(elements), - newOffset: currentOffset, - }; - } - - static #decodeLargeTuple(dataView, bytes, offset) { - const arity = dataView.getUint32(offset); - const elements = []; - let currentOffset = offset + 4; - - for (let i = 0; i < arity; i++) { - const result = $.#decodeTerm(dataView, bytes, currentOffset); - elements.push(result.term); - currentOffset = result.newOffset; - } - - return { - term: Type.tuple(elements), - newOffset: currentOffset, - }; - } - - // List decoders - - static #decodeString(dataView, bytes, offset) { - const length = dataView.getUint16(offset); - const elements = []; - - for (let i = 0; i < length; i++) { - const byte = bytes[offset + 2 + i]; - elements.push(Type.integer(byte)); - } - - return { - term: Type.list(elements), - newOffset: offset + 2 + length, - }; - } - - static #decodeList(dataView, bytes, offset) { - const length = dataView.getUint32(offset); - const elements = []; - let currentOffset = offset + 4; - - for (let i = 0; i < length; i++) { - const result = $.#decodeTerm(dataView, bytes, currentOffset); - elements.push(result.term); - currentOffset = result.newOffset; - } - - // Decode the tail - const tailResult = $.#decodeTerm(dataView, bytes, currentOffset); - currentOffset = tailResult.newOffset; - - // If tail is NIL (empty list), it's a proper list - if (Type.isList(tailResult.term) && tailResult.term.data.length === 0) { - return { - term: Type.list(elements), - newOffset: currentOffset, - }; - } - - // Otherwise, it's an improper list - elements.push(tailResult.term); - return { - term: Type.improperList(elements), - newOffset: currentOffset, - }; - } - - // Map decoder - - static #decodeMap(dataView, bytes, offset) { - const arity = dataView.getUint32(offset); - const entries = []; - let currentOffset = offset + 4; - - for (let i = 0; i < arity; i++) { - const keyResult = $.#decodeTerm(dataView, bytes, currentOffset); - const valueResult = $.#decodeTerm(dataView, bytes, keyResult.newOffset); - - entries.push([keyResult.term, valueResult.term]); - currentOffset = valueResult.newOffset; - } - - return { - term: Type.map(entries), - newOffset: currentOffset, - }; - } -} - -const $ = EtfDecoder; From cdc6fff5c3ffb65b3437c99ffb43b7af2dafae67 Mon Sep 17 00:00:00 2001 From: vinicius Date: Sun, 18 Jan 2026 23:26:30 -0300 Subject: [PATCH 06/33] fix: adjust mix aliases - remove unused import from erlan.mjs - adjust type hint in test function and renderer_dom - 'mix f' formatted renderer_test.exs --- assets/js/erlang/erlang.mjs | 3 +++ lib/hologram/template/renderer.ex | 8 ++++---- test/elixir/hologram/template/renderer_test.exs | 12 ++++++------ .../fixtures/compiler/transformer/module_109.ex | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/assets/js/erlang/erlang.mjs b/assets/js/erlang/erlang.mjs index d57b94b54f..e4bfd7ae60 100644 --- a/assets/js/erlang/erlang.mjs +++ b/assets/js/erlang/erlang.mjs @@ -1,7 +1,10 @@ "use strict"; import Bitstring from "../bitstring.mjs"; +<<<<<<< HEAD import ERTS from "../erts.mjs"; +======= +>>>>>>> 992abcfdf (fix: adjust mix aliases) import HologramBoxedError from "../errors/boxed_error.mjs"; import HologramInterpreterError from "../errors/interpreter_error.mjs"; import Interpreter from "../interpreter.mjs"; diff --git a/lib/hologram/template/renderer.ex b/lib/hologram/template/renderer.ex index 8a76e2739d..cd040e4ae9 100644 --- a/lib/hologram/template/renderer.ex +++ b/lib/hologram/template/renderer.ex @@ -64,11 +64,11 @@ defmodule Hologram.Template.Renderer do {"", %{}, server_struct} end - def render_dom({:element, "slot", _attrs_dom, []}, env, server_struct) do + def render_dom({:element, "slot", _attrs_dom, []}, %Env{} = env, server_struct) do render_dom(env.slots[:default], %Env{env | slots: []}, server_struct) end - def render_dom({:element, tag_name, attrs_dom, children_dom}, env, server_struct) do + def render_dom({:element, tag_name, attrs_dom, children_dom}, %Env{} = env, server_struct) do attrs_html = render_attributes(attrs_dom) children_env = %Env{env | node_type: :element, tag_name: tag_name} @@ -90,7 +90,7 @@ defmodule Hologram.Template.Renderer do {stringify_for_interpolation(value), %{}, server_struct} end - def render_dom({:public_comment, children_dom}, env, server_struct) do + def render_dom({:public_comment, children_dom}, %Env{} = env, server_struct) do children_env = %Env{env | node_type: :public_comment} {children_html, component_registry, mutated_server_struct} = @@ -192,7 +192,7 @@ defmodule Hologram.Template.Renderer do iex> stringify_for_interpolation("hello") "hello" - + iex> stringify_for_interpolation("