diff --git a/.credo.exs b/.credo.exs index bd8b9498..3088698f 100644 --- a/.credo.exs +++ b/.credo.exs @@ -198,7 +198,9 @@ # and be sure to use `mix credo --strict` to see low priority checks) # {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.AliasUsage, []}, {Credo.Check.Design.DuplicatedCode, []}, {Credo.Check.Design.SkipTestWithoutComment, []}, {Credo.Check.Readability.AliasAs, []}, @@ -214,15 +216,21 @@ {Credo.Check.Readability.Specs, []}, {Credo.Check.Readability.StrictModuleLayout, []}, {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, {Credo.Check.Refactor.ABCSize, []}, {Credo.Check.Refactor.AppendSingleItem, []}, {Credo.Check.Refactor.CondInsteadOfIfElse, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, {Credo.Check.Refactor.DoubleBooleanNegation, []}, {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.FunctionArity, []}, {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, {Credo.Check.Refactor.MapMap, []}, {Credo.Check.Refactor.ModuleDependencies, []}, {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.Nesting, []}, {Credo.Check.Refactor.PassAsyncInTestCases, []}, {Credo.Check.Refactor.PipeChainStart, []}, {Credo.Check.Refactor.RejectFilter, []}, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 374e4a1d..aa3707ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,6 @@ jobs: run: curl -fsSL https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/tasks/install.sh | bash - run: mix deps.get - - run: mix npm.get - run: npm install - name: CI run: | @@ -91,7 +90,6 @@ jobs: restore-keys: ${{ runner.os }}-ubsan-27.0-1.18- - run: mix deps.get - - run: mix npm.get - run: mix compile - name: Test run: | diff --git a/.gitignore b/.gitignore index d8ac1dc5..173e0614 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ bun.lock # Git worktrees for parallel agent work .worktrees/ test/support/test_addon.node +fprof.trace diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..ceffbe3b --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,845 @@ +# QuickBEAM Roadmap: BEAM-Native JS Execution + +## Why + +Benchmarks (April 2026, M-series Mac, Zig ReleaseSafe): + +``` +Benchmark QJS (NIF) BEAM (Elixir) Ratio +────────────────────────────────────────────────────── +sum 1M 25,274 µs 715 µs BEAM 35x faster +sum 10M 247,400 µs 7,271 µs BEAM 34x faster +fibonacci(30) 73,183 µs 2,311 µs BEAM 32x faster +arithmetic 10M 220,736 µs 45,104 µs BEAM 5x faster +``` + +BEAM's JIT-compiled tail calls beat native QuickJS by 5-35x on numeric workloads. +The question was: can we run JS on BEAM and still be fast? + +**Yes.** The key insight from measuring different interpreter architectures: + +``` +Interpreter approach µs vs Direct BEAM +────────────────────────────────────────────────────────────────── +Direct BEAM (tail-recursive) 757 1.0x +Flat fn args, one fn per opcode 3,345 4.4x +Binary bytecode + fn clause dispatch 18,868 24.9x +Binary bytecode + case dispatch 17,974 23.7x +Process Dictionary (per-variable) 15,259 20.2x +Map state (update per opcode) 40,936 54.1x +ETS table (per-variable) 64,645 85.4x +``` + +The "flat fn args" approach is **4.4x slower** than direct BEAM but still **7.5x faster +than native QuickJS**. This means a well-designed BEAM interpreter already beats +QuickJS without any JIT compilation. The JIT (compiling hot JS to BEAM bytecode) +is an optimization that closes the 4.4x gap to 1x — important but not existential. + +--- + +## Architecture + +``` +JS source + │ + ▼ +QuickJS compiler (existing, via NIF) ───► QJS bytecode binary + │ + ▼ + ┌─ Pre-decode (one-time) ─┐ + │ Binary → instruction │ + │ tuple array + atom table │ + └────────────┬────────────┘ + │ + ┌────────────────────────────┴───────────────────────┐ + │ BEAM Process │ + │ │ + │ Instruction Tuple Array │ + │ │ │ + │ ▼ │ + │ step(op, stk, locs, frefs, ...) │ + │ │ │ + │ ┌────┴────┐ │ + │ │ 187 fn │ one defp per opcode │ + │ │ clauses │ flat args, no map/tuple alloc │ + │ └────┬────┘ │ + │ │ │ + │ ▼ │ + │ JS Runtime │ + │ (only for dynamic ops: coercion, prototypes, │ + │ property chains, typeof, etc.) │ + │ │ + └────────────────────────────────────────────────────┘ +``` + +**State representation** (flat function args, zero heap allocation in hot loop): + +```elixir +# Hot-path state: all flat args, no struct/map/tuple wrapping +step(op, stk, locs, frefs, atom_tab, const_pool, ip, gas) +# op — current instruction (pre-decoded tuple, e.g. {:add, []}) +# stk — JS stack (Elixir list, prepend/pop = O(1)) +# locs — locals + args (tuple, elem/put_elem = O(1)) +# frefs — closure/var references (tuple) +# atom_tab — atom string table (tuple of binaries) +# const_pool — constant pool (tuple of pre-converted BEAM terms) +# ip — instruction pointer (integer index into instruction array) +# gas — reduction counter for BEAM scheduler cooperation +``` + +--- + +## Phase 0: Bytecode Loader + Decoder + +**Goal**: Parse QJS bytecode binary into a BEAM-friendly format. + +### 0.1 Bytecode Loader + +QuickJS `JS_WriteObject` produces a serialized binary containing: +- Header: magic bytes, version flags +- Atom table: all string atoms used in the module +- Constant pool: numbers, strings, functions, object templates +- Per-function: `JSFunctionBytecode` — args, vars, stack_size, bytecode bytes, closure vars + +QuickBEAM already has `do_compile` in `worker.zig` that produces this binary. +We reuse the existing QuickJS compiler — no need to write our own. + +Deliverables: +- `QuickBEAM.BeamVM.Bytecode` — parses QJS bytecode binary into Elixir structs + +```elixir +defmodule QuickBEAM.BeamVM.Bytecode do + @type function_id :: non_neg_integer() + + @type t :: %__MODULE__{ + atoms: tuple(), # {<<"foo">>, <<"bar">>, ...} + constants: tuple(), # {42, "hello", {:fn_ref, 3}, ...} + functions: %{function_id() => Function.t()}, + module_name: binary() + } + + @type Function :: %Function{ + id: function_id(), + name: binary(), + arg_count: non_neg_integer(), + var_count: non_neg_integer(), + stack_size: non_neg_integer(), + # Pre-decoded instructions: tuple of {opcode_atom, args} + # e.g. {{:push_i32, [42]}, {:get_loc, [0]}, {:add, []}, {:return, []}} + instructions: tuple(), + # Index maps for control flow targets (label → instruction index) + labels: %{non_neg_integer() => non_neg_integer()}, + # Closure variable descriptors + closure_vars: [ClosureVar.t()], + # Source location info (for errors/debugging) + line_number: pos_integer(), + filename: binary() + } +end +``` + +### 0.2 Opcode Decoder + +246 opcodes (187 core + 59 short-form aliases). 32 byte formats. + +**Key design decision**: decode binary bytecode to Elixir terms **once** at load time, +not on every step. The instruction array is a tuple for O(1) indexed access. + +Short-form aliases expand to their canonical form at decode time: +- `get_loc0` → `{:get_loc, [0]}` +- `push_0` → `{:push_i32, [0]}` +- `call0` → `{:call, [0]}` + +This means the interpreter only needs to handle ~187 distinct opcodes. + +Deliverables: +- `QuickBEAM.BeamVM.Decoder` — converts raw QJS bytecode bytes → instruction tuple + +```elixir +defmodule QuickBEAM.BeamVM.Decoder do + @spec decode(binary(), atoms :: tuple(), constants :: tuple()) :: + {:ok, {instructions :: tuple(), labels :: map()}} | {:error, term()} + + # Each instruction is a tuple: {opcode_atom, [args...]} + # Examples: + # {:push_i32, [42]} + # {:get_loc, [3]} + # {:add, []} + # {:if_false, [label_42]} # labels resolved to instruction indices + # {:call, [3]} # arg count + # {:get_field, [atom_index]} +end +``` + +Opcode groups (implementation priority): + +| Priority | Group | Count | Core ops | Notes | +|----------|-------|-------|----------|-------| +| 1 | Stack manipulation | 21 | ~8 | push/dup/drop/swap — trivial | +| 2 | Variables | 58 | ~6 | get_loc/put_loc/get_arg via tuple index | +| 3 | Binary ops | 43 | 20 | add/sub/lt/eq etc. — JSRuntime for coercion | +| 4 | Control flow | 10 | 4 | if_true/if_false/goto/return | +| 5 | Call/control | 24 | 8 | call/return/throw/apply | +| 6 | Property access | 16 | 10 | get_field/put_field — prototype walk | +| 7 | Iterators | ~15 | 6 | for_in/for_of/iterator_next | +| 8 | Scope/closure | 7 | 4 | make_var_ref/closure_var — boxed cells | +| 9 | Helpers | 9 | 5 | typeof/delete/is_undefined | +| 10 | Short forms | 59 | 0 | Expanded at decode time | + +**Estimated effort**: 2-3 weeks +**Lines of code**: ~3000 + +--- + +## Phase 1: Interpreter Core + +**Goal**: Run any pre-decoded JS function in a BEAM process. + +### 1.1 The `step` Function + +One `defp` per opcode. All state as flat function arguments. No struct, no map, +no ETS in the hot path. + +```elixir +defmodule QuickBEAM.BeamVM.Interpreter do + # Fetch next instruction and dispatch + defp next(stk, locs, frefs, insns, ip, gas) do + case gas do + 0 -> {:reduce, stk, locs, frefs, ip} + _ -> + {op, args} = elem(insns, ip) + step(op, args, stk, locs, frefs, insns, ip + 1, gas - 1) + end + end + + # ── Stack manipulation ── + defp step(:push_i32, [val], stk, locs, frefs, insns, ip, gas) do + next([val | stk], locs, frefs, insns, ip, gas) + end + + defp step(:drop, [], [_ | stk], locs, frefs, insns, ip, gas) do + next(stk, locs, frefs, insns, ip, gas) + end + + defp step(:dup, [], [a | _] = stk, locs, frefs, insns, ip, gas) do + next([a | stk], locs, frefs, insns, ip, gas) + end + + defp step(:swap, [], [b, a | rest], locs, frefs, insns, ip, gas) do + next([a, b | rest], locs, frefs, insns, ip, gas) + end + + # ── Variables ── + defp step(:get_loc, [idx], stk, locs, frefs, insns, ip, gas) do + next([elem(locs, idx) | stk], locs, frefs, insns, ip, gas) + end + + defp step(:put_loc, [idx], [val | stk], locs, frefs, insns, ip, gas) do + next(stk, put_elem(locs, idx, val), frefs, insns, ip, gas) + end + + defp step(:get_arg, [idx], stk, locs, frefs, insns, ip, gas) do + # args are stored in locs[0..arg_count-1] + next([elem(locs, idx) | stk], locs, frefs, insns, ip, gas) + end + + # ── Binary ops (delegate to JSRuntime for JS coercion) ── + defp step(:add, [], [b, a | rest], locs, frefs, insns, ip, gas) do + next([JSRuntime.add(a, b) | rest], locs, frefs, insns, ip, gas) + end + + defp step(:sub, [], [b, a | rest], locs, frefs, insns, ip, gas) do + next([JSRuntime.sub(a, b) | rest], locs, frefs, insns, ip, gas) + end + + # ... all 20 binary ops + + # ── Control flow ── + defp step(:if_false, [target], [val | stk], locs, frefs, insns, ip, gas) do + if val == false or val == nil do + next(stk, locs, frefs, insns, target, gas) + else + next(stk, locs, frefs, insns, ip, gas) + end + end + + defp step(:goto, [target], stk, locs, frefs, insns, _ip, gas) do + next(stk, locs, frefs, insns, target, gas) + end + + defp step(:return, [], [val | _], _locs, _frefs, _insns, _ip, _gas) do + {:return, val} + end + + # ── Property access ── + defp step(:get_field, [atom_idx], [obj | stk], locs, frefs, insns, ip, gas) do + key = elem(atoms, atom_idx) # atoms from closure, not shown here + next([JSRuntime.get_property(obj, key) | stk], locs, frefs, insns, ip, gas) + end + + # ── Calls ── + defp step(:call, [argc], stk, locs, frefs, insns, ip, gas) do + {args, [func | rest_stk]} = pop_n(stk, argc) + case JSRuntime.call_function(func, args) do + {:native_return, val} -> + next([val | rest_stk], locs, frefs, insns, ip, gas) + {:call_js, target_fref, new_locs} -> + # Recursive call into another JS function — push return address + # and re-enter the interpreter for the target + call_js(target_fref, new_locs, rest_stk, insns, ip, gas, locs, frefs) + end + end + + # ... ~170 more opcode implementations +end +``` + +### 1.2 Stack Operations + +The JS stack is an Elixir list. All operations are O(1) prepend/pop: + +```elixir +# Push: [val | stack] +# Pop: [top | rest] = stack +# Pop N: Enum.split(stack, n) → {popped, remaining} +# Peek: hd(stack) +``` + +No heap allocation for the list cells themselves in the hot path — BEAM can +reuse list cells when they're provably unreachable (generational GC young-gen +collection is effectively free for short-lived data). + +### 1.3 Locals + +Locals (including args) are a tuple. Indexed by the `loc` argument: + +```elixir +# get_loc(3) → elem(locs, 3) +# put_loc(3, val) → put_elem(locs, 3, val) +``` + +`elem/2` is a BEAM BIF that compiles to a single native instruction (array index). +`put_elem/3` allocates a new tuple but for small tuples (< 10 elements) this is +extremely cheap — just a memcpy of a few words. + +### 1.4 Reduction Counting (BEAM Scheduler Cooperation) + +BEAM preemptively reschedules processes that consume too many reductions. +The `gas` parameter decrements on every opcode. When it hits 0, the interpreter +yields and reschedules itself: + +```elixir +@default_gas 2000 # ~2000 opcodes per time slice + +def run(insns, locs, frefs, gas \\ @default_gas) + +# Entry from caller: +def call_js(fref, args, stk, insns, ip, gas, ret_locs, ret_frefs) do + fun = resolve_function(fref) + locs = build_locals(fun, args) + result = step(elem(fun.instructions, 0), stk, locs, fun.frefs, fun.instructions, 1, gas) + handle_result(result, stk, insns, ip, ret_locs, ret_frefs) +end + +defp handle_result({:return, val}, stk, insns, ip, locs, frefs) do + # Push return value and continue in calling function + next([val | stk], locs, frefs, insns, ip, @default_gas) +end + +defp handle_result({:reduce, stk, locs, frefs, ip}, ...) do + # Yield to BEAM scheduler, then resume + send(self(), {:continue, stk, locs, frefs, ip}) + # Process will be rescheduled, picks up the message, continues +end +``` + +### 1.5 Process Loop + +The interpreter runs inside a BEAM process that also handles messages +(`resolve_call`, `send_message`, etc. — same API as the current NIF-based runtime): + +```elixir +defmodule QuickBEAM.BeamVM.Context do + use GenServer + + def init(opts) do + {:ok, %{bytecode: nil, functions: %{}, globals: %{}}} + end + + def handle_call({:eval, code}, _from, state) do + # 1. Compile JS → QJS bytecode (via existing NIF, one-time) + {:ok, bytecode_binary} = QuickBEAM.compile_nif(code) + # 2. Decode into instruction tuples + {:ok, bytecode} = QuickBEAM.BeamVM.Bytecode.decode(bytecode_binary) + # 3. Run the top-level function + result = QuickBEAM.BeamVM.Interpreter.run(bytecode, 0, state) + {:reply, {:ok, result}, state} + end +end +``` + +### 1.6 Tests + +- Use existing QuickBEAM test suite (1300+ tests) as correctness target +- Compile JS with existing QuickJS NIF, decode bytecode, run on BEAM interpreter +- Compare results byte-for-byte with NIF execution + +**Estimated effort**: 3-5 weeks +**Lines of code**: ~4000 + +--- + +## Phase 2: JS Runtime Library + +**Goal**: Correct JS semantics for all dynamic operations. + +### 2.1 Value Representation + +JS values as plain BEAM terms — no wrappers in the hot path: + +```elixir +# JS values are BEAM terms. No tagged tuples for common types. +# +# JS number (integer) → BEAM integer +# JS number (float) → BEAM float +# JS boolean → BEAM boolean (true/false atoms) +# JS null/undefined → BEAM nil +# JS string → BEAM binary (UTF-8) +# JS symbol → {:js_symbol, binary()} +# JS bigint → {:js_bigint, integer()} +# JS object → {:js_obj, ref()} (ref into process-local store) +# JS array → {:js_arr, ref(), length :: integer()} +# JS function → {:js_fn, fn_ref()} +# JS undefined → nil (same as null — differentiated by context) +``` + +For the interpreter's hot path, numbers/booleans/nil are unboxed BEAM immediates. +Zero overhead for integer arithmetic — the BEAM JIT compiles `a + b` to a single +native `add` instruction when both are small integers. + +### 2.2 Object Store + +Objects live in a process-local store. Two options, benchmarked: + +**Option A: Process dictionary** (20x overhead vs direct — acceptable for property access +which is inherently dynamic): + +```elixir +# Object = {shape_ref, proto_ref, {val1, val2, ...}} +# Stored in process dictionary keyed by ref() +# Shape describes which property names are at which tuple positions +# +# get_property: walk proto chain, find property in shape → elem(values, idx) +# set_property: check shape matches, put_elem(values, idx, val) or transition shape +``` + +**Option B: Dedicated ETS table** (85x overhead — too slow for hot path, but necessary +for shared objects across linked BEAM processes): + +Use Option A for single-context objects, Option B only when crossing process boundaries. + +### 2.3 Shapes (Hidden Classes) + +V8-style hidden classes for fast property access: + +```elixir +defmodule JSRuntime.Shape do + # A shape is an immutable data structure: + # {parent_shape | nil, property_name, index, next_shapes :: %{name => shape_ref}} + # + # Empty object → shape_0 (no properties) + # obj.x = 1 → shape_1 = transition(shape_0, :x, 0) + # obj.y = 2 → shape_2 = transition(shape_1, :y, 1) + # obj.x = 3 → stays at shape_2 (property already exists) + # + # Objects with the same shape store values at the same tuple indices. + # Shape transitions are cached — most property accesses are monomorphic. +end +``` + +Shapes are stored in the process dictionary (they're shared across objects, +not per-object). Transition lookups are O(1) via the `next_shapes` map. + +### 2.4 Core Operations + +```elixir +defmodule JSRuntime do + # ── Type coercion (JS spec: ToPrimitive, ToNumber, ToString, ToBoolean) ── + + @spec add(term(), term()) :: term() + # JS + : ToPrimitive both, if either is string → concat, else numeric add + # Hot path optimization: if both are integers, just return a + b + + @spec strict_eq(term(), term()) :: boolean() + # JS === : no coercion, type + value must match + # nil !== nil is false in JS (null !== undefined), but both map to BEAM nil + # → need special handling + + @spec abstract_eq(term(), term()) :: boolean() + # JS == : complex coercion rules (the infamous == table) + + @spec get_property(term(), term()) :: term() + # 1. ToObject(receiver) + # 2. Walk prototype chain + # 3. Check property descriptor (getter? throw if not writable?) + # Hot path: if shape matches cached shape → direct elem access + + @spec set_property(term(), term(), term()) :: term() + # Similar to get but modifies the object store entry + + @spec call_function(term(), [term()]) :: term() + # JS function call with `this` = undefined (strict) or global (sloppy) + + @spec call_method(term(), term(), [term()]) :: term() + # JS obj.method() with `this` = obj + + @spec typeof(term()) :: binary() + @spec instanceof(term(), term()) :: boolean() + @spec new_object() :: term() + @spec new_array([term()]) :: term() +end +``` + +### 2.5 Built-in Objects + +Minimum viable set: +- `Object` (keys, entries, assign, freeze, defineProperty) +- `Array` (push, pop, map, filter, reduce, slice, splice, indexOf) +- `Function` (bind, call, apply) +- `String` (charAt, substring, split, indexOf, trim, slice, includes) +- `Number` (parseInt, parseFloat, isNaN, isFinite) +- `Math` (floor, ceil, round, abs, min, max, random, PI) +- `JSON` (parse, stringify) +- `Promise` (then, catch, all, race, resolve, reject) +- `Error` (+ TypeError, RangeError, SyntaxError, with stack traces) +- `Date`, `RegExp`, `Map`, `Set` + +**Estimated effort**: 6-8 weeks +**Lines of code**: ~3000 (core) + ~6000 (built-ins) = ~9000 + +--- + +## Phase 3: Integration + Testing + +**Goal**: Replace the NIF thread with a BEAM process for the `:beam` mode, +run the full test suite. + +### 3.1 Dual-Mode API + +```elixir +defmodule QuickBEAM do + def start(opts \\ []) do + mode = Keyword.get(opts, :mode, :nif) + # :nif → current GenServer + NIF thread (unchanged) + # :beam → GenServer + BEAM interpreter (new) + # :both → side-by-side for testing/comparison + case mode do + :nif -> QuickBEAM.Runtime.start_link(opts) + :beam -> QuickBEAM.BeamVM.Context.start_link(opts) + end + end + + # Same public API regardless of mode + def eval(server, code, opts \\ []) + def call(server, name, args, opts \\ []) + def compile(server, code) + def define(server, name, value) + def stop(server) +end +``` + +### 3.2 Compilation Pipeline + +```elixir +# In :beam mode, eval/2 does: +def handle_call({:eval, code}, _from, state) do + # 1. Use existing QuickJS NIF to compile JS → bytecode binary + {:ok, bytecode_binary} = QuickBEAM.Native.compile(code) + + # 2. Decode bytecode into instruction tuples + {:ok, bytecode} = QuickBEAM.BeamVM.Bytecode.decode(bytecode_binary) + + # 3. Execute on BEAM interpreter + result = QuickBEAM.BeamVM.Interpreter.run(bytecode, state) + + {:reply, {:ok, result}, update_state(state, result)} +end +``` + +This reuses the existing QuickJS compiler (battle-tested, spec-compliant). +Only the *execution* moves to BEAM. + +### 3.3 Test Strategy + +``` +1. Port existing test suite to run in :beam mode +2. Compare results between :nif and :beam for every test +3. Discrepancies → runtime library bugs +4. Target: 100% pass rate on existing 1300+ tests +``` + +### 3.4 async/await + +JS `async/await` compiles to a state machine in QJS bytecode. The interpreter +handles the `await` opcode by suspending the function and resuming when the +promise resolves: + +```elixir +defp step(:await, [], [promise | stk], locs, frefs, insns, ip, gas) do + # Return a continuation that the process loop can resume later + {:await, promise, stk, locs, frefs, insns, ip, gas} +end + +# In the process loop: +defp handle_result({:await, promise, stk, locs, frefs, insns, ip, gas}, state) do + # When promise resolves, send {:resolved, value} to self() + # and store the continuation to resume + Promise.on_resolve(promise, fn val -> + send(self(), {:resume_await, val, stk, locs, frefs, insns, ip, gas}) + end) + {:noreply, state} +end + +def handle_info({:resume_await, val, stk, locs, frefs, insns, ip, gas}, state) do + result = next([val | stk], locs, frefs, insns, ip, gas) + handle_result(result, state) +end +``` + +This is more natural on BEAM than in the NIF — the process can actually suspend +and wait for a message, which is exactly how BEAM processes work natively. + +**Estimated effort**: 3-4 weeks +**Lines of code**: ~2500 + +--- + +## Phase 4: Type Profiling (JIT Preparation) + +**Goal**: Collect type information at runtime. + +At this point we already have a working interpreter that beats QuickJS. +Phase 4-5 are optimizations that close the 4.4x gap to ~1x for hot code. + +### 4.1 Inline Caches + +```elixir +# Each call site (function_id, ip_offset) tracks: +# - observed argument types +# - observed object shapes (for property access) +# - observed call targets (for virtual calls) + +# Stored in the process dictionary (one per interpreter process) +# Key: {function_id, ip_offset} +# Value: %IC{types: [...], hits: N, cached: ...} + +defmodule QuickBEAM.BeamVM.IC do + defstruct [:types, :hits, :cached_shape, :cached_target] + + # After 1000 hits with the same type signature → function is "hot" + @hot_threshold 1000 + + def record(ic, types) do + %{ic | hits: ic.hits + 1, types: [types | ic.types |> Enum.take(9)]} + end + + def hot?(%{hits: h}), do: h >= @hot_threshold +end +``` + +### 4.2 Type Feedback Summary + +Before JIT compilation, summarize the ICs: + +```elixir +%{ + {:add, 42} => %{types: [{:int, :int}], hit_rate: 1.0}, + {:get_field, 88} => %{shape: shape_ref_123, hit_rate: 0.99}, + {:call, 156} => %{target: fn_ref_456, hit_rate: 1.0}, +} +``` + +**Estimated effort**: 2-3 weeks +**Lines of code**: ~1200 + +--- + +## Phase 5: JS Bytecode → BEAM Compiler (The JIT) + +**Goal**: Compile hot JS functions to BEAM bytecode at runtime. + +### 5.1 When Is It Worth It? + +The interpreter with flat fn args is 4.4x slower than direct BEAM. +The JIT closes this to ~1x. For a function that takes 1000µs in the interpreter, +the JIT version takes ~230µs. + +The JIT is worth it when: +- A function is called frequently (hot threshold met) +- The function has loops or is called in a loop +- Type profiles show monomorphic types (enables specialization) + +The JIT is NOT worth it when: +- A function is called once (startup/cold code) +- The function is I/O bound (network, file, database) +- Types are megamorphic (no single type dominates — guard overhead > interpreter overhead) + +### 5.2 Translation Pipeline + +``` +Pre-decoded instruction tuple + │ + ▼ +Stack depth analysis (static) + │ + ▼ +Basic blocks + control flow graph + │ + ▼ +BEAM Erlang Abstract Format + │ + ▼ +compile:forms(Forms, [binary]) → beam_bytes + │ + ▼ +code:load_binary(Module, '', beam_bytes) +``` + +Note: we target **Erlang Abstract Format** (not raw BEAM SSA). +`compile:forms/2` accepts the same format as the Erlang parser outputs, +which is much simpler to generate than raw SSA. + +### 5.3 Example Translation + +JS: `function add(a, b) { return a + b; }` + +QJS bytecode: `get_arg0, get_arg1, add, return` + +Type profile: 100% `(integer, integer)` + +Generated Erlang Abstract Format: +```erlang +[ + {attribute, 1, module, js_fn_42_v1}, + {attribute, 1, export, [{func, 2}]}, + {function, 1, func, 2, [ + {clause, 1, + [{var, 1, a}, {var, 1, b}], + [[{call, 1, {remote, 1, {atom,1,erlang},{atom,1,is_integer}}, [{var,1,a}]}, + {call, 1, {remote, 1, {atom,1,erlang},{atom,1,is_integer}}, [{var,1,b}]}]], + [{op, 1, '+', {var,1,a}, {var,1,b}}]}, + {clause, 1, + [{var, 1, a}, {var, 1, b}], + [], + [{call, 1, {remote, 1, {atom,1,js_runtime},{atom,1,add}}, [{var,1,a}, {var,1,b}]}]} + ]}, + {eof, 1} +] +``` + +This compiles to: +``` +func(A, B) -> + case is_integer(A) and is_integer(B) of + true -> A + B; %% ← raw BEAM BIF, JIT-compiles to native add + false -> js_runtime:add(A, B) %% ← fallback to runtime + end. +``` + +For a loop (`for (let i = 0; i < n; i++) sum += arr[i]`), the JIT generates +a tail-recursive BEAM function with type guards. After the BEAM JIT processes it, +it becomes a native loop — the same code as `DirectBEAM.sum` from our benchmarks. + +### 5.4 Deoptimization + +When a type guard fails, fall back to the interpreter: + +```elixir +defp deopt(function_id, ip, stk, locs, frefs, bytecode) do + # Reconstruct interpreter state from SSA values + # Resume at the same bytecode position + fun = Map.fetch!(bytecode.functions, function_id) + QuickBEAM.BeamVM.Interpreter.next(stk, locs, frefs, fun.instructions, ip, @default_gas) +end +``` + +### 5.5 Module Lifecycle + +```elixir +defmodule QuickBEAM.BeamVM.JIT do + @compile_threshold 1000 + + def maybe_compile(function_id, type_feedback, bytecode) do + if hot?(type_feedback) and monomorphic?(type_feedback) do + version = next_version(function_id) + module = :"js_fn_#{function_id}_v#{version}" + + forms = translate(bytecode.functions[function_id], type_feedback) + {:ok, ^module, beam_bytes} = :compile.forms(forms, [binary]) + :code.load_binary(module, '', beam_bytes) + + # Purge previous version + purge_old(function_id, version) + + {:ok, {module, :func}} + else + :interpret + end + end +end +``` + +**Estimated effort**: 6-8 weeks +**Lines of code**: ~4000 (translator) + ~1000 (deopt/module mgmt) = ~5000 + +--- + +## Effort Summary + +| Phase | Description | Effort | LOC | Performance | +|-------|------------|--------|-----|-------------| +| 0 | Bytecode loader + decoder | 2-3 wks | ~3,000 | — | +| 1 | Interpreter core | 3-5 wks | ~4,000 | ~4.4x vs direct BEAM, **7.5x faster than QJS** | +| 2 | JS runtime library | 6-8 wks | ~9,000 | enables all JS programs | +| 3 | Integration + testing | 3-4 wks | ~2,500 | 1300+ tests passing | +| 4 | Type profiling | 2-3 wks | ~1,200 | feeds JIT | +| 5 | JIT compiler | 6-8 wks | ~5,000 | ~1x vs direct BEAM | +| **Total** | | **22-31 wks** | **~25,000** | | + +**The interpreter (Phase 0-2, ~11-16 weeks) already beats QuickJS by 7.5x.** +Phases 3-5 are correctness and further optimization. + +--- + +## Key Risks + +1. **JS coercion semantics**: `JSRuntime.add("1", 2)` must return `"12"` and + `JSRuntime.add(1, 2)` must return `3`. The full ToPrimitive/ToNumber/ToString + chain is complex. Test exhaustively against QuickJS. + +2. **null vs undefined**: Both map to BEAM `nil`. In JS they're different + (`null == undefined` is true, `null === undefined` is false). + May need a sentinel: `{:js_undefined, nil}`. + +3. **Prototype chain performance**: Property access through deep prototype chains + is inherently O(chain_depth). Shape caching (inline caches) mitigates this for + monomorphic access patterns. + +4. **Closure mutability**: JS closures capture by reference. Use cells (boxed refs) + that the closure environment shares. Works but adds indirection. + +5. **Circular references**: BEAM GC doesn't handle cycles in process dictionary + stored objects. Need periodic cycle detection or manual refcounting for the + object store. + +6. **Atom table growth**: Dynamic module names (`js_fn_42_v1`, `js_fn_42_v2`, ...) + create atoms that are never garbage collected. Cap the version count and reuse + module names. + +## Not In Scope + +- Full test262 compliance (target: common real-world JS) +- Web APIs (DOM, fetch) — stay in NIF runtime +- Source-level debugging +- WASM (already in QuickBEAM via separate path) +- Bytecode loader replaces QuickJS execution engine only; the compiler stays as-is diff --git a/bench/README.md b/bench/README.md index fd40a318..574f509f 100644 --- a/bench/README.md +++ b/bench/README.md @@ -17,10 +17,27 @@ Run individual benchmarks: MIX_ENV=bench mix run bench/eval_roundtrip.exs MIX_ENV=bench mix run bench/call_with_data.exs MIX_ENV=bench mix run bench/beam_call.exs +MIX_ENV=bench mix run bench/vm_compiler.exs +MIX_ENV=bench mix run bench/preact_vm.exs MIX_ENV=bench mix run bench/startup.exs MIX_ENV=bench mix run bench/concurrent.exs ``` +### Preact VM benchmark + +`bench/preact_vm.exs` bundles `bench/assets/preact_ssr.js` with Bun and compares +steady-state `QuickBEAM.VM.Interpreter.invoke/3` against +`QuickBEAM.VM.Compiler.invoke/2` on a real Preact component tree workload. +Each Benchee worker builds its own VM/runtime state once, so measurements stay +in-process and do not include cross-process heap setup on every iteration. + +`bench/preact_vm_profile.exs` writes supporting artifacts to `/tmp/`: + +- `preact_vm_render_app_quickjs.txt` +- `preact_vm_render_app_opcodes.txt` +- `preact_vm_beam_disasm.txt` +- `preact_vm_profile_summary.txt` when `:eprof` is unavailable locally + ## Results Apple M1 Pro, Elixir 1.18.4, OTP 27, Zig 0.15.2 (ReleaseFast). diff --git a/bench/assets/preact_ssr.js b/bench/assets/preact_ssr.js new file mode 100644 index 00000000..891a4666 --- /dev/null +++ b/bench/assets/preact_ssr.js @@ -0,0 +1,123 @@ +import { Fragment, cloneElement, h, toChildArray } from "preact"; + +(() => { + const createElement = h; + const clone = cloneElement; + const flatten = toChildArray; + const Frag = Fragment; + + function formatPrice(cents) { + return `$${(cents / 100).toFixed(2)}`; + } + + function Badge({ tone, text }) { + return createElement("span", { class: `badge badge-${tone}` }, text); + } + + function Stat({ label, value }) { + return createElement( + "div", + { class: "stat" }, + createElement("dt", null, label), + createElement("dd", null, value) + ); + } + + function ProductRow({ product, selectedId }) { + const selected = product.id === selectedId; + + return createElement( + "li", + { + class: selected ? "product is-selected" : "product", + "data-id": product.id, + key: product.id + }, + createElement( + "div", + { class: "product-main" }, + createElement("h3", null, product.name), + createElement("p", null, product.description), + createElement( + Frag, + null, + product.tags.map((tag, index) => + createElement(Badge, { + key: `${product.id}:${tag}:${index}`, + tone: index % 2 === 0 ? "info" : "muted", + text: tag + }) + ) + ) + ), + createElement( + "aside", + { class: "product-side" }, + Stat({ label: "Price", value: formatPrice(product.priceCents) }), + Stat({ label: "Stock", value: product.inStock ? "In stock" : "Backorder" }), + Stat({ label: "Rating", value: product.rating.toFixed(1) }) + ) + ); + } + + function ProductList({ title, subtitle, products, selectedId, footerNote }) { + const inStock = products.filter((product) => product.inStock).length; + const averagePrice = + products.reduce((sum, product) => sum + product.priceCents, 0) / products.length; + + return createElement( + "section", + { class: "catalog" }, + createElement( + "header", + { class: "catalog-header" }, + createElement("h1", null, title), + createElement("p", null, subtitle), + createElement( + "div", + { class: "catalog-meta" }, + Stat({ label: "Products", value: products.length }), + Stat({ label: "Available", value: inStock }), + Stat({ label: "Avg price", value: formatPrice(averagePrice) }) + ) + ), + createElement( + "ul", + { class: "products" }, + products.map((product) => ProductRow({ product, selectedId })) + ), + createElement("footer", { class: "catalog-footer" }, footerNote) + ); + } + + function summarizeTree(node) { + if (node == null || typeof node === "boolean") { + return { elements: 0, text: 0, selected: 0 }; + } + + if (typeof node === "string" || typeof node === "number") { + return { elements: 0, text: String(node).length, selected: 0 }; + } + + const props = node.props || {}; + const children = flatten(props.children); + let elements = 1; + let text = 0; + let selected = props.class && props.class.includes("is-selected") ? 1 : 0; + + for (const child of children) { + const stats = summarizeTree(child); + elements += stats.elements; + text += stats.text; + selected += stats.selected; + } + + return { elements, text, selected }; + } + + return function renderApp(props) { + const tree = clone(ProductList(props), { "data-bench": "preact" }); + const stats = summarizeTree(tree); + return `${stats.elements}:${stats.text}:${stats.selected}`; + }; +})(); diff --git a/bench/preact_vm_profile.exs b/bench/preact_vm_profile.exs new file mode 100644 index 00000000..2b37e273 --- /dev/null +++ b/bench/preact_vm_profile.exs @@ -0,0 +1,55 @@ +Code.require_file("support/preact_vm.exs", __DIR__) + +source = Bench.PreactVM.bundle_source!() +props = Bench.PreactVM.props() +{:ok, rt} = Bench.PreactVM.start_runtime() + +%{parsed: parsed, render_app: render_app, render_fun: render_fun, js_props: js_props} = + Bench.PreactVM.build_case!(rt, source, props) + +Bench.PreactVM.warmup(render_app, js_props, 30) + +render_app_qjs = Bench.PreactVM.find_vm_function(parsed.value, &(&1.name == "renderApp")) +beam = Bench.PreactVM.beam_disasm!(render_fun) + +File.write!( + "/tmp/preact_vm_render_app_quickjs.txt", + inspect(render_app_qjs, pretty: true, limit: :infinity) +) + +File.write!( + "/tmp/preact_vm_render_app_opcodes.txt", + inspect(Bench.PreactVM.opcode_histogram(render_app_qjs), pretty: true, limit: :infinity) +) + +File.write!("/tmp/preact_vm_beam_disasm.txt", inspect(beam, pretty: true, limit: :infinity)) + +iterations = System.get_env("PROFILE_ITERS", "200") |> String.to_integer() + +case :code.which(:eprof) do + path when is_list(path) -> + :eprof.start() + + :eprof.profile(fn -> + Enum.each(1..iterations, fn _ -> + Bench.PreactVM.run_compiler!(render_app, js_props) + end) + end) + + :eprof.analyze([:total]) + + :non_existing -> + {elapsed_us, _} = + :timer.tc(fn -> + Enum.each(1..iterations, fn _ -> + Bench.PreactVM.run_compiler!(render_app, js_props) + end) + end) + + File.write!( + "/tmp/preact_vm_profile_summary.txt", + "eprof unavailable on this Erlang installation\niterations=#{iterations}\nelapsed_us=#{elapsed_us}\n" + ) +end + +QuickBEAM.stop(rt) diff --git a/bench/support/preact_vm.exs b/bench/support/preact_vm.exs new file mode 100644 index 00000000..608c3916 --- /dev/null +++ b/bench/support/preact_vm.exs @@ -0,0 +1,148 @@ +defmodule Bench.PreactVM do + alias QuickBEAM.Bytecode + alias QuickBEAM.JS.Bundler + alias QuickBEAM.VM.{Compiler, Heap, Interpreter} + + def bundle_source! do + entry = Path.expand("../assets/preact_ssr.js", __DIR__) + :ok = ensure_bench_deps!() + {:ok, bundled} = Bundler.bundle_file(entry, format: :esm) + bundled + end + + def props do + %{ + "title" => "Featured products", + "subtitle" => "Preact component tree benchmark", + "selectedId" => 18, + "footerNote" => "Updated every 5 minutes", + "products" => + for id <- 1..48 do + %{ + "id" => id, + "name" => "Product #{id}", + "description" => + "A compact component benchmark payload with nested props, tags, and metadata.", + "priceCents" => 1_995 + id * 37, + "inStock" => rem(id, 5) != 0, + "rating" => 3.5 + rem(id, 7) * 0.2, + "tags" => ["fast", "vm", if(rem(id, 2) == 0, do: "featured", else: "sale")] + } + end + } + end + + def start_runtime, do: QuickBEAM.start(apis: false, mode: :beam) + + def build_case!(rt, source, props) do + {:ok, bytecode} = QuickBEAM.compile(rt, source) + {:ok, parsed} = QuickBEAM.VM.Bytecode.decode(bytecode) + cache_function_atoms(parsed) + + :ok = QuickBEAM.set_global(rt, "__bench_props", props, mode: :beam) + js_props = Heap.get_persistent_globals() |> Map.fetch!("__bench_props") + + {:ok, {:closure, _, render_fun} = render_app} = QuickBEAM.eval(rt, source, mode: :beam) + + %{parsed: parsed, render_app: render_app, render_fun: render_fun, js_props: js_props} + end + + def ensure_case!(source, props) do + key = {:preact_vm_case, :erlang.phash2(source)} + + case Process.get(key) do + %{render_app: _render_app, js_props: _js_props} = case_data -> + case_data + + _ -> + {:ok, rt} = start_runtime() + case_data = Map.put(build_case!(rt, source, props), :rt, rt) + warmup(case_data.render_app, case_data.js_props) + Process.put(key, case_data) + case_data + end + end + + def run_interpreter!(render_app, props), + do: Interpreter.invoke(render_app, [props], 1_000_000_000) + + def run_compiler!(render_app, props) do + {:ok, result} = Compiler.invoke(render_app, [props]) + result + end + + def warmup(render_app, props, iterations \\ 20) do + Enum.each(1..iterations, fn _ -> run_compiler!(render_app, props) end) + end + + def beam_disasm!(fun) do + {:ok, beam} = Compiler.disasm(fun) + beam + end + + def find_function(%Bytecode{} = root, name) do + cond do + root.name == name -> + root + + true -> + Enum.find_value(root.cpool, fn + %Bytecode{} = inner -> find_function(inner, name) + _ -> nil + end) + end + end + + def find_vm_function(%QuickBEAM.VM.Bytecode.Function{} = root, pred) do + cond do + pred.(root) -> + root + + true -> + Enum.find_value(root.constants, fn + %QuickBEAM.VM.Bytecode.Function{} = inner -> find_vm_function(inner, pred) + _ -> nil + end) + end + end + + def opcode_histogram(%Bytecode{} = fun) do + fun.opcodes + |> Enum.frequencies_by(&elem(&1, 1)) + |> Enum.sort_by(fn {_op, count} -> -count end) + end + + def opcode_histogram(%QuickBEAM.VM.Bytecode.Function{} = fun) do + {:ok, ops} = QuickBEAM.VM.Decoder.decode(fun.byte_code, fun.arg_count) + + ops + |> Enum.frequencies_by(fn {op, _args} -> elem(QuickBEAM.VM.Opcodes.info(op), 0) end) + |> Enum.sort_by(fn {_op, count} -> -count end) + end + + defp cache_function_atoms(parsed) do + cache_fun = + fn + %QuickBEAM.VM.Bytecode.Function{} = fun, atoms, recur -> + Process.put({:qb_fn_atoms, fun.byte_code}, atoms) + + Enum.each(fun.constants, fn + %QuickBEAM.VM.Bytecode.Function{} = inner -> recur.(inner, atoms, recur) + _ -> :ok + end) + + _other, _atoms, _recur -> + :ok + end + + cache_fun.(parsed.value, parsed.atoms, cache_fun) + end + + defp ensure_bench_deps! do + if "preact" in NPM.NodeModules.installed() do + :ok + else + NPM.install() + end + end +end diff --git a/bench/test262_compare.exs b/bench/test262_compare.exs new file mode 100644 index 00000000..93491cfe --- /dev/null +++ b/bench/test262_compare.exs @@ -0,0 +1,209 @@ +#!/usr/bin/env elixir +# Compare test262 pass rates: QuickJS NIF vs BEAM Compiler vs BEAM Interpreter +# +# Usage: MIX_ENV=test mix run bench/test262_compare.exs [dir_pattern] + +defmodule Test262Compare do + @test262_dir Path.expand("../../quickjs/test262", __DIR__) + @harness_dir Path.join(@test262_dir, "harness") + + def run(dir_pattern \\ "language/expressions") do + test_dir = Path.join(@test262_dir, "test/#{dir_pattern}") + + unless File.dir?(test_dir) do + IO.puts("Directory not found: #{test_dir}") + System.halt(1) + end + + {:ok, rt} = QuickBEAM.start(apis: false, mode: :beam) + + harness = load_harness() + tests = find_tests(test_dir) + IO.puts("Found #{length(tests)} test files in #{dir_pattern}\n") + + results = %{nif: %{pass: 0, fail: 0, error: 0, skip: 0}, + compiler: %{pass: 0, fail: 0, error: 0, skip: 0}, + interpreter: %{pass: 0, fail: 0, error: 0, skip: 0}} + + {total_time, results} = :timer.tc(fn -> + Enum.reduce(tests, results, fn test_file, acc -> + case parse_test(test_file) do + {:skip, _reason} -> + update_all(acc, :skip) + + {:ok, metadata, source} -> + full_source = build_source(harness, metadata, source) + run_test(acc, rt, full_source, metadata) + end + end) + end) + + QuickBEAM.stop(rt) + + IO.puts("\n\n=== Results for #{dir_pattern} (#{length(tests)} tests, #{div(total_time, 1000)}ms) ===\n") + for {mode, stats} <- results do + total = stats.pass + stats.fail + stats.error + pct = if total > 0, do: Float.round(stats.pass / total * 100, 1), else: 0.0 + IO.puts("#{String.pad_trailing(Atom.to_string(mode), 12)} pass=#{stats.pass} fail=#{stats.fail} error=#{stats.error} skip=#{stats.skip} (#{pct}%)") + end + end + + defp load_harness do + assert_js = File.read!(Path.join(@harness_dir, "assert.js")) + sta_js = File.read!(Path.join(@harness_dir, "sta.js")) + + test262error = """ + function Test262Error(message) { + this.message = message || ""; + this.name = "Test262Error"; + } + Test262Error.prototype.toString = function() { + return "Test262Error: " + this.message; + }; + """ + + %{assert: assert_js, sta: sta_js, test262error: test262error} + end + + defp find_tests(dir) do + Path.wildcard(Path.join(dir, "**/*.js")) + |> Enum.reject(&String.contains?(&1, "_FIXTURE")) + |> Enum.sort() + end + + defp parse_test(file) do + source = File.read!(file) + + # Extract YAML metadata + case Regex.run(~r|/\*---\n(.*?)\n---\*/|s, source) do + [_, yaml] -> + features = extract_list(yaml, "features") + flags = extract_list(yaml, "flags") + includes = extract_list(yaml, "includes") + negative = extract_negative(yaml) + + # Skip tests with unsupported features + unsupported = ["SharedArrayBuffer", "Atomics", "Temporal", + "import-assertions", "import-attributes", + "decorators", "regexp-v-flag", "symbols-as-weakmap-keys", + "ShadowRealm", "iterator-helpers", "explicit-resource-management", + "resizable-arraybuffer", "arraybuffer-transfer", + "Float16Array", "uint8array-base64", + "source-phase-imports", "import-defer", + "RegExp.escape", "json-parse-with-source", + "import-text", "import-bytes"] + + if Enum.any?(features, &(&1 in unsupported)) do + {:skip, "unsupported feature"} + else if "async" in flags do + {:skip, "async"} + else if "module" in flags do + {:skip, "module"} + else + {:ok, %{features: features, flags: flags, includes: includes, + negative: negative, raw: flags}, source} + end end end + + _ -> + {:ok, %{features: [], flags: [], includes: [], negative: nil, raw: []}, source} + end + end + + defp extract_list(yaml, key) do + case Regex.run(~r/#{key}:\s*\n((?:\s+-\s+.*\n?)+)/m, yaml) do + [_, list_str] -> + Regex.scan(~r/-\s+(.+)/, list_str) + |> Enum.map(fn [_, v] -> String.trim(v) end) + _ -> [] + end + end + + defp extract_negative(yaml) do + case Regex.run(~r/negative:\s*\n\s+phase:\s+(\w+)\s*\n\s+type:\s+(\w+)/m, yaml) do + [_, phase, type] -> %{phase: phase, type: type} + _ -> nil + end + end + + defp build_source(harness, metadata, test_source) do + includes = Enum.map(metadata.includes, fn inc -> + path = Path.join(@harness_dir, inc) + if File.exists?(path), do: File.read!(path), else: "" + end) + + [harness.test262error, harness.sta, harness.assert | includes] + |> Enum.join("\n") + |> Kernel.<>("\n" <> test_source) + end + + defp run_test(acc, rt, source, metadata) do + expects_error = metadata.negative != nil and metadata.negative.phase in ["parse", "runtime"] + + # NIF + nif_result = run_nif(rt, source, expects_error) + acc = update_in(acc, [:nif, result_key(nif_result)], &(&1 + 1)) + + # Only run BEAM modes if NIF can compile the source + case QuickBEAM.compile(rt, "(function(){ #{source} \n})") do + {:ok, _bc} -> + compiler_result = run_beam(rt, source, :beam, expects_error) + interpreter_result = run_beam(rt, source, :interpreter, expects_error) + acc = update_in(acc, [:compiler, result_key(compiler_result)], &(&1 + 1)) + update_in(acc, [:interpreter, result_key(interpreter_result)], &(&1 + 1)) + + {:error, _} -> + if expects_error do + acc = update_in(acc, [:compiler, :pass], &(&1 + 1)) + update_in(acc, [:interpreter, :pass], &(&1 + 1)) + else + acc = update_in(acc, [:compiler, :error], &(&1 + 1)) + update_in(acc, [:interpreter, :error], &(&1 + 1)) + end + end + end + + defp run_nif(rt, source, expects_error) do + try do + case QuickBEAM.eval(rt, source) do + {:ok, _} -> if expects_error, do: :fail, else: :pass + {:error, %{name: name}} -> + if expects_error, do: :pass, else: :fail + {:error, _} -> if expects_error, do: :pass, else: :fail + end + rescue + _ -> :error + catch + _, _ -> :error + end + end + + defp run_beam(rt, source, mode, expects_error) do + try do + wrapped = "(function(){ #{source} \n})" + case QuickBEAM.eval(rt, wrapped, mode: mode) do + {:ok, _} -> if expects_error, do: :fail, else: :pass + {:error, %{name: name}} -> + if expects_error, do: :pass, else: :fail + {:error, _} -> if expects_error, do: :pass, else: :fail + end + rescue + _ -> :error + catch + _, _ -> :error + end + end + + defp result_key(:pass), do: :pass + defp result_key(:fail), do: :fail + defp result_key(:error), do: :error + + defp update_all(acc, key) do + acc + |> update_in([:nif, key], &(&1 + 1)) + |> update_in([:compiler, key], &(&1 + 1)) + |> update_in([:interpreter, key], &(&1 + 1)) + end +end + +dir = List.first(System.argv()) || "language/expressions" +Test262Compare.run(dir) diff --git a/bench/vm.exs b/bench/vm.exs new file mode 100644 index 00000000..17819ab3 --- /dev/null +++ b/bench/vm.exs @@ -0,0 +1,42 @@ +# Benchmark: NIF (QuickJS native) vs BEAM compiler vs BEAM interpreter +# on Preact SSR — real-world workload. +# +# NIF uses QuickBEAM.call (GenServer round-trip). +# VM paths are in-process (no GenServer). + +Code.require_file("support/preact_vm.exs", __DIR__) + +source = Bench.PreactVM.bundle_source!() +props = Bench.PreactVM.props() + +# ── BEAM VM (interpreter + compiler share the same decoded bytecode) ── + +beam_run = fn invoke -> + %{render_app: app, js_props: jp} = Bench.PreactVM.ensure_case!(source, props) + invoke.(app, jp) +end + +# ── NIF (QuickJS native via Zig NIF) ── + +nif_source = String.replace(source, "(() => {", "var renderApp = (() => {", global: false) + +{:ok, nif_rt} = QuickBEAM.start(apis: false) +{:ok, _} = QuickBEAM.eval(nif_rt, nif_source) +{:ok, _} = QuickBEAM.call(nif_rt, "renderApp", [props]) + +# ── run ── + +Benchee.run( + %{ + "NIF" => fn _ -> {:ok, _} = QuickBEAM.call(nif_rt, "renderApp", [props]) end, + "VM.Compiler" => fn _ -> beam_run.(&Bench.PreactVM.run_compiler!/2) end, + "VM.Interpreter" => fn _ -> beam_run.(&Bench.PreactVM.run_interpreter!/2) end + }, + inputs: %{"preact_ssr" => nil}, + warmup: System.get_env("BENCH_WARMUP", "2") |> String.to_integer(), + time: System.get_env("BENCH_TIME", "5") |> String.to_integer(), + memory_time: System.get_env("BENCH_MEMORY_TIME", "2") |> String.to_integer(), + print: [configuration: false] +) + +QuickBEAM.stop(nif_rt) diff --git a/bun.lock b/bun.lock index b6342099..51ad55f9 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,12 @@ "workspaces": { "": { "name": "quickbeam", + "dependencies": { + "@node-rs/argon2": "^2.0.2", + "@node-rs/bcrypt": "^1.10.7", + "@node-rs/crc32": "^1.10.6", + "sqlite-napi": "^1.0.1", + }, "devDependencies": { "jscpd": "^4.0.8", "oxfmt": "^0.37.0", @@ -23,6 +29,12 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@jscpd/badge-reporter": ["@jscpd/badge-reporter@4.0.4", "", { "dependencies": { "badgen": "^3.2.3", "colors": "^1.4.0", "fs-extra": "^11.2.0" } }, "sha512-I9b4MmLXPM2vo0SxSUWnNGKcA4PjQlD3GzXvFK60z43cN/EIdLbOq3FVwCL+dg2obUqGXKIzAm7EsDFTg0D+mQ=="], "@jscpd/core": ["@jscpd/core@4.0.4", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-QGMT3iXEX1fI6lgjPH+x8eyJwhwr2KkpSF5uBpjC0Z5Xloj0yFTFLtwJT+RhxP/Ob4WYrtx2jvpKB269oIwgMQ=="], @@ -33,12 +45,128 @@ "@jscpd/tokenizer": ["@jscpd/tokenizer@4.0.4", "", { "dependencies": { "@jscpd/core": "4.0.4", "reprism": "^0.0.11", "spark-md5": "^3.0.2" } }, "sha512-xxYYY/qaLah/FlwogEbGIxx9CjDO+G9E6qawcy26WwrflzJb6wsnhjwdneN6Wb0RNCDsqvzY+bzG453jsin4UQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@node-rs/argon2": ["@node-rs/argon2@2.0.2", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "2.0.2", "@node-rs/argon2-android-arm64": "2.0.2", "@node-rs/argon2-darwin-arm64": "2.0.2", "@node-rs/argon2-darwin-x64": "2.0.2", "@node-rs/argon2-freebsd-x64": "2.0.2", "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", "@node-rs/argon2-linux-arm64-gnu": "2.0.2", "@node-rs/argon2-linux-arm64-musl": "2.0.2", "@node-rs/argon2-linux-x64-gnu": "2.0.2", "@node-rs/argon2-linux-x64-musl": "2.0.2", "@node-rs/argon2-wasm32-wasi": "2.0.2", "@node-rs/argon2-win32-arm64-msvc": "2.0.2", "@node-rs/argon2-win32-ia32-msvc": "2.0.2", "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg=="], + + "@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@2.0.2", "", { "os": "android", "cpu": "arm" }, "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw=="], + + "@node-rs/argon2-android-arm64": ["@node-rs/argon2-android-arm64@2.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg=="], + + "@node-rs/argon2-darwin-arm64": ["@node-rs/argon2-darwin-arm64@2.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA=="], + + "@node-rs/argon2-darwin-x64": ["@node-rs/argon2-darwin-x64@2.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw=="], + + "@node-rs/argon2-freebsd-x64": ["@node-rs/argon2-freebsd-x64@2.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg=="], + + "@node-rs/argon2-linux-arm-gnueabihf": ["@node-rs/argon2-linux-arm-gnueabihf@2.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww=="], + + "@node-rs/argon2-linux-arm64-gnu": ["@node-rs/argon2-linux-arm64-gnu@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew=="], + + "@node-rs/argon2-linux-arm64-musl": ["@node-rs/argon2-linux-arm64-musl@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA=="], + + "@node-rs/argon2-linux-x64-gnu": ["@node-rs/argon2-linux-x64-gnu@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA=="], + + "@node-rs/argon2-linux-x64-musl": ["@node-rs/argon2-linux-x64-musl@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw=="], + + "@node-rs/argon2-wasm32-wasi": ["@node-rs/argon2-wasm32-wasi@2.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ=="], + + "@node-rs/argon2-win32-arm64-msvc": ["@node-rs/argon2-win32-arm64-msvc@2.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ=="], + + "@node-rs/argon2-win32-ia32-msvc": ["@node-rs/argon2-win32-ia32-msvc@2.0.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ=="], + + "@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@2.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw=="], + + "@node-rs/bcrypt": ["@node-rs/bcrypt@1.10.7", "", { "optionalDependencies": { "@node-rs/bcrypt-android-arm-eabi": "1.10.7", "@node-rs/bcrypt-android-arm64": "1.10.7", "@node-rs/bcrypt-darwin-arm64": "1.10.7", "@node-rs/bcrypt-darwin-x64": "1.10.7", "@node-rs/bcrypt-freebsd-x64": "1.10.7", "@node-rs/bcrypt-linux-arm-gnueabihf": "1.10.7", "@node-rs/bcrypt-linux-arm64-gnu": "1.10.7", "@node-rs/bcrypt-linux-arm64-musl": "1.10.7", "@node-rs/bcrypt-linux-x64-gnu": "1.10.7", "@node-rs/bcrypt-linux-x64-musl": "1.10.7", "@node-rs/bcrypt-wasm32-wasi": "1.10.7", "@node-rs/bcrypt-win32-arm64-msvc": "1.10.7", "@node-rs/bcrypt-win32-ia32-msvc": "1.10.7", "@node-rs/bcrypt-win32-x64-msvc": "1.10.7" } }, "sha512-1wk0gHsUQC/ap0j6SJa2K34qNhomxXRcEe3T8cI5s+g6fgHBgLTN7U9LzWTG/HE6G4+2tWWLeCabk1wiYGEQSA=="], + + "@node-rs/bcrypt-android-arm-eabi": ["@node-rs/bcrypt-android-arm-eabi@1.10.7", "", { "os": "android", "cpu": "arm" }, "sha512-8dO6/PcbeMZXS3VXGEtct9pDYdShp2WBOWlDvSbcRwVqyB580aCBh0BEFmKYtXLzLvUK8Wf+CG3U6sCdILW1lA=="], + + "@node-rs/bcrypt-android-arm64": ["@node-rs/bcrypt-android-arm64@1.10.7", "", { "os": "android", "cpu": "arm64" }, "sha512-UASFBS/CucEMHiCtL/2YYsAY01ZqVR1N7vSb94EOvG5iwW7BQO06kXXCTgj+Xbek9azxixrCUmo3WJnkJZ0hTQ=="], + + "@node-rs/bcrypt-darwin-arm64": ["@node-rs/bcrypt-darwin-arm64@1.10.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DgzFdAt455KTuiJ/zYIyJcKFobjNDR/hnf9OS7pK5NRS13Nq4gLcSIIyzsgHwZHxsJWbLpHmFc1H23Y7IQoQBw=="], + + "@node-rs/bcrypt-darwin-x64": ["@node-rs/bcrypt-darwin-x64@1.10.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-SPWVfQ6sxSokoUWAKWD0EJauvPHqOGQTd7CxmYatcsUgJ/bruvEHxZ4bIwX1iDceC3FkOtmeHO0cPwR480n/xA=="], + + "@node-rs/bcrypt-freebsd-x64": ["@node-rs/bcrypt-freebsd-x64@1.10.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gpa+Ixs6GwEx6U6ehBpsQetzUpuAGuAFbOiuLB2oo4N58yU4AZz1VIcWyWAHrSWRs92O0SHtmo2YPrMrwfBbSw=="], + + "@node-rs/bcrypt-linux-arm-gnueabihf": ["@node-rs/bcrypt-linux-arm-gnueabihf@1.10.7", "", { "os": "linux", "cpu": "arm" }, "sha512-kYgJnTnpxrzl9sxYqzflobvMp90qoAlaX1oDL7nhNTj8OYJVDIk0jQgblj0bIkjmoPbBed53OJY/iu4uTS+wig=="], + + "@node-rs/bcrypt-linux-arm64-gnu": ["@node-rs/bcrypt-linux-arm64-gnu@1.10.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-7cEkK2RA+gBCj2tCVEI1rDSJV40oLbSq7bQ+PNMHNI6jCoXGmj9Uzo7mg7ZRbNZ7piIyNH5zlJqutjo8hh/tmA=="], + + "@node-rs/bcrypt-linux-arm64-musl": ["@node-rs/bcrypt-linux-arm64-musl@1.10.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-X7DRVjshhwxUqzdUKDlF55cwzh+wqWJ2E/tILvZPboO3xaNO07Um568Vf+8cmKcz+tiZCGP7CBmKbBqjvKN/Pw=="], + + "@node-rs/bcrypt-linux-x64-gnu": ["@node-rs/bcrypt-linux-x64-gnu@1.10.7", "", { "os": "linux", "cpu": "x64" }, "sha512-LXRZsvG65NggPD12hn6YxVgH0W3VR5fsE/o1/o2D5X0nxKcNQGeLWnRzs5cP8KpoFOuk1ilctXQJn8/wq+Gn/Q=="], + + "@node-rs/bcrypt-linux-x64-musl": ["@node-rs/bcrypt-linux-x64-musl@1.10.7", "", { "os": "linux", "cpu": "x64" }, "sha512-tCjHmct79OfcO3g5q21ME7CNzLzpw1MAsUXCLHLGWH+V6pp/xTvMbIcLwzkDj6TI3mxK6kehTn40SEjBkZ3Rog=="], + + "@node-rs/bcrypt-wasm32-wasi": ["@node-rs/bcrypt-wasm32-wasi@1.10.7", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-4qXSihIKeVXYglfXZEq/QPtYtBUvR8d3S85k15Lilv3z5B6NSGQ9mYiNleZ7QHVLN2gEc5gmi7jM353DMH9GkA=="], + + "@node-rs/bcrypt-win32-arm64-msvc": ["@node-rs/bcrypt-win32-arm64-msvc@1.10.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-FdfUQrqmDfvC5jFhntMBkk8EI+fCJTx/I1v7Rj+Ezlr9rez1j1FmuUnywbBj2Cg15/0BDhwYdbyZ5GCMFli2aQ=="], + + "@node-rs/bcrypt-win32-ia32-msvc": ["@node-rs/bcrypt-win32-ia32-msvc@1.10.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-lZLf4Cx+bShIhU071p5BZft4OvP4PGhyp542EEsb3zk34U5GLsGIyCjOafcF/2DGewZL6u8/aqoxbSuROkgFXg=="], + + "@node-rs/bcrypt-win32-x64-msvc": ["@node-rs/bcrypt-win32-x64-msvc@1.10.7", "", { "os": "win32", "cpu": "x64" }, "sha512-hdw7tGmN1DxVAMTzICLdaHpXjy+4rxaxnBMgI8seG1JL5e3VcRGsd1/1vVDogVp2cbsmgq+6d6yAY+D9lW/DCg=="], + + "@node-rs/crc32": ["@node-rs/crc32@1.10.6", "", { "optionalDependencies": { "@node-rs/crc32-android-arm-eabi": "1.10.6", "@node-rs/crc32-android-arm64": "1.10.6", "@node-rs/crc32-darwin-arm64": "1.10.6", "@node-rs/crc32-darwin-x64": "1.10.6", "@node-rs/crc32-freebsd-x64": "1.10.6", "@node-rs/crc32-linux-arm-gnueabihf": "1.10.6", "@node-rs/crc32-linux-arm64-gnu": "1.10.6", "@node-rs/crc32-linux-arm64-musl": "1.10.6", "@node-rs/crc32-linux-x64-gnu": "1.10.6", "@node-rs/crc32-linux-x64-musl": "1.10.6", "@node-rs/crc32-wasm32-wasi": "1.10.6", "@node-rs/crc32-win32-arm64-msvc": "1.10.6", "@node-rs/crc32-win32-ia32-msvc": "1.10.6", "@node-rs/crc32-win32-x64-msvc": "1.10.6" } }, "sha512-+llXfqt+UzgoDzT9of5vPQPGqTAVCohU74I9zIBkNo5TH6s2P31DFJOGsJQKN207f0GHnYv5pV3wh3BCY/un/A=="], + + "@node-rs/crc32-android-arm-eabi": ["@node-rs/crc32-android-arm-eabi@1.10.6", "", { "os": "android", "cpu": "arm" }, "sha512-vZAMuJXm3TpWPOkkhxdrofWDv+Q+I2oO7ucLRbXyAPmXFNDhHtBxbO1rk9Qzz+M3eep8ieS4/+jCL1Q0zacNMQ=="], + + "@node-rs/crc32-android-arm64": ["@node-rs/crc32-android-arm64@1.10.6", "", { "os": "android", "cpu": "arm64" }, "sha512-Vl/JbjCinCw/H9gEpZveWCMjxjcEChDcDBM8S4hKay5yyoRCUHJPuKr4sjVDBeOm+1nwU3oOm6Ca8dyblwp4/w=="], + + "@node-rs/crc32-darwin-arm64": ["@node-rs/crc32-darwin-arm64@1.10.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kARYANp5GnmsQiViA5Qu74weYQ3phOHSYQf0G+U5wB3NB5JmBHnZcOc46Ig21tTypWtdv7u63TaltJQE41noyg=="], + + "@node-rs/crc32-darwin-x64": ["@node-rs/crc32-darwin-x64@1.10.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q99bevJVMfLTISpkpKBlXgtPUItrvTWKFyiqoKH5IvscZmLV++NH4V13Pa17GTBmv9n18OwzgQY4/SRq6PQNVA=="], + + "@node-rs/crc32-freebsd-x64": ["@node-rs/crc32-freebsd-x64@1.10.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-66hpawbNjrgnS9EDMErta/lpaqOMrL6a6ee+nlI2viduVOmRZWm9Rg9XdGTK/+c4bQLdtC6jOd+Kp4EyGRYkAg=="], + + "@node-rs/crc32-linux-arm-gnueabihf": ["@node-rs/crc32-linux-arm-gnueabihf@1.10.6", "", { "os": "linux", "cpu": "arm" }, "sha512-E8Z0WChH7X6ankbVm8J/Yym19Cq3otx6l4NFPS6JW/cWdjv7iw+Sps2huSug+TBprjbcEA+s4TvEwfDI1KScjg=="], + + "@node-rs/crc32-linux-arm64-gnu": ["@node-rs/crc32-linux-arm64-gnu@1.10.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-LmWcfDbqAvypX0bQjQVPmQGazh4dLiVklkgHxpV4P0TcQ1DT86H/SWpMBMs/ncF8DGuCQ05cNyMv1iddUDugoQ=="], + + "@node-rs/crc32-linux-arm64-musl": ["@node-rs/crc32-linux-arm64-musl@1.10.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-k8ra/bmg0hwRrIEE8JL1p32WfaN9gDlUUpQRWsbxd1WhjqvXea7kKO6K4DwVxyxlPhBS9Gkb5Urq7Y4mXANzaw=="], + + "@node-rs/crc32-linux-x64-gnu": ["@node-rs/crc32-linux-x64-gnu@1.10.6", "", { "os": "linux", "cpu": "x64" }, "sha512-IfjtqcuFK7JrSZ9mlAFhb83xgium30PguvRjIMI45C3FJwu18bnLk1oR619IYb/zetQT82MObgmqfKOtgemEKw=="], + + "@node-rs/crc32-linux-x64-musl": ["@node-rs/crc32-linux-x64-musl@1.10.6", "", { "os": "linux", "cpu": "x64" }, "sha512-LbFYsA5M9pNunOweSt6uhxenYQF94v3bHDAQRPTQ3rnjn+mK6IC7YTAYoBjvoJP8lVzcvk9hRj8wp4Jyh6Y80g=="], + + "@node-rs/crc32-wasm32-wasi": ["@node-rs/crc32-wasm32-wasi@1.10.6", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-KaejdLgHMPsRaxnM+OG9L9XdWL2TabNx80HLdsCOoX9BVhEkfh39OeahBo8lBmidylKbLGMQoGfIKDjq0YMStw=="], + + "@node-rs/crc32-win32-arm64-msvc": ["@node-rs/crc32-win32-arm64-msvc@1.10.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-x50AXiSxn5Ccn+dCjLf1T7ZpdBiV1Sp5aC+H2ijhJO4alwznvXgWbopPRVhbp2nj0i+Gb6kkDUEyU+508KAdGQ=="], + + "@node-rs/crc32-win32-ia32-msvc": ["@node-rs/crc32-win32-ia32-msvc@1.10.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-DpDxQLaErJF9l36aghe1Mx+cOnYLKYo6qVPqPL9ukJ5rAGLtCdU0C+Zoi3gs9ySm8zmbFgazq/LvmsZYU42aBw=="], + + "@node-rs/crc32-win32-x64-msvc": ["@node-rs/crc32-win32-x64-msvc@1.10.6", "", { "os": "win32", "cpu": "x64" }, "sha512-5B1vXosIIBw1m2Rcnw62IIfH7W9s9f7H7Ma0rRuhT8HR4Xh8QCgw6NJSI2S2MCngsGktYnAhyUvs81b7efTyQw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qAS6Hg8Q14ckfBuqJ2Zh7gBQSVSUHeibSq4OFqBTv6DzyJuxYlr0sdYQzmYmnbPxbqobekqUDTa/4XEaqRi7vg=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-kGePeDD4IN4imo+H4uLjQGZLmvyYQg+nKr2P0nt4ksXXrWA4HE+mb0/TUPHfRI127DocXQpew+fvrHuHR5mpJQ=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-gMEQayUpmCPYaE9zkNBj9TiQqHupnhjOYcuSzxFjzIjHJBUO4VjNnrpbKVeXNs+rKHFothORDd2QKquu5paSPQ=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-NbLOJdr+RBFO1vFZ2YUFg4oVJ+2ua6zrwo4ZWRs0jKKcGJWtbY2wY5uz+i0PkwH6b9HYaYDgVTzE4ev06ncYZw=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-UV9EE18VE5aRhWtV2L6MTAGGn3slhJJ2OW/m+FJM15maHm0qf1V7TaZY0FovxhdQRvnklSiQ7Ntv0H5TUX4w0g=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-UwttIUXoe9fS+40OcjoaRHgZw+HCPFqBVWEXkXqAJ3W7wA0XPZrWsoMAD9sGh3TaLqrwdiMo5xPogwpXhOtVXA=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-fOi4ziKzgJG4UrrNd4AicBs6Fu9GY5xOqg+9tC76nuZNDAdSh6++kzab6TNi1Ck0Yzq6zIBIdGit6/0uSbBn8A=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-+VHhE44kEjCXcTFHyc81zfTxL9+vzh9RqIh7gM1iWNhxpctD9kzntbUkP3UTFTwwNjoou1o8VRyxQafvc4OepA=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-fqBKuiiWLEu2dVkowZaXgKS98xfrvBqivdoxRtRP3eINcpI1dcelGbsOz+Xphn7tbGAuBiE1/0AelvvvdqS9rg=="], + + "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-+EvdRWRCRg95Xea4M2lqSJFTjzQBTJDQTMlbG8bmwFkVTN16MdmSH7xhfxVQWUOyZBLEpIwuNFIlBBxVCwSUyQ=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-vqDEFX63ZZQF3YstPSpPD+RxNm5AILPdUuuKpNwsj7ld4NjhdHUYkAmLXDtKNWt9JMRL10bop//W8faY/LV+RQ=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-6gy4hhQSjq/T/S9hC9m3NxY0RY+9Ww+XNlB+8koIMTsMSYEjk7Ho+hFHQz1Bn4W61Ub7Vykufg+jgDgPfa2GFA=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.37.0", "", { "os": "android", "cpu": "arm" }, "sha512-2AW4VHG6mePEb1r4l6nBOVz1MwevNa0obayXd5Xce+gtP+cL/FCaoVK7JtpqCj4cEVxbLU4jijBUIWK41X2GGg=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.37.0", "", { "os": "android", "cpu": "arm64" }, "sha512-fW/oGfK337wYb/qfoeqKrcv3tMv7DlsKVmHca0DZrWHLMUYftpYD9z7TYOD5VQ1Lg8D/iTzQiTneT2CAMThPxg=="], @@ -127,6 +255,8 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-wikx9I9J9/lPOZlrCCNgm8YjWkia8NZfhWd1TTvZTMguyChbw/oA2VEM6Fzx+kkpA+1qu5Mo7nrLdOXEJavw8g=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], "acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], @@ -145,6 +275,8 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "bun": ["bun@1.3.13", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.13", "@oven/bun-darwin-x64": "1.3.13", "@oven/bun-darwin-x64-baseline": "1.3.13", "@oven/bun-linux-aarch64": "1.3.13", "@oven/bun-linux-aarch64-musl": "1.3.13", "@oven/bun-linux-x64": "1.3.13", "@oven/bun-linux-x64-baseline": "1.3.13", "@oven/bun-linux-x64-musl": "1.3.13", "@oven/bun-linux-x64-musl-baseline": "1.3.13", "@oven/bun-windows-aarch64": "1.3.13", "@oven/bun-windows-x64": "1.3.13", "@oven/bun-windows-x64-baseline": "1.3.13" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-b9T4xZ8KqCHs4+TkHJv540LG1B8OD7noKu0Qaizusx3jFtMDHY6osNqgbaOlwW2B8RB2AKzz+sjzlGKIGxIjZw=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -325,6 +457,8 @@ "spark-md5": ["spark-md5@3.0.2", "", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], + "sqlite-napi": ["sqlite-napi@1.0.1", "", { "dependencies": { "sqlite-napi": "^1.0.0" }, "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-qgW6ebeXgg+4JebrFe00sn62lieyMHoK+6ExF7h+QwoP8Lq8H8TxuxGWqpc+b1n+67suPpdpLF3h6AGiJ25P4g=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -339,6 +473,8 @@ "token-stream": ["token-stream@1.0.0", "", {}, "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], diff --git a/lib/quickbeam.ex b/lib/quickbeam.ex index 8271c0fc..3520d36e 100644 --- a/lib/quickbeam.ex +++ b/lib/quickbeam.ex @@ -1,4 +1,17 @@ defmodule QuickBEAM do + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.Bytecode + alias QuickBEAM.JSError + alias QuickBEAM.Native + alias QuickBEAM.Runtime + alias QuickBEAM.VM.Bytecode, as: BeamBytecode + alias QuickBEAM.VM.Compiler, as: BeamCompiler + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.PromiseState, as: Promise + alias QuickBEAM.VM.Runtime, as: BeamRuntime + @moduledoc """ QuickJS-NG JavaScript engine embedded in the BEAM. @@ -58,7 +71,7 @@ defmodule QuickBEAM do @doc false def child_spec(opts) do - QuickBEAM.Runtime.child_spec(opts) + Runtime.child_spec(opts) end @doc """ @@ -88,7 +101,17 @@ defmodule QuickBEAM do """ @spec start(keyword()) :: GenServer.on_start() def start(opts \\ []) do - QuickBEAM.Runtime.start_link(opts) + opts = + if Keyword.has_key?(opts, :mode) do + opts + else + case System.get_env("QUICKBEAM_MODE") do + "beam" -> Keyword.put(opts, :mode, :beam) + _ -> opts + end + end + + Runtime.start_link(opts) end @doc """ @@ -127,7 +150,194 @@ defmodule QuickBEAM do """ @spec eval(runtime(), String.t(), keyword()) :: js_result() def eval(runtime, code, opts \\ []) do - QuickBEAM.Runtime.eval(runtime, code, opts) + if resolve_mode(runtime, opts) == :beam do + eval_beam(runtime, code, opts) + else + Runtime.eval(runtime, code, opts) + end + end + + defp resolve_mode(runtime, opts) do + case Keyword.get(opts, :mode) do + nil -> + case Heap.get_runtime_mode(runtime) do + nil -> + mode = + try do + GenServer.call(runtime, :get_mode, 1000) + catch + :exit, _ -> :nif + end + + Heap.put_runtime_mode(runtime, mode) + mode + + cached -> + cached + end + + mode -> + mode + end + end + + defp eval_beam(runtime, code, opts) do + handler_globals = + case Heap.get_handler_globals() do + nil -> + handlers = + try do + GenServer.call(runtime, :get_handlers, 1000) + catch + :exit, _ -> %{} + end + + globals = + for {name, handler} <- handlers, into: %{} do + {name, + {:builtin, name, + fn args -> + case handler do + {:with_caller, fun} -> fun.(args, self()) + fun when is_function(fun, 1) -> fun.(args) + _ -> :undefined + end + end}} + end + + Heap.put_handler_globals(globals) + globals + + cached -> + cached + end + + case Runtime.compile(runtime, code, Keyword.get(opts, :filename, "")) do + {:ok, bc} -> + case BeamBytecode.decode(bc) do + {:ok, parsed} -> + result = + Interpreter.eval( + parsed.value, + [], + %{gas: 1_000_000_000, runtime_pid: runtime, globals: handler_globals}, + parsed.atoms + ) + + Promise.drain_microtasks() + converted = convert_beam_result(result) + Heap.gc(beam_gc_roots(result)) + converted + + {:error, _} = err -> + err + end + + {:error, _} = err -> + err + end + end + + defp convert_beam_result({:error, {:js_throw, {:obj, _ref} = obj}}) do + val = convert_beam_value(obj) + {:error, wrap_js_error(val)} + end + + defp convert_beam_result({:error, {:js_throw, val}}) do + {:error, wrap_js_error(convert_beam_value(val))} + end + + defp convert_beam_result({:ok, {:obj, ref}}) do + {:ok, convert_beam_value({:obj, ref})} + end + + defp convert_beam_result({:ok, val}), do: {:ok, convert_beam_value(val)} + defp convert_beam_result({:error, _} = err), do: err + + defp wrap_js_error(val), do: JSError.from_js_value(val) + + defp beam_gc_roots({:ok, value}), do: [value] + defp beam_gc_roots({:error, {:js_throw, value}}), do: [value] + defp beam_gc_roots(_), do: [] + + defp elixir_to_js(val) when is_map(val) do + ref = make_ref() + obj = Map.new(val, fn {k, v} -> {to_string(k), elixir_to_js(v)} end) + Heap.put_obj(ref, obj) + {:obj, ref} + end + + defp elixir_to_js(val) when is_list(val) do + ref = make_ref() + Heap.put_obj(ref, Enum.map(val, &elixir_to_js/1)) + {:obj, ref} + end + + defp elixir_to_js(val), do: val + + defp convert_beam_value(:undefined), do: nil + + defp convert_beam_value({:obj, ref}) do + case Heap.get_obj(ref) do + nil -> + nil + + {:qb_arr, arr} -> + :array.to_list(arr) |> Enum.map(&convert_beam_value/1) + + list when is_list(list) -> + Enum.map(list, &convert_beam_value/1) + + map when is_map(map) -> + map + |> Map.drop([key_order()]) + |> Map.new(fn {k, v} -> {convert_beam_key(k), convert_beam_value(v)} end) + |> Map.reject(fn {k, _} -> + is_binary(k) and String.starts_with?(k, "__") and String.ends_with?(k, "__") + end) + end + end + + defp convert_beam_value(list) when is_list(list), do: Enum.map(list, &convert_beam_value/1) + defp convert_beam_value(v), do: v + + defp convert_beam_key(k) when is_binary(k), do: k + defp convert_beam_key(k) when is_integer(k), do: Integer.to_string(k) + defp convert_beam_key(k), do: inspect(k) + + defp load_module_beam(runtime, name, code) do + wrapper = + "(function() { var module = {exports: {}}; var exports = module.exports; " <> + code <> "; return module.exports })()" + + case Runtime.compile(runtime, wrapper) do + {:ok, bc} -> + case BeamBytecode.decode(bc) do + {:ok, parsed} -> + case Interpreter.eval( + parsed.value, + [], + %{gas: 1_000_000_000, runtime_pid: runtime}, + parsed.atoms + ) do + {:ok, mod_exports} -> + Heap.register_module(name, mod_exports) + :ok + + {:error, {:js_throw, _}} = error -> + convert_beam_result(error) + + error -> + error + end + + error -> + error + end + + error -> + error + end end @doc """ @@ -149,7 +359,37 @@ defmodule QuickBEAM do """ @spec call(runtime(), String.t(), list(), keyword()) :: js_result() def call(runtime, fn_name, args \\ [], opts \\ []) do - QuickBEAM.Runtime.call(runtime, fn_name, args, opts) + if resolve_mode(runtime, opts) == :beam do + call_beam(runtime, fn_name, args) + else + Runtime.call(runtime, fn_name, args, opts) + end + end + + defp call_beam(_runtime, fn_name, args) do + handler_globals = Heap.get_handler_globals() || %{} + + globals = + BeamRuntime.global_bindings() + |> Map.merge(handler_globals) + |> Map.merge(Heap.get_persistent_globals()) + + case Map.get(globals, fn_name) do + nil -> + {:error, + JSError.from_js_value(%{ + "message" => "#{fn_name} is not defined", + "name" => "ReferenceError" + })} + + fun -> + try do + result = Interpreter.invoke(fun, args, 1_000_000_000) + convert_beam_result({:ok, result}) + catch + {:js_throw, val} -> convert_beam_result({:error, {:js_throw, val}}) + end + end end @doc """ @@ -167,22 +407,41 @@ defmodule QuickBEAM do """ @spec disasm(binary()) :: {:ok, QuickBEAM.Bytecode.t()} | {:error, String.t()} def disasm(bytecode) when is_binary(bytecode) do - case QuickBEAM.Native.disasm_bytecode(bytecode) do - {:ok, map} -> {:ok, QuickBEAM.Bytecode.from_map(map)} + case Native.disasm_bytecode(bytecode) do + {:ok, map} -> {:ok, Bytecode.from_map(map)} {:error, _} = error -> error end end @doc """ - Compile JavaScript source and disassemble the resulting bytecode. + Compile JavaScript source and disassemble it. + + In the default NIF mode this returns `%QuickBEAM.Bytecode{}`. In `:beam` + mode it returns the raw `:beam_disasm.file/1` result. {:ok, %QuickBEAM.Bytecode{cpool: [%QuickBEAM.Bytecode{name: "add"}]}} = QuickBEAM.disasm(rt, "function add(a, b) { return a + b }") + + {:ok, rt} = QuickBEAM.start(mode: :beam, apis: false) + {:ok, {:beam_file, _, _, _, _, _}} = + QuickBEAM.disasm(rt, "function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2) }") """ - @spec disasm(runtime(), String.t()) :: {:ok, QuickBEAM.Bytecode.t()} | {:error, term()} - def disasm(runtime, code) when is_binary(code) do - with {:ok, bytecode} <- compile(runtime, code) do - disasm(bytecode) + @spec disasm(runtime(), String.t(), keyword()) :: + {:ok, QuickBEAM.Bytecode.t() | tuple()} | {:error, term()} + def disasm(runtime, code, opts \\ []) when is_binary(code) do + if resolve_mode(runtime, opts) == :beam do + disasm_beam(runtime, code, opts) + else + with {:ok, bytecode} <- Runtime.compile(runtime, code, Keyword.get(opts, :filename, "")) do + disasm(bytecode) + end + end + end + + defp disasm_beam(runtime, code, opts) do + with {:ok, bytecode} <- Runtime.compile(runtime, code, Keyword.get(opts, :filename, "")), + {:ok, parsed} <- BeamBytecode.decode(bytecode) do + BeamCompiler.disasm(parsed.value) end end @@ -195,7 +454,7 @@ defmodule QuickBEAM do """ @spec compile(runtime(), String.t()) :: {:ok, binary()} | {:error, QuickBEAM.JSError.t()} def compile(runtime, code) do - QuickBEAM.Runtime.compile(runtime, code) + Runtime.compile(runtime, code) end @doc """ @@ -206,7 +465,7 @@ defmodule QuickBEAM do """ @spec load_bytecode(runtime(), binary()) :: js_result() def load_bytecode(runtime, bytecode) do - QuickBEAM.Runtime.load_bytecode(runtime, bytecode) + Runtime.load_bytecode(runtime, bytecode) end @doc """ @@ -219,9 +478,13 @@ defmodule QuickBEAM do iex> QuickBEAM.stop(rt) :ok """ - @spec load_module(runtime(), String.t(), String.t()) :: :ok | {:error, String.t()} - def load_module(runtime, name, code) do - QuickBEAM.Runtime.load_module(runtime, name, code) + @spec load_module(runtime(), String.t(), String.t(), keyword()) :: :ok | {:error, String.t()} + def load_module(runtime, name, code, opts \\ []) do + if resolve_mode(runtime, opts) == :beam do + load_module_beam(runtime, name, code) + else + Runtime.load_module(runtime, name, code) + end end @doc """ @@ -244,7 +507,7 @@ defmodule QuickBEAM do """ @spec load_addon(runtime(), String.t(), keyword()) :: {:ok, term()} | {:error, term()} def load_addon(runtime, path, opts \\ []) do - QuickBEAM.Runtime.load_addon(runtime, path, opts) + Runtime.load_addon(runtime, path, opts) end @doc """ @@ -261,13 +524,13 @@ defmodule QuickBEAM do """ @spec reset(runtime()) :: :ok | {:error, String.t()} def reset(runtime) do - QuickBEAM.Runtime.reset(runtime) + Runtime.reset(runtime) end @doc "Stop a runtime and free its resources." @spec stop(runtime()) :: :ok def stop(runtime) do - QuickBEAM.Runtime.stop(runtime) + Runtime.stop(runtime) end @doc """ @@ -305,7 +568,7 @@ defmodule QuickBEAM do @doc "Return QuickJS memory usage statistics." @spec memory_usage(runtime()) :: map() def memory_usage(runtime) do - QuickBEAM.Runtime.memory_usage(runtime) + Runtime.memory_usage(runtime) end @doc """ @@ -316,7 +579,7 @@ defmodule QuickBEAM do """ @spec send_message(runtime(), term()) :: :ok def send_message(runtime, message) do - QuickBEAM.Runtime.send_message(runtime, message) + Runtime.send_message(runtime, message) end @doc """ @@ -362,8 +625,14 @@ defmodule QuickBEAM do {:ok, nil} """ @spec get_global(runtime(), String.t()) :: js_result() - def get_global(runtime, name) when is_binary(name) do - GenServer.call(runtime, {:get_global, name}, :infinity) + def get_global(runtime, name, opts \\ []) when is_binary(name) do + if resolve_mode(runtime, opts) == :beam do + persistent = Heap.get_persistent_globals() + raw = Map.get(persistent, name, :undefined) + {:ok, convert_beam_value(raw)} + else + GenServer.call(runtime, {:get_global, name}, :infinity) + end end @doc """ @@ -380,8 +649,15 @@ defmodule QuickBEAM do {:ok, 3} = QuickBEAM.eval(rt, "items.length") """ @spec set_global(runtime(), String.t(), term()) :: :ok - def set_global(runtime, name, value) when is_binary(name) do - GenServer.call(runtime, {:set_global, name, value}, :infinity) + def set_global(runtime, name, value, opts \\ []) when is_binary(name) do + if resolve_mode(runtime, opts) == :beam do + persistent = Heap.get_persistent_globals() + js_val = elixir_to_js(value) + Heap.put_persistent_globals(Map.put(persistent, name, js_val)) + :ok + else + GenServer.call(runtime, {:set_global, name, value}, :infinity) + end end @doc """ @@ -413,7 +689,7 @@ defmodule QuickBEAM do """ @spec dom_find(runtime(), String.t()) :: {:ok, tuple() | nil} def dom_find(runtime, selector) do - QuickBEAM.Runtime.dom_find(runtime, selector) + Runtime.dom_find(runtime, selector) end @doc """ @@ -428,7 +704,7 @@ defmodule QuickBEAM do """ @spec dom_find_all(runtime(), String.t()) :: {:ok, list()} def dom_find_all(runtime, selector) do - QuickBEAM.Runtime.dom_find_all(runtime, selector) + Runtime.dom_find_all(runtime, selector) end @doc """ @@ -440,7 +716,7 @@ defmodule QuickBEAM do """ @spec dom_text(runtime(), String.t()) :: {:ok, String.t()} def dom_text(runtime, selector) do - QuickBEAM.Runtime.dom_text(runtime, selector) + Runtime.dom_text(runtime, selector) end @doc """ @@ -454,7 +730,7 @@ defmodule QuickBEAM do """ @spec dom_attr(runtime(), String.t(), String.t()) :: {:ok, String.t() | nil} def dom_attr(runtime, selector, attr_name) do - QuickBEAM.Runtime.dom_attr(runtime, selector, attr_name) + Runtime.dom_attr(runtime, selector, attr_name) end @doc """ @@ -466,6 +742,6 @@ defmodule QuickBEAM do """ @spec dom_html(runtime()) :: {:ok, String.t()} def dom_html(runtime) do - QuickBEAM.Runtime.dom_html(runtime) + Runtime.dom_html(runtime) end end diff --git a/lib/quickbeam/context.ex b/lib/quickbeam/context.ex index fade773c..429f0711 100644 --- a/lib/quickbeam/context.ex +++ b/lib/quickbeam/context.ex @@ -79,7 +79,8 @@ defmodule QuickBEAM.Context do @spec eval(GenServer.server(), String.t(), keyword()) :: {:ok, term()} | {:error, String.t()} def eval(server, code, opts \\ []) when is_binary(code) do timeout_ms = Keyword.get(opts, :timeout, 0) - GenServer.call(server, {:eval, code, timeout_ms}, :infinity) + filename = Keyword.get(opts, :filename, "") + GenServer.call(server, {:eval, code, timeout_ms, filename}, :infinity) end @spec reset(GenServer.server()) :: :ok | {:error, String.t()} @@ -318,16 +319,42 @@ defmodule QuickBEAM.Context do # ── NIF dispatch callbacks ── - defp nif_eval(state, code, timeout), do: QuickBEAM.Native.pool_eval(state.pool_resource, state.context_id, code, timeout) - defp nif_call(state, fn_name, args, timeout), do: QuickBEAM.Native.pool_call_function(state.pool_resource, state.context_id, fn_name, args, timeout) - defp nif_dom_find(state, selector), do: QuickBEAM.Native.pool_dom_find(state.pool_resource, state.context_id, selector) - defp nif_dom_find_all(state, selector), do: QuickBEAM.Native.pool_dom_find_all(state.pool_resource, state.context_id, selector) - defp nif_dom_text(state, selector), do: QuickBEAM.Native.pool_dom_text(state.pool_resource, state.context_id, selector) - defp nif_dom_html(state), do: QuickBEAM.Native.pool_dom_html(state.pool_resource, state.context_id) - defp nif_reset(state), do: QuickBEAM.Native.pool_reset_context(state.pool_resource, state.context_id) - defp nif_get_global(state, name), do: QuickBEAM.Native.pool_get_global(state.pool_resource, state.context_id, name) - defp nif_set_global(state, name, value), do: QuickBEAM.Native.pool_define_global(state.pool_resource, state.context_id, name, value) - defp nif_send_message(state, message), do: QuickBEAM.Native.pool_send_message(state.pool_resource, state.context_id, message) + defp nif_eval(state, code, timeout, _filename \\ ""), + do: QuickBEAM.Native.pool_eval(state.pool_resource, state.context_id, code, timeout) + + defp nif_call(state, fn_name, args, timeout), + do: + QuickBEAM.Native.pool_call_function( + state.pool_resource, + state.context_id, + fn_name, + args, + timeout + ) + + defp nif_dom_find(state, selector), + do: QuickBEAM.Native.pool_dom_find(state.pool_resource, state.context_id, selector) + + defp nif_dom_find_all(state, selector), + do: QuickBEAM.Native.pool_dom_find_all(state.pool_resource, state.context_id, selector) + + defp nif_dom_text(state, selector), + do: QuickBEAM.Native.pool_dom_text(state.pool_resource, state.context_id, selector) + + defp nif_dom_html(state), + do: QuickBEAM.Native.pool_dom_html(state.pool_resource, state.context_id) + + defp nif_reset(state), + do: QuickBEAM.Native.pool_reset_context(state.pool_resource, state.context_id) + + defp nif_get_global(state, name), + do: QuickBEAM.Native.pool_get_global(state.pool_resource, state.context_id, name) + + defp nif_set_global(state, name, value), + do: QuickBEAM.Native.pool_define_global(state.pool_resource, state.context_id, name, value) + + defp nif_send_message(state, message), + do: QuickBEAM.Native.pool_send_message(state.pool_resource, state.context_id, message) @impl true def handle_info({:beam_call, call_id, handler_name, args}, state) do @@ -426,24 +453,6 @@ defmodule QuickBEAM.Context do handle_websocket_started(socket_id, pid, state) end - def handle_info({:ws_send, socket_id, kind, payload}, state) do - case Map.get(state.websockets, socket_id) do - {pid, _ref} -> GenServer.cast(pid, {:send, kind, payload}) - nil -> :ok - end - - {:noreply, state} - end - - def handle_info({:ws_close, socket_id, code, reason}, state) do - case Map.get(state.websockets, socket_id) do - {pid, _ref} -> GenServer.cast(pid, {:close, code, reason}) - nil -> :ok - end - - {:noreply, state} - end - def handle_info({:websocket_event, message}, state) do QuickBEAM.Native.pool_send_message(state.pool_resource, state.context_id, message) {:noreply, state} diff --git a/lib/quickbeam/js/bundler.ex b/lib/quickbeam/js/bundler.ex index 562d0ca4..ce069a48 100644 --- a/lib/quickbeam/js/bundler.ex +++ b/lib/quickbeam/js/bundler.ex @@ -9,38 +9,34 @@ defmodule QuickBEAM.JS.Bundler do @spec bundle_file(String.t(), keyword()) :: {:ok, String.t()} | {:error, term()} def bundle_file(entry_path, opts \\ []) do entry_path = Path.expand(entry_path) - node_modules = Keyword.get(opts, :node_modules) || find_node_modules(entry_path) - project_root = project_root(entry_path, node_modules) - entry_label = Path.relative_to(entry_path, project_root, separator: "/") bundle_opts = opts |> Keyword.drop([:node_modules]) - |> Keyword.put_new(:entry, entry_label) + |> Keyword.put_new(:entry, normalize_path(entry_path)) - case collect_modules(entry_path, project_root) do + case collect_modules(entry_path) do {:ok, files} -> OXC.bundle(files, bundle_opts) {:error, _} = error -> error end end - defp collect_modules(entry_path, project_root) do - case do_collect(entry_path, project_root, [], MapSet.new()) do + defp collect_modules(entry_path) do + case do_collect(entry_path, [], MapSet.new()) do {:ok, files, _seen} -> {:ok, Enum.reverse(files)} {:error, _} = error -> error end end - defp do_collect(abs_path, project_root, files, seen) do + defp do_collect(abs_path, files, seen) do if MapSet.member?(seen, abs_path) do {:ok, files, seen} else with {:ok, source} <- File.read(abs_path), - {:ok, rewritten, resolved_paths} <- rewrite_and_resolve(source, abs_path, project_root) do - label = Path.relative_to(abs_path, project_root, separator: "/") + {:ok, rewritten, resolved_paths} <- rewrite_and_resolve(source, abs_path) do seen = MapSet.put(seen, abs_path) - files = [{label, rewritten} | files] - collect_deps(resolved_paths, project_root, files, seen) + files = [{normalize_path(abs_path), rewritten} | files] + collect_deps(resolved_paths, files, seen) else {:error, reason} when is_atom(reason) -> {:error, {:file_read_error, abs_path, reason}} {:error, _} = error -> error @@ -48,22 +44,22 @@ defmodule QuickBEAM.JS.Bundler do end end - defp collect_deps([], _project_root, files, seen), do: {:ok, files, seen} + defp collect_deps([], files, seen), do: {:ok, files, seen} - defp collect_deps([path | rest], project_root, files, seen) do - case do_collect(path, project_root, files, seen) do - {:ok, files, seen} -> collect_deps(rest, project_root, files, seen) + defp collect_deps([path | rest], files, seen) do + case do_collect(path, files, seen) do + {:ok, files, seen} -> collect_deps(rest, files, seen) {:error, _} = error -> error end end - defp rewrite_and_resolve(source, importer, project_root) do + defp rewrite_and_resolve(source, importer) do Process.put(:bundler_resolved, []) from_dir = Path.dirname(importer) result = OXC.rewrite_specifiers(source, Path.basename(importer), fn specifier -> - resolve_and_track(specifier, from_dir, project_root) + resolve_and_track(specifier, from_dir) end) resolved_paths = Process.delete(:bundler_resolved) || [] @@ -78,7 +74,7 @@ defmodule QuickBEAM.JS.Bundler do error end - defp resolve_and_track(specifier, from_dir, project_root) do + defp resolve_and_track(specifier, from_dir) do case PackageResolver.resolve(specifier, from_dir, @resolve_opts) do {:builtin, _} -> :keep @@ -89,7 +85,7 @@ defmodule QuickBEAM.JS.Bundler do if PackageResolver.relative?(specifier) do :keep else - {:rewrite, PackageResolver.relative_import_path(from_dir, resolved_path, project_root)} + {:rewrite, normalize_path(resolved_path)} end :error -> @@ -97,25 +93,5 @@ defmodule QuickBEAM.JS.Bundler do end end - defp find_node_modules(entry_path) do - PackageResolver.find_node_modules(Path.dirname(entry_path)) - end - - defp project_root(entry_path, nil), do: Path.dirname(entry_path) - - defp project_root(entry_path, node_modules) do - [entry_path, node_modules] - |> Enum.map(&Path.split/1) - |> shared_segments() - |> Path.join() - end - - defp shared_segments([first | rest]) do - first - |> Enum.with_index() - |> Enum.take_while(fn {segment, index} -> - Enum.all?(rest, &(Enum.at(&1, index) == segment)) - end) - |> Enum.map(&elem(&1, 0)) - end + defp normalize_path(path), do: String.replace(path, "\\", "/") end diff --git a/lib/quickbeam/native.ex b/lib/quickbeam/native.ex index 943b32b2..22eebf54 100644 --- a/lib/quickbeam/native.ex +++ b/lib/quickbeam/native.ex @@ -154,8 +154,10 @@ defmodule QuickBEAM.Native do ], resources: [:RuntimeResource, :PoolResource, :WasmModuleResource, :WasmInstanceResource], nifs: [ + regexp_compile: 2, + regexp_exec: 3, eval: 4, - compile: 2, + compile: 3, call_function: 4, load_module: 3, load_bytecode: 2, diff --git a/lib/quickbeam/quickbeam.zig b/lib/quickbeam/quickbeam.zig index fb2b32f5..d7201414 100644 --- a/lib/quickbeam/quickbeam.zig +++ b/lib/quickbeam/quickbeam.zig @@ -130,7 +130,7 @@ pub fn eval(resource: RuntimeResource, code: []const u8, timeout_ms: u64, filena return beam.term{ .v = e.enif_make_copy(env, ref_term) }; } -pub fn compile(resource: RuntimeResource, code: []const u8) beam.term { +pub fn compile(resource: RuntimeResource, code: []const u8, filename: []const u8) beam.term { const data = resource.unpack(); const env = beam.context.env orelse return beam.make(.{ .@"error", "no env" }, .{}); @@ -139,12 +139,20 @@ pub fn compile(resource: RuntimeResource, code: []const u8) beam.term { const ref_term = e.enif_make_ref(ref_env); const code_copy = gpa.dupe(u8, code) catch return beam.make(.{ .@"error", "OOM" }, .{}); + const fname_copy = if (filename.len > 0) + (gpa.dupe(u8, filename) catch { + gpa.free(code_copy); + return beam.make(.{ .@"error", "OOM" }, .{}); + }) + else + &[_]u8{}; enqueue(data, .{ .compile = .{ .code = code_copy, .caller_pid = caller_pid, .ref_env = ref_env, .ref_term = ref_term, + .filename = fname_copy, } }); return beam.term{ .v = e.enif_make_copy(env, ref_term) }; @@ -907,3 +915,88 @@ pub fn disasm_bytecode(bytecode: []const u8) beam.term { const term = js_to_beam.convert(ctx, result, env); return beam.make(.{ .ok, beam.term{ .v = term } }, .{}); } + +// ── RegExp NIF ── +const lre = @cImport(@cInclude("libregexp.h")); + +threadlocal var tls_rt: ?*types.qjs.JSRuntime = null; +threadlocal var tls_ctx: ?*types.qjs.JSContext = null; + +fn ensure_regexp_ctx() ?*types.qjs.JSContext { + if (tls_ctx) |ctx| return ctx; + const rt = types.qjs.JS_NewRuntime() orelse return null; + types.qjs.JS_SetMemoryLimit(rt, 8 * 1024 * 1024); + const ctx = types.qjs.JS_NewContext(rt) orelse return null; + tls_rt = rt; + tls_ctx = ctx; + return ctx; +} + +pub fn regexp_exec(bc_buf: []const u8, input: []const u8, last_index: u32) beam.term { + const ctx = ensure_regexp_ctx() orelse return beam.make(null, .{}); + + if (bc_buf.len < 8) return beam.make(null, .{}); + const capture_count: u32 = @intCast(bc_buf[2]); // RE_HEADER_CAPTURE_COUNT + if (capture_count == 0 or capture_count > 255) return beam.make(null, .{}); + + // Read flags from header to determine unicode mode + const flags: u32 = @as(u32, bc_buf[0]) | (@as(u32, bc_buf[1]) << 8); + const is_unicode: c_int = if (flags & 0x10 != 0) 1 else 0; // LRE_FLAG_UNICODE = 1 << 4 + + // Allocate capture array via C malloc + const alloc_count = capture_count * 2; + const capture_mem = std.c.malloc(alloc_count * @sizeOf(?[*]u8)) orelse return beam.make(null, .{}); + defer std.c.free(capture_mem); + const capture: [*]?[*]u8 = @ptrCast(@alignCast(capture_mem)); + for (0..alloc_count) |i| { + capture[i] = null; + } + + const ret = lre.lre_exec( + @ptrCast(capture), + bc_buf.ptr, + input.ptr, + @intCast(last_index), + @intCast(input.len), + is_unicode, + @ptrCast(ctx), + ); + + if (ret != 1) return beam.make(null, .{}); + + var result_terms: [256]beam.term = undefined; + for (0..capture_count) |i| { + const sp = capture[i * 2]; + const ep = capture[i * 2 + 1]; + if (sp != null and ep != null) { + const s: u32 = @intCast(@intFromPtr(sp.?) - @intFromPtr(input.ptr)); + const end_off: u32 = @intCast(@intFromPtr(ep.?) - @intFromPtr(input.ptr)); + result_terms[i] = beam.make(.{ s, end_off }, .{}); + } else { + result_terms[i] = beam.make(null, .{}); + } + } + return beam.make(result_terms[0..capture_count], .{}); +} + +pub fn regexp_compile(pattern: []const u8, flags: u32) beam.term { + const ctx = ensure_regexp_ctx() orelse return beam.make(null, .{}); + + var bc_len: c_int = 0; + var error_msg: [64]u8 = undefined; + + const bc_ptr: ?[*]u8 = lre.lre_compile( + &bc_len, + &error_msg, + error_msg.len, + @ptrCast(pattern.ptr), + pattern.len, + @intCast(flags), + @ptrCast(ctx), + ); + + if (bc_ptr == null or bc_len <= 0) return beam.make(null, .{}); + defer std.c.free(bc_ptr.?); + + return beam.make(bc_ptr.?[0..@intCast(bc_len)], .{}); +} diff --git a/lib/quickbeam/runtime.ex b/lib/quickbeam/runtime.ex index 02937f1f..cce6c81e 100644 --- a/lib/quickbeam/runtime.ex +++ b/lib/quickbeam/runtime.ex @@ -5,7 +5,15 @@ defmodule QuickBEAM.Runtime do require Logger @enforce_keys [:resource] - defstruct [:resource, handlers: %{}, monitors: %{}, workers: %{}, websockets: %{}, pending: %{}] + defstruct [ + :resource, + mode: :nif, + handlers: %{}, + monitors: %{}, + workers: %{}, + websockets: %{}, + pending: %{} + ] @type t :: %__MODULE__{ resource: reference(), @@ -68,18 +76,20 @@ defmodule QuickBEAM.Runtime do @spec eval(GenServer.server(), String.t(), keyword()) :: QuickBEAM.js_result() def eval(server, code, opts \\ []) when is_binary(code) do timeout_ms = Keyword.get(opts, :timeout, 0) + filename = Keyword.get(opts, :filename, "") vars = Keyword.get(opts, :vars) if vars && vars != %{} do - GenServer.call(server, {:eval_with_vars, code, timeout_ms, vars}, :infinity) + GenServer.call(server, {:eval_with_vars, code, timeout_ms, vars, filename}, :infinity) else - GenServer.call(server, {:eval, code, timeout_ms}, :infinity) + GenServer.call(server, {:eval, code, timeout_ms, filename}, :infinity) end end - @spec compile(GenServer.server(), String.t()) :: {:ok, binary()} | {:error, String.t()} - def compile(server, code) when is_binary(code) do - GenServer.call(server, {:compile, code}, :infinity) + @spec compile(GenServer.server(), String.t(), String.t()) :: + {:ok, binary()} | {:error, QuickBEAM.JSError.t() | String.t()} + def compile(server, code, filename \\ "") when is_binary(code) and is_binary(filename) do + GenServer.call(server, {:compile, code, filename}, :infinity) end @spec load_bytecode(GenServer.server(), binary()) :: @@ -295,6 +305,7 @@ defmodule QuickBEAM.Runtime do do: Map.merge(builtin_handlers, @browser_handlers), else: builtin_handlers + mode = Keyword.get(opts, :mode, :nif) merged_handlers = builtin_handlers |> Map.merge(user_handlers) nif_opts = @@ -303,7 +314,7 @@ defmodule QuickBEAM.Runtime do |> Map.new() resource = QuickBEAM.Native.start_runtime(self(), nif_opts) - state = %__MODULE__{resource: resource, handlers: merged_handlers} + state = %__MODULE__{resource: resource, mode: mode, handlers: merged_handlers} if QuickBEAM.Cover.enabled?(), do: sync_enable_coverage(resource) install_builtins(state, apis) install_defines(state, Keyword.get(opts, :define, %{})) @@ -440,6 +451,14 @@ defmodule QuickBEAM.Runtime do end @impl true + def handle_call(:get_mode, _from, state) do + {:reply, state.mode, state} + end + + def handle_call(:get_handlers, _from, state) do + {:reply, state.handlers, state} + end + def handle_call(:info, _from, state) do handlers = state.handlers @@ -451,14 +470,14 @@ defmodule QuickBEAM.Runtime do end @impl true - def handle_call({:eval_with_vars, code, timeout_ms, vars}, from, state) do + def handle_call({:eval_with_vars, code, timeout_ms, vars, filename}, from, state) do names = Map.keys(vars) Enum.each(vars, fn {name, value} -> QuickBEAM.Native.define_global(state.resource, name, value) end) - ref = QuickBEAM.Native.eval(state.resource, code, timeout_ms, "") + ref = QuickBEAM.Native.eval(state.resource, code, timeout_ms, filename) transform = fn result -> QuickBEAM.Native.delete_globals(state.resource, names) @@ -483,8 +502,8 @@ defmodule QuickBEAM.Runtime do {:noreply, %{state | pending: Map.put(state.pending, ref, {from, transform})}} end - def handle_call({:compile, code}, from, state) do - ref = QuickBEAM.Native.compile(state.resource, code) + def handle_call({:compile, code, filename}, from, state) do + ref = QuickBEAM.Native.compile(state.resource, code, filename) transform = fn {:ok, {:bytes, bytecode}} -> {:ok, bytecode} @@ -539,7 +558,8 @@ defmodule QuickBEAM.Runtime do # ── NIF dispatch callbacks ── - defp nif_eval(state, code, timeout), do: QuickBEAM.Native.eval(state.resource, code, timeout, "") + defp nif_eval(state, code, timeout, filename \\ ""), + do: QuickBEAM.Native.eval(state.resource, code, timeout, filename) defp nif_call(state, fn_name, args, timeout), do: QuickBEAM.Native.call_function(state.resource, fn_name, args, timeout) @@ -656,24 +676,6 @@ defmodule QuickBEAM.Runtime do handle_websocket_started(socket_id, pid, state) end - def handle_info({:ws_send, socket_id, kind, payload}, state) do - case Map.get(state.websockets, socket_id) do - {pid, _ref} -> GenServer.cast(pid, {:send, kind, payload}) - nil -> :ok - end - - {:noreply, state} - end - - def handle_info({:ws_close, socket_id, code, reason}, state) do - case Map.get(state.websockets, socket_id) do - {pid, _ref} -> GenServer.cast(pid, {:close, code, reason}) - nil -> :ok - end - - {:noreply, state} - end - def handle_info({:websocket_event, message}, state) do QuickBEAM.Native.send_message(state.resource, message) {:noreply, state} diff --git a/lib/quickbeam/server.ex b/lib/quickbeam/server.ex index ceefa426..2d65b614 100644 --- a/lib/quickbeam/server.ex +++ b/lib/quickbeam/server.ex @@ -45,6 +45,12 @@ defmodule QuickBEAM.Server do {:noreply, put_pending(state, ref, from, js_error_transform())} end + @impl true + def handle_call({:eval, code, timeout_ms, filename}, from, state) do + ref = nif_eval(state, code, timeout_ms, filename) + {:noreply, put_pending(state, ref, from, js_error_transform())} + end + @impl true def handle_call({:call, fn_name, args, timeout_ms}, from, state) do ref = nif_call(state, fn_name, args, timeout_ms) @@ -108,6 +114,26 @@ defmodule QuickBEAM.Server do {:noreply, state} end + @impl true + def handle_info({:ws_send, socket_id, kind, payload}, state) do + case Map.get(state.websockets, socket_id) do + {pid, _ref} -> GenServer.cast(pid, {:send, kind, payload}) + nil -> :ok + end + + {:noreply, state} + end + + @impl true + def handle_info({:ws_close, socket_id, code, reason}, state) do + case Map.get(state.websockets, socket_id) do + {pid, _ref} -> GenServer.cast(pid, {:close, code, reason}) + nil -> :ok + end + + {:noreply, state} + end + # ── WebSocket helpers ── defp handle_websocket_started(socket_id, pid, state) do @@ -117,7 +143,9 @@ defmodule QuickBEAM.Server do end defp pop_websocket(state, ref) do - case Enum.find(state.websockets, fn {_socket_id, {_pid, monitor_ref}} -> monitor_ref == ref end) do + case Enum.find(state.websockets, fn {_socket_id, {_pid, monitor_ref}} -> + monitor_ref == ref + end) do {socket_id, {_pid, _monitor_ref}} -> {true, %{state | websockets: Map.delete(state.websockets, socket_id)}} diff --git a/lib/quickbeam/vm/builtin.ex b/lib/quickbeam/vm/builtin.ex new file mode 100644 index 00000000..4a6bde29 --- /dev/null +++ b/lib/quickbeam/vm/builtin.ex @@ -0,0 +1,187 @@ +defmodule QuickBEAM.VM.Builtin do + @moduledoc false + + @doc """ + Uniform macros for defining JS builtins in all contexts. + + All builtins use 2-arity `fn args, this ->` convention. + + ## Module-level dispatch (generates def clauses) + + proto "push" do ... end # → def proto_property("push") + static "isArray" do ... end # → def static_property("isArray") + static_val "PI", :math.pi() # → def static_property("PI"), do: value + + ## Named object (generates def object/0) + + js_object "Math" do + method "floor" do ... end + val "PI", :math.pi() + end + + ## Inline maps (returns %{} at call site) + + build_methods do # returns %{"name" => {:builtin, ...}, ...} + method "add" do ... end + val "size", 0 + end + + build_object do # returns Heap.wrap(%{"name" => {:builtin, ...}, ...}) + method "add" do ... end + val "size", 0 + end + + `args` and `this` are injected in all `proto`/`static`/`method` bodies. + Catch-all fallbacks are auto-generated by @before_compile. + """ + + defmacro __using__(_opts) do + quote do + import QuickBEAM.VM.Builtin, + only: [ + proto: 2, + static: 2, + static_val: 2, + js_object: 2, + build_methods: 1, + build_object: 1 + ] + + Module.register_attribute(__MODULE__, :__has_proto, accumulate: false) + Module.register_attribute(__MODULE__, :__has_static, accumulate: false) + @before_compile QuickBEAM.VM.Builtin + end + end + + defmacro __before_compile__(env) do + has_proto = Module.get_attribute(env.module, :__has_proto) + has_static = Module.get_attribute(env.module, :__has_static) + + proto_fallback = + if has_proto do + quote do: def(proto_property(_), do: :undefined) + end + + static_fallback = + if has_static do + quote do: def(static_property(_), do: :undefined) + end + + [proto_fallback, static_fallback] + |> Enum.reject(&is_nil/1) + |> case do + [] -> nil + blocks -> {:__block__, [], blocks} + end + end + + # ── Module-level dispatch macros ── + + defmacro proto(name, do: body) do + quote do + @__has_proto true + def proto_property(unquote(name)) do + unquote(build_builtin(name, body)) + end + end + end + + defmacro static(name, do: body) do + quote do + @__has_static true + def static_property(unquote(name)) do + unquote(build_builtin(name, body)) + end + end + end + + defmacro static_val(name, value) do + quote do + @__has_static true + def static_property(unquote(name)), do: unquote(value) + end + end + + # ── Named object macro ── + + defmacro js_object(name, do: block) do + entries = normalize_block(block) + map_entries = Enum.map(entries, &build_map_entry/1) + + quote do + def object do + {:builtin, unquote(name), %{unquote_splicing(map_entries)}} + end + end + end + + # ── Inline map/list macros ── + + defmacro build_methods(do: block) do + entries = normalize_block(block) + map_entries = Enum.map(entries, &build_map_entry/1) + quote do: %{unquote_splicing(map_entries)} + end + + defmacro build_object(do: block) do + entries = normalize_block(block) + map_entries = Enum.map(entries, &build_map_entry/1) + + quote do + QuickBEAM.VM.Heap.wrap(%{unquote_splicing(map_entries)}) + end + end + + # ── Shared builders ── + + defp build_builtin(name, body) do + quote do + {:builtin, unquote(name), + fn var!(args), var!(this) -> + _ = var!(args) + _ = var!(this) + unquote(body) + end} + end + end + + defp normalize_block({:__block__, _, entries}), do: entries + defp normalize_block(single), do: [single] + + defp build_map_entry({:method, _, [name, [do: body]]}) do + {name, build_builtin(name, body)} + end + + defp build_map_entry({:val, _, [name, value]}) do + {name, value} + end + + # ── Runtime dispatch ── + + alias QuickBEAM.VM.{Bytecode, Heap} + + def call({:builtin, _, cb}, args, this), do: cb.(args, this) + + def call({:bound, _, inner, _, _}, args, this), do: call(inner, args, this) + + def call(f, args, _this) when is_function(f, 2), do: f.(args, nil) + + def call(f, args, _this) when is_function(f, 1), do: f.(args) + + def call(f, args, _this) when is_function(f), do: apply(f, args) + + def call(_, _, _), + do: throw({:js_throw, Heap.make_error("not a function", "TypeError")}) + + def callable?(%Bytecode.Function{}), do: true + + def callable?({:closure, _, %Bytecode.Function{}}), do: true + + def callable?({:builtin, _, _}), do: true + + def callable?({:bound, _, _, _, _}), do: true + + def callable?(f) when is_function(f), do: true + + def callable?(_), do: false +end diff --git a/lib/quickbeam/vm/bytecode.ex b/lib/quickbeam/vm/bytecode.ex new file mode 100644 index 00000000..ccbfb921 --- /dev/null +++ b/lib/quickbeam/vm/bytecode.ex @@ -0,0 +1,614 @@ +defmodule QuickBEAM.VM.Bytecode do + @moduledoc """ + Parses QuickJS bytecode binaries into Elixir data structures. + + Binary format matches JS_WriteObjectAtoms / JS_ReadObjectAtoms / JS_ReadFunctionTag + in priv/c_src/quickjs.c exactly. + """ + + alias QuickBEAM.VM.{LEB128, Opcodes} + import Bitwise + + # JS_ATOM_NULL=0, plus 228 DEF entries from quickjs-atom.h + @js_atom_end Opcodes.js_atom_end() + + # Pre-compute tag constants for use in match clauses + @tag_null Opcodes.bc_tag_null() + @tag_undefined Opcodes.bc_tag_undefined() + @tag_bool_false Opcodes.bc_tag_bool_false() + @tag_bool_true Opcodes.bc_tag_bool_true() + @tag_int32 Opcodes.bc_tag_int32() + @tag_float64 Opcodes.bc_tag_float64() + @tag_string Opcodes.bc_tag_string() + @tag_function_bytecode Opcodes.bc_tag_function_bytecode() + @tag_object Opcodes.bc_tag_object() + @tag_array Opcodes.bc_tag_array() + @tag_big_int Opcodes.bc_tag_big_int() + @tag_template_object Opcodes.bc_tag_template_object() + @tag_regexp Opcodes.bc_tag_regexp() + + defmodule Function do + @moduledoc false + @type t :: %__MODULE__{} + defstruct [ + :name, + :filename, + line_num: 1, + col_num: 1, + pc2line: <<>>, + source: <<>>, + arg_count: 0, + var_count: 0, + defined_arg_count: 0, + stack_size: 0, + var_ref_count: 0, + locals: [], + closure_vars: [], + constants: [], + byte_code: <<>>, + has_prototype: false, + has_simple_parameter_list: false, + is_derived_class_constructor: false, + need_home_object: false, + func_kind: 0, + new_target_allowed: false, + super_call_allowed: false, + super_allowed: false, + arguments_allowed: false, + is_strict_mode: false, + has_debug_info: false + ] + end + + defmodule VarDef do + @moduledoc false + defstruct [ + :name, + :scope_level, + :scope_next, + :var_kind, + :is_const, + :is_lexical, + :is_captured, + :var_ref_idx + ] + end + + defmodule ClosureVar do + @moduledoc false + defstruct [:name, :var_idx, :closure_type, :is_const, :is_lexical, :var_kind] + end + + defstruct [:version, :atoms, :value] + + @spec decode(binary()) :: {:ok, struct()} | {:error, term()} + def decode(data) when is_binary(data) do + with {:ok, version, rest} <- LEB128.read_u8(data), + :ok <- validate_version(version), + <<_checksum::little-unsigned-32, rest2::binary>> <- rest, + {:ok, atoms, rest3} <- read_atoms(rest2), + {:ok, value, _rest4} <- read_object(rest3, atoms) do + {:ok, %__MODULE__{version: version, atoms: atoms, value: value}} + else + {:error, _} = err -> err + _ -> {:error, :unexpected_end} + end + end + + # ── Atom table ── + # Matches JS_ReadObjectAtoms: reads idx_to_atom_count entries. + # Each entry: type=0 → const atom (u32), type≠0 → string atom. + + defp read_atoms(data) do + with {:ok, count, rest} <- LEB128.read_unsigned(data) do + read_atom_list(rest, count, []) + end + end + + defp read_atom_list(data, 0, acc), do: {:ok, List.to_tuple(Enum.reverse(acc)), data} + + defp read_atom_list(data, count, acc) do + with {:ok, type, rest} <- LEB128.read_u8(data) do + if type == 0 do + with {:ok, _atom_id, rest2} <- LEB128.read_u32(rest) do + read_atom_list(rest2, count - 1, [:__const_atom__ | acc]) + end + else + with {:ok, str, rest2} <- read_string_raw(rest) do + read_atom_list(rest2, count - 1, [str | acc]) + end + end + end + end + + # bc_get_atom: reads LEB128 value v. + # If v & 1 → tagged int (v >> 1). + # If v even → idx = v >> 1: + # idx < JS_ATOM_END → predefined runtime atom (return as {:predefined, idx}) + # idx >= JS_ATOM_END → atom table at idx - JS_ATOM_END + defp read_atom_ref(data, atoms) do + with {:ok, v, rest} <- LEB128.read_unsigned(data) do + if band(v, 1) == 1 do + {:ok, {:tagged_int, bsr(v, 1)}, rest} + else + idx = bsr(v, 1) + + name = + case idx do + 0 -> + "" + + n when n < @js_atom_end -> + {:predefined, n} + + _ -> + local_idx = idx - @js_atom_end + + if local_idx < tuple_size(atoms), + do: elem(atoms, local_idx), + else: {:unknown_atom, idx} + end + + {:ok, name, rest} + end + end + end + + # ── String reading ── + # bc_get_leb128 for len (where bit0=is_wide, bits1+=actual_len), then raw bytes. + + defp read_binary_raw(data) do + with {:ok, len_encoded, rest} <- LEB128.read_unsigned(data) do + byte_len = bsr(len_encoded, 1) + + if byte_size(rest) < byte_len do + {:error, :unexpected_end} + else + <> = rest + {:ok, raw, rest2} + end + end + end + + defp read_string_raw(data) do + with {:ok, len_encoded, rest} <- LEB128.read_unsigned(data) do + is_wide = band(len_encoded, 1) == 1 + char_len = bsr(len_encoded, 1) + byte_len = if is_wide, do: char_len * 2, else: char_len + + if byte_size(rest) < byte_len do + {:error, :unexpected_end} + else + <> = rest + + if is_wide do + {:ok, wide_to_utf8(str), rest2} + else + {:ok, latin1_to_utf8(str), rest2} + end + end + end + end + + defp latin1_to_utf8(data) do + for <>, into: <<>>, do: <> + end + + defp wide_to_utf8(data) do + data + |> decode_utf16_le() + |> :unicode.characters_to_binary(:utf8) + end + + defp decode_utf16_le(data, acc \\ []) + defp decode_utf16_le(<<>>, acc), do: Enum.reverse(acc) + + defp decode_utf16_le(<>, acc) + when hi >= 0xD800 and hi <= 0xDBFF and lo >= 0xDC00 and lo <= 0xDFFF do + cp = (hi - 0xD800) * 0x400 + (lo - 0xDC00) + 0x10000 + decode_utf16_le(rest, [cp | acc]) + end + + defp decode_utf16_le(<>, acc) do + decode_utf16_le(rest, [c | acc]) + end + + # ── Object deserialization ── + # Matches JS_ReadObjectRec switch(tag). + + defp read_object(<<@tag_null, rest::binary>>, _atoms), do: {:ok, nil, rest} + defp read_object(<<@tag_undefined, rest::binary>>, _atoms), do: {:ok, :undefined, rest} + defp read_object(<<@tag_bool_false, rest::binary>>, _atoms), do: {:ok, false, rest} + defp read_object(<<@tag_bool_true, rest::binary>>, _atoms), do: {:ok, true, rest} + + defp read_object(<<@tag_int32, rest::binary>>, _atoms) do + LEB128.read_signed(rest) + end + + defp read_object(<<@tag_float64, rest::binary>>, _atoms) do + case rest do + <> -> {:ok, val, rest2} + _ -> {:error, :unexpected_end} + end + end + + defp read_object(<<@tag_string, rest::binary>>, _atoms), do: read_string_raw(rest) + + defp read_object(<<@tag_function_bytecode, rest::binary>>, atoms), + do: read_function(rest, atoms) + + defp read_object(<<@tag_object, rest::binary>>, atoms), do: read_plain_object(rest, atoms) + defp read_object(<<@tag_array, rest::binary>>, atoms), do: read_array(rest, atoms) + + defp read_object(<<@tag_big_int, rest::binary>>, _atoms) do + with {:ok, str, rest2} <- read_string_raw(rest), do: {:ok, {:bigint, str}, rest2} + end + + defp read_object(<<@tag_template_object, rest::binary>>, atoms) do + with {:ok, count, rest2} <- LEB128.read_unsigned(rest), + {:ok, elems, rest3} <- read_array_elems(rest2, count, [], atoms), + {:ok, raw, rest4} <- read_object(rest3, atoms) do + {:ok, {:template_object, elems, raw}, rest4} + end + end + + defp read_object(<<@tag_regexp, rest::binary>>, _atoms) do + with {:ok, bytecode, rest2} <- read_binary_raw(rest), + {:ok, source, rest3} <- read_string_raw(rest2) do + {:ok, {:regexp, bytecode, source}, rest3} + end + end + + defp read_object(<>, _atoms), do: {:error, {:unknown_tag, tag}} + defp read_object(<<>>, _atoms), do: {:error, :unexpected_end} + + defp read_plain_object(data, atoms) do + with {:ok, count, rest} <- LEB128.read_unsigned(data) do + read_props(rest, count, %{}, atoms) + end + end + + defp read_array(data, atoms) do + with {:ok, count, rest} <- LEB128.read_unsigned(data) do + read_array_elems(rest, count, [], atoms) + end + end + + defp read_props(data, 0, acc, _atoms), do: {:ok, {:object, acc}, data} + + defp read_props(data, count, acc, atoms) do + with {:ok, key, rest} <- read_prop_key(data, atoms), + {:ok, val, rest2} <- read_object(rest, atoms) do + read_props(rest2, count - 1, Map.put(acc, key, val), atoms) + end + end + + defp read_array_elems(data, 0, acc, _atoms), do: {:ok, {:array, Enum.reverse(acc)}, data} + + defp read_array_elems(data, count, acc, atoms) do + with {:ok, val, rest} <- read_object(data, atoms) do + read_array_elems(rest, count - 1, [val | acc], atoms) + end + end + + # Property keys: JS_ReadObjectRec handles them as full objects. + # In practice: tag=BC_TAG_INT32 for integer keys, or tag=BC_TAG_STRING/atom. + defp read_prop_key(data, atoms), do: read_object(data, atoms) + + # ── Function bytecode ── + # Matches JS_ReadFunctionTag exactly. + # + # Layout: + # flags (u16 raw LE) + # is_strict_mode (u8) + # func_name (bc_get_atom → LEB128) + # arg_count (leb128_u16) + # var_count (leb128_u16) + # defined_arg_count (leb128_u16) + # stack_size (leb128_u16) + # var_ref_count (leb128_u16) + # closure_var_count (leb128_u16) + # cpool_count (leb128_int) + # byte_code_len (leb128_int) + # local_count (leb128_int) + # [vardefs × local_count] + # [closure_vars × closure_var_count] + # [cpool × cpool_count] — cpool written BEFORE bytecode + # [bytecode × byte_code_len] + # [debug_info if has_debug_info: filename_atom + line_num] + + defp read_function(data, atoms) do + # flags: raw u16 little-endian (bc_put_u16 / bc_get_u16) + case data do + <> -> + read_function_body(flags, rest, atoms) + + _ -> + {:error, :unexpected_end} + end + end + + defp validate_version(version) do + if version == Opcodes.bc_version(), do: :ok, else: {:error, {:bad_version, version}} + end + + defp read_function_body(flags, data, atoms) do + flags_map = decode_func_flags(flags) + + with {:ok, strict, rest} <- LEB128.read_u8(data), + {:ok, func_name, rest} <- read_atom_ref(rest, atoms), + {:ok, arg_count, rest} <- LEB128.read_unsigned(rest), + {:ok, var_count, rest} <- LEB128.read_unsigned(rest), + {:ok, defined_arg_count, rest} <- LEB128.read_unsigned(rest), + {:ok, stack_size, rest} <- LEB128.read_unsigned(rest), + {:ok, var_ref_count, rest} <- LEB128.read_unsigned(rest), + {:ok, closure_var_count, rest} <- LEB128.read_unsigned(rest), + {:ok, cpool_count, rest} <- LEB128.read_signed(rest), + {:ok, byte_code_len, rest} <- LEB128.read_signed(rest), + {:ok, local_count, rest} <- LEB128.read_signed(rest), + {:ok, locals, rest} <- read_vardefs(rest, local_count, atoms), + {:ok, closure_vars, rest} <- read_closure_vars(rest, closure_var_count, atoms), + {:ok, cpool, rest} <- read_cpool(rest, cpool_count, atoms) do + if byte_size(rest) < byte_code_len do + {:error, :unexpected_end} + else + <> = rest + + {debug_info, rest} = read_debug_info(rest, flags_map.has_debug_info, atoms) + + fun = %Function{ + name: func_name, + arg_count: arg_count, + var_count: var_count, + defined_arg_count: defined_arg_count, + stack_size: stack_size, + var_ref_count: var_ref_count, + locals: locals, + closure_vars: closure_vars, + constants: cpool, + byte_code: byte_code, + filename: debug_info.filename, + line_num: debug_info.line_num, + col_num: debug_info.col_num, + pc2line: debug_info.pc2line, + source: debug_info.source, + is_strict_mode: strict > 0, + has_prototype: flags_map.has_prototype, + has_simple_parameter_list: flags_map.has_simple_parameter_list, + is_derived_class_constructor: flags_map.is_derived_class_constructor, + need_home_object: flags_map.need_home_object, + func_kind: flags_map.func_kind, + new_target_allowed: flags_map.new_target_allowed, + super_call_allowed: flags_map.super_call_allowed, + super_allowed: flags_map.super_allowed, + arguments_allowed: flags_map.arguments_allowed, + has_debug_info: flags_map.has_debug_info + } + + {:ok, fun, rest} + end + end + end + + # Must match JS_WriteFunctionTag bit layout: + # bit 0: has_prototype + # bit 1: has_simple_parameter_list + # bit 2: is_derived_class_constructor + # bit 3: need_home_object + # bits 4-5: func_kind (2 bits) + # bit 6: new_target_allowed + # bit 7: super_call_allowed + # bit 8: super_allowed + # bit 9: arguments_allowed + # bit 10: has_debug_info (backtrace_barrier in writer, has_debug_info in reader) + defp decode_func_flags(v16) do + %{ + has_prototype: band(bsr(v16, 0), 1) == 1, + has_simple_parameter_list: band(bsr(v16, 1), 1) == 1, + is_derived_class_constructor: band(bsr(v16, 2), 1) == 1, + need_home_object: band(bsr(v16, 3), 1) == 1, + func_kind: band(bsr(v16, 4), 0x3), + new_target_allowed: band(bsr(v16, 6), 1) == 1, + super_call_allowed: band(bsr(v16, 7), 1) == 1, + super_allowed: band(bsr(v16, 8), 1) == 1, + arguments_allowed: band(bsr(v16, 9), 1) == 1, + backtrace_barrier: band(bsr(v16, 10), 1) == 1, + has_debug_info: band(bsr(v16, 11), 1) == 1 + } + end + + # ── Vardefs ── + # Matches JS_ReadFunctionTag vardef loop: + # var_name (bc_get_atom), scope_level (leb128_int), scope_next (leb128_int, then -1), + # flags (u8): var_kind(4), is_const(1), is_lexical(1), is_captured(1) + # if is_captured: var_ref_idx (leb128_u16) + + defp read_vardefs(data, 0, _atoms), do: {:ok, [], data} + + defp read_vardefs(data, count, atoms) do + read_vardefs_loop(data, count, atoms, []) + end + + defp read_vardefs_loop(data, 0, _atoms, acc), do: {:ok, Enum.reverse(acc), data} + + defp read_vardefs_loop(data, count, atoms, acc) do + with {:ok, name, rest} <- read_atom_ref(data, atoms), + {:ok, scope_level, rest} <- LEB128.read_signed(rest), + {:ok, scope_next_raw, rest} <- LEB128.read_signed(rest), + <> <- rest do + scope_next = scope_next_raw - 1 + var_kind = band(flags, 0xF) + is_const = band(bsr(flags, 4), 1) == 1 + is_lexical = band(bsr(flags, 5), 1) == 1 + is_captured = band(bsr(flags, 6), 1) == 1 + + {var_ref_idx, rest} = + if is_captured do + with {:ok, idx, rest} <- LEB128.read_unsigned(rest), do: {idx, rest} + else + {nil, rest} + end + + vd = %VarDef{ + name: name, + scope_level: scope_level, + scope_next: scope_next, + var_kind: var_kind, + is_const: is_const, + is_lexical: is_lexical, + is_captured: is_captured, + var_ref_idx: var_ref_idx + } + + read_vardefs_loop(rest, count - 1, atoms, [vd | acc]) + end + end + + # ── Closure vars ── + # Matches JS_ReadFunctionTag closure_var loop: + # var_name (bc_get_atom), var_idx (leb128_int), flags (leb128_int): + # closure_type(3), is_const(1), is_lexical(1), var_kind(4) + + defp read_closure_vars(data, 0, _atoms), do: {:ok, [], data} + defp read_closure_vars(data, count, atoms), do: read_closure_vars_loop(data, count, atoms, []) + + defp read_closure_vars_loop(data, 0, _atoms, acc), do: {:ok, Enum.reverse(acc), data} + + defp read_closure_vars_loop(data, count, atoms, acc) do + with {:ok, name, rest} <- read_atom_ref(data, atoms), + {:ok, var_idx, rest} <- LEB128.read_signed(rest), + {:ok, flags, rest} <- LEB128.read_signed(rest) do + closure_type = band(flags, 0x7) + is_const = band(bsr(flags, 3), 1) == 1 + is_lexical = band(bsr(flags, 4), 1) == 1 + var_kind = band(bsr(flags, 5), 0xF) + + cv = %ClosureVar{ + name: name, + var_idx: var_idx, + closure_type: closure_type, + is_const: is_const, + is_lexical: is_lexical, + var_kind: var_kind + } + + read_closure_vars_loop(rest, count - 1, atoms, [cv | acc]) + end + end + + defp read_cpool(data, 0, _atoms), do: {:ok, [], data} + defp read_cpool(data, count, atoms), do: read_cpool_loop(data, count, atoms, []) + + defp read_cpool_loop(data, 0, _atoms, acc), do: {:ok, Enum.reverse(acc), data} + + defp read_cpool_loop(data, count, atoms, acc) do + case read_object(data, atoms) do + {:ok, val, rest} -> read_cpool_loop(rest, count - 1, atoms, [val | acc]) + {:error, _} = err -> err + end + end + + defp read_debug_info(data, false, _atoms) do + {%{filename: nil, line_num: 1, col_num: 1, pc2line: <<>>, source: <<>>}, data} + end + + defp read_debug_info(data, true, atoms) do + with {:ok, filename, rest} <- read_atom_ref(data, atoms), + {:ok, line_num, rest} <- LEB128.read_signed(rest), + {:ok, col_num, rest} <- LEB128.read_signed(rest), + {:ok, pc2line_len, rest} <- LEB128.read_signed(rest), + true <- byte_size(rest) >= pc2line_len, + <> <- rest, + {:ok, source_len, rest} <- LEB128.read_signed(rest), + true <- byte_size(rest) >= source_len, + <> <- rest do + {%{ + filename: filename, + line_num: line_num, + col_num: col_num, + pc2line: pc2line, + source: source + }, rest} + else + _ -> {%{filename: nil, line_num: 1, col_num: 1, pc2line: <<>>, source: <<>>}, data} + end + end + + @pc2line_base -1 + @pc2line_range 5 + @pc2line_op_first 1 + + def instruction_offset(byte_code, insn_index) + when is_binary(byte_code) and is_integer(insn_index) do + do_instruction_offset(byte_code, byte_size(byte_code), 0, 0, insn_index) + end + + defp do_instruction_offset(_bc, _len, pos, idx, target) when idx >= target, do: pos + + defp do_instruction_offset(bc, len, pos, idx, target) when pos < len do + op = :binary.at(bc, pos) + + case Opcodes.info(op) do + {_name, size, _n_pop, _n_push, _fmt} -> + do_instruction_offset(bc, len, pos + size, idx + 1, target) + + _ -> + pos + end + end + + defp do_instruction_offset(_bc, _len, pos, _idx, _target), do: pos + + def source_position(%Function{} = fun, insn_index) do + pc = instruction_offset(fun.byte_code, insn_index) + + fun + |> decode_pc2line(pc) + |> maybe_apply_source_hint(fun) + end + + defp decode_pc2line(%Function{pc2line: <<>>} = fun, _pc), do: {fun.line_num, fun.col_num} + + defp decode_pc2line(%Function{} = fun, target_pc) do + do_decode_pc2line(fun.pc2line, target_pc, 0, fun.line_num, fun.col_num) + end + + defp do_decode_pc2line(<<>>, _target_pc, _pc, line_num, col_num), do: {line_num, col_num} + + defp do_decode_pc2line(data, target_pc, pc, line_num, col_num) do + <> = data + + {next_pc, next_line, next_col, rest2} = + if op_byte == 0 do + {:ok, diff_pc, rest1} = LEB128.read_unsigned(rest) + {:ok, diff_line, rest2} = LEB128.read_signed(rest1) + {:ok, diff_col, rest3} = LEB128.read_signed(rest2) + {pc + diff_pc, line_num + diff_line, col_num + diff_col, rest3} + else + op = op_byte - @pc2line_op_first + {:ok, diff_col, rest3} = LEB128.read_signed(rest) + + {pc + div(op, @pc2line_range), line_num + rem(op, @pc2line_range) + @pc2line_base, + col_num + diff_col, rest3} + end + + if target_pc < next_pc do + {line_num, col_num} + else + do_decode_pc2line(rest2, target_pc, next_pc, next_line, next_col) + end + end + + defp maybe_apply_source_hint(pos, %Function{source: source}) when is_binary(source) do + case Regex.scan(~r/line\s+(\d+),\s*column\s+(\d+)/, source, capture: :all_but_first) do + [[hint_line, hint_col]] -> + hint = {String.to_integer(hint_line), String.to_integer(hint_col)} + if pos > hint, do: hint, else: pos + + _ -> + pos + end + end + + defp maybe_apply_source_hint(pos, _fun), do: pos +end diff --git a/lib/quickbeam/vm/compiler.ex b/lib/quickbeam/vm/compiler.ex new file mode 100644 index 00000000..6158949f --- /dev/null +++ b/lib/quickbeam/vm/compiler.ex @@ -0,0 +1,111 @@ +defmodule QuickBEAM.VM.Compiler do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Decoder, Heap} + alias QuickBEAM.VM.Compiler.{Forms, Lowering, Optimizer, Runner} + + @type compiled_fun :: {module(), atom()} + @type beam_file :: {:beam_file, module(), list(), list(), list(), list()} + + def invoke(fun, args) do + depth = Process.get(:qb_invoke_depth, 0) + Process.put(:qb_invoke_depth, depth + 1) + + result = Runner.invoke(fun, args) + + Process.put(:qb_invoke_depth, depth) + + if depth == 0 and Heap.gc_needed?() do + extra = case result do + {:ok, v} -> [v, fun | args] + _ -> [fun | args] + end + Heap.gc(extra) + end + + result + end + + def compile(%Bytecode.Function{} = fun) do + module = module_name(fun) + entry = ctx_entry_name() + + case :code.is_loaded(module) do + {:file, _} -> + {:ok, {module, entry}} + + false -> + with {:ok, ^module, ^entry, binary} <- compile_binary(fun), + {:module, ^module} <- :code.load_binary(module, ~c"quickbeam_compiler", binary) do + {:ok, {module, entry}} + else + {:error, _} = error -> error + other -> {:error, {:load_failed, other}} + end + end + end + + def compile(_), do: {:error, :var_refs_not_supported} + + def disasm(%Bytecode.Function{} = fun) do + case disasm_compiled(fun) do + {:ok, _} = ok -> ok + {:error, _} = error -> disasm_single_nested(fun.constants, error) + end + end + + def disasm(_), do: {:error, :var_refs_not_supported} + + defp disasm_compiled(%Bytecode.Function{} = fun) do + with {:ok, _module, _entry, binary} <- compile_binary(fun), + {:beam_file, _, _, _, _, _} = beam_file <- :beam_disasm.file(binary) do + {:ok, beam_file} + else + {:error, _, _} = error -> {:error, error} + {:error, _} = error -> error + end + end + + defp disasm_single_nested(constants, original_error) do + case Enum.filter(constants, &match?(%Bytecode.Function{}, &1)) do + [%Bytecode.Function{} = fun] -> disasm(fun) + _ -> original_error + end + end + + defp compile_binary(%Bytecode.Function{} = fun) do + module = module_name(fun) + entry = entry_name() + ctx_entry = ctx_entry_name() + + with {:ok, instructions} <- Decoder.decode(fun.byte_code, fun.arg_count), + optimized = Optimizer.optimize(instructions, fun.constants), + {:ok, {slot_count, block_forms}} <- Lowering.lower(fun, optimized), + {:ok, _module, binary} <- + Forms.compile_module( + module, + entry, + ctx_entry, + fun, + fun.arg_count, + slot_count, + block_forms + ) do + {:ok, module, ctx_entry, binary} + end + end + + defp module_name(fun) do + hash = + fun + |> :erlang.term_to_binary() + |> then(&:crypto.hash(:sha256, &1)) + |> binary_part(0, 8) + |> Base.encode16(case: :lower) + + Module.concat(QuickBEAM.VM.Compiled, "F#{hash}") + end + + defp entry_name, do: :run + defp ctx_entry_name, do: :run_ctx +end diff --git a/lib/quickbeam/vm/compiler/analysis/cfg.ex b/lib/quickbeam/vm/compiler/analysis/cfg.ex new file mode 100644 index 00000000..b38d3add --- /dev/null +++ b/lib/quickbeam/vm/compiler/analysis/cfg.ex @@ -0,0 +1,189 @@ +defmodule QuickBEAM.VM.Compiler.Analysis.CFG do + @moduledoc false + + alias QuickBEAM.VM.Opcodes + + def block_entries(instructions) do + entries = + instructions + |> Enum.with_index() + |> Enum.reduce(MapSet.new([0]), fn {{op, args}, idx}, acc -> + case opcode_name(op) do + {:ok, name} when name in [:if_false, :if_false8, :if_true, :if_true8] -> + [target] = args + acc |> MapSet.put(target) |> MapSet.put(idx + 1) + + {:ok, name} when name in [:goto, :goto8, :goto16, :catch] -> + [target] = args + MapSet.put(acc, target) + + {:ok, name} + when name in [ + :with_get_var, + :with_put_var, + :with_delete_var, + :with_make_ref, + :with_get_ref, + :with_get_ref_undef + ] -> + [_atom_idx, target, _is_with] = args + acc |> MapSet.put(target) |> MapSet.put(idx + 1) + + {:ok, name} + when name in [ + :initial_yield, + :yield, + :yield_star, + :async_yield_star, + :gosub + ] -> + MapSet.put(acc, idx + 1) + + _ -> + acc + end + end) + + entries + |> MapSet.to_list() + |> Enum.sort() + end + + def next_entry(entries, start), do: Enum.find(entries, &(&1 > start)) + + def predecessor_counts(instructions, entries) do + predecessor_sources(instructions, entries) + |> Enum.into(%{}, fn {target, preds} -> {target, length(preds)} end) + end + + def predecessor_sources(instructions, entries) do + Enum.reduce(entries, %{}, fn start, preds -> + next = next_entry(entries, start) + + case block_terminal(instructions, start, next) do + {:branch, target, term_idx} when is_integer(next) -> + preds + |> add_predecessor(target, term_idx) + |> add_predecessor(next, term_idx) + + {:catch, target, term_idx} when is_integer(next) -> + preds + |> add_predecessor(target, term_idx) + |> add_predecessor(next, term_idx) + + {:goto, target, term_idx} -> + add_predecessor(preds, target, term_idx) + + {:fallthrough, target_idx} -> + add_predecessor(preds, target_idx, target_idx - 1) + + _ -> + preds + end + end) + end + + def inlineable_entries(instructions, entries) do + instructions + |> predecessor_sources(entries) + |> Enum.reduce(MapSet.new(), fn {target, preds}, acc -> + case preds do + [pred_end] -> + if pred_end < target and not protected_target?(instructions, target) do + MapSet.put(acc, target) + else + acc + end + + _ -> + acc + end + end) + end + + def opcode_name(op) do + case Opcodes.info(op) do + {name, _size, _pop, _push, _fmt} -> {:ok, name} + nil -> {:error, {:unknown_opcode, op}} + end + end + + def matching_nip_catch(instructions, catch_idx), + do: find_nip_catch(instructions, catch_idx + 1, 0) + + def block_terminal(instructions, start, next_entry), + do: do_block_terminal(instructions, start, next_entry) + + def block_successors(instructions, entries, start) do + next = next_entry(entries, start) + + case block_terminal(instructions, start, next) do + {:branch, target, _idx} when is_integer(next) -> [target, next] + {:catch, target, _idx} when is_integer(next) -> [target, next] + {:goto, target, _idx} -> [target] + {:fallthrough, target_idx} -> [target_idx] + _ -> [] + end + end + + defp do_block_terminal(instructions, idx, _next_entry) when idx >= length(instructions), + do: {:done, idx} + + defp do_block_terminal(_instructions, idx, idx), do: {:fallthrough, idx} + + defp do_block_terminal(instructions, idx, next_entry) do + {op, args} = Enum.at(instructions, idx) + + case {opcode_name(op), args} do + {{:ok, name}, [target]} when name in [:if_false, :if_false8, :if_true, :if_true8] -> + {:branch, target, idx} + + {{:ok, :catch}, [target]} -> + {:catch, target, idx} + + {{:ok, name}, [target]} when name in [:goto, :goto8, :goto16] -> + {:goto, target, idx} + + {{:ok, name}, [_argc]} when name in [:tail_call, :tail_call_method] -> + {:done, idx} + + {{:ok, name}, _args} when name in [:return, :return_undef, :throw, :throw_error] -> + {:done, idx} + + _ -> + do_block_terminal(instructions, idx + 1, next_entry) + end + end + + defp add_predecessor(preds, target, pred_end), + do: Map.update(preds, target, [pred_end], &[pred_end | &1]) + + defp protected_target?(instructions, target) do + Enum.any?(instructions, fn {op, args} -> + case {opcode_name(op), args} do + {{:ok, name}, [^target]} when name in [:catch, :gosub] -> true + _ -> false + end + end) + end + + defp find_nip_catch(instructions, idx, _depth) when idx >= length(instructions), do: :error + + defp find_nip_catch(instructions, idx, depth) do + {op, _args} = Enum.at(instructions, idx) + + case opcode_name(op) do + {:ok, :catch} -> + find_nip_catch(instructions, idx + 1, depth + 1) + + {:ok, :nip_catch} when depth == 0 -> + {:ok, idx} + + {:ok, :nip_catch} -> + find_nip_catch(instructions, idx + 1, depth - 1) + + _ -> + find_nip_catch(instructions, idx + 1, depth) + end + end +end diff --git a/lib/quickbeam/vm/compiler/analysis/stack.ex b/lib/quickbeam/vm/compiler/analysis/stack.ex new file mode 100644 index 00000000..d1aabe83 --- /dev/null +++ b/lib/quickbeam/vm/compiler/analysis/stack.ex @@ -0,0 +1,139 @@ +defmodule QuickBEAM.VM.Compiler.Analysis.Stack do + @moduledoc false + + alias QuickBEAM.VM.Compiler.Analysis.CFG + alias QuickBEAM.VM.Opcodes + + def infer_block_stack_depths(instructions, entries) do + walk_block_stack_depths(instructions, entries, [{0, 0}], %{}) + end + + def stack_effect(op, args) do + case {CFG.opcode_name(op), args} do + {{:ok, name}, [argc]} when name in [:call, :call0, :call1, :call2, :call3, :tail_call] -> + {:ok, argc + 1, if(name == :tail_call, do: 0, else: 1)} + + {{:ok, name}, [argc]} when name in [:call_method, :tail_call_method] -> + {:ok, argc + 2, if(name == :tail_call_method, do: 0, else: 1)} + + {{:ok, :call_constructor}, [argc]} -> + {:ok, argc + 2, 1} + + {{:ok, :array_from}, [argc]} -> + {:ok, argc, 1} + + {{:ok, _name}, _} -> + case Opcodes.info(op) do + {_name, _size, pop_count, push_count, _fmt} -> {:ok, pop_count, push_count} + nil -> {:error, {:unknown_opcode, op}} + end + + {{:error, _} = error, _} -> + error + end + end + + defp walk_block_stack_depths(_instructions, _entries, [], depths), do: {:ok, depths} + + defp walk_block_stack_depths(instructions, entries, [{start, depth} | rest], depths) do + case Map.fetch(depths, start) do + {:ok, ^depth} -> + walk_block_stack_depths(instructions, entries, rest, depths) + + {:ok, other_depth} -> + {:error, {:inconsistent_block_stack_depth, start, other_depth, depth}} + + :error -> + with {:ok, successors} <- simulate_block_stack_depths(instructions, entries, start, depth) do + walk_block_stack_depths( + instructions, + entries, + rest ++ successors, + Map.put(depths, start, depth) + ) + end + end + end + + defp simulate_block_stack_depths(instructions, entries, start, depth) do + next_entry = CFG.next_entry(entries, start) + do_simulate_block_stack_depths(instructions, start, next_entry, depth) + end + + defp do_simulate_block_stack_depths(instructions, idx, _next_entry, _depth) + when idx >= length(instructions) do + {:error, {:missing_terminator, idx}} + end + + defp do_simulate_block_stack_depths(_instructions, idx, idx, depth), do: {:ok, [{idx, depth}]} + + defp do_simulate_block_stack_depths(instructions, idx, next_entry, depth) do + {op, args} = Enum.at(instructions, idx) + + with {:ok, next_depth} <- apply_stack_effect(op, args, depth) do + case {CFG.opcode_name(op), args} do + {{:ok, name}, [target]} when name in [:if_false, :if_false8, :if_true, :if_true8] -> + if is_nil(next_entry) do + {:error, {:missing_fallthrough_block, target, name}} + else + {:ok, [{target, next_depth}, {next_entry, next_depth}]} + end + + {{:ok, :catch}, [target]} -> + with {:ok, successors} <- + do_simulate_block_stack_depths(instructions, idx + 1, next_entry, next_depth) do + {:ok, [{target, next_depth} | successors]} + end + + {{:ok, name}, [target]} when name in [:goto, :goto8, :goto16] -> + {:ok, [{target, next_depth}]} + + {{:ok, name}, [_argc]} when name in [:tail_call, :tail_call_method] -> + {:ok, []} + + {{:ok, :return}, []} -> + {:ok, []} + + {{:ok, :return_undef}, []} -> + {:ok, []} + + {{:ok, :throw}, []} -> + {:ok, []} + + {{:ok, :throw_error}, _} -> + {:ok, []} + + {{:ok, :return_async}, []} -> + {:ok, []} + + {{:ok, :initial_yield}, []} -> + {:ok, []} + + {{:ok, :yield}, []} -> + {:ok, []} + + {{:ok, :yield_star}, []} -> + {:ok, []} + + {{:ok, :async_yield_star}, []} -> + {:ok, []} + + {{:ok, :gosub}, [_target]} -> + {:ok, []} + + {{:ok, :ret}, []} -> + {:ok, []} + + _ -> + do_simulate_block_stack_depths(instructions, idx + 1, next_entry, next_depth) + end + end + end + + defp apply_stack_effect(op, args, depth) do + with {:ok, pop_count, push_count} <- stack_effect(op, args), + true <- depth >= pop_count or {:error, {:stack_underflow_at, op, args, depth, pop_count}} do + {:ok, depth - pop_count + push_count} + end + end +end diff --git a/lib/quickbeam/vm/compiler/analysis/types.ex b/lib/quickbeam/vm/compiler/analysis/types.ex new file mode 100644 index 00000000..a467d3c2 --- /dev/null +++ b/lib/quickbeam/vm/compiler/analysis/types.ex @@ -0,0 +1,949 @@ +defmodule QuickBEAM.VM.Compiler.Analysis.Types do + @moduledoc false + + alias QuickBEAM.VM.Bytecode + alias QuickBEAM.VM.Compiler.Analysis.{CFG, Stack} + alias QuickBEAM.VM.Decoder + + def infer_block_entry_types(fun, instructions, entries, stack_depths) do + slot_count = fun.arg_count + fun.var_count + initial = initial_type_state(fun, slot_count, Map.get(stack_depths, 0, 0)) + + iterate_block_entry_types( + instructions, + entries, + stack_depths, + fun.constants, + %{0 => initial}, + :unknown, + 0 + ) + end + + def function_type(%Bytecode.Function{} = fun) do + stack = Process.get(:qb_function_type_stack, MapSet.new()) + + if MapSet.member?(stack, fun.byte_code) do + :function + else + next_stack = MapSet.put(stack, fun.byte_code) + Process.put(:qb_function_type_stack, next_stack) + + try do + case Decoder.decode(fun.byte_code, fun.arg_count) do + {:ok, instructions} -> + entries = CFG.block_entries(instructions) + + with {:ok, stack_depths} <- Stack.infer_block_stack_depths(instructions, entries), + {:ok, {_entry_types, return_type}} <- + infer_block_entry_types(fun, instructions, entries, stack_depths) do + {:function, return_type} + else + _ -> :function + end + + _ -> + :function + end + after + if MapSet.size(stack) == 0, + do: Process.delete(:qb_function_type_stack), + else: Process.put(:qb_function_type_stack, stack) + end + end + end + + defp iterate_block_entry_types( + instructions, + entries, + stack_depths, + constants, + entry_types, + return_type, + iteration + ) + when iteration < 12 do + with {:ok, {next_entry_types, next_return_type}} <- + walk_block_entry_types( + instructions, + entries, + stack_depths, + constants, + entry_types, + return_type + ) do + if next_entry_types == entry_types and next_return_type == return_type do + {:ok, {next_entry_types, next_return_type}} + else + iterate_block_entry_types( + instructions, + entries, + stack_depths, + constants, + next_entry_types, + next_return_type, + iteration + 1 + ) + end + end + end + + defp iterate_block_entry_types( + _instructions, + _entries, + _stack_depths, + _constants, + _entry_types, + _return_type, + iteration + ) do + {:error, {:type_inference_did_not_converge, iteration}} + end + + defp walk_block_entry_types( + instructions, + entries, + stack_depths, + constants, + entry_types, + return_type + ) do + Enum.reduce_while(entries, {:ok, {entry_types, return_type}}, fn start, {:ok, acc} -> + case Map.fetch(elem(acc, 0), start) do + :error -> + {:cont, {:ok, acc}} + + {:ok, state} -> + next = CFG.next_entry(entries, start) + + case simulate_block_types( + instructions, + entries, + stack_depths, + constants, + start, + next, + state, + elem(acc, 1) + ) do + {:ok, {updates, block_return_type}} -> + merged_entry_types = merge_block_updates(elem(acc, 0), updates) + merged_return_type = join_type(elem(acc, 1), block_return_type) + {:cont, {:ok, {merged_entry_types, merged_return_type}}} + + {:error, _} = error -> + {:halt, error} + end + end + end) + end + + defp simulate_block_types( + instructions, + entries, + stack_depths, + _constants, + idx, + next_entry, + state, + return_type + ) + when idx >= length(instructions) do + {:error, + {:missing_type_terminator, idx, next_entry, state, return_type, entries, stack_depths}} + end + + defp simulate_block_types( + _instructions, + _entries, + _stack_depths, + _constants, + idx, + idx, + state, + return_type + ) do + {:ok, {[{idx, state}], return_type}} + end + + defp simulate_block_types( + instructions, + entries, + stack_depths, + constants, + idx, + next_entry, + state, + return_type + ) do + instruction = Enum.at(instructions, idx) + + with {:ok, result} <- transfer_types(instruction, state, return_type, constants) do + case result do + {:continue, next_state, next_return_type} -> + simulate_block_types( + instructions, + entries, + stack_depths, + constants, + idx + 1, + next_entry, + next_state, + next_return_type + ) + + {:catch, target, next_state, next_return_type} -> + with {:ok, {updates, final_return_type}} <- + simulate_block_types( + instructions, + entries, + stack_depths, + constants, + idx + 1, + next_entry, + next_state, + next_return_type + ) do + {:ok, {[{target, next_state} | updates], final_return_type}} + end + + {:branch, target, next_state, next_return_type} -> + if is_nil(next_entry) do + {:error, {:missing_fallthrough_type_block, target, idx}} + else + {:ok, {[{target, next_state}, {next_entry, next_state}], next_return_type}} + end + + {:goto, target, next_state, next_return_type} -> + {:ok, {[{target, next_state}], next_return_type}} + + {:halt, next_return_type} -> + {:ok, {[], next_return_type}} + end + end + end + + defp transfer_types({op, args}, state, return_type, constants) do + case {CFG.opcode_name(op), args} do + {{:ok, name}, [value]} when name in [:push_i32, :push_i16, :push_i8] -> + {:ok, {:continue, push_type(state, literal_type(value)), return_type}} + + {{:ok, :push_minus1}, _} -> + {:ok, {:continue, push_type(state, :integer), return_type}} + + {{:ok, name}, _} + when name in [:push_0, :push_1, :push_2, :push_3, :push_4, :push_5, :push_6, :push_7] -> + {:ok, {:continue, push_type(state, :integer), return_type}} + + {{:ok, name}, _} when name in [:push_true, :push_false] -> + {:ok, {:continue, push_type(state, :boolean), return_type}} + + {{:ok, :null}, _} -> + {:ok, {:continue, push_type(state, :null), return_type}} + + {{:ok, :undefined}, _} -> + {:ok, {:continue, push_type(state, :undefined), return_type}} + + {{:ok, :push_empty_string}, _} -> + {:ok, {:continue, push_type(state, :string), return_type}} + + {{:ok, :object}, _} -> + {:ok, {:continue, push_type(state, :object), return_type}} + + {{:ok, :array_from}, [argc]} -> + with {:ok, state} <- pop_types(state, argc) do + {:ok, {:continue, push_type(state, :object), return_type}} + end + + {{:ok, name}, [const_idx]} when name in [:push_const, :push_const8] -> + {:ok, {:continue, push_type(state, constant_type(constants, const_idx)), return_type}} + + {{:ok, name}, [const_idx]} when name in [:fclosure, :fclosure8] -> + {:ok, {:continue, push_type(state, closure_type(constants, const_idx)), return_type}} + + {{:ok, :special_object}, [type]} -> + {:ok, {:continue, push_type(state, special_object_type(type)), return_type}} + + {{:ok, name}, [slot_idx]} + when name in [ + :get_arg, + :get_arg0, + :get_arg1, + :get_arg2, + :get_arg3, + :get_loc, + :get_loc0, + :get_loc1, + :get_loc2, + :get_loc3, + :get_loc8, + :get_loc_check + ] -> + {:ok, {:continue, push_type(state, slot_type(state, slot_idx)), return_type}} + + {{:ok, :get_loc0_loc1}, [slot0, slot1]} -> + {:ok, + {:continue, + state + |> push_type(slot_type(state, slot0)) + |> push_type(slot_type(state, slot1)), return_type}} + + {{:ok, name}, [_idx]} + when name in [ + :get_var_ref, + :get_var_ref0, + :get_var_ref1, + :get_var_ref2, + :get_var_ref3, + :get_var_ref_check + ] -> + {:ok, {:continue, push_type(state, :unknown), return_type}} + + {{:ok, :set_loc_uninitialized}, [slot_idx]} -> + {:ok, + {:continue, state |> put_slot_type(slot_idx, :unknown) |> put_slot_init(slot_idx, false), + return_type}} + + {{:ok, :define_var}, [_atom_idx, _scope]} -> + {:ok, {:continue, state, return_type}} + + {{:ok, :check_define_var}, [_atom_idx, _scope]} -> + {:ok, {:continue, state, return_type}} + + {{:ok, :put_var}, [_atom_idx]} -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, :put_var_init}, [_atom_idx]} -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, :define_func}, [_atom_idx, _flags]} -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, name}, [slot_idx]} + when name in [ + :put_loc, + :put_loc0, + :put_loc1, + :put_loc2, + :put_loc3, + :put_loc8, + :put_arg, + :put_arg0, + :put_arg1, + :put_arg2, + :put_arg3, + :put_loc_check, + :put_loc_check_init + ] -> + with {:ok, type, state} <- pop_type(state) do + {:ok, + {:continue, state |> put_slot_type(slot_idx, type) |> put_slot_init(slot_idx, true), + return_type}} + end + + {{:ok, name}, [_idx]} + when name in [ + :put_var_ref, + :put_var_ref0, + :put_var_ref1, + :put_var_ref2, + :put_var_ref3, + :put_var_ref_check, + :put_var_ref_check_init + ] -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, name}, [slot_idx]} + when name in [ + :set_loc, + :set_loc0, + :set_loc1, + :set_loc2, + :set_loc3, + :set_loc8, + :set_arg, + :set_arg0, + :set_arg1, + :set_arg2, + :set_arg3 + ] -> + with {:ok, type, state} <- pop_type(state) do + next_state = + state + |> put_slot_type(slot_idx, type) + |> put_slot_init(slot_idx, true) + |> push_type(type) + + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, name}, [_idx]} + when name in [:set_var_ref, :set_var_ref0, :set_var_ref1, :set_var_ref2, :set_var_ref3] -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :unknown), return_type}} + end + + {{:ok, :dup}, _} -> + with {:ok, type, state} <- pop_type(state) do + next_state = state |> push_type(type) |> push_type(type) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :regexp}, _} -> + with {:ok, _pattern_type, state} <- pop_type(state), + {:ok, _flags_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :unknown), return_type}} + end + + {{:ok, :dup2}, _} -> + with {:ok, first, state} <- pop_type(state), + {:ok, second, state} <- pop_type(state) do + next_state = + state + |> push_type(second) + |> push_type(first) + |> push_type(second) + |> push_type(first) + + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :insert2}, _} -> + with {:ok, first, state} <- pop_type(state), + {:ok, second, state} <- pop_type(state) do + next_state = + state + |> push_type(first) + |> push_type(second) + |> push_type(first) + + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :insert3}, _} -> + with {:ok, first, state} <- pop_type(state), + {:ok, second, state} <- pop_type(state), + {:ok, third, state} <- pop_type(state) do + next_state = + state + |> push_type(first) + |> push_type(third) + |> push_type(second) + |> push_type(first) + + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :drop}, _} -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, :swap}, _} -> + with {:ok, first, state} <- pop_type(state), + {:ok, second, state} <- pop_type(state) do + {:ok, {:continue, state |> push_type(first) |> push_type(second), return_type}} + end + + {{:ok, :perm3}, _} -> + with {:ok, first, state} <- pop_type(state), + {:ok, second, state} <- pop_type(state), + {:ok, third, state} <- pop_type(state) do + next_state = + state + |> push_type(second) + |> push_type(third) + |> push_type(first) + + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :nip_catch}, _} -> + with {:ok, value_type, state} <- pop_type(state), + {:ok, _catch_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, value_type), return_type}} + end + + {{:ok, name}, _} + when name in [ + :neg, + :plus, + :typeof, + :delete, + :not, + :lnot, + :is_undefined, + :is_null, + :typeof_is_undefined, + :typeof_is_function, + :is_undefined_or_null + ] -> + transfer_unary_type(name, state, return_type) + + {{:ok, name}, _} + when name in [ + :add, + :sub, + :mul, + :div, + :mod, + :pow, + :lt, + :lte, + :gt, + :gte, + :eq, + :neq, + :strict_eq, + :strict_neq, + :shl, + :sar, + :shr, + :band, + :bor, + :bxor, + :instanceof, + :in + ] -> + transfer_binaryish_type(name, state, return_type) + + {{:ok, name}, _} when name in [:inc, :dec] -> + with {:ok, type, state} <- pop_type(state) do + next_type = if type == :integer, do: :integer, else: :number + {:ok, {:continue, push_type(state, next_type), return_type}} + end + + {{:ok, name}, _} when name in [:post_inc, :post_dec] -> + with {:ok, type, state} <- pop_type(state) do + next_type = if type == :integer, do: :integer, else: :number + next_state = state |> push_type(next_type) |> push_type(next_type) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :get_length}, _} -> + with {:ok, _type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :integer), return_type}} + end + + {{:ok, :get_field}, _} -> + with {:ok, _obj_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :unknown), return_type}} + end + + {{:ok, :get_field2}, _} -> + with {:ok, obj_type, state} <- pop_type(state) do + next_state = state |> push_type(obj_type) |> push_type(:unknown) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, name}, _} when name in [:get_array_el, :get_super_value, :get_private_field] -> + with {:ok, _idx_type, state} <- pop_type(state), + {:ok, _obj_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :unknown), return_type}} + end + + {{:ok, :get_array_el2}, _} -> + with {:ok, _idx_type, state} <- pop_type(state), + {:ok, obj_type, state} <- pop_type(state) do + next_state = state |> push_type(obj_type) |> push_type(:unknown) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, name}, [argc]} when name in [:call, :call0, :call1, :call2, :call3] -> + with {:ok, state} <- pop_types(state, argc), + {:ok, fun_type, state} <- pop_type(state) do + {:ok, + {:continue, push_type(state, invoke_result_type(fun_type, return_type)), return_type}} + end + + {{:ok, :tail_call}, [argc]} -> + with {:ok, state} <- pop_types(state, argc), + {:ok, fun_type, _state} <- pop_type(state) do + {:ok, {:halt, join_type(return_type, invoke_result_type(fun_type, return_type))}} + end + + {{:ok, :call_method}, [argc]} -> + with {:ok, state} <- pop_types(state, argc), + {:ok, fun_type, state} <- pop_type(state), + {:ok, _obj_type, state} <- pop_type(state) do + {:ok, + {:continue, push_type(state, invoke_result_type(fun_type, return_type)), return_type}} + end + + {{:ok, :tail_call_method}, [argc]} -> + with {:ok, state} <- pop_types(state, argc), + {:ok, fun_type, state} <- pop_type(state), + {:ok, _obj_type, _state} <- pop_type(state) do + {:ok, {:halt, join_type(return_type, invoke_result_type(fun_type, return_type))}} + end + + {{:ok, :call_constructor}, [argc]} -> + with {:ok, state} <- pop_types(state, argc), + {:ok, _new_target_type, state} <- pop_type(state), + {:ok, _ctor_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :object), return_type}} + end + + {{:ok, :append}, _} -> + with {:ok, _obj_type, state} <- pop_type(state), + {:ok, _idx_type, state} <- pop_type(state), + {:ok, _arr_type, state} <- pop_type(state) do + next_state = state |> push_type(:object) |> push_type(:number) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :copy_data_properties}, _} -> + {:ok, {:continue, state, return_type}} + + {{:ok, :define_field}, _} -> + with {:ok, _val_type, state} <- pop_type(state), + {:ok, _obj_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :object), return_type}} + end + + {{:ok, name}, _} + when name in [ + :put_field, + :put_array_el, + :put_super_value, + :put_private_field, + :define_private_field, + :check_brand, + :add_brand, + :set_home_object + ] -> + with {:ok, state} <- apply_generic_stack_effect(state, op, args) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, name}, _} when name in [:define_method, :define_method_computed] -> + with {:ok, state} <- apply_generic_stack_effect(state, op, args) do + {:ok, {:continue, push_type(state, :object), return_type}} + end + + {{:ok, :define_class}, _} -> + with {:ok, _ctor_type, state} <- pop_type(state), + {:ok, _parent_type, state} <- pop_type(state) do + next_state = state |> push_type(:function) |> push_type(:object) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :set_name}, _} -> + with {:ok, _fun_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :function), return_type}} + end + + {{:ok, :set_name_computed}, _} -> + with {:ok, fun_type, state} <- pop_type(state), + {:ok, name_type, state} <- pop_type(state) do + next_state = state |> push_type(name_type) |> push_type(join_type(fun_type, :function)) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :push_this}, _} -> + {:ok, {:continue, push_type(state, :object), return_type}} + + {{:ok, :push_atom_value}, _} -> + {:ok, {:continue, push_type(state, :string), return_type}} + + {{:ok, :close_loc}, _} -> + {:ok, {:continue, state, return_type}} + + {{:ok, :for_in_start}, _} -> + with {:ok, _src_type, state} <- pop_type(state) do + {:ok, {:continue, push_type(state, :unknown), return_type}} + end + + {{:ok, :for_in_next}, _} -> + case state.stack_types do + [iter_type | rest] -> + next_state = %{state | stack_types: [iter_type | rest]} + next_state = next_state |> push_type(:unknown) |> push_type(:boolean) + {:ok, {:continue, next_state, return_type}} + + _ -> + {:error, :stack_underflow} + end + + {{:ok, :for_of_start}, _} -> + with {:ok, _src_type, state} <- pop_type(state) do + next_state = state |> push_type(:object) |> push_type(:function) |> push_type(:integer) + {:ok, {:continue, next_state, return_type}} + end + + {{:ok, :for_of_next}, _} -> + case state.stack_types do + [catch_type, next_type, _iter_type | rest] -> + next_state = %{state | stack_types: [catch_type, next_type, :object | rest]} + next_state = next_state |> push_type(:unknown) |> push_type(:boolean) + {:ok, {:continue, next_state, return_type}} + + _ -> + {:error, :stack_underflow} + end + + {{:ok, :iterator_close}, _} -> + with {:ok, _catch_type, state} <- pop_type(state), + {:ok, _next_type, state} <- pop_type(state), + {:ok, _iter_type, state} <- pop_type(state) do + {:ok, {:continue, state, return_type}} + end + + {{:ok, :catch}, [target]} -> + with {:ok, state} <- apply_generic_stack_effect(state, op, args) do + {:ok, {:catch, target, state, return_type}} + end + + {{:ok, name}, [target]} when name in [:if_false, :if_false8, :if_true, :if_true8] -> + with {:ok, _cond_type, state} <- pop_type(state) do + {:ok, {:branch, target, state, return_type}} + end + + {{:ok, name}, [target]} when name in [:goto, :goto8, :goto16] -> + {:ok, {:goto, target, state, return_type}} + + {{:ok, :return}, _} -> + with {:ok, type, _state} <- pop_type(state) do + {:ok, {:halt, join_type(return_type, type)}} + end + + {{:ok, :return_undef}, _} -> + {:ok, {:halt, join_type(return_type, :undefined)}} + + {{:ok, name}, _} when name in [:throw, :throw_error] -> + {:ok, {:halt, return_type}} + + {{:ok, :return_async}, _} -> + {:ok, {:halt, return_type}} + + {{:ok, name}, _} + when name in [:initial_yield, :yield, :yield_star, :async_yield_star, :gosub, :ret] -> + {:ok, {:halt, return_type}} + + {{:ok, :nop}, _} -> + {:ok, {:continue, state, return_type}} + + _ -> + with {:ok, state} <- apply_generic_stack_effect(state, op, args) do + {:ok, {:continue, state, return_type}} + end + end + end + + defp transfer_unary_type(name, state, return_type) do + with {:ok, type, state} <- pop_type(state) do + result_type = unary_result_type(name, type) + {:ok, {:continue, push_type(state, result_type), return_type}} + end + end + + defp transfer_binaryish_type(name, state, return_type) do + with {:ok, right_type, state} <- pop_type(state), + {:ok, left_type, state} <- pop_type(state) do + result_type = binary_result_type(name, left_type, right_type) + {:ok, {:continue, push_type(state, result_type), return_type}} + end + end + + defp initial_type_state(fun, slot_count, stack_depth) do + slot_types = + if slot_count == 0, + do: %{}, + else: Map.new(0..(slot_count - 1), fn idx -> {idx, :unknown} end) + + slot_inits = + if slot_count == 0, + do: %{}, + else: Map.new(0..(slot_count - 1), fn idx -> {idx, initially_initialized?(fun, idx)} end) + + %{ + slot_types: slot_types, + slot_inits: slot_inits, + stack_types: List.duplicate(:unknown, stack_depth) + } + end + + defp merge_block_updates(entry_types, updates) do + Enum.reduce(updates, entry_types, fn {target, state}, acc -> + Map.update(acc, target, state, &merge_type_state(&1, state)) + end) + end + + defp merge_type_state(left, right) do + %{ + slot_types: + Map.merge(left.slot_types, right.slot_types, fn _idx, left_type, right_type -> + join_type(left_type, right_type) + end), + slot_inits: + Map.merge(left.slot_inits, right.slot_inits, fn _idx, left_init, right_init -> + left_init and right_init + end), + stack_types: merge_stack_types(left.stack_types, right.stack_types) + } + end + + defp merge_stack_types(left, right) when length(left) == length(right), + do: Enum.zip_with(left, right, &join_type/2) + + defp merge_stack_types(left, _right), do: Enum.map(left, fn _ -> :unknown end) + + defp put_slot_type(state, idx, type), + do: %{state | slot_types: Map.put(state.slot_types, idx, type)} + + defp put_slot_init(state, idx, initialized), + do: %{state | slot_inits: Map.put(state.slot_inits, idx, initialized)} + + defp slot_type(state, idx), do: Map.get(state.slot_types, idx, :unknown) + defp push_type(state, type), do: %{state | stack_types: [type | state.stack_types]} + + defp pop_type(%{stack_types: [type | rest]} = state), + do: {:ok, type, %{state | stack_types: rest}} + + defp pop_type(_state), do: {:error, :stack_underflow} + + defp pop_types(state, 0), do: {:ok, state} + + defp pop_types(state, count) when count > 0 do + with {:ok, _type, state} <- pop_type(state) do + pop_types(state, count - 1) + end + end + + defp apply_generic_stack_effect(state, op, args) do + with {:ok, pop_count, push_count} <- Stack.stack_effect(op, args), + {:ok, state} <- pop_types(state, pop_count) do + next_state = + if push_count == 0 do + state + else + Enum.reduce(1..push_count, state, fn _, acc -> push_type(acc, :unknown) end) + end + + {:ok, next_state} + end + end + + defp unary_result_type(:neg, type) when type in [:integer, :number], do: type + defp unary_result_type(:plus, type) when type in [:integer, :number], do: type + defp unary_result_type(:typeof, _type), do: :string + defp unary_result_type(:delete, _type), do: :boolean + defp unary_result_type(:not, _type), do: :integer + defp unary_result_type(:lnot, _type), do: :boolean + defp unary_result_type(:is_undefined, _type), do: :boolean + defp unary_result_type(:is_null, _type), do: :boolean + defp unary_result_type(_name, _type), do: :unknown + + defp binary_result_type(:add, :integer, :integer), do: :integer + defp binary_result_type(:add, :string, :string), do: :string + + defp binary_result_type(:add, left, right) + when left in [:integer, :number] and right in [:integer, :number], + do: :number + + defp binary_result_type(name, left, right) + when name in [:sub, :mul] and left == :integer and right == :integer, + do: :integer + + defp binary_result_type(name, left, right) + when name in [:sub, :mul, :div, :mod, :pow] and left in [:integer, :number] and + right in [:integer, :number], + do: :number + + defp binary_result_type(name, left, right) + when name in [:lt, :lte, :gt, :gte] and left in [:integer, :number] and + right in [:integer, :number], + do: :boolean + + defp binary_result_type(name, _left, _right) + when name in [ + :lt, + :lte, + :gt, + :gte, + :eq, + :neq, + :strict_eq, + :strict_neq, + :instanceof, + :in, + :typeof_is_undefined, + :typeof_is_function, + :is_undefined_or_null + ], + do: :boolean + + defp binary_result_type(name, _left, _right) + when name in [:shl, :sar, :shr, :band, :bor, :bxor], + do: :integer + + defp binary_result_type(_name, _left, _right), do: :unknown + + defp invoke_result_type(:self_fun, return_type), do: return_type + defp invoke_result_type({:function, type}, _return_type), do: type + defp invoke_result_type(_fun_type, _return_type), do: :unknown + + defp constant_type(constants, idx) do + case Enum.at(constants, idx) do + value when is_integer(value) -> :integer + value when is_float(value) -> :number + value when is_boolean(value) -> :boolean + value when is_binary(value) -> :string + nil -> :null + :undefined -> :undefined + %Bytecode.Function{} = fun -> function_type(fun) + _ -> :unknown + end + end + + defp closure_type(constants, idx) do + case Enum.at(constants, idx) do + %Bytecode.Function{} = fun -> function_type(fun) + _ -> :function + end + end + + defp special_object_type(2), do: :self_fun + defp special_object_type(3), do: :function + defp special_object_type(type) when type in [0, 1, 5, 6, 7], do: :object + defp special_object_type(_type), do: :unknown + + defp literal_type(value) when is_integer(value), do: :integer + defp literal_type(value) when is_float(value), do: :number + defp literal_type(value) when is_boolean(value), do: :boolean + defp literal_type(value) when is_binary(value), do: :string + defp literal_type(nil), do: :null + defp literal_type(:undefined), do: :undefined + defp literal_type(_value), do: :unknown + + defp join_type(:unknown, other), do: other + defp join_type(other, :unknown), do: other + defp join_type(type, type), do: type + defp join_type(:integer, :number), do: :number + defp join_type(:number, :integer), do: :number + defp join_type(:self_fun, :function), do: :function + defp join_type(:function, :self_fun), do: :function + defp join_type({:function, left}, {:function, right}), do: {:function, join_type(left, right)} + defp join_type({:function, type}, :function), do: {:function, type} + defp join_type(:function, {:function, type}), do: {:function, type} + defp join_type(:self_fun, {:function, type}), do: {:function, type} + defp join_type({:function, type}, :self_fun), do: {:function, type} + defp join_type(_left, _right), do: :unknown + + defp initially_initialized?(fun, idx) when idx < fun.arg_count, do: true + + defp initially_initialized?(fun, idx) do + case Enum.at(fun.locals, idx) do + %{is_lexical: true} -> false + _ -> true + end + end +end diff --git a/lib/quickbeam/vm/compiler/forms.ex b/lib/quickbeam/vm/compiler/forms.ex new file mode 100644 index 00000000..3b27c429 --- /dev/null +++ b/lib/quickbeam/vm/compiler/forms.ex @@ -0,0 +1,313 @@ +defmodule QuickBEAM.VM.Compiler.Forms do + @moduledoc false + + alias QuickBEAM.VM.Compiler.RuntimeHelpers + alias QuickBEAM.VM.Interpreter.Values + alias QuickBEAM.VM.Invocation + + @line 1 + + def compile_module(module, entry, ctx_entry, fun, arity, slot_count, block_forms) do + forms = [ + {:attribute, @line, :module, module}, + {:attribute, @line, :export, [{entry, arity}, {ctx_entry, arity + 1}]}, + entry_form(entry, ctx_entry, arity), + ctx_entry_form(ctx_entry, arity, slot_count) + | helper_forms(fun) ++ block_forms + ] + + case :compile.forms(forms, [:binary, :return_errors, :return_warnings]) do + {:ok, mod, binary} -> {:ok, mod, binary} + {:ok, mod, binary, _warnings} -> {:ok, mod, binary} + {:error, errors, _warnings} -> {:error, {:compile_failed, errors}} + end + end + + defp entry_form(entry, ctx_entry, arity) do + args = slot_vars(arity) + body = [local_call(ctx_entry, [remote_call(RuntimeHelpers, :entry_ctx, []) | args])] + {:function, @line, entry, arity, [{:clause, @line, args, [], body}]} + end + + defp ctx_entry_form(ctx_entry, arity, slot_count) do + ctx = var("Ctx") + args = [ctx | slot_vars(arity)] + + locals = + if slot_count <= arity, + do: [], + else: Enum.map(arity..(slot_count - 1), fn _ -> atom(:undefined) end) + + capture_cells = + if slot_count == 0, do: [], else: Enum.map(1..slot_count, fn _ -> atom(:undefined) end) + + body = [local_call(block_name(0), [ctx | slot_vars(arity) ++ locals ++ capture_cells])] + + {:function, @line, ctx_entry, arity + 1, [{:clause, @line, args, [], body}]} + end + + defp helper_forms(_fun) do + [ + add_helper(), + guarded_binary_helper(:op_sub, :-, Values, :sub), + guarded_binary_helper(:op_mul, :*, Values, :mul), + guarded_binary_helper(:op_div, :/, Values, :div), + guarded_binary_helper(:op_lt, :<, Values, :lt), + guarded_binary_helper(:op_lte, :"=<", Values, :lte), + guarded_binary_helper(:op_gt, :>, Values, :gt), + guarded_binary_helper(:op_gte, :>=, Values, :gte), + eq_helper(), + neq_helper(), + strict_eq_helper(), + strict_neq_helper(), + guarded_unary_helper(:op_neg, :-, Values, :neg), + unary_fallback_helper(:op_plus, Values, :to_number), + get_field_inline_helper(), + truthy_inline_helper(), + typeof_inline_helper() + | invoke_var_ref_runtime_helpers() + ] + end + + defp invoke_var_ref_runtime_helpers do + for prefix <- [:op_invoke_var_ref, :op_invoke_var_ref_check], + arity <- [:list, 0, 1, 2, 3] do + invoke_var_ref_runtime_helper(prefix, arity) + end + end + + defp invoke_var_ref_runtime_helper(prefix, :list) do + ctx = var("Ctx") + idx = var("Idx") + args = var("Args") + + {:function, @line, String.to_atom("#{prefix}"), 3, + [ + {:clause, @line, [ctx, idx, args], [], + [ + remote_call(Invocation, :invoke_runtime, [ + ctx, + remote_call(RuntimeHelpers, getter_name(prefix), [ctx, idx]), + args + ]) + ]} + ]} + end + + defp invoke_var_ref_runtime_helper(prefix, argc) when argc in 0..3 do + ctx = var("Ctx") + idx = var("Idx") + args = if argc == 0, do: [], else: Enum.map(1..argc, &var("Arg#{&1}")) + + {:function, @line, String.to_atom("#{prefix}#{argc}"), argc + 2, + [ + {:clause, @line, [ctx, idx | args], [], + [ + remote_call(Invocation, :invoke_runtime, [ + ctx, + remote_call(RuntimeHelpers, getter_name(prefix), [ctx, idx]), + list_expr(args) + ]) + ]} + ]} + end + + defp getter_name(:op_invoke_var_ref), do: :get_var_ref + defp getter_name(:op_invoke_var_ref_check), do: :get_var_ref_check + + defp add_helper do + a = var("A") + b = var("B") + + {:function, @line, :op_add, 2, + [ + {:clause, @line, [a, b], [integer_guards(a, b)], [{:op, @line, :+, a, b}]}, + {:clause, @line, [a, b], [binary_guards(a, b)], [binary_concat(a, b)]}, + {:clause, @line, [a, b], [], [remote_call(Values, :add, [a, b])]} + ]} + end + + defp guarded_binary_helper(name, op, fallback_mod, fallback_fun) do + a = var("A") + b = var("B") + + {:function, @line, name, 2, + [ + {:clause, @line, [a, b], [integer_guards(a, b)], [{:op, @line, op, a, b}]}, + {:clause, @line, [a, b], [], [remote_call(fallback_mod, fallback_fun, [a, b])]} + ]} + end + + defp guarded_unary_helper(name, op, fallback_mod, fallback_fun) do + a = var("A") + + {:function, @line, name, 1, + [ + {:clause, @line, [a], [[integer_guard(a)]], [{:op, @line, op, a}]}, + {:clause, @line, [a], [], [remote_call(fallback_mod, fallback_fun, [a])]} + ]} + end + + defp unary_fallback_helper(name, fallback_mod, fallback_fun) do + a = var("A") + + {:function, @line, name, 1, + [ + {:clause, @line, [a], [[integer_guard(a)]], [a]}, + {:clause, @line, [a], [], [remote_call(fallback_mod, fallback_fun, [a])]} + ]} + end + + defp eq_helper do + a = var("A") + b = var("B") + + same = var("Same") + {:function, @line, :op_eq, 2, + [ + {:clause, @line, [same, same], [], [{:atom, @line, true}]}, + {:clause, @line, [a, b], [number_guards(a, b)], [{:op, @line, :==, a, b}]}, + {:clause, @line, [a, b], [[{:op, @line, :andalso, {:call, @line, {:atom, @line, :is_binary}, [a]}, {:call, @line, {:atom, @line, :is_binary}, [b]}}]], [{:op, @line, :==, a, b}]}, + {:clause, @line, [a, b], [], [remote_call(Values, :eq, [a, b])]} + ]} + end + + defp neq_helper do + a = var("A") + b = var("B") + + {:function, @line, :op_neq, 2, + [ + {:clause, @line, [a, b], [], [{:op, @line, :not, local_call(:op_eq, [a, b])}]} + ]} + end + + defp strict_eq_helper do + a = var("A") + b = var("B") + + {:function, @line, :op_strict_eq, 2, + [ + {:clause, @line, [a, b], [number_guards(a, b)], [{:op, @line, :==, a, b}]}, + {:clause, @line, [a, b], [], [remote_call(Values, :strict_eq, [a, b])]} + ]} + end + + defp strict_neq_helper do + a = var("A") + b = var("B") + + {:function, @line, :op_strict_neq, 2, + [ + {:clause, @line, [a, b], [], [{:op, @line, :not, local_call(:op_strict_eq, [a, b])}]} + ]} + end + + defp integer_guards(a, b), do: [integer_guard(a), integer_guard(b)] + defp number_guards(a, b), do: [number_guard(a), number_guard(b)] + defp binary_guards(a, b), do: [binary_guard(a), binary_guard(b)] + defp integer_guard(expr), do: {:call, @line, {:atom, @line, :is_integer}, [expr]} + + defp number_guard(expr), do: {:call, @line, {:atom, @line, :is_number}, [expr]} + defp binary_guard(expr), do: {:call, @line, {:atom, @line, :is_binary}, [expr]} + + defp block_name(idx), do: String.to_atom("block_#{idx}") + defp slot_var(idx), do: var("Slot#{idx}") + defp slot_vars(0), do: [] + defp slot_vars(count), do: Enum.map(0..(count - 1), &slot_var/1) + defp var(name) when is_binary(name), do: {:var, @line, String.to_atom(name)} + defp atom(value), do: {:atom, @line, value} + + defp remote_call(mod, fun, args) do + {:call, @line, {:remote, @line, {:atom, @line, mod}, {:atom, @line, fun}}, args} + end + + defp binary_concat(left, right) do + {:bin, @line, + [ + {:bin_element, @line, left, :default, [:binary]}, + {:bin_element, @line, right, :default, [:binary]} + ]} + end + + defp get_field_inline_helper do + obj = var("Obj") + key = var("Key") + id = var("Id") + offsets = var("Offsets") + vals = var("Vals") + off = var("Off") + wild = var("_") + obj2 = var("Obj2") + key2 = var("Key2") + + shape_match = {:tuple, @line, [{:atom, @line, :shape}, wild, offsets, vals, wild]} + obj_tuple = {:tuple, @line, [{:atom, @line, :obj}, id]} + + {:function, @line, :op_get_field, 2, + [ + {:clause, @line, [obj_tuple, key], [], + [ + {:case, @line, + {:call, @line, {:remote, @line, {:atom, @line, :erlang}, {:atom, @line, :get}}, [id]}, + [ + {:clause, @line, [shape_match], [], + [ + {:case, @line, + {:call, @line, {:remote, @line, {:atom, @line, :maps}, {:atom, @line, :find}}, + [key, offsets]}, + [ + {:clause, @line, [{:tuple, @line, [{:atom, @line, :ok}, off]}], [], + [ + {:call, @line, {:remote, @line, {:atom, @line, :erlang}, {:atom, @line, :element}}, + [{:op, @line, :+, off, {:integer, @line, 1}}, vals]} + ]}, + {:clause, @line, [{:atom, @line, :error}], [], + [remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [obj_tuple, key])]} + ]} + ]}, + {:clause, @line, [wild], [], + [remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [obj_tuple, key])]} + ]} + ]}, + {:clause, @line, [obj2, key2], [], + [remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [obj2, key2])]} + ]} + end + + defp local_call(fun, args), do: {:call, @line, {:atom, @line, fun}, args} + + + defp truthy_inline_helper do + v = var("V") + + {:function, @line, :op_truthy, 1, + [ + {:clause, @line, [{:atom, @line, nil}], [], [{:atom, @line, false}]}, + {:clause, @line, [{:atom, @line, :undefined}], [], [{:atom, @line, false}]}, + {:clause, @line, [{:atom, @line, false}], [], [{:atom, @line, false}]}, + {:clause, @line, [{:integer, @line, 0}], [], [{:atom, @line, false}]}, + {:clause, @line, [{:float, @line, 0.0}], [], [{:atom, @line, false}]}, + {:clause, @line, [{:bin, @line, []}], [], [{:atom, @line, false}]}, + {:clause, @line, [v], [], [{:atom, @line, true}]} + ]} + end + + defp typeof_inline_helper do + v = var("V") + + {:function, @line, :op_typeof, 1, + [ + {:clause, @line, [{:atom, @line, :undefined}], [], [:erl_parse.abstract("undefined")]}, + {:clause, @line, [{:atom, @line, nil}], [], [:erl_parse.abstract("object")]}, + {:clause, @line, [{:atom, @line, true}], [], [:erl_parse.abstract("boolean")]}, + {:clause, @line, [{:atom, @line, false}], [], [:erl_parse.abstract("boolean")]}, + {:clause, @line, [v], [[{:call, @line, {:atom, @line, :is_number}, [v]}]], [:erl_parse.abstract("number")]}, + {:clause, @line, [v], [[{:call, @line, {:atom, @line, :is_binary}, [v]}]], [:erl_parse.abstract("string")]}, + {:clause, @line, [v], [], [remote_call(Values, :typeof, [v])]} + ]} + end + + defp list_expr([]), do: {nil, @line} + defp list_expr([h | t]), do: {:cons, @line, h, list_expr(t)} +end diff --git a/lib/quickbeam/vm/compiler/generator_iterator.ex b/lib/quickbeam/vm/compiler/generator_iterator.ex new file mode 100644 index 00000000..36afb62e --- /dev/null +++ b/lib/quickbeam/vm/compiler/generator_iterator.ex @@ -0,0 +1,88 @@ +defmodule QuickBEAM.VM.Compiler.GeneratorIterator do + @moduledoc """ + Iterator protocol for compiled generator functions. + + Compiled generators throw `{:generator_yield, value, continuation}` to + suspend. The continuation is a `fun(arg)` that resumes the generator + body from the yield point with `arg` as the yield return value. + """ + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.PromiseState, as: Promise + + def build(gen_ref) do + next_fn = + {:builtin, "next", + fn + [arg | _], _this -> do_next(gen_ref, arg) + [], _this -> do_next(gen_ref, :undefined) + end} + + return_fn = + {:builtin, "return", + fn + [val | _], _this -> do_return(gen_ref, val) + [], _this -> do_return(gen_ref, :undefined) + end} + + Heap.wrap(%{"next" => next_fn, "return" => return_fn}) + end + + def build_async(gen_ref) do + next_fn = + {:builtin, "next", + fn + [arg | _], _this -> Promise.resolved(do_next(gen_ref, arg)) + [], _this -> Promise.resolved(do_next(gen_ref, :undefined)) + end} + + return_fn = + {:builtin, "return", + fn + [val | _], _this -> Promise.resolved(do_return(gen_ref, val)) + [], _this -> Promise.resolved(do_return(gen_ref, :undefined)) + end} + + Heap.wrap(%{"next" => next_fn, "return" => return_fn}) + end + + defp do_next(gen_ref, arg) do + case Heap.get_obj(gen_ref) do + %{state: :suspended, continuation: cont} when is_function(cont, 1) -> + resume(gen_ref, cont, arg) + + _ -> + done(:undefined) + end + end + + defp do_return(gen_ref, val) do + Heap.put_obj(gen_ref, %{state: :completed}) + done(val) + end + + defp resume(gen_ref, cont, arg) do + result = cont.(arg) + Heap.put_obj(gen_ref, %{state: :completed}) + done(result) + catch + {:generator_yield, val, next_cont} -> + Heap.put_obj(gen_ref, %{state: :suspended, continuation: next_cont}) + yield(val) + + {:generator_yield_star, val, next_cont} -> + Heap.put_obj(gen_ref, %{state: :suspended, continuation: next_cont}) + val + + {:generator_return, val} -> + Heap.put_obj(gen_ref, %{state: :completed}) + done(val) + + {:js_throw, _} = thrown -> + Heap.put_obj(gen_ref, %{state: :completed}) + throw(thrown) + end + + defp yield(val), do: Heap.wrap(%{"value" => val, "done" => false}) + defp done(val), do: Heap.wrap(%{"value" => val, "done" => true}) +end diff --git a/lib/quickbeam/vm/compiler/lowering.ex b/lib/quickbeam/vm/compiler/lowering.ex new file mode 100644 index 00000000..8cce1464 --- /dev/null +++ b/lib/quickbeam/vm/compiler/lowering.ex @@ -0,0 +1,851 @@ +defmodule QuickBEAM.VM.Compiler.Lowering do + @moduledoc false + + alias QuickBEAM.VM.Compiler.Analysis.{CFG, Stack, Types} + alias QuickBEAM.VM.Compiler.Lowering.Builder + alias QuickBEAM.VM.Compiler.{Lowering.Ops, Lowering.State} + + @guardable_types [:integer, :number, :boolean, :string, :undefined, :null] + @line 1 + + def lower(fun, instructions) do + entries = CFG.block_entries(instructions) + slot_count = fun.arg_count + fun.var_count + constants = fun.constants + + with {:ok, stack_depths} <- Stack.infer_block_stack_depths(instructions, entries), + {:ok, {entry_types, return_type}} <- + Types.infer_block_entry_types(fun, instructions, entries, stack_depths) do + inline_targets = CFG.inlineable_entries(instructions, entries) + + blocks = + for start <- entries, + Map.has_key?(stack_depths, start), + not MapSet.member?(inline_targets, start), + into: [] do + {start, + block_form( + fun, + start, + fun.arg_count, + slot_count, + instructions, + entries, + Map.fetch!(stack_depths, start), + stack_depths, + constants, + inline_targets, + Map.get(entry_types, start), + return_type + )} + end + + case Enum.find(blocks, fn {_start, form} -> match?({:error, _}, form) end) do + nil -> {:ok, {slot_count, Enum.map(blocks, &elem(&1, 1))}} + {_start, error} -> error + end + end + end + + defp block_form( + fun, + start, + arg_count, + slot_count, + instructions, + entries, + stack_depth, + stack_depths, + constants, + inline_targets, + entry_type_state, + return_type + ) do + next_entry = CFG.next_entry(entries, start) + + args = + [Builder.ctx_var() | Builder.slot_vars(slot_count)] ++ + Builder.stack_vars(stack_depth) ++ Builder.capture_vars(slot_count) + + fast_guards = block_clause_guards(slot_count, stack_depth, entry_type_state) + + with {:ok, fast_body} <- + lower_block( + instructions, + start, + next_entry, + arg_count, + block_state( + fun, + arg_count, + slot_count, + stack_depth, + return_type, + entry_type_state, + true + ), + stack_depths, + constants, + entries, + inline_targets + ) do + clauses = + if fast_guards == [] do + [{:clause, @line, args, [], fast_body}] + else + with {:ok, slow_body} <- + lower_block( + instructions, + start, + next_entry, + arg_count, + block_state( + fun, + arg_count, + slot_count, + stack_depth, + return_type, + entry_type_state, + false + ), + stack_depths, + constants, + entries, + inline_targets + ) do + [ + {:clause, @line, args, [fast_guards], fast_body}, + {:clause, @line, args, [], slow_body} + ] + end + end + + case clauses do + {:error, _} = error -> + error + + clauses -> + {:function, @line, Builder.block_name(start), 1 + slot_count + stack_depth + slot_count, + clauses} + end + end + end + + defp block_state(fun, arg_count, slot_count, stack_depth, return_type, entry_type_state, typed?) do + state_opts = + [ + locals: fun.locals, + closure_vars: fun.closure_vars, + atoms: Process.get({:qb_fn_atoms, fun.byte_code}), + arg_count: arg_count, + return_type: return_type + ] ++ + case {entry_type_state, typed?} do + {nil, _} -> + [] + + {entry_type_state, true} -> + [ + slot_types: entry_type_state.slot_types, + slot_inits: entry_type_state.slot_inits, + stack_types: entry_type_state.stack_types + ] + + {entry_type_state, false} -> + [slot_inits: entry_type_state.slot_inits] + end + + State.new(slot_count, stack_depth, state_opts) + end + + defp block_clause_guards(_slot_count, _stack_depth, nil), do: [] + + defp block_clause_guards(slot_count, stack_depth, entry_type_state) do + slot_guards = + if slot_count == 0 do + [] + else + for idx <- 0..(slot_count - 1), + guard = + type_guard( + Builder.slot_var(idx), + Map.get(entry_type_state.slot_types, idx, :unknown) + ), + guard != nil, + do: guard + end + + stack_guards = + for {type, idx} <- Enum.with_index(entry_type_state.stack_types || []), + idx < stack_depth, + guard = type_guard(Builder.stack_var(idx), type), + guard != nil, + do: guard + + slot_guards ++ stack_guards + end + + defp type_guard(_expr, type) when type not in @guardable_types, do: nil + defp type_guard(expr, :integer), do: {:call, @line, {:atom, @line, :is_integer}, [expr]} + defp type_guard(expr, :number), do: {:call, @line, {:atom, @line, :is_number}, [expr]} + defp type_guard(expr, :boolean), do: {:call, @line, {:atom, @line, :is_boolean}, [expr]} + defp type_guard(expr, :string), do: {:call, @line, {:atom, @line, :is_binary}, [expr]} + defp type_guard(expr, :undefined), do: {:op, @line, :==, expr, {:atom, @line, :undefined}} + defp type_guard(expr, :null), do: {:op, @line, :==, expr, {:atom, @line, nil}} + + defp lower_block( + instructions, + idx, + next_entry, + arg_count, + state, + _stack_depths, + _constants, + _entries, + _inline_targets + ) + when idx >= length(instructions) do + {:error, {:missing_terminator, idx, next_entry, arg_count, state.body}} + end + + defp lower_block( + instructions, + idx, + idx, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + if MapSet.member?(inline_targets, idx) do + lower_block( + instructions, + idx, + CFG.next_entry(entries, idx), + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) + else + with {:ok, call} <- State.block_jump_call(state, idx, stack_depths) do + {:ok, state.body ++ [call]} + end + end + end + + defp lower_block( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + instruction = Enum.at(instructions, idx) + + case instruction do + {op, [target]} -> + case CFG.opcode_name(op) do + {:ok, :catch} -> + lower_catch_suffix( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target + ) + + {:ok, :gosub} -> + lower_gosub_suffix( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target + ) + + _ -> + lower_instruction( + instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) + end + + {op, []} -> + case CFG.opcode_name(op) do + {:ok, :object} -> + case collect_define_fields(instructions, idx + 1, arg_count, state) do + {:ok, map_pairs, skip_to, state} -> + # Extract sorted keys and corresponding values + sorted_pairs = Enum.sort_by(map_pairs, fn {k, _v} -> + # k is Builder.literal(string) — extract the string + case k do + {:bin, _, [{:bin_element, _, {:string, _, chars}, _, _}]} -> List.to_string(chars) + _ -> "" + end + end) + + keys_list = Enum.map(sorted_pairs, &elem(&1, 0)) + vals_list = Enum.map(sorted_pairs, &elem(&1, 1)) + keys_tuple = {:tuple, @line, keys_list} + vals_tuple = {:tuple, @line, vals_list} + + # Build compile-time offsets map from sorted key names + ct_offsets = sorted_pairs + |> Enum.with_index() + |> Enum.reduce(%{}, fn {{k_expr, _v}, idx}, acc -> + key_str = case k_expr do + {:bin, _, [{:bin_element, _, {:string, _, chars}, _, _}]} -> List.to_string(chars) + _ -> nil + end + if key_str, do: Map.put(acc, key_str, idx), else: acc + end) + + {obj, state} = + State.bind( + state, + Builder.temp_name(state.temp), + Builder.remote_call(QuickBEAM.VM.Heap, :wrap_keyed, [keys_tuple, vals_tuple]) + ) + + lower_block( + instructions, skip_to, next_entry, arg_count, + State.push(state, obj, {:shaped_object, ct_offsets}), + stack_depths, constants, entries, inline_targets + ) + + :not_literal -> + lower_instruction( + instruction, instructions, idx, next_entry, arg_count, + state, stack_depths, constants, entries, inline_targets + ) + end + + _ -> + lower_instruction( + instruction, instructions, idx, next_entry, arg_count, + state, stack_depths, constants, entries, inline_targets + ) + end + + _ -> + lower_instruction( + instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) + end + end + + defp collect_define_fields(instructions, idx, arg_count, state) do + collect_define_fields(instructions, idx, arg_count, state, []) + end + + defp collect_define_fields(instructions, idx, arg_count, state, acc) do + val_instr = Enum.at(instructions, idx) + df_instr = Enum.at(instructions, idx + 1) + + with {val_op, val_args} <- val_instr, + {df_op, [key_idx]} <- df_instr, + {:ok, :define_field} <- CFG.opcode_name(df_op), + {:ok, val_expr, new_state} <- lower_value_opcode(val_op, val_args, arg_count, state) do + key_name = Builder.atom_name(new_state, key_idx) + + if is_binary(key_name) do + key_expr = Builder.literal(key_name) + collect_define_fields(instructions, idx + 2, arg_count, new_state, [{key_expr, val_expr} | acc]) + else + # Atom not resolvable at compile time — abort batching + if acc == [], do: :not_literal, else: {:ok, Enum.reverse(acc), idx, state} + end + else + _ -> + if acc == [] do + :not_literal + else + {:ok, Enum.reverse(acc), idx, state} + end + end + end + + defp lower_value_opcode(op, args, _arg_count, state) do + case CFG.opcode_name(op) do + {:ok, :push_i32} -> {:ok, Builder.integer(hd(args)), state} + {:ok, :push_i8} -> {:ok, Builder.integer(hd(args)), state} + {:ok, :push_0} -> {:ok, Builder.integer(0), state} + {:ok, :push_1} -> {:ok, Builder.integer(1), state} + {:ok, :push_2} -> {:ok, Builder.integer(2), state} + {:ok, :push_3} -> {:ok, Builder.integer(3), state} + {:ok, :push_4} -> {:ok, Builder.integer(4), state} + {:ok, :push_5} -> {:ok, Builder.integer(5), state} + {:ok, :push_6} -> {:ok, Builder.integer(6), state} + {:ok, :push_7} -> {:ok, Builder.integer(7), state} + {:ok, :push_minus1} -> {:ok, Builder.integer(-1), state} + {:ok, :null} -> {:ok, Builder.atom(nil), state} + {:ok, :undefined} -> {:ok, Builder.atom(:undefined), state} + {:ok, :push_false} -> {:ok, Builder.atom(false), state} + {:ok, :push_true} -> {:ok, Builder.atom(true), state} + {:ok, :push_empty_string} -> {:ok, Builder.literal(""), state} + {:ok, n} when n in [:get_arg0, :get_arg1, :get_arg2, :get_arg3] -> + slot_idx = case n do + :get_arg0 -> 0; :get_arg1 -> 1; :get_arg2 -> 2; :get_arg3 -> 3 + end + {:ok, State.slot_expr(state, slot_idx), state} + {:ok, :get_arg} -> {:ok, State.slot_expr(state, hd(args)), state} + {:ok, n} when n in [:get_loc0, :get_loc1, :get_loc2, :get_loc3] -> + slot_idx = case n do + :get_loc0 -> 0; :get_loc1 -> 1; :get_loc2 -> 2; :get_loc3 -> 3 + end + {:ok, State.slot_expr(state, slot_idx), state} + {:ok, :get_loc} -> {:ok, State.slot_expr(state, hd(args)), state} + {:ok, :push_atom_value} -> + {:ok, State.compiler_call(state, :push_atom_value, [Builder.literal(hd(args))]), state} + _ -> :error + end + end + + defp lower_instruction( + {op, [target]} = instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + case CFG.opcode_name(op) do + {:ok, :if_false} -> + lower_branch_instruction( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target, + false + ) + + {:ok, :if_false8} -> + lower_branch_instruction( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target, + false + ) + + {:ok, :if_true} -> + lower_branch_instruction( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target, + true + ) + + {:ok, :if_true8} -> + lower_branch_instruction( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target, + true + ) + + _ -> + lower_non_branch_instruction( + instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) + end + end + + defp lower_instruction( + instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + lower_non_branch_instruction( + instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) + end + + defp lower_non_branch_instruction( + instruction, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + case Ops.lower_instruction( + instruction, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + {:ok, next_state} -> + lower_block( + instructions, + idx + 1, + next_entry, + arg_count, + next_state, + stack_depths, + constants, + entries, + inline_targets + ) + + {:inline_goto, target, next_state} -> + lower_block( + instructions, + target, + CFG.next_entry(entries, target), + arg_count, + next_state, + stack_depths, + constants, + entries, + inline_targets + ) + + {:done, body} -> + {:ok, body} + + {:error, _} = error -> + error + end + end + + defp lower_branch_instruction( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target, + sense + ) do + if MapSet.member?(inline_targets, target) or MapSet.member?(inline_targets, next_entry) do + with {:ok, cond_expr, cond_type, state} <- State.pop_typed(state), + {:ok, target_body} <- + lower_branch_target_body( + instructions, + target, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ), + {:ok, next_body} <- + lower_branch_target_body( + instructions, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + truthy = Builder.branch_condition(cond_expr, cond_type) + false_body = if(sense, do: next_body, else: target_body) + true_body = if(sense, do: target_body, else: next_body) + {:ok, state.body ++ [Builder.branch_case(truthy, false_body, true_body)]} + end + else + lower_non_branch_instruction( + {if(sense, + do: QuickBEAM.VM.Opcodes.num(:if_true), + else: QuickBEAM.VM.Opcodes.num(:if_false) + ), [target]}, + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) + end + end + + defp lower_branch_target_body( + _instructions, + nil, + _arg_count, + _state, + _stack_depths, + _constants, + _entries, + _inline_targets + ), + do: {:error, :missing_branch_fallthrough} + + defp lower_branch_target_body( + instructions, + target, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets + ) do + if MapSet.member?(inline_targets, target) do + lower_block( + instructions, + target, + CFG.next_entry(entries, target), + arg_count, + %{state | body: []}, + stack_depths, + constants, + entries, + inline_targets + ) + else + with {:ok, call} <- State.block_jump_call(state, target, stack_depths) do + {:ok, [call]} + end + end + end + + defp lower_catch_suffix( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target + ) do + with :ok <- ensure_catch_region_supported(instructions, idx, target), + {saved_stack, state} <- freeze_stack(state), + {:ok, handler_call} <- + State.block_jump_call_values( + target, + stack_depths, + State.ctx_expr(state), + State.current_slots(state), + [Builder.var("Caught#{idx}") | saved_stack], + State.current_capture_cells(state) + ), + {:ok, try_body} <- + lower_block( + instructions, + idx + 1, + next_entry, + arg_count, + %{ + state + | body: [], + stack: [Builder.literal(target) | saved_stack], + stack_types: [:integer | state.stack_types] + }, + stack_depths, + constants, + entries, + inline_targets + ) do + {:ok, + state.body ++ + [Builder.try_catch_expr(try_body, Builder.var("Caught#{idx}"), [handler_call])]} + end + end + + defp lower_gosub_suffix( + instructions, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + entries, + inline_targets, + target + ) do + with {:ok, inlined_state} <- lower_finally_inline(instructions, target, state) do + lower_block( + instructions, + idx + 1, + next_entry, + arg_count, + inlined_state, + stack_depths, + constants, + entries, + inline_targets + ) + end + end + + defp lower_finally_inline(instructions, idx, _state) when idx >= length(instructions) do + {:error, {:missing_ret, idx}} + end + + defp lower_finally_inline(instructions, idx, state) do + instruction = Enum.at(instructions, idx) + + case instruction do + {op, []} -> + case CFG.opcode_name(op) do + {:ok, :ret} -> + {:ok, state} + + {:ok, name} when name in [:catch, :gosub, :goto, :goto8, :goto16] -> + {:error, {:unsupported_finally_opcode, name, idx}} + + _ -> + lower_finally_instruction(instructions, instruction, idx, state) + end + + {op, _args} -> + case CFG.opcode_name(op) do + {:ok, :gosub} -> + {:error, {:unsupported_finally_opcode, :gosub, idx}} + + {:ok, :catch} -> + {:error, {:unsupported_finally_opcode, :catch, idx}} + + {:ok, name} + when name in [:if_false, :if_false8, :if_true, :if_true8, :goto, :goto8, :goto16] -> + {:error, {:unsupported_finally_opcode, name, idx}} + + _ -> + lower_finally_instruction(instructions, instruction, idx, state) + end + end + end + + defp lower_finally_instruction(instructions, instruction, idx, state) do + case Ops.lower_instruction(instruction, idx, nil, 0, state, %{}, [], [], MapSet.new()) do + {:ok, next_state} -> + lower_finally_inline(instructions, idx + 1, next_state) + + {:done, body} -> + {:ok, %{state | body: body, stack: state.stack, stack_types: state.stack_types}} + + {:error, _} = error -> + error + end + end + + defp freeze_stack(%{stack: []} = state), do: {[], state} + + defp freeze_stack(state) do + state = + Enum.reduce(0..(length(state.stack) - 1), state, fn idx, state -> + {:ok, state, _bound} = State.bind_stack_entry(state, idx) + state + end) + + {state.stack, state} + end + + defp ensure_catch_region_supported(_instructions, _catch_idx, _target), do: :ok +end diff --git a/lib/quickbeam/vm/compiler/lowering/builder.ex b/lib/quickbeam/vm/compiler/lowering/builder.ex new file mode 100644 index 00000000..6586af07 --- /dev/null +++ b/lib/quickbeam/vm/compiler/lowering/builder.ex @@ -0,0 +1,131 @@ +defmodule QuickBEAM.VM.Compiler.Lowering.Builder do + @moduledoc false + + alias QuickBEAM.VM.Compiler.RuntimeHelpers + alias QuickBEAM.VM.Interpreter.Values + alias QuickBEAM.VM.PredefinedAtoms + + @line 1 + + def block_name(idx), do: String.to_atom("block_#{idx}") + def slot_name(idx, n), do: "Slot#{idx}_#{n}" + def capture_name(idx, n), do: "Capture#{idx}_#{n}" + def temp_name(n), do: "Tmp#{n}" + def ctx_var, do: var("Ctx") + def slot_var(idx), do: var("Slot#{idx}") + def stack_var(idx), do: var("Stack#{idx}") + def capture_var(idx), do: var("Capture#{idx}") + def slot_vars(0), do: [] + def slot_vars(count), do: Enum.map(0..(count - 1), &slot_var/1) + def stack_vars(0), do: [] + def stack_vars(count), do: Enum.map(0..(count - 1), &stack_var/1) + def capture_vars(0), do: [] + def capture_vars(count), do: Enum.map(0..(count - 1), &capture_var/1) + + def var(name) when is_binary(name), do: {:var, @line, String.to_atom(name)} + def var(name) when is_integer(name), do: {:var, @line, String.to_atom(Integer.to_string(name))} + def var(name) when is_atom(name), do: {:var, @line, name} + + def integer(value), do: {:integer, @line, value} + def atom(value), do: {:atom, @line, value} + def literal(value), do: :erl_parse.abstract(value) + def match(left, right), do: {:match, @line, left, right} + def tuple_expr(values), do: {:tuple, @line, values} + + def tuple_element(tuple, index) do + remote_call(:erlang, :element, [integer(index), tuple]) + end + + def map_expr(entries) do + {:map, @line, Enum.map(entries, fn {key, value} -> {:map_field_assoc, @line, key, value} end)} + end + + def list_expr([]), do: {nil, @line} + def list_expr([head | tail]), do: {:cons, @line, head, list_expr(tail)} + + def remote_call(mod, fun, args) do + {:call, @line, {:remote, @line, literal(mod), {:atom, @line, fun}}, args} + end + + def local_call(fun, args), do: {:call, @line, {:atom, @line, fun}, args} + def compiler_call(fun, args), do: remote_call(RuntimeHelpers, fun, [ctx_var() | args]) + + def throw_js(expr), do: remote_call(:erlang, :throw, [{:tuple, @line, [atom(:js_throw), expr]}]) + + def try_catch_expr(try_body, err_var, catch_body) do + {:try, @line, try_body, [], [catch_clause(err_var, catch_body)], []} + end + + def undefined_or_null_expr(expr) do + {:op, @line, :orelse, {:op, @line, :==, expr, atom(:undefined)}, + {:op, @line, :==, expr, atom(nil)}} + end + + def branch_condition(expr, :boolean), do: expr + + def branch_condition({:call, _, {:atom, _, fun}, [left, right]} = expr, _type) + when fun in [:op_lt, :op_lte, :op_gt, :op_gte], + do: comparison_branch_condition(fun, left, right, expr) + + def branch_condition({:call, _, {:atom, _, fun}, _args} = expr, _type) + when fun in [:op_eq, :op_neq, :op_strict_eq, :op_strict_neq], + do: expr + + def branch_condition(expr, :integer), do: {:op, @line, :"=/=", expr, integer(0)} + def branch_condition(_expr, :undefined), do: atom(false) + def branch_condition(_expr, :null), do: atom(false) + def branch_condition(expr, :string), do: {:op, @line, :"=/=", expr, literal("")} + def branch_condition(_expr, :object), do: atom(true) + def branch_condition(_expr, :function), do: atom(true) + def branch_condition(_expr, {:function, _}), do: atom(true) + def branch_condition(_expr, :self_fun), do: atom(true) + def branch_condition(expr, _type), do: local_call(:op_truthy, [expr]) + def branch_case(expr, false_body, true_body), do: case_expr(expr, false_body, true_body) + + def atom_name(%{atoms: atoms}, atom_idx), do: resolve_atom_name(atom_idx, atoms) + + defp resolve_local_name(name, _atoms) when is_binary(name), do: name + defp resolve_local_name({:predefined, idx}, _atoms), do: PredefinedAtoms.lookup(idx) + + defp resolve_local_name(idx, atoms) + when is_integer(idx) and is_tuple(atoms) and idx >= 0 and idx < tuple_size(atoms), + do: elem(atoms, idx) + + defp resolve_local_name(_name, _atoms), do: nil + defp resolve_atom_name(atom_idx, atoms), do: resolve_local_name(atom_idx, atoms) + + defp comparison_branch_condition(fun, left, right, fallback_expr) do + lhs = var(:BranchLhs) + rhs = var(:BranchRhs) + + {:case, @line, tuple_expr([left, right]), + [ + {:clause, @line, [tuple_expr([lhs, rhs])], [number_guards(lhs, rhs)], + [{:op, @line, comparison_operator(fun), lhs, rhs}]}, + {:clause, @line, [var(:_)], [], [fallback_expr]} + ]} + end + + defp comparison_operator(:op_lt), do: :< + defp comparison_operator(:op_lte), do: :"=<" + defp comparison_operator(:op_gt), do: :> + defp comparison_operator(:op_gte), do: :>= + + defp number_guards(a, b), do: [number_guard(a), number_guard(b)] + defp number_guard(expr), do: {:call, @line, {:atom, @line, :is_number}, [expr]} + + defp case_expr(expr, false_body, true_body) do + {:case, @line, expr, + [ + {:clause, @line, [atom(false)], [], false_body}, + {:clause, @line, [atom(true)], [], true_body} + ]} + end + + defp catch_clause(err_var, catch_body) do + pattern = + {:tuple, @line, [atom(:throw), {:tuple, @line, [atom(:js_throw), err_var]}, var(:_)]} + + {:clause, @line, [pattern], [], catch_body} + end +end diff --git a/lib/quickbeam/vm/compiler/lowering/captures.ex b/lib/quickbeam/vm/compiler/lowering/captures.ex new file mode 100644 index 00000000..59122a30 --- /dev/null +++ b/lib/quickbeam/vm/compiler/lowering/captures.ex @@ -0,0 +1,60 @@ +defmodule QuickBEAM.VM.Compiler.Lowering.Captures do + @moduledoc false + + alias QuickBEAM.VM.Compiler.Lowering.{Builder, State} + + def ensure_capture_cell(state, idx) do + {bound, state} = + State.bind( + state, + Builder.capture_name(idx, state.temp), + State.compiler_call(state, :ensure_capture_cell, [ + State.capture_cell_expr(state, idx), + State.slot_expr(state, idx) + ]) + ) + + {:ok, State.put_capture_cell(state, idx, bound), bound} + end + + def close_capture_cell(state, idx) do + {bound, state} = + State.bind( + state, + Builder.capture_name(idx, state.temp), + State.compiler_call(state, :close_capture_cell, [ + State.capture_cell_expr(state, idx), + State.slot_expr(state, idx) + ]) + ) + + {:ok, State.put_capture_cell(state, idx, bound)} + end + + def sync_capture_cell(state, idx, expr) do + if slot_captured?(state, idx) do + %{ + state + | body: + state.body ++ + [ + State.compiler_call(state, :sync_capture_cell, [ + State.capture_cell_expr(state, idx), + expr + ]) + ] + } + else + state + end + end + + def slot_captured?(%{locals: locals}, idx) when is_list(locals) do + case Enum.at(locals, idx) do + %{is_captured: true} -> true + _ -> false + end + end + + def slot_captured?(_state, _idx), do: false +end diff --git a/lib/quickbeam/vm/compiler/lowering/ops.ex b/lib/quickbeam/vm/compiler/lowering/ops.ex new file mode 100644 index 00000000..03c89ab3 --- /dev/null +++ b/lib/quickbeam/vm/compiler/lowering/ops.ex @@ -0,0 +1,1676 @@ +defmodule QuickBEAM.VM.Compiler.Lowering.Ops do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, GlobalEnv} + alias QuickBEAM.VM.Compiler.Analysis.CFG + alias QuickBEAM.VM.Compiler.Analysis.Types, as: AnalysisTypes + alias QuickBEAM.VM.Compiler.Lowering.{Builder, Captures, State} + alias QuickBEAM.VM.Compiler.RuntimeHelpers + alias QuickBEAM.VM.Interpreter.Values + alias QuickBEAM.VM.ObjectModel.{Class, Private} + + @tdz :__tdz__ + + def lower_instruction( + {op, args}, + idx, + next_entry, + arg_count, + state, + stack_depths, + constants, + _entries, + inline_targets + ) do + name = CFG.opcode_name(op) + + case {name, args} do + {{:ok, :push_i32}, [value]} -> + {:ok, State.push(state, Builder.integer(value))} + + {{:ok, :push_i16}, [value]} -> + {:ok, State.push(state, Builder.integer(value))} + + {{:ok, :push_i8}, [value]} -> + {:ok, State.push(state, Builder.integer(value))} + + {{:ok, :push_minus1}, [_]} -> + {:ok, State.push(state, Builder.integer(-1))} + + {{:ok, :push_0}, [_]} -> + {:ok, State.push(state, Builder.integer(0))} + + {{:ok, :push_1}, [_]} -> + {:ok, State.push(state, Builder.integer(1))} + + {{:ok, :push_2}, [_]} -> + {:ok, State.push(state, Builder.integer(2))} + + {{:ok, :push_3}, [_]} -> + {:ok, State.push(state, Builder.integer(3))} + + {{:ok, :push_4}, [_]} -> + {:ok, State.push(state, Builder.integer(4))} + + {{:ok, :push_5}, [_]} -> + {:ok, State.push(state, Builder.integer(5))} + + {{:ok, :push_6}, [_]} -> + {:ok, State.push(state, Builder.integer(6))} + + {{:ok, :push_7}, [_]} -> + {:ok, State.push(state, Builder.integer(7))} + + {{:ok, :push_true}, []} -> + {:ok, State.push(state, Builder.atom(true))} + + {{:ok, :push_false}, []} -> + {:ok, State.push(state, Builder.atom(false))} + + {{:ok, :null}, []} -> + {:ok, State.push(state, Builder.atom(nil))} + + {{:ok, :undefined}, []} -> + {:ok, State.push(state, Builder.atom(:undefined))} + + {{:ok, :push_empty_string}, []} -> + {:ok, State.push(state, Builder.literal(""))} + + {{:ok, :object}, []} -> + {obj, state} = + State.bind( + state, + Builder.temp_name(state.temp), + Builder.remote_call(QuickBEAM.VM.Heap, :wrap, [Builder.literal(%{})]) + ) + + {:ok, State.push(state, obj, {:shaped_object, %{}})} + + {{:ok, :array_from}, [argc]} -> + State.array_from_call(state, argc) + + {{:ok, :push_const}, [const_idx]} -> + push_const(state, constants, arg_count, const_idx) + + {{:ok, :push_const8}, [const_idx]} -> + push_const(state, constants, arg_count, const_idx) + + {{:ok, :fclosure}, [const_idx]} -> + lower_fclosure(state, constants, arg_count, const_idx) + + {{:ok, :fclosure8}, [const_idx]} -> + lower_fclosure(state, constants, arg_count, const_idx) + + {{:ok, :regexp}, []} -> + State.regexp_literal(state) + + {{:ok, :private_symbol}, [atom_idx]} -> + {:ok, + State.push( + state, + State.compiler_call(state, :private_symbol, [ + Builder.literal(Builder.atom_name(state, atom_idx)) + ]), + :unknown + )} + + {{:ok, :push_atom_value}, [atom_idx]} -> + {:ok, State.push(state, Builder.literal(Builder.atom_name(state, atom_idx)), :string)} + + {{:ok, :push_this}, []} -> + {:ok, State.push(state, State.compiler_call(state, :push_this, []), :object)} + + {{:ok, :special_object}, [type]} -> + {:ok, + State.push( + state, + State.compiler_call(state, :special_object, [Builder.literal(type)]), + special_object_type(type) + )} + + {{:ok, :set_name}, [atom_idx]} -> + State.set_name_atom(state, Builder.atom_name(state, atom_idx)) + + {{:ok, :set_name_computed}, []} -> + State.set_name_computed(state) + + {{:ok, :set_home_object}, []} -> + State.set_home_object(state) + + {{:ok, :close_loc}, [slot_idx]} -> + Captures.close_capture_cell(state, slot_idx) + + {{:ok, :get_var}, [atom_idx]} -> + name = Builder.atom_name(state, atom_idx) + if is_binary(name) do + {:ok, State.push(state, inline_get_var(state, name))} + else + {:ok, + State.push( + state, + State.compiler_call(state, :get_var, [Builder.literal(name)]) + )} + end + + {{:ok, :get_var_undef}, [atom_idx]} -> + name = Builder.atom_name(state, atom_idx) + if is_binary(name) do + {:ok, State.push(state, inline_get_var_undef(state, name))} + else + {:ok, + State.push( + state, + State.compiler_call(state, :get_var_undef, [Builder.literal(name)]) + )} + end + + {{:ok, :get_super}, []} -> + State.unary_call(state, RuntimeHelpers, :get_super) + + {{:ok, :get_arg}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_arg0}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_arg1}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_arg2}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_arg3}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc0}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc1}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc2}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc3}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc8}, [slot_idx]} -> + {:ok, + State.push(state, State.slot_expr(state, slot_idx), State.slot_type(state, slot_idx))} + + {{:ok, :get_loc0_loc1}, [slot0, slot1]} -> + {:ok, + %{ + state + | stack: [State.slot_expr(state, slot1), State.slot_expr(state, slot0) | state.stack], + stack_types: [ + State.slot_type(state, slot1), + State.slot_type(state, slot0) | state.stack_types + ] + }} + + {{:ok, :get_loc_check}, [slot_idx]} -> + lower_get_loc_check(state, slot_idx) + + {{:ok, name}, [idx]} + when name in [:get_var_ref, :get_var_ref0, :get_var_ref1, :get_var_ref2, :get_var_ref3] -> + {expr, state} = State.inline_get_var_ref(state, idx) + {:ok, State.push(state, expr)} + + {{:ok, :get_var_ref_check}, [idx]} -> + {expr, state} = State.inline_get_var_ref(state, idx) + {:ok, State.push(state, expr)} + + {{:ok, :set_loc_uninitialized}, [slot_idx]} -> + {:ok, State.put_uninitialized_slot(state, slot_idx, Builder.atom(@tdz))} + + {{:ok, :define_var}, [atom_idx, _scope]} -> + {:ok, + State.update_ctx( + state, + Builder.remote_call(GlobalEnv, :define_var, [ + State.ctx_expr(state), + Builder.literal(atom_idx) + ]) + )} + + {{:ok, :check_define_var}, [atom_idx, _scope]} -> + {:ok, + State.update_ctx( + state, + Builder.remote_call(GlobalEnv, :check_define_var, [ + State.ctx_expr(state), + Builder.literal(atom_idx) + ]) + )} + + {{:ok, :put_var}, [atom_idx]} -> + lower_put_var(state, atom_idx) + + {{:ok, :put_var_init}, [atom_idx]} -> + lower_put_var(state, atom_idx) + + {{:ok, :define_func}, [atom_idx, _flags]} -> + lower_put_var(state, atom_idx) + + {{:ok, :put_loc}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_loc0}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_loc1}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_loc2}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_loc3}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_loc8}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_arg}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_arg0}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_arg1}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_arg2}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :put_arg3}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, name}, [idx]} + when name in [ + :put_var_ref, + :put_var_ref0, + :put_var_ref1, + :put_var_ref2, + :put_var_ref3, + :put_var_ref_check, + :put_var_ref_check_init + ] -> + lower_put_var_ref(state, idx) + + {{:ok, :put_loc_check}, [slot_idx]} -> + lower_put_loc_check(state, slot_idx) + + {{:ok, :put_loc_check_init}, [slot_idx]} -> + State.assign_slot(state, slot_idx, false) + + {{:ok, :set_loc}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_loc0}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_loc1}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_loc2}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_loc3}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_loc8}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_arg}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_arg0}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_arg1}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_arg2}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, :set_arg3}, [slot_idx]} -> + State.assign_slot(state, slot_idx, true) + + {{:ok, name}, [idx]} + when name in [:set_var_ref, :set_var_ref0, :set_var_ref1, :set_var_ref2, :set_var_ref3] -> + lower_set_var_ref(state, idx) + + {{:ok, :dup}, []} -> + State.duplicate_top(state) + + {{:ok, :dup1}, []} -> + lower_dup1(state) + + {{:ok, :dup2}, []} -> + State.duplicate_top_two(state) + + {{:ok, :dup3}, []} -> + lower_dup3(state) + + {{:ok, :insert2}, []} -> + State.insert_top_two(state) + + {{:ok, :insert3}, []} -> + State.insert_top_three(state) + + {{:ok, :insert4}, []} -> + lower_insert4(state) + + {{:ok, :drop}, []} -> + State.drop_top(state) + + {{:ok, :nip}, []} -> + lower_nip(state) + + {{:ok, :nip1}, []} -> + lower_nip1(state) + + {{:ok, :swap}, []} -> + State.swap_top(state) + + {{:ok, :swap2}, []} -> + lower_swap2(state) + + {{:ok, :rot3l}, []} -> + lower_rot3l(state) + + {{:ok, :rot3r}, []} -> + lower_rot3r(state) + + {{:ok, :rot4l}, []} -> + lower_rot4l(state) + + {{:ok, :rot5l}, []} -> + lower_rot5l(state) + + {{:ok, :perm3}, []} -> + State.permute_top_three(state) + + {{:ok, :perm4}, []} -> + lower_perm4(state) + + {{:ok, :perm5}, []} -> + lower_perm5(state) + + {{:ok, :neg}, []} -> + State.unary_local_call(state, :op_neg) + + {{:ok, :plus}, []} -> + State.unary_local_call(state, :op_plus) + + {{:ok, :not}, []} -> + State.unary_call(state, RuntimeHelpers, :bit_not) + + {{:ok, :lnot}, []} -> + State.unary_call(state, RuntimeHelpers, :lnot) + + {{:ok, :is_undefined}, []} -> + State.unary_call(state, RuntimeHelpers, :undefined?) + + {{:ok, :is_null}, []} -> + State.unary_call(state, RuntimeHelpers, :null?) + + {{:ok, :typeof_is_undefined}, []} -> + State.unary_call(state, RuntimeHelpers, :typeof_is_undefined) + + {{:ok, :typeof_is_function}, []} -> + State.unary_call(state, RuntimeHelpers, :typeof_is_function) + + {{:ok, :inc}, []} -> + State.unary_call(state, RuntimeHelpers, :inc) + + {{:ok, :dec}, []} -> + State.unary_call(state, RuntimeHelpers, :dec) + + {{:ok, :inc_loc}, [slot_idx]} -> + State.inc_slot(state, slot_idx) + + {{:ok, :dec_loc}, [slot_idx]} -> + State.dec_slot(state, slot_idx) + + {{:ok, :add_loc}, [slot_idx]} -> + State.add_to_slot(state, slot_idx) + + {{:ok, :post_inc}, []} -> + State.post_update(state, :post_inc) + + {{:ok, :post_dec}, []} -> + State.post_update(state, :post_dec) + + {{:ok, :add}, []} -> + State.binary_local_call(state, :op_add) + + {{:ok, :sub}, []} -> + State.binary_local_call(state, :op_sub) + + {{:ok, :mul}, []} -> + State.binary_local_call(state, :op_mul) + + {{:ok, :div}, []} -> + State.binary_local_call(state, :op_div) + + {{:ok, :mod}, []} -> + State.binary_call(state, Values, :mod) + + {{:ok, :pow}, []} -> + State.binary_call(state, Values, :pow) + + {{:ok, :band}, []} -> + State.binary_call(state, Values, :band) + + {{:ok, :bor}, []} -> + State.binary_call(state, Values, :bor) + + {{:ok, :bxor}, []} -> + State.binary_call(state, Values, :bxor) + + {{:ok, :shl}, []} -> + State.binary_call(state, Values, :shl) + + {{:ok, :sar}, []} -> + State.binary_call(state, Values, :sar) + + {{:ok, :shr}, []} -> + State.binary_call(state, Values, :shr) + + {{:ok, :typeof}, []} -> + with {:ok, expr, _type, state} <- State.pop_typed(state) do + {:ok, State.push(state, Builder.local_call(:op_typeof, [expr]))} + end + + {{:ok, :instanceof}, []} -> + State.binary_call(state, RuntimeHelpers, :instanceof) + + {{:ok, :in}, []} -> + State.in_call(state) + + {{:ok, :delete}, []} -> + State.delete_call(state) + + {{:ok, :get_length}, []} -> + State.get_length_call(state) + + {{:ok, :get_array_el}, []} -> + State.binary_call(state, QuickBEAM.VM.ObjectModel.Put, :get_element) + + {{:ok, :get_array_el2}, []} -> + State.get_array_el2(state) + + {{:ok, :get_field}, [atom_idx]} -> + State.get_field_call(state, Builder.literal(Builder.atom_name(state, atom_idx))) + + {{:ok, :get_field2}, [atom_idx]} -> + State.get_field2(state, Builder.literal(Builder.atom_name(state, atom_idx))) + + {{:ok, :put_field}, [atom_idx]} -> + State.put_field_call(state, Builder.literal(Builder.atom_name(state, atom_idx))) + + {{:ok, :define_field}, [atom_idx]} -> + State.define_field_name_call(state, Builder.literal(Builder.atom_name(state, atom_idx))) + + {{:ok, :define_method}, [atom_idx, flags]} -> + State.define_method_call(state, Builder.atom_name(state, atom_idx), flags) + + {{:ok, :define_method_computed}, [flags]} -> + State.define_method_computed_call(state, flags) + + {{:ok, :define_class}, [atom_idx, _flags]} -> + State.define_class_call(state, atom_idx) + + {{:ok, :define_class_computed}, [atom_idx, _flags]} -> + lower_define_class_computed(state, atom_idx) + + {{:ok, :set_proto}, []} -> + lower_set_proto(state) + + {{:ok, :get_super_value}, []} -> + lower_get_super_value(state) + + {{:ok, :put_super_value}, []} -> + lower_put_super_value(state) + + {{:ok, :check_ctor_return}, []} -> + lower_check_ctor_return(state) + + {{:ok, :init_ctor}, []} -> + lower_init_ctor(state) + + {{:ok, :put_array_el}, []} -> + State.put_array_el_call(state) + + {{:ok, :define_array_el}, []} -> + State.define_array_el_call(state) + + {{:ok, :append}, []} -> + State.append_call(state) + + {{:ok, :copy_data_properties}, [mask]} -> + State.copy_data_properties_call(state, mask) + + {{:ok, :to_object}, []} -> + {:ok, state} + + {{:ok, :to_propkey}, []} -> + {:ok, state} + + {{:ok, :to_propkey2}, []} -> + {:ok, state} + + {{:ok, :check_ctor}, []} -> + {:ok, state} + + {{:ok, :lt}, []} -> + State.binary_local_call(state, :op_lt) + + {{:ok, :lte}, []} -> + State.binary_local_call(state, :op_lte) + + {{:ok, :gt}, []} -> + State.binary_local_call(state, :op_gt) + + {{:ok, :gte}, []} -> + State.binary_local_call(state, :op_gte) + + {{:ok, :eq}, []} -> + State.binary_local_call(state, :op_eq) + + {{:ok, :neq}, []} -> + State.binary_local_call(state, :op_neq) + + {{:ok, :strict_eq}, []} -> + State.binary_local_call(state, :op_strict_eq) + + {{:ok, :strict_neq}, []} -> + State.binary_local_call(state, :op_strict_neq) + + {{:ok, :for_in_start}, []} -> + lower_for_in_start(state) + + {{:ok, :for_in_next}, []} -> + lower_for_in_next(state) + + {{:ok, :for_of_start}, []} -> + lower_for_of_start(state) + + {{:ok, :for_of_next}, [iter_idx]} -> + lower_for_of_next(state, iter_idx) + + {{:ok, :iterator_close}, []} -> + lower_iterator_close(state) + + {{:ok, :add_brand}, []} -> + State.add_brand(state) + + {{:ok, :check_brand}, []} -> + lower_check_brand(state) + + {{:ok, :get_private_field}, []} -> + lower_get_private_field(state) + + {{:ok, :put_private_field}, []} -> + lower_put_private_field(state) + + {{:ok, :define_private_field}, []} -> + lower_define_private_field(state) + + {{:ok, :private_in}, []} -> + lower_private_in(state) + + {{:ok, :nip_catch}, []} -> + State.nip_catch(state) + + {{:ok, :throw}, []} -> + State.throw_top(state) + + {{:ok, :throw_error}, [atom_idx, reason]} -> + lower_throw_error(state, atom_idx, reason) + + {{:ok, :call_constructor}, [argc]} -> + State.invoke_constructor_call(state, argc) + + {{:ok, :call}, [argc]} -> + State.invoke_call(state, argc) + + {{:ok, :call0}, [argc]} -> + State.invoke_call(state, argc) + + {{:ok, :call1}, [argc]} -> + State.invoke_call(state, argc) + + {{:ok, :call2}, [argc]} -> + State.invoke_call(state, argc) + + {{:ok, :call3}, [argc]} -> + State.invoke_call(state, argc) + + {{:ok, :tail_call}, [argc]} -> + State.invoke_tail_call(state, argc) + + {{:ok, :call_method}, [argc]} -> + State.invoke_method_call(state, argc) + + {{:ok, :tail_call_method}, [argc]} -> + State.invoke_tail_method_call(state, argc) + + {{:ok, :make_loc_ref}, [idx]} -> + lower_make_loc_ref(state, idx) + + {{:ok, :make_arg_ref}, [idx]} -> + lower_make_arg_ref(state, idx) + + {{:ok, :make_var_ref}, [idx]} -> + lower_make_loc_ref(state, idx) + + {{:ok, :make_var_ref_ref}, [idx]} -> + lower_make_var_ref_ref(state, idx) + + {{:ok, :get_ref_value}, []} -> + lower_get_ref_value(state) + + {{:ok, :put_ref_value}, []} -> + lower_put_ref_value(state) + + {{:ok, :rest}, [start_idx]} -> + lower_rest(state, start_idx) + + {{:ok, :push_bigint_i32}, [value]} -> + {:ok, State.push(state, Builder.tuple_expr([Builder.atom(:bigint), Builder.integer(value)]))} + + {{:ok, :delete_var}, [_atom_idx]} -> + {:ok, State.push(state, Builder.atom(true), :boolean)} + + {{:ok, :is_undefined_or_null}, []} -> + lower_is_undefined_or_null(state) + + {{:ok, :if_false}, [target]} -> + State.branch(state, idx, next_entry, target, false, stack_depths) + + {{:ok, :if_false8}, [target]} -> + State.branch(state, idx, next_entry, target, false, stack_depths) + + {{:ok, :if_true}, [target]} -> + State.branch(state, idx, next_entry, target, true, stack_depths) + + {{:ok, :if_true8}, [target]} -> + State.branch(state, idx, next_entry, target, true, stack_depths) + + {{:ok, :goto}, [target]} -> + lower_goto(state, target, stack_depths, inline_targets) + + {{:ok, :goto8}, [target]} -> + lower_goto(state, target, stack_depths, inline_targets) + + {{:ok, :goto16}, [target]} -> + lower_goto(state, target, stack_depths, inline_targets) + + {{:ok, :return}, []} -> + State.return_top(state) + + {{:ok, :return_undef}, []} -> + {:done, state.body ++ [Builder.atom(:undefined)]} + + {{:ok, :nop}, []} -> + {:ok, state} + + # ── Generators / async ── + + {{:ok, :initial_yield}, []} -> + lower_initial_yield(state, next_entry, stack_depths) + + {{:ok, :yield}, []} -> + lower_yield(state, next_entry, stack_depths) + + {{:ok, :yield_star}, []} -> + lower_yield_star(state, next_entry, stack_depths) + + {{:ok, :async_yield_star}, []} -> + lower_yield_star(state, next_entry, stack_depths) + + {{:ok, :await}, []} -> + lower_await(state) + + {{:ok, :return_async}, []} -> + lower_return_async(state) + + {{:ok, :gosub}, [target]} -> + # gosub is used for finally blocks — the block at target is + # the finally body. We inline it as a direct call since + # the compiler already duplicates finally blocks via CFG. + State.goto(state, target, stack_depths) + + {{:ok, :ret}, []} -> + # ret returns from a gosub. In the compiler's block model + # the finally body falls through to the next block, so ret + # is a no-op terminal. + {:done, state.body ++ [Builder.atom(:undefined)]} + + {{:ok, :catch}, [_target]} -> + # catch pushes catch offset — the compiler handles try/catch + # via BEAM exceptions; push a dummy offset for nip_catch. + {:ok, State.push(state, Builder.integer(0))} + + # ── eval / apply / import ── + + {{:ok, :eval}, [argc | _scope_args]} -> + with {:ok, args, _types, state} <- State.pop_n_typed(state, argc + 1) do + [eval_ref | call_args] = Enum.reverse(args) + State.effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.Invocation, :invoke_runtime, [ + State.ctx_expr(state), eval_ref, Builder.list_expr(call_args) + ]) + ) + end + + {{:ok, :apply_eval}, [_scope_idx]} -> + with {:ok, arg_array, state} <- State.pop(state), + {:ok, fun, state} <- State.pop(state) do + State.effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.Invocation, :invoke_runtime, [ + State.ctx_expr(state), + fun, + Builder.remote_call(QuickBEAM.VM.Heap, :to_list, [arg_array]) + ]) + ) + end + + {{:ok, :apply}, [1]} -> + # super(...args): stack is [arg_array, new_target, fun] + with {:ok, arg_array, state} <- State.pop(state), + {:ok, new_target, state} <- State.pop(state), + {:ok, fun, state} <- State.pop(state) do + {result, state} = + State.bind( + state, + Builder.temp_name(state.temp), + State.compiler_call(state, :apply_super, [ + fun, + new_target, + Builder.remote_call(QuickBEAM.VM.Heap, :to_list, [arg_array]) + ]) + ) + + state = State.update_ctx( + state, + State.compiler_call(state, :update_this, [result]) + ) + {:ok, State.push(state, result)} + end + + {{:ok, :apply}, [_magic]} -> + with {:ok, arg_array, state} <- State.pop(state), + {:ok, this_obj, state} <- State.pop(state), + {:ok, fun, state} <- State.pop(state) do + State.effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.Invocation, :invoke_method_runtime, [ + State.ctx_expr(state), + fun, + this_obj, + Builder.remote_call(QuickBEAM.VM.Heap, :to_list, [arg_array]) + ]) + ) + end + + {{:ok, :import}, []} -> + with {:ok, _meta, state} <- State.pop(state), + {:ok, specifier, state} <- State.pop(state) do + State.effectful_push( + state, + State.compiler_call(state, :import_module, [specifier]) + ) + end + + # ── with statement ── + + {{:ok, name}, [atom_idx, _target, _is_with]} + when name in [:with_get_var, :with_get_ref, :with_get_ref_undef] -> + with {:ok, obj, _type, state} <- State.pop_typed(state) do + key = State.compiler_call(state, :push_atom_value, [Builder.literal(atom_idx)]) + val = Builder.remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [obj, key]) + case name do + :with_get_var -> {:ok, State.push(state, val)} + :with_get_ref -> {:ok, state |> State.push(obj) |> State.push(val)} + :with_get_ref_undef -> {:ok, state |> State.push(Builder.atom(:undefined)) |> State.push(val)} + end + end + + {{:ok, :with_put_var}, [atom_idx, _target, _is_with]} -> + with {:ok, obj, state} <- State.pop(state), + {:ok, val, state} <- State.pop(state) do + key = State.compiler_call(state, :push_atom_value, [Builder.literal(atom_idx)]) + {:ok, %{state | body: state.body ++ + [Builder.remote_call(QuickBEAM.VM.ObjectModel.Put, :put, [obj, key, val])]}} + end + + {{:ok, :with_delete_var}, [atom_idx, _target, _is_with]} -> + with {:ok, obj, state} <- State.pop(state) do + key = State.compiler_call(state, :push_atom_value, [Builder.literal(atom_idx)]) + State.effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.ObjectModel.Delete, :delete_property, [obj, key]) + ) + end + + {{:ok, :with_make_ref}, [atom_idx, _target, _is_with]} -> + with {:ok, obj, state} <- State.pop(state) do + key = State.compiler_call(state, :push_atom_value, [Builder.literal(atom_idx)]) + {:ok, state |> State.push(obj) |> State.push(key)} + end + + # ── Async iterators ── + + {{:ok, :for_await_of_start}, []} -> + with {:ok, obj, _type, state} <- State.pop_typed(state) do + State.effectful_push(state, State.compiler_call(state, :for_of_start, [obj])) + end + + {{:ok, :iterator_next}, []} -> + with {:ok, iter, state} <- State.pop(state) do + next_fn = Builder.remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [iter, Builder.literal("next")]) + State.effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.Runtime, :call_callback, [next_fn, Builder.list_expr([])]) + ) + end + + {{:ok, :iterator_call}, [_method]} -> + with {:ok, iter, state} <- State.pop(state) do + {:ok, %{state | body: state.body ++ + [State.compiler_call(state, :iterator_close, [iter])]}} + end + + {{:ok, :iterator_check_object}, []} -> + {:ok, state} + + {{:ok, :iterator_get_value_done}, []} -> + with {:ok, result, state} <- State.pop(state) do + done = Builder.remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [result, Builder.literal("done")]) + value = Builder.remote_call(QuickBEAM.VM.ObjectModel.Get, :get, [result, Builder.literal("value")]) + {:ok, state |> State.push(done) |> State.push(value)} + end + + {{:ok, :invalid}, _} -> + {:error, {:unsupported_opcode, :invalid}} + + {{:error, _} = error, _} -> + error + + {{:ok, name}, _} -> + {:error, {:unsupported_opcode, name}} + end + end + + defp lower_for_in_start(state) do + with {:ok, obj, _type, state} <- State.pop_typed(state) do + {:ok, State.push(state, State.compiler_call(state, :for_in_start, [obj]), :unknown)} + end + end + + defp lower_for_in_next(state) do + case State.bind_stack_entry(state, 0) do + {:ok, state, iter} -> + {result, state} = + State.bind( + state, + Builder.temp_name(state.temp), + State.compiler_call(state, :for_in_next, [iter]) + ) + + state = %{ + state + | stack: List.replace_at(state.stack, 0, Builder.tuple_element(result, 3)), + stack_types: List.replace_at(state.stack_types, 0, :unknown) + } + + state = State.push(state, Builder.tuple_element(result, 2), :unknown) + state = State.push(state, Builder.tuple_element(result, 1), :boolean) + {:ok, state} + + :error -> + {:error, :for_in_state_missing} + end + end + + defp lower_for_of_start(state) do + with {:ok, obj, _type, state} <- State.pop_typed(state) do + {pair, state} = + State.bind( + state, + Builder.temp_name(state.temp), + State.compiler_call(state, :for_of_start, [obj]) + ) + + state = State.push(state, Builder.tuple_element(pair, 1), :object) + state = State.push(state, Builder.tuple_element(pair, 2), :function) + state = State.push(state, Builder.integer(0), :integer) + {:ok, state} + end + end + + defp lower_for_of_next(state, iter_idx) do + with {:ok, state, next_fn} <- State.bind_stack_entry(state, iter_idx + 1), + {:ok, state, iter_obj} <- State.bind_stack_entry(state, iter_idx + 2) do + {result, state} = + State.bind( + state, + Builder.temp_name(state.temp), + State.compiler_call(state, :for_of_next, [next_fn, iter_obj]) + ) + + state = %{ + state + | stack: List.replace_at(state.stack, iter_idx + 2, Builder.tuple_element(result, 3)), + stack_types: List.replace_at(state.stack_types, iter_idx + 2, :object) + } + + state = State.push(state, Builder.tuple_element(result, 2), :unknown) + state = State.push(state, Builder.tuple_element(result, 1), :boolean) + {:ok, state} + else + :error -> {:error, {:for_of_state_missing, iter_idx}} + end + end + + defp lower_iterator_close(state) do + with {:ok, _catch_offset, _catch_type, state} <- State.pop_typed(state), + {:ok, _next_fn, _next_type, state} <- State.pop_typed(state), + {:ok, iter_obj, _iter_type, state} <- State.pop_typed(state) do + {:ok, + %{state | body: state.body ++ [State.compiler_call(state, :iterator_close, [iter_obj])]}} + end + end + + defp lower_fclosure(state, constants, arg_count, const_idx) do + case Enum.at(constants, const_idx) do + %Bytecode.Function{closure_vars: []} = fun -> + {:ok, State.push(state, Builder.literal(fun), AnalysisTypes.function_type(fun))} + + %Bytecode.Function{} = fun -> + with {:ok, state, entries} <- + lower_closure_entries(state, arg_count, fun.closure_vars, []) do + closure = + Builder.tuple_expr([ + Builder.atom(:closure), + Builder.map_expr(Enum.reverse(entries)), + Builder.literal(fun) + ]) + + {:ok, State.push(state, closure, AnalysisTypes.function_type(fun))} + end + + nil -> + {:error, {:unsupported_const, const_idx}} + + other -> + {:error, {:unsupported_fclosure_const, const_idx, other}} + end + end + + defp lower_closure_entries(state, _arg_count, [], acc), do: {:ok, state, acc} + + defp lower_closure_entries( + state, + arg_count, + [%{closure_type: 2, var_idx: idx} = cv | rest], + acc + ) do + {parent_ref, state} = + State.bind( + state, + Builder.temp_name(state.temp), + Builder.remote_call(QuickBEAM.VM.Compiler.RuntimeHelpers, :get_var_ref, [ + State.ctx_expr(state), + Builder.literal(idx) + ]) + ) + + {cell, state} = + State.bind( + state, + Builder.temp_name(state.temp), + State.compiler_call(state, :ensure_capture_cell, [parent_ref, parent_ref]) + ) + + key = Builder.literal({cv.closure_type, cv.var_idx}) + lower_closure_entries(state, arg_count, rest, [{key, cell} | acc]) + end + + defp lower_closure_entries(state, arg_count, [cv | rest], acc) do + with {:ok, slot_idx} <- closure_slot_index(arg_count, cv), + {:ok, state, cell} <- Captures.ensure_capture_cell(state, slot_idx) do + key = Builder.literal({cv.closure_type, cv.var_idx}) + lower_closure_entries(state, arg_count, rest, [{key, cell} | acc]) + end + end + + defp closure_slot_index(_arg_count, %{closure_type: 1, var_idx: idx}), do: {:ok, idx} + defp closure_slot_index(arg_count, %{closure_type: 0, var_idx: idx}), do: {:ok, idx + arg_count} + + defp closure_slot_index(_arg_count, %{closure_type: 2, var_idx: idx}), + do: {:error, {:closure_var_ref_not_supported, idx}} + + defp closure_slot_index(_arg_count, %{closure_type: type, var_idx: idx}), + do: {:error, {:closure_type_not_supported, type, idx}} + + defp push_const(state, constants, arg_count, idx) do + case Enum.at(constants, idx) do + nil -> + {:error, {:unsupported_const, idx}} + + value + when is_integer(value) or is_float(value) or is_binary(value) or is_boolean(value) or + is_nil(value) -> + {:ok, State.push(state, Builder.literal(value))} + + :undefined -> + {:ok, State.push(state, Builder.atom(:undefined), :undefined)} + + %Bytecode.Function{} = fun when fun.closure_vars == [] -> + {:ok, State.push(state, Builder.literal(fun), AnalysisTypes.function_type(fun))} + + %Bytecode.Function{} -> + lower_fclosure(state, constants, arg_count, idx) + + _ -> + {:error, {:unsupported_const, idx}} + end + end + + defp lower_put_var(state, atom_idx) do + with {:ok, val, _type, state} <- State.pop_typed(state) do + {:ok, + State.update_ctx( + state, + Builder.remote_call(GlobalEnv, :put, [ + State.ctx_expr(state), + Builder.literal(atom_idx), + val + ]) + )} + end + end + + defp lower_put_var_ref(state, idx) do + with {:ok, val, _type, state} <- State.pop_typed(state) do + {:ok, + %{ + state + | body: + state.body ++ [State.compiler_call(state, :put_var_ref, [Builder.literal(idx), val])] + }} + end + end + + defp lower_set_var_ref(state, idx) do + with {:ok, val, _type, state} <- State.pop_typed(state) do + State.effectful_push( + state, + State.compiler_call(state, :set_var_ref, [Builder.literal(idx), val]) + ) + end + end + + defp lower_is_undefined_or_null(state) do + with {:ok, expr, type, state} <- State.pop_typed(state) do + result = + case type do + :undefined -> Builder.atom(true) + :null -> Builder.atom(true) + _ -> Builder.undefined_or_null_expr(expr) + end + + {:ok, State.push(state, result, :boolean)} + end + end + + defp lower_goto(state, target, stack_depths, inline_targets) do + if MapSet.member?(inline_targets, target) do + {:inline_goto, target, state} + else + State.goto(state, target, stack_depths) + end + end + + defp lower_get_loc_check(state, slot_idx) do + slot_expr = State.slot_expr(state, slot_idx) + slot_type = State.slot_type(state, slot_idx) + + expr = + if State.slot_initialized?(state, slot_idx) do + slot_expr + else + State.compiler_call(state, :ensure_initialized_local!, [slot_expr]) + end + + {:ok, State.push(state, expr, slot_type)} + end + + defp lower_put_loc_check(state, slot_idx) do + wrapper = + if State.slot_initialized?(state, slot_idx) do + nil + else + :ensure_initialized_local! + end + + State.assign_slot(state, slot_idx, false, wrapper) + end + + # ── Stack manipulation helpers ── + + # dup1: [a, b | rest] → [a, b, a, b | rest] + # Note: in QuickJS, dup1 duplicates the top 2 entries + defp lower_dup1(state) do + with {:ok, a, ta, state} <- State.pop_typed(state), + {:ok, b, tb, state} <- State.pop_typed(state) do + {b_bound, state} = State.bind(state, Builder.temp_name(state.temp), b) + {a_bound, state} = State.bind(state, Builder.temp_name(state.temp), a) + + {:ok, + %{ + state + | stack: [a_bound, b_bound, a_bound, b_bound | state.stack], + stack_types: [ta, tb, ta, tb | state.stack_types] + }} + end + end + + # dup3: [a, b, c | rest] → [a, b, c, a, b, c | rest] + defp lower_dup3(state) do + with {:ok, a, ta, state} <- State.pop_typed(state), + {:ok, b, tb, state} <- State.pop_typed(state), + {:ok, c, tc, state} <- State.pop_typed(state) do + {c_bound, state} = State.bind(state, Builder.temp_name(state.temp), c) + {b_bound, state} = State.bind(state, Builder.temp_name(state.temp), b) + {a_bound, state} = State.bind(state, Builder.temp_name(state.temp), a) + + {:ok, + %{ + state + | stack: [a_bound, b_bound, c_bound, a_bound, b_bound, c_bound | state.stack], + stack_types: [ta, tb, tc, ta, tb, tc | state.stack_types] + }} + end + end + + # insert4: [a, b, c, d | rest] → [a, b, c, d, a | rest] + defp lower_insert4(state) do + with {:ok, a, ta, state} <- State.pop_typed(state), + {:ok, b, tb, state} <- State.pop_typed(state), + {:ok, c, tc, state} <- State.pop_typed(state), + {:ok, d, td, state} <- State.pop_typed(state) do + {a_bound, state} = State.bind(state, Builder.temp_name(state.temp), a) + + {:ok, + %{ + state + | stack: [a_bound, b, c, d, a_bound | state.stack], + stack_types: [ta, tb, tc, td, ta | state.stack_types] + }} + end + end + + # nip: [a, b | rest] → [a | rest] + defp lower_nip(%{stack: [a, _b | rest], stack_types: [ta, _tb | type_rest]} = state), + do: {:ok, %{state | stack: [a | rest], stack_types: [ta | type_rest]}} + + defp lower_nip(_state), do: {:error, :stack_underflow} + + # nip1: [a, b, c | rest] → [a, b | rest] + defp lower_nip1( + %{stack: [a, b, _c | rest], stack_types: [ta, tb, _tc | type_rest]} = state + ), + do: {:ok, %{state | stack: [a, b | rest], stack_types: [ta, tb | type_rest]}} + + defp lower_nip1(_state), do: {:error, :stack_underflow} + + # swap2: [a, b, c, d | rest] → [c, d, a, b | rest] + defp lower_swap2( + %{ + stack: [a, b, c, d | rest], + stack_types: [ta, tb, tc, td | type_rest] + } = state + ), + do: {:ok, %{state | stack: [c, d, a, b | rest], stack_types: [tc, td, ta, tb | type_rest]}} + + defp lower_swap2(_state), do: {:error, :stack_underflow} + + # rot3l: [a, b, c | rest] → [c, a, b | rest] (rotate left: bottom goes to top) + defp lower_rot3l( + %{stack: [a, b, c | rest], stack_types: [ta, tb, tc | type_rest]} = state + ), + do: {:ok, %{state | stack: [c, a, b | rest], stack_types: [tc, ta, tb | type_rest]}} + + defp lower_rot3l(_state), do: {:error, :stack_underflow} + + # rot3r: [a, b, c | rest] → [b, c, a | rest] (rotate right: top goes to bottom) + defp lower_rot3r( + %{stack: [a, b, c | rest], stack_types: [ta, tb, tc | type_rest]} = state + ), + do: {:ok, %{state | stack: [b, c, a | rest], stack_types: [tb, tc, ta | type_rest]}} + + defp lower_rot3r(_state), do: {:error, :stack_underflow} + + # rot4l: [a, b, c, d | rest] → [d, a, b, c | rest] + defp lower_rot4l( + %{ + stack: [a, b, c, d | rest], + stack_types: [ta, tb, tc, td | type_rest] + } = state + ), + do: + {:ok, + %{state | stack: [d, a, b, c | rest], stack_types: [td, ta, tb, tc | type_rest]}} + + defp lower_rot4l(_state), do: {:error, :stack_underflow} + + # rot5l: [a, b, c, d, e | rest] → [e, a, b, c, d | rest] + defp lower_rot5l( + %{ + stack: [a, b, c, d, e | rest], + stack_types: [ta, tb, tc, td, te | type_rest] + } = state + ), + do: + {:ok, + %{ + state + | stack: [e, a, b, c, d | rest], + stack_types: [te, ta, tb, tc, td | type_rest] + }} + + defp lower_rot5l(_state), do: {:error, :stack_underflow} + + # perm4: [a, b, c, d | rest] → [a, c, d, b | rest] + defp lower_perm4( + %{ + stack: [a, b, c, d | rest], + stack_types: [ta, tb, tc, td | type_rest] + } = state + ), + do: + {:ok, + %{state | stack: [a, c, d, b | rest], stack_types: [ta, tc, td, tb | type_rest]}} + + defp lower_perm4(_state), do: {:error, :stack_underflow} + + # perm5: [a, b, c, d, e | rest] → [a, c, d, e, b | rest] + defp lower_perm5( + %{ + stack: [a, b, c, d, e | rest], + stack_types: [ta, tb, tc, td, te | type_rest] + } = state + ), + do: + {:ok, + %{ + state + | stack: [a, c, d, e, b | rest], + stack_types: [ta, tc, td, te, tb | type_rest] + }} + + defp lower_perm5(_state), do: {:error, :stack_underflow} + + # ── Private field helpers ── + + defp lower_get_private_field(state) do + with {:ok, key, state} <- State.pop(state), + {:ok, obj, state} <- State.pop(state) do + State.effectful_push( + state, + State.compiler_call(state, :get_private_field, [obj, key]) + ) + end + end + + defp lower_put_private_field(state) do + with {:ok, key, state} <- State.pop(state), + {:ok, val, state} <- State.pop(state), + {:ok, obj, state} <- State.pop(state) do + {:ok, + %{ + state + | body: + state.body ++ + [State.compiler_call(state, :put_private_field, [obj, key, val])] + }} + end + end + + defp lower_define_private_field(state) do + with {:ok, val, state} <- State.pop(state), + {:ok, key, state} <- State.pop(state), + {:ok, obj, _obj_type, state} <- State.pop_typed(state) do + {:ok, + %{ + state + | body: + state.body ++ + [State.compiler_call(state, :define_private_field, [obj, key, val])], + stack: [obj | state.stack], + stack_types: [:object | state.stack_types] + }} + end + end + + defp lower_check_brand(state) do + with {:ok, state, brand} <- State.bind_stack_entry(state, 0), + {:ok, state, obj} <- State.bind_stack_entry(state, 1) do + {:ok, + %{ + state + | body: + state.body ++ + [State.compiler_call(state, :check_brand, [obj, brand])] + }} + else + :error -> {:error, :check_brand_state_missing} + end + end + + defp lower_private_in(state) do + with {:ok, key, state} <- State.pop(state), + {:ok, obj, state} <- State.pop(state) do + {:ok, + State.push( + state, + Builder.remote_call(Private, :has_field?, [obj, key]), + :boolean + )} + end + end + + # ── Class helpers ── + + defp lower_set_proto(state) do + with {:ok, proto, state} <- State.pop(state), + {:ok, obj, _obj_type, state} <- State.pop_typed(state) do + {:ok, + %{ + state + | body: + state.body ++ + [State.compiler_call(state, :set_proto, [obj, proto])], + stack: [obj | state.stack], + stack_types: [:object | state.stack_types] + }} + end + end + + defp lower_get_super_value(state) do + with {:ok, key, state} <- State.pop(state), + {:ok, proto, state} <- State.pop(state), + {:ok, this_obj, state} <- State.pop(state) do + State.effectful_push( + state, + Builder.remote_call(Class, :get_super_value, [proto, this_obj, key]) + ) + end + end + + defp lower_put_super_value(state) do + with {:ok, val, state} <- State.pop(state), + {:ok, key, state} <- State.pop(state), + {:ok, proto_obj, state} <- State.pop(state), + {:ok, this_obj, state} <- State.pop(state) do + {:ok, + %{ + state + | body: + state.body ++ + [Builder.remote_call(Class, :put_super_value, [proto_obj, this_obj, key, val])] + }} + end + end + + defp lower_check_ctor_return(state) do + with {:ok, val, state} <- State.pop(state) do + {pair, state} = + State.bind( + state, + Builder.temp_name(state.temp), + Builder.remote_call(Class, :check_ctor_return, [val]) + ) + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), Builder.tuple_element(pair, 2) | state.stack], + stack_types: [:unknown, :unknown | state.stack_types] + }} + end + end + + defp lower_init_ctor(state) do + State.effectful_push( + state, + State.compiler_call(state, :init_ctor, []), + :object + ) + end + + defp lower_define_class_computed(state, atom_idx) do + with {:ok, ctor, state} <- State.pop(state), + {:ok, parent_ctor, state} <- State.pop(state), + {:ok, _computed_name, state} <- State.pop(state) do + {pair, state} = + State.bind( + state, + Builder.temp_name(state.temp), + State.compiler_call(state, :define_class, [ + ctor, + parent_ctor, + Builder.literal(atom_idx) + ]) + ) + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), Builder.tuple_element(pair, 2) | state.stack], + stack_types: [:object, :function | state.stack_types] + }} + end + end + + # ── Ref creation helpers ── + + defp lower_make_loc_ref(state, idx) do + State.effectful_push( + state, + State.compiler_call(state, :make_loc_ref, [Builder.literal(idx)]), + :unknown + ) + end + + defp lower_make_arg_ref(state, idx) do + State.effectful_push( + state, + State.compiler_call(state, :make_arg_ref, [Builder.literal(idx)]), + :unknown + ) + end + + defp lower_make_var_ref_ref(state, idx) do + State.effectful_push( + state, + State.compiler_call(state, :make_var_ref_ref, [Builder.literal(idx)]), + :unknown + ) + end + + defp lower_get_ref_value(state) do + with {:ok, ref, state} <- State.pop(state) do + State.effectful_push( + state, + State.compiler_call(state, :get_ref_value, [ref]) + ) + end + end + + defp lower_put_ref_value(state) do + with {:ok, val, state} <- State.pop(state), + {:ok, ref, state} <- State.pop(state) do + {:ok, + %{ + state + | body: + state.body ++ + [State.compiler_call(state, :put_ref_value, [val, ref])] + }} + end + end + + defp lower_rest(state, start_idx) do + State.effectful_push( + state, + State.compiler_call(state, :rest, [Builder.literal(start_idx)]), + :object + ) + end + + defp lower_throw_error(state, atom_idx, reason) do + {:done, + state.body ++ + [ + State.compiler_call(state, :throw_error, [ + Builder.literal(atom_idx), + Builder.literal(reason) + ]) + ]} + end + + defp special_object_type(2), do: :self_fun + defp special_object_type(3), do: :function + defp special_object_type(type) when type in [0, 1, 5, 6, 7], do: :object + defp special_object_type(_), do: :unknown + + # ── Generator / async helpers ── + + defp lower_initial_yield(state, next_entry, stack_depths) do + # initial_yield: yield :undefined, resume at next block + yield_throw(state, Builder.atom(:undefined), next_entry, stack_depths) + end + + defp lower_yield(state, next_entry, stack_depths) do + with {:ok, val, _type, state} <- State.pop_typed(state) do + yield_throw(state, val, next_entry, stack_depths) + end + end + + defp lower_yield_star(state, next_entry, stack_depths) do + # yield* delegates to an inner iterator — for now, treat same as yield + with {:ok, val, _type, state} <- State.pop_typed(state) do + {:done, + state.body ++ + [Builder.remote_call(:erlang, :throw, [ + Builder.tuple_expr([ + Builder.atom(:generator_yield_star), + val, + yield_continuation(state, next_entry, stack_depths) + ]) + ])]} + end + end + + defp lower_await(state) do + # await: synchronously resolve promise in BEAM VM + with {:ok, val, _type, state} <- State.pop_typed(state) do + State.effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.Compiler.RuntimeHelpers, :await, [ + State.ctx_expr(state), + val + ]) + ) + end + end + + defp lower_return_async(state) do + with {:ok, val, _state} <- State.pop(state) do + {:done, + state.body ++ + [Builder.remote_call(:erlang, :throw, [ + Builder.tuple_expr([Builder.atom(:generator_return), val]) + ])]} + end + end + + defp yield_throw(state, val, next_entry, stack_depths) do + {:done, + state.body ++ + [Builder.remote_call(:erlang, :throw, [ + Builder.tuple_expr([ + Builder.atom(:generator_yield), + val, + yield_continuation(state, next_entry, stack_depths) + ]) + ])]} + end + + defp yield_continuation(state, next_entry, stack_depths) do + # The continuation is a fun(Arg) -> block_N(Ctx, Slots..., Arg, Captures...) + # "Arg" is what next() passes — it becomes the yield return value + # which the interpreter pushes as [false, arg | stack] + # The "false" indicates "not a return", arg is the yielded-back value. + arg_var = Builder.var("YieldArg") + false_var = Builder.atom(false) + + ctx = State.ctx_expr(state) + slots = State.current_slots(state) + # The resumed block expects [false, arg | original_stack] on the stack + stack = [false_var, arg_var | State.current_stack(state)] + captures = State.current_capture_cells(state) + + expected_depth = Map.get(stack_depths, next_entry) + + if expected_depth && expected_depth == length(stack) do + call = + Builder.local_call(Builder.block_name(next_entry), [ + ctx | slots ++ stack ++ captures + ]) + + {:fun, 1, {:clauses, [{:clause, 1, [arg_var], [], [call]}]}} + else + # Stack depth mismatch — fall back to a noop continuation + {:fun, 1, {:clauses, [{:clause, 1, [arg_var], [], [Builder.atom(:undefined)]}]}} + end + end + + defp inline_get_var(state, name) do + globals_expr = {:map_field_assoc, 1, {:atom, 1, :globals}, State.ctx_expr(state)} + globals = {:map_field_exact, 1, State.ctx_expr(state), {:atom, 1, :globals}} + Builder.remote_call(RuntimeHelpers, :get_global, [ + {:call, 1, {:remote, 1, {:atom, 1, :erlang}, {:atom, 1, :map_get}}, [{:atom, 1, :globals}, State.ctx_expr(state)]}, + Builder.literal(name) + ]) + end + + defp inline_get_var_undef(state, name) do + Builder.remote_call(RuntimeHelpers, :get_global_undef, [ + {:call, 1, {:remote, 1, {:atom, 1, :erlang}, {:atom, 1, :map_get}}, [{:atom, 1, :globals}, State.ctx_expr(state)]}, + Builder.literal(name) + ]) + end +end diff --git a/lib/quickbeam/vm/compiler/lowering/state.ex b/lib/quickbeam/vm/compiler/lowering/state.ex new file mode 100644 index 00000000..43271a9a --- /dev/null +++ b/lib/quickbeam/vm/compiler/lowering/state.ex @@ -0,0 +1,1023 @@ +defmodule QuickBEAM.VM.Compiler.Lowering.State do + @moduledoc false + + alias QuickBEAM.VM.Compiler.Lowering.{Builder, Captures, Types} + alias QuickBEAM.VM.Compiler.RuntimeHelpers + + @line 1 + + def new(slot_count, stack_depth, opts \\ []) do + slots = + if slot_count == 0, + do: %{}, + else: Map.new(0..(slot_count - 1), fn idx -> {idx, Builder.slot_var(idx)} end) + + capture_cells = + if slot_count == 0, + do: %{}, + else: Map.new(0..(slot_count - 1), fn idx -> {idx, Builder.capture_var(idx)} end) + + stack = + if stack_depth == 0, + do: [], + else: Enum.map(0..(stack_depth - 1), &Builder.stack_var/1) + + arg_count = Keyword.get(opts, :arg_count, 0) + locals = Keyword.get(opts, :locals, []) + + %{ + body: [], + ctx: Builder.ctx_var(), + slots: slots, + slot_types: + Keyword.get(opts, :slot_types, Map.new(slots, fn {idx, _expr} -> {idx, :unknown} end)), + slot_inits: + Keyword.get(opts, :slot_inits, initial_slot_inits(slot_count, arg_count, locals)), + capture_cells: capture_cells, + stack: stack, + stack_types: Keyword.get(opts, :stack_types, List.duplicate(:unknown, stack_depth)), + temp: 0, + locals: locals, + closure_vars: Keyword.get(opts, :closure_vars, []), + atoms: Keyword.get(opts, :atoms), + arg_count: arg_count, + return_type: Keyword.get(opts, :return_type, :unknown) + } + end + + def ctx_expr(%{ctx: ctx}), do: ctx + + def closure_vars_expr(%{closure_vars: cvs}), do: cvs + + def inline_get_var_ref(state, idx) do + cvs = closure_vars_expr(state) + + case Enum.at(cvs, idx) do + %{closure_type: type, var_idx: var_idx} -> + key = Builder.literal({type, var_idx}) + {bound, state} = bind(state, Builder.temp_name(state.temp), compiler_call(state, :get_capture, [key])) + {bound, state} + + nil -> + {Builder.atom(:undefined), state} + end + end + + def compiler_call(state, fun, args), + do: Builder.remote_call(RuntimeHelpers, fun, [ctx_expr(state) | args]) + + def push(state, expr), do: push(state, expr, Types.infer_expr_type(expr)) + + def push(state, expr, type), + do: %{state | stack: [expr | state.stack], stack_types: [type | state.stack_types]} + + def pop_typed(%{stack: [expr | rest], stack_types: [type | type_rest]} = state), + do: {:ok, expr, type, %{state | stack: rest, stack_types: type_rest}} + + def pop_typed(_state), do: {:error, :stack_underflow} + + def pop(%{stack: [expr | rest], stack_types: [_type | type_rest]} = state), + do: {:ok, expr, %{state | stack: rest, stack_types: type_rest}} + + def pop(_state), do: {:error, :stack_underflow} + + def pop_n(state, 0), do: {:ok, [], state} + + def pop_n(state, count) when count > 0 do + with {:ok, expr, state} <- pop(state), + {:ok, rest, state} <- pop_n(state, count - 1) do + {:ok, [expr | rest], state} + end + end + + def pop_n_typed(state, 0), do: {:ok, [], [], state} + + def pop_n_typed(state, count) when count > 0 do + with {:ok, expr, type, state} <- pop_typed(state), + {:ok, rest, rest_types, state} <- pop_n_typed(state, count - 1) do + {:ok, [expr | rest], [type | rest_types], state} + end + end + + def put_slot(state, idx, expr), do: put_slot(state, idx, expr, Types.infer_expr_type(expr)) + + def put_slot(state, idx, expr, type) do + %{ + state + | slots: Map.put(state.slots, idx, expr), + slot_types: Map.put(state.slot_types, idx, type), + slot_inits: Map.put(state.slot_inits, idx, true) + } + end + + def put_uninitialized_slot(state, idx, expr), + do: put_uninitialized_slot(state, idx, expr, Types.infer_expr_type(expr)) + + def put_uninitialized_slot(state, idx, expr, type) do + %{ + state + | slots: Map.put(state.slots, idx, expr), + slot_types: Map.put(state.slot_types, idx, type), + slot_inits: Map.put(state.slot_inits, idx, false) + } + end + + def slot_expr(state, idx), do: Map.get(state.slots, idx, Builder.atom(:undefined)) + def slot_type(state, idx), do: Map.get(state.slot_types, idx, :unknown) + def slot_initialized?(state, idx), do: Map.get(state.slot_inits, idx, false) + + def put_capture_cell(state, idx, expr), + do: %{state | capture_cells: Map.put(state.capture_cells, idx, expr)} + + def capture_cell_expr(state, idx), + do: Map.get(state.capture_cells, idx, Builder.atom(:undefined)) + + def bind_stack_entry(state, idx) do + case Enum.fetch(state.stack, idx) do + {:ok, expr} -> + {bound, state} = bind(state, Builder.temp_name(state.temp), expr) + {:ok, %{state | stack: List.replace_at(state.stack, idx, bound)}, bound} + + :error -> + :error + end + end + + def assign_slot(state, idx, keep?, wrapper \\ nil) do + with {:ok, expr, type, state} <- pop_typed(state) do + expr = if wrapper, do: compiler_call(state, wrapper, [expr]), else: expr + + {slot_expr, state} = + if keep? or not Types.pure_expr?(expr) or Captures.slot_captured?(state, idx) do + bind(state, Builder.slot_name(idx, state.temp), expr) + else + {expr, state} + end + + state = put_slot(state, idx, slot_expr, type) + state = Captures.sync_capture_cell(state, idx, slot_expr) + state = if keep?, do: push(state, slot_expr, type), else: state + {:ok, state} + end + end + + def update_slot(state, idx, expr), + do: update_slot(state, idx, expr, false, Types.infer_expr_type(expr)) + + def update_slot(state, idx, expr, keep?), + do: update_slot(state, idx, expr, keep?, Types.infer_expr_type(expr)) + + def update_slot(state, idx, expr, keep?, type) do + {slot_expr, state} = + if keep? or not Types.pure_expr?(expr) or Captures.slot_captured?(state, idx) do + bind(state, Builder.slot_name(idx, state.temp), expr) + else + {expr, state} + end + + state = put_slot(state, idx, slot_expr, type) + state = Captures.sync_capture_cell(state, idx, slot_expr) + state = if keep?, do: push(state, slot_expr, type), else: state + {:ok, state} + end + + def duplicate_top(state) do + with {:ok, expr, type, state} <- pop_typed(state) do + {bound, state} = bind(state, Builder.temp_name(state.temp), expr) + + {:ok, + %{ + state + | stack: [bound, bound | state.stack], + stack_types: [type, type | state.stack_types] + }} + end + end + + def duplicate_top_two(state) do + with {:ok, first, first_type, state} <- pop_typed(state), + {:ok, second, second_type, state} <- pop_typed(state) do + {second_bound, state} = bind(state, Builder.temp_name(state.temp), second) + {first_bound, state} = bind(state, Builder.temp_name(state.temp), first) + + {:ok, + %{ + state + | stack: [first_bound, second_bound, first_bound, second_bound | state.stack], + stack_types: [first_type, second_type, first_type, second_type | state.stack_types] + }} + end + end + + def insert_top_two(state) do + with {:ok, first, first_type, state} <- pop_typed(state), + {:ok, second, second_type, state} <- pop_typed(state) do + {first_bound, state} = bind(state, Builder.temp_name(state.temp), first) + + {:ok, + %{ + state + | stack: [first_bound, second, first_bound | state.stack], + stack_types: [first_type, second_type, first_type | state.stack_types] + }} + end + end + + def insert_top_three(state) do + with {:ok, first, first_type, state} <- pop_typed(state), + {:ok, second, second_type, state} <- pop_typed(state), + {:ok, third, third_type, state} <- pop_typed(state) do + {first_bound, state} = bind(state, Builder.temp_name(state.temp), first) + + {:ok, + %{ + state + | stack: [first_bound, second, third, first_bound | state.stack], + stack_types: [first_type, second_type, third_type, first_type | state.stack_types] + }} + end + end + + def drop_top(%{stack: [_ | rest], stack_types: [_ | type_rest]} = state), + do: {:ok, %{state | stack: rest, stack_types: type_rest}} + + def drop_top(_state), do: {:error, :stack_underflow} + + def swap_top(%{stack: [a, b | rest], stack_types: [ta, tb | type_rest]} = state), + do: {:ok, %{state | stack: [b, a | rest], stack_types: [tb, ta | type_rest]}} + + def swap_top(_state), do: {:error, :stack_underflow} + + def permute_top_three( + %{stack: [a, b, c | rest], stack_types: [ta, tb, tc | type_rest]} = state + ), + do: {:ok, %{state | stack: [a, c, b | rest], stack_types: [ta, tc, tb | type_rest]}} + + def permute_top_three(_state), do: {:error, :stack_underflow} + + def nip_catch( + %{stack: [val, _catch_offset | rest], stack_types: [type, _ | type_rest]} = state + ), + do: {:ok, %{state | stack: [val | rest], stack_types: [type | type_rest]}} + + def nip_catch(_state), do: {:error, :stack_underflow} + + def post_update(state, fun) do + with {:ok, expr, type, state} <- pop_typed(state) do + result_type = if type == :integer, do: :integer, else: :number + + {pair, state} = + bind(state, Builder.temp_name(state.temp), compiler_call(state, fun, [expr])) + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), Builder.tuple_element(pair, 2) | state.stack], + stack_types: [result_type, result_type | state.stack_types] + }} + end + end + + def add_to_slot(state, idx) do + with {:ok, expr, expr_type, state} <- pop_typed(state) do + {op_expr, result_type} = + specialize_binary(:op_add, slot_expr(state, idx), slot_type(state, idx), expr, expr_type) + + update_slot(state, idx, op_expr, false, result_type) + end + end + + def inc_slot(state, idx), + do: + update_slot( + state, + idx, + compiler_call(state, :inc, [slot_expr(state, idx)]), + false, + if(slot_type(state, idx) == :integer, do: :integer, else: :number) + ) + + def dec_slot(state, idx), + do: + update_slot( + state, + idx, + compiler_call(state, :dec, [slot_expr(state, idx)]), + false, + if(slot_type(state, idx) == :integer, do: :integer, else: :number) + ) + + def regexp_literal(state) do + with {:ok, pattern, _pattern_type, state} <- pop_typed(state), + {:ok, flags, _flags_type, state} <- pop_typed(state) do + {:ok, push(state, Builder.tuple_expr([Builder.atom(:regexp), pattern, flags]), :unknown)} + end + end + + def unary_call(state, mod, fun, extra_args \\ []) do + with {:ok, expr, _type, state} <- pop_typed(state) do + {:ok, push(state, Builder.remote_call(mod, fun, [expr | extra_args]))} + end + end + + def get_length_call(state) do + with {:ok, expr, type, state} <- pop_typed(state) do + {result_expr, result_type} = specialize_get_length(expr, type) + {:ok, push(state, result_expr, result_type)} + end + end + + def effectful_push(state, expr), do: effectful_push(state, expr, Types.infer_expr_type(expr)) + + def effectful_push(state, expr, type) do + {bound, state} = bind(state, Builder.temp_name(state.temp), expr) + {:ok, push(state, bound, type)} + end + + def unary_local_call(state, fun) do + with {:ok, expr, type, state} <- pop_typed(state) do + {result_expr, result_type} = specialize_unary(fun, expr, type) + {:ok, push(state, result_expr, result_type)} + end + end + + def binary_call(state, mod, fun) do + with {:ok, right, _right_type, state} <- pop_typed(state), + {:ok, left, _left_type, state} <- pop_typed(state) do + {:ok, push(state, Builder.remote_call(mod, fun, [left, right]))} + end + end + + def binary_local_call(state, fun) do + with {:ok, right, right_type, state} <- pop_typed(state), + {:ok, left, left_type, state} <- pop_typed(state) do + {result_expr, result_type} = specialize_binary(fun, left, left_type, right, right_type) + {:ok, push(state, result_expr, result_type)} + end + end + + def get_field_call(state, key_expr) do + with {:ok, obj, type, state} <- pop_typed(state) do + key_str = extract_literal_string(key_expr) + + case {type, key_str} do + {{:shaped_object, offsets}, key} when is_binary(key) and is_map_key(offsets, key) -> + offset = Map.fetch!(offsets, key) + # Emit: case Obj of {:obj, Id} -> case :erlang.get(Id) of {:shape, _, _, Vals, _} -> element(Off+1, Vals) end end + id_var = Builder.var(Builder.temp_name(state.temp)) + vals_var = Builder.var(Builder.temp_name(state.temp + 1)) + state = %{state | temp: state.temp + 2} + + access_expr = {:case, @line, obj, [ + {:clause, @line, [{:tuple, @line, [{:atom, @line, :obj}, id_var]}], [], [ + {:case, @line, + {:call, @line, {:remote, @line, {:atom, @line, :erlang}, {:atom, @line, :get}}, [id_var]}, + [ + {:clause, @line, + [{:tuple, @line, [{:atom, @line, :shape}, {:var, @line, :_}, {:var, @line, :_}, vals_var, {:var, @line, :_}]}], + [], + [{:call, @line, {:remote, @line, {:atom, @line, :erlang}, {:atom, @line, :element}}, + [{:integer, @line, offset + 1}, vals_var]}]}, + {:clause, @line, [{:var, @line, :_}], [], + [Builder.local_call(:op_get_field, [obj, key_expr])]} + ]} + ]}, + {:clause, @line, [{:var, @line, :_}], [], + [Builder.local_call(:op_get_field, [obj, key_expr])]} + ]} + + {:ok, push(state, access_expr)} + + _ -> + {:ok, push(state, Builder.local_call(:op_get_field, [obj, key_expr]))} + end + end + end + + defp extract_literal_string({:bin, _, [{:bin_element, _, {:string, _, chars}, _, _}]}), + do: List.to_string(chars) + defp extract_literal_string(_), do: nil + + def get_field2(state, key_expr) do + with {:ok, obj, _type, state} <- pop_typed(state) do + field = Builder.local_call(:op_get_field, [obj, key_expr]) + + {:ok, + %{ + state + | stack: [field, obj | state.stack], + stack_types: [:unknown, :object | state.stack_types] + }} + end + end + + def get_array_el2(state) do + with {:ok, idx, _idx_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, state} <- pop_typed(state) do + {pair, state} = + bind( + state, + Builder.temp_name(state.temp), + compiler_call(state, :get_array_el2, [obj, idx]) + ) + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), Builder.tuple_element(pair, 2) | state.stack], + stack_types: [:unknown, :object | state.stack_types] + }} + end + end + + def set_name_atom(state, atom_name) do + with {:ok, fun, fun_type, state} <- pop_typed(state) do + {:ok, + push( + state, + compiler_call(state, :set_function_name, [fun, Builder.literal(atom_name)]), + fun_type + )} + end + end + + def set_name_computed(state) do + with {:ok, fun, fun_type, state} <- pop_typed(state), + {:ok, name, name_type, state} <- pop_typed(state) do + named = compiler_call(state, :set_function_name_computed, [fun, name]) + + {:ok, + %{ + state + | stack: [named, name | state.stack], + stack_types: [fun_type, name_type | state.stack_types] + }} + end + end + + def set_home_object(state) do + with {:ok, state, method} <- bind_stack_entry(state, 0), + {:ok, state, target} <- bind_stack_entry(state, 1) do + {:ok, + %{state | body: state.body ++ [compiler_call(state, :set_home_object, [method, target])]}} + else + :error -> {:error, :set_home_object_state_missing} + end + end + + def add_brand(state) do + with {:ok, obj, state} <- pop(state), + {:ok, brand, state} <- pop(state) do + {:ok, %{state | body: state.body ++ [compiler_call(state, :add_brand, [obj, brand])]}} + end + end + + def put_field_call(state, key_expr) do + with {:ok, val, _val_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, state} <- pop_typed(state) do + {:ok, + %{ + state + | body: + state.body ++ + [Builder.remote_call(QuickBEAM.VM.ObjectModel.Put, :put, [obj, key_expr, val])] + }} + end + end + + def define_field_name_call(state, key_expr) do + with {:ok, val, _val_type, state} <- pop_typed(state), + {:ok, obj, obj_type, state} <- pop_typed(state) do + key_str = extract_literal_string(key_expr) + + new_type = + case {obj_type, key_str} do + {{:shaped_object, offsets}, k} when is_binary(k) -> + new_offset = map_size(offsets) + {:shaped_object, Map.put(offsets, k, new_offset)} + + _ -> + :object + end + + {:ok, + %{ + state + | body: + state.body ++ + [Builder.remote_call(QuickBEAM.VM.ObjectModel.Put, :put_field, [obj, key_expr, val])], + stack: [obj | state.stack], + stack_types: [new_type | state.stack_types] + }} + end + end + + def define_method_call(state, method_name, flags) do + with {:ok, method, _method_type, state} <- pop_typed(state), + {:ok, target, _target_type, state} <- pop_typed(state) do + effectful_push( + state, + compiler_call(state, :define_method, [ + target, + method, + Builder.literal(method_name), + Builder.literal(flags) + ]), + :object + ) + end + end + + def define_method_computed_call(state, flags) do + with {:ok, method, state} <- pop(state), + {:ok, field_name, state} <- pop(state), + {:ok, target, state} <- pop(state) do + effectful_push( + state, + compiler_call(state, :define_method_computed, [ + target, + method, + field_name, + Builder.literal(flags) + ]) + ) + end + end + + def define_class_call(state, atom_idx) do + with {:ok, ctor, state} <- pop(state), + {:ok, parent_ctor, state} <- pop(state) do + {pair, state} = + bind( + state, + Builder.temp_name(state.temp), + compiler_call(state, :define_class, [ctor, parent_ctor, Builder.literal(atom_idx)]) + ) + + ctor = Builder.tuple_element(pair, 2) + + ctor_type = function_type_from_expr(ctor) + + state = + case class_binding_slot(state, atom_idx) do + nil -> state + slot_idx -> update_slot!(state, slot_idx, ctor, ctor_type) + end + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), ctor | state.stack], + stack_types: [:object, ctor_type | state.stack_types] + }} + end + end + + defp update_slot!(state, idx, expr, type) do + {:ok, state} = update_slot(state, idx, expr, false, type) + state + end + + defp class_binding_slot(%{locals: locals, atoms: atoms}, atom_idx) do + class_name = resolve_atom_name(atom_idx, atoms) + + locals + |> Enum.with_index() + |> Enum.filter(fn {%{name: name, scope_level: scope_level, is_lexical: is_lexical}, _idx} -> + is_lexical and scope_level > 1 and resolve_local_name(name, atoms) == class_name + end) + |> Enum.max_by(fn {%{scope_level: scope_level}, _idx} -> scope_level end, fn -> nil end) + |> case do + nil -> nil + {_local, idx} -> idx + end + end + + defp resolve_local_name(name, _atoms) when is_binary(name), do: name + + defp resolve_local_name({:predefined, idx}, _atoms), + do: QuickBEAM.VM.PredefinedAtoms.lookup(idx) + + defp resolve_local_name(idx, atoms) + when is_integer(idx) and is_tuple(atoms) and idx < tuple_size(atoms), + do: elem(atoms, idx) + + defp resolve_local_name(_name, _atoms), do: nil + + defp resolve_atom_name(atom_idx, atoms), do: resolve_local_name(atom_idx, atoms) + + def put_array_el_call(state) do + with {:ok, val, _val_type, state} <- pop_typed(state), + {:ok, idx, _idx_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, state} <- pop_typed(state) do + {:ok, %{state | body: state.body ++ [compiler_call(state, :put_array_el, [obj, idx, val])]}} + end + end + + def define_array_el_call(state) do + with {:ok, val, _val_type, state} <- pop_typed(state), + {:ok, idx, idx_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, state} <- pop_typed(state) do + {pair, state} = + bind( + state, + Builder.temp_name(state.temp), + compiler_call(state, :define_array_el, [obj, idx, val]) + ) + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), Builder.tuple_element(pair, 2) | state.stack], + stack_types: [idx_type, :object | state.stack_types] + }} + end + end + + def invoke_call(state, argc) do + with {:ok, args, arg_types, state} <- pop_n_typed(state, argc), + {:ok, fun, fun_type, state} <- pop_typed(state) do + invoke_call_expr(state, fun, fun_type, Enum.reverse(args), Enum.reverse(arg_types)) + end + end + + def invoke_constructor_call(state, argc) do + with {:ok, args, _arg_types, state} <- pop_n_typed(state, argc), + {:ok, new_target, _new_target_type, state} <- pop_typed(state), + {:ok, ctor, _ctor_type, state} <- pop_typed(state) do + effectful_push( + state, + compiler_call(state, :construct_runtime, [ + ctor, + new_target, + Builder.list_expr(Enum.reverse(args)) + ]), + :object + ) + end + end + + def invoke_tail_call(state, argc) do + with {:ok, args, arg_types, state} <- pop_n_typed(state, argc), + {:ok, fun, fun_type, %{stack: [], stack_types: []} = state} <- pop_typed(state) do + {:done, tail_call_expr(state, fun, fun_type, Enum.reverse(args), Enum.reverse(arg_types))} + else + {:ok, _fun, _fun_type, _state} -> {:error, :stack_not_empty_on_tail_call} + {:error, _} = error -> error + end + end + + def invoke_method_call(state, argc) do + with {:ok, args, _arg_types, state} <- pop_n_typed(state, argc), + {:ok, fun, fun_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, state} <- pop_typed(state) do + effectful_push( + state, + Builder.remote_call(QuickBEAM.VM.Invocation, :invoke_method_runtime, [ + ctx_expr(state), + fun, + obj, + Builder.list_expr(Enum.reverse(args)) + ]), + function_return_type(fun_type, state.return_type) + ) + end + end + + def array_from_call(state, argc) do + with {:ok, elems, _types, state} <- pop_n_typed(state, argc) do + {:ok, + push( + state, + compiler_call(state, :array_from, [Builder.list_expr(Enum.reverse(elems))]), + :object + )} + end + end + + def in_call(state) do + with {:ok, obj, _obj_type, state} <- pop_typed(state), + {:ok, key, _key_type, state} <- pop_typed(state) do + {:ok, + push( + state, + Builder.remote_call(QuickBEAM.VM.ObjectModel.Put, :has_property, [obj, key]), + :boolean + )} + end + end + + def append_call(state) do + with {:ok, obj, _obj_type, state} <- pop_typed(state), + {:ok, idx, _idx_type, state} <- pop_typed(state), + {:ok, arr, _arr_type, state} <- pop_typed(state) do + {pair, state} = + bind( + state, + Builder.temp_name(state.temp), + compiler_call(state, :append_spread, [arr, idx, obj]) + ) + + {:ok, + %{ + state + | stack: [Builder.tuple_element(pair, 1), Builder.tuple_element(pair, 2) | state.stack], + stack_types: [:number, :object | state.stack_types] + }} + end + end + + def copy_data_properties_call(state, mask) do + target_idx = Bitwise.band(mask, 3) + source_idx = Bitwise.band(Bitwise.bsr(mask, 2), 7) + + with {:ok, state, target} <- bind_stack_entry(state, target_idx), + {:ok, state, source} <- bind_stack_entry(state, source_idx) do + {:ok, + %{ + state + | body: state.body ++ [compiler_call(state, :copy_data_properties, [target, source])] + }} + else + :error -> {:error, {:copy_data_properties_missing, mask, target_idx, source_idx}} + end + end + + def delete_call(state) do + with {:ok, key, _key_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, state} <- pop_typed(state) do + effectful_push(state, compiler_call(state, :delete_property, [obj, key]), :boolean) + end + end + + def invoke_tail_method_call(state, argc) do + with {:ok, args, _arg_types, state} <- pop_n_typed(state, argc), + {:ok, fun, _fun_type, state} <- pop_typed(state), + {:ok, obj, _obj_type, %{stack: [], stack_types: []} = state} <- pop_typed(state) do + {:done, + state.body ++ + [ + Builder.remote_call(QuickBEAM.VM.Invocation, :invoke_method_runtime, [ + ctx_expr(state), + fun, + obj, + Builder.list_expr(Enum.reverse(args)) + ]) + ]} + else + {:ok, _obj, _obj_type, _state} -> {:error, :stack_not_empty_on_tail_call} + {:error, _} = error -> error + end + end + + def goto(state, target, stack_depths) do + with {:ok, call} <- block_jump_call(state, target, stack_depths) do + {:done, state.body ++ [call]} + end + end + + def branch(%{stack: stack}, idx, next_entry, target, sense, _stack_depths) when stack == [] do + {:error, {:missing_branch_condition, idx, target, sense, next_entry}} + end + + def branch(state, _idx, next_entry, target, sense, _stack_depths) when is_nil(next_entry) do + {:error, {:missing_fallthrough_block, target, sense, state.body}} + end + + def branch(state, _idx, next_entry, target, sense, stack_depths) do + with {:ok, cond_expr, cond_type, state} <- pop_typed(state), + {:ok, target_call} <- block_jump_call(state, target, stack_depths), + {:ok, next_call} <- block_jump_call(state, next_entry, stack_depths) do + truthy = Builder.branch_condition(cond_expr, cond_type) + false_body = [target_call] + true_body = [next_call] + + body = + if sense do + state.body ++ [Builder.branch_case(truthy, true_body, false_body)] + else + state.body ++ [Builder.branch_case(truthy, false_body, true_body)] + end + + {:done, body} + end + end + + def return_top(state) do + with {:ok, expr, _state} <- pop(state) do + {:done, state.body ++ [expr]} + end + end + + def throw_top(state) do + with {:ok, expr, _state} <- pop(state) do + {:done, state.body ++ [Builder.throw_js(expr)]} + end + end + + def bind(state, name, expr) do + var = Builder.var(name) + {var, %{state | body: state.body ++ [Builder.match(var, expr)], temp: state.temp + 1}} + end + + def update_ctx(state, expr) do + {ctx, state} = bind(state, "Ctx#{state.temp}", expr) + %{state | ctx: ctx} + end + + def block_jump_call(state, target, stack_depths) do + block_jump_call_values( + target, + stack_depths, + ctx_expr(state), + current_slots(state), + current_stack(state), + current_capture_cells(state) + ) + end + + def block_jump_call_values(target, stack_depths, ctx, slots, stack, capture_cells) do + expected_depth = Map.get(stack_depths, target) + actual_depth = length(stack) + + cond do + is_nil(expected_depth) -> + {:error, {:unknown_block_target, target}} + + expected_depth != actual_depth -> + {:error, {:stack_depth_mismatch, target, expected_depth, actual_depth}} + + true -> + {:ok, + Builder.local_call(Builder.block_name(target), [ + ctx | slots ++ stack ++ capture_cells + ])} + end + end + + def current_slots(state), do: ordered_values(state.slots) + def current_stack(state), do: state.stack + def current_capture_cells(state), do: ordered_values(state.capture_cells) + + defp initial_slot_inits(0, _arg_count, _locals), do: %{} + + defp initial_slot_inits(slot_count, arg_count, locals) do + Map.new(0..(slot_count - 1), fn idx -> + initialized = + cond do + idx < arg_count -> true + match?(%{is_lexical: true}, Enum.at(locals, idx)) -> false + true -> true + end + + {idx, initialized} + end) + end + + defp invoke_call_expr(%{return_type: return_type} = state, _fun, :self_fun, args, _arg_types) do + effectful_push( + state, + Builder.local_call(:run_ctx, [ctx_expr(state) | normalize_self_call_args(state, args)]), + return_type + ) + end + + defp invoke_call_expr(state, fun, fun_type, args, _arg_types) do + effectful_push( + state, + invoke_runtime_expr(state, fun, args), + function_return_type(fun_type, state.return_type) + ) + end + + defp tail_call_expr(state, _fun, :self_fun, args, _arg_types), + do: + state.body ++ + [ + Builder.local_call(:run_ctx, [ctx_expr(state) | normalize_self_call_args(state, args)]) + ] + + defp tail_call_expr(state, fun, _fun_type, args, _arg_types), + do: state.body ++ [invoke_runtime_expr(state, fun, args)] + + defp specialize_unary(:op_neg, expr, :integer), do: {{:op, @line, :-, expr}, :integer} + defp specialize_unary(:op_neg, expr, :number), do: {{:op, @line, :-, expr}, :number} + defp specialize_unary(:op_plus, expr, type) when type in [:integer, :number], do: {expr, type} + defp specialize_unary(fun, expr, _type), do: {Builder.local_call(fun, [expr]), :unknown} + + defp specialize_binary(:op_add, left, :integer, right, :integer), + do: {{:op, @line, :+, left, right}, :integer} + + defp specialize_binary(:op_add, left, left_type, right, right_type) + when left_type in [:integer, :number] and right_type in [:integer, :number], + do: + {{:op, @line, :+, left, right}, + if(left_type == :integer and right_type == :integer, do: :integer, else: :number)} + + defp specialize_binary(:op_add, left, :string, right, :string), + do: {binary_concat(left, right), :string} + + defp specialize_binary(:op_strict_eq, left, type, right, type) + when type in [:integer, :boolean, :string, :null, :undefined], + do: {{:op, @line, :"=:=", left, right}, :boolean} + + defp specialize_binary(:op_strict_neq, left, type, right, type) + when type in [:integer, :boolean, :string, :null, :undefined], + do: {{:op, @line, :"=/=", left, right}, :boolean} + + defp specialize_binary(fun, left, left_type, right, right_type) + when fun in [:op_sub, :op_mul] and left_type == :integer and right_type == :integer, + do: {{:op, @line, binary_operator(fun), left, right}, :integer} + + defp specialize_binary(fun, left, left_type, right, right_type) + when fun in [:op_sub, :op_mul, :op_div, :op_lt, :op_lte, :op_gt, :op_gte] and + left_type in [:integer, :number] and right_type in [:integer, :number] do + {type, op} = + case fun do + :op_sub -> {:number, :-} + :op_mul -> {:number, :*} + :op_div -> {:number, :/} + :op_lt -> {:boolean, :<} + :op_lte -> {:boolean, :"=<"} + :op_gt -> {:boolean, :>} + :op_gte -> {:boolean, :>=} + end + + {{:op, @line, op, left, right}, type} + end + + defp specialize_binary(fun, left, _left_type, right, _right_type), + do: {Builder.local_call(fun, [left, right]), :unknown} + + defp specialize_get_length(expr, _type), + do: {Builder.remote_call(QuickBEAM.VM.ObjectModel.Get, :length_of, [expr]), :integer} + + defp invoke_runtime_expr(state, fun, args) do + case var_ref_fun_call(fun, length(args)) do + {:ok, helper, idx, argc} when argc in 0..3 -> + Builder.local_call(helper, [ctx_expr(state), idx | args]) + + {:ok, helper, idx, _argc} -> + Builder.local_call(helper, [ctx_expr(state), idx, Builder.list_expr(args)]) + + :error -> + Builder.remote_call(QuickBEAM.VM.Invocation, :invoke_runtime, [ + ctx_expr(state), + fun, + Builder.list_expr(args) + ]) + end + end + + defp var_ref_fun_call( + {:call, _, {:remote, _, {:atom, _, RuntimeHelpers}, {:atom, _, fun}}, [_ctx, idx]}, + argc + ) + when fun in [:get_var_ref, :get_var_ref_check] do + {:ok, invoke_var_ref_helper(fun, argc), idx, argc} + end + + defp var_ref_fun_call(_expr, _argc), do: :error + + defp invoke_var_ref_helper(:get_var_ref, argc), + do: invoke_var_ref_helper_name(:invoke_var_ref, argc) + + defp invoke_var_ref_helper(:get_var_ref_check, argc), + do: invoke_var_ref_helper_name(:invoke_var_ref_check, argc) + + defp invoke_var_ref_helper_name(prefix, argc) when argc in 0..3, + do: String.to_atom("op_#{prefix}#{argc}") + + defp invoke_var_ref_helper_name(prefix, _argc), do: String.to_atom("op_#{prefix}") + + defp binary_operator(:op_sub), do: :- + defp binary_operator(:op_mul), do: :* + + defp normalize_self_call_args(%{arg_count: arg_count}, args) do + args + |> Enum.take(arg_count) + |> then(fn args -> + args ++ List.duplicate(Builder.atom(:undefined), arg_count - length(args)) + end) + end + + defp function_return_type(:self_fun, return_type), do: return_type + defp function_return_type({:function, type}, _return_type), do: type + defp function_return_type(_fun_type, _return_type), do: :unknown + + defp function_type_from_expr(expr), do: Types.infer_expr_type(expr) + + defp binary_concat(left, right) do + {:bin, @line, + [ + {:bin_element, @line, left, :default, [:binary]}, + {:bin_element, @line, right, :default, [:binary]} + ]} + end + + defp ordered_values(values) do + values + |> Enum.sort_by(fn {idx, _expr} -> idx end) + |> Enum.map(fn {_idx, expr} -> expr end) + end +end diff --git a/lib/quickbeam/vm/compiler/lowering/types.ex b/lib/quickbeam/vm/compiler/lowering/types.ex new file mode 100644 index 00000000..be366bcf --- /dev/null +++ b/lib/quickbeam/vm/compiler/lowering/types.ex @@ -0,0 +1,34 @@ +defmodule QuickBEAM.VM.Compiler.Lowering.Types do + @moduledoc false + + def infer_expr_type({:integer, _, _}), do: :integer + def infer_expr_type({:float, _, _}), do: :number + def infer_expr_type({:char, _, _}), do: :integer + def infer_expr_type({:string, _, _}), do: :string + def infer_expr_type({:bin, _, _}), do: :string + def infer_expr_type({:atom, _, true}), do: :boolean + def infer_expr_type({:atom, _, false}), do: :boolean + def infer_expr_type({:atom, _, :undefined}), do: :undefined + def infer_expr_type({:atom, _, nil}), do: :null + def infer_expr_type(_), do: :unknown + + def definitely_initialized?(:unknown), do: false + def definitely_initialized?(_), do: true + + def pure_expr?({:integer, _, _}), do: true + def pure_expr?({:float, _, _}), do: true + def pure_expr?({:char, _, _}), do: true + def pure_expr?({:string, _, _}), do: true + def pure_expr?({:atom, _, _}), do: true + def pure_expr?({nil, _}), do: true + def pure_expr?({:var, _, _}), do: true + def pure_expr?({:tuple, _, values}), do: Enum.all?(values, &pure_expr?/1) + def pure_expr?({:cons, _, head, tail}), do: pure_expr?(head) and pure_expr?(tail) + def pure_expr?({:map, _, fields}), do: Enum.all?(fields, &pure_map_field?/1) + def pure_expr?(_), do: false + + defp pure_map_field?({:map_field_assoc, _, key, value}), + do: pure_expr?(key) and pure_expr?(value) + + defp pure_map_field?(_), do: false +end diff --git a/lib/quickbeam/vm/compiler/optimizer.ex b/lib/quickbeam/vm/compiler/optimizer.ex new file mode 100644 index 00000000..c8c34f45 --- /dev/null +++ b/lib/quickbeam/vm/compiler/optimizer.ex @@ -0,0 +1,301 @@ +defmodule QuickBEAM.VM.Compiler.Optimizer do + @moduledoc false + + alias QuickBEAM.VM.Compiler.Analysis.CFG + alias QuickBEAM.VM.Opcodes + + @push_one_ops [ + Opcodes.num(:push_i32), + Opcodes.num(:push_i16), + Opcodes.num(:push_i8), + Opcodes.num(:push_1) + ] + @get_loc_ops [ + Opcodes.num(:get_loc), + Opcodes.num(:get_loc0), + Opcodes.num(:get_loc1), + Opcodes.num(:get_loc2), + Opcodes.num(:get_loc3), + Opcodes.num(:get_loc8) + ] + + def optimize(instructions, constants \\ []) do + instructions + |> fold_literals(constants) + |> peephole_loc_updates() + |> simplify_constant_branches() + |> rewrite_forwarding_targets() + end + + defp fold_literals(instructions, constants) do + instructions + |> Enum.with_index() + |> Enum.reduce(instructions, fn {{_op, _args}, idx}, acc -> + maybe_fold_at(acc, idx, constants) + end) + end + + defp maybe_fold_at(instructions, idx, constants) do + case Enum.slice(instructions, idx, 3) do + [a, b, c] -> + fold_binary_window(instructions, idx, a, b, c, constants) + + _ -> + case Enum.slice(instructions, idx, 2) do + [a, b] -> fold_unary_window(instructions, idx, a, b, constants) + _ -> instructions + end + end + end + + defp fold_binary_window(instructions, idx, a, b, c, constants) do + with {:ok, left} <- instruction_literal(a, constants), + {:ok, right} <- instruction_literal(b, constants), + {:ok, op_name} <- CFG.opcode_name(elem(c, 0)), + {:ok, result} <- fold_binary(op_name, left, right), + {:ok, replacement} <- literal_instruction(result) do + replace_window(instructions, idx, [replacement, nop(), nop()]) + else + _ -> instructions + end + end + + defp fold_unary_window(instructions, idx, a, b, constants) do + with {:ok, value} <- instruction_literal(a, constants), + {:ok, op_name} <- CFG.opcode_name(elem(b, 0)), + {:ok, result} <- fold_unary(op_name, value), + {:ok, replacement} <- literal_instruction(result) do + replace_window(instructions, idx, [replacement, nop()]) + else + _ -> instructions + end + end + + defp fold_binary(:add, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left + right} + + defp fold_binary(:sub, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left - right} + + defp fold_binary(:mul, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left * right} + + defp fold_binary(:lt, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left < right} + + defp fold_binary(:lte, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left <= right} + + defp fold_binary(:gt, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left > right} + + defp fold_binary(:gte, left, right) when is_integer(left) and is_integer(right), + do: {:ok, left >= right} + + defp fold_binary(:strict_eq, left, right), do: {:ok, left === right} + defp fold_binary(:strict_neq, left, right), do: {:ok, left !== right} + defp fold_binary(_name, _left, _right), do: :error + + defp fold_unary(:neg, value) when is_integer(value), do: {:ok, -value} + defp fold_unary(:plus, value) when is_integer(value), do: {:ok, value} + defp fold_unary(:lnot, value) when is_boolean(value), do: {:ok, not value} + defp fold_unary(_name, _value), do: :error + + defp peephole_loc_updates(instructions) do + instructions + |> Enum.with_index() + |> Enum.reduce(instructions, fn {{_op, _args}, idx}, acc -> + case Enum.slice(acc, idx, 4) do + [a, b, c, d] -> rewrite_loc_update_window(acc, idx, a, b, c, d) + _ -> acc + end + end) + end + + defp rewrite_loc_update_window(instructions, idx, a, b, c, d) do + with {:ok, get_name} <- CFG.opcode_name(elem(a, 0)), + true <- get_name in [:get_loc, :get_loc0, :get_loc1, :get_loc2, :get_loc3, :get_loc8], + [slot_idx] <- elem(a, 1), + {:ok, put_name} <- CFG.opcode_name(elem(d, 0)), + true <- put_name in [:put_loc, :put_loc0, :put_loc1, :put_loc2, :put_loc3, :put_loc8], + [^slot_idx] <- elem(d, 1) do + case {b, CFG.opcode_name(elem(c, 0))} do + {{op_b, [1]}, {:ok, :add}} when op_b in @push_one_ops -> + replace_window(instructions, idx, [nop(), nop(), inc_loc(slot_idx), nop()]) + + {{op_b, [1]}, {:ok, :sub}} when op_b in @push_one_ops -> + replace_window(instructions, idx, [nop(), nop(), dec_loc(slot_idx), nop()]) + + {{op_b, [other_slot]}, {:ok, :add}} + when op_b in @get_loc_ops and is_integer(other_slot) -> + replace_window(instructions, idx, [nop(), b, add_loc(slot_idx), nop()]) + + _ -> + instructions + end + else + _ -> instructions + end + end + + defp simplify_constant_branches(instructions) do + instructions + |> Enum.with_index() + |> Enum.reduce(instructions, fn {{_op, _args}, idx}, acc -> + case Enum.slice(acc, idx, 2) do + [cond_insn, branch_insn] -> simplify_branch_window(acc, idx, cond_insn, branch_insn) + _ -> acc + end + end) + end + + defp simplify_branch_window(instructions, idx, cond_insn, branch_insn) do + case {instruction_boolean(cond_insn), branch_insn} do + {{:ok, true}, {op, [target]}} -> + case CFG.opcode_name(op) do + {:ok, :if_true} -> replace_window(instructions, idx, [nop(), goto(target)]) + {:ok, :if_true8} -> replace_window(instructions, idx, [nop(), goto(target)]) + {:ok, :if_false} -> replace_window(instructions, idx, [nop(), nop()]) + {:ok, :if_false8} -> replace_window(instructions, idx, [nop(), nop()]) + _ -> instructions + end + + {{:ok, false}, {op, [target]}} -> + case CFG.opcode_name(op) do + {:ok, :if_false} -> replace_window(instructions, idx, [nop(), goto(target)]) + {:ok, :if_false8} -> replace_window(instructions, idx, [nop(), goto(target)]) + {:ok, :if_true} -> replace_window(instructions, idx, [nop(), nop()]) + {:ok, :if_true8} -> replace_window(instructions, idx, [nop(), nop()]) + _ -> instructions + end + + _ -> + instructions + end + end + + defp rewrite_forwarding_targets(instructions) do + if Enum.any?(instructions, fn {op, _args} -> + match?({:ok, name} when name in [:catch, :gosub, :ret], CFG.opcode_name(op)) + end) do + instructions + else + entries = CFG.block_entries(instructions) + next_entry = fn start -> CFG.next_entry(entries, start) || length(instructions) end + + forwarding = + Enum.reduce(entries, %{}, fn start, acc -> + case {next_entry.(start), Enum.at(instructions, start)} do + {next, {op, [target]}} when next == start + 1 -> + case CFG.opcode_name(op) do + {:ok, name} when name in [:goto, :goto8, :goto16] -> Map.put(acc, start, target) + _ -> acc + end + + _ -> + acc + end + end) + + if forwarding == %{} do + instructions + else + Enum.map(instructions, fn {op, args} = insn -> + case {CFG.opcode_name(op), args} do + {{:ok, name}, [target]} + when name in [:goto, :goto8, :goto16, :if_true, :if_true8, :if_false, :if_false8] -> + {op, [follow_forwarding(target, forwarding)]} + + _ -> + insn + end + end) + end + end + end + + defp follow_forwarding(target, forwarding) do + case Map.get(forwarding, target) do + nil -> target + next when next == target -> target + next -> follow_forwarding(next, forwarding) + end + end + + defp instruction_boolean(insn) do + case instruction_literal(insn, []) do + {:ok, value} when is_boolean(value) -> {:ok, value} + _ -> :error + end + end + + defp instruction_literal({op, args}, constants) do + case CFG.opcode_name(op) do + {:ok, :push_i32} -> + {:ok, hd(args)} + + {:ok, :push_i16} -> + {:ok, hd(args)} + + {:ok, :push_i8} -> + {:ok, hd(args)} + + {:ok, :push_minus1} -> + {:ok, -1} + + {:ok, name} + when name in [:push_0, :push_1, :push_2, :push_3, :push_4, :push_5, :push_6, :push_7] -> + {:ok, String.to_integer(String.replace_prefix(Atom.to_string(name), "push_", ""))} + + {:ok, :push_true} -> + {:ok, true} + + {:ok, :push_false} -> + {:ok, false} + + {:ok, :null} -> + {:ok, nil} + + {:ok, :undefined} -> + {:ok, :undefined} + + {:ok, :push_empty_string} -> + {:ok, ""} + + {:ok, name} when name in [:push_const, :push_const8] -> + literal_const(constants, hd(args)) + + _ -> + :error + end + end + + defp literal_const(constants, idx) do + case Enum.at(constants, idx) do + value when is_integer(value) -> {:ok, value} + value when is_boolean(value) -> {:ok, value} + nil -> {:ok, nil} + :undefined -> {:ok, :undefined} + "" -> {:ok, ""} + _ -> :error + end + end + + defp literal_instruction(value) when is_integer(value), + do: {:ok, {Opcodes.num(:push_i32), [value]}} + + defp literal_instruction(true), do: {:ok, {Opcodes.num(:push_true), []}} + defp literal_instruction(false), do: {:ok, {Opcodes.num(:push_false), []}} + + defp replace_window(instructions, idx, replacements) do + prefix = Enum.take(instructions, idx) + suffix = Enum.drop(instructions, idx + length(replacements)) + prefix ++ replacements ++ suffix + end + + defp nop, do: {Opcodes.num(:nop), []} + defp goto(target), do: {Opcodes.num(:goto16), [target]} + defp inc_loc(idx), do: {Opcodes.num(:inc_loc), [idx]} + defp dec_loc(idx), do: {Opcodes.num(:dec_loc), [idx]} + defp add_loc(idx), do: {Opcodes.num(:add_loc), [idx]} +end diff --git a/lib/quickbeam/vm/compiler/runner.ex b/lib/quickbeam/vm/compiler/runner.ex new file mode 100644 index 00000000..137b6031 --- /dev/null +++ b/lib/quickbeam/vm/compiler/runner.ex @@ -0,0 +1,333 @@ +defmodule QuickBEAM.VM.Compiler.Runner do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, GlobalEnv, Heap} + alias QuickBEAM.VM.Compiler + alias QuickBEAM.VM.Compiler.GeneratorIterator + alias QuickBEAM.VM.Interpreter.Context + alias QuickBEAM.VM.ObjectModel.{Class, Functions} + alias QuickBEAM.VM.PromiseState, as: Promise + + def invoke(%Bytecode.Function{} = fun, args), do: invoke(fun, args, nil) + def invoke({:closure, _, %Bytecode.Function{}} = closure, args), do: invoke(closure, args, nil) + def invoke(_, _), do: :error + + def invoke(%Bytecode.Function{} = fun, args, base_ctx), + do: invoke_target(fun, fun, args, %{}, base_ctx) + + def invoke({:closure, _, %Bytecode.Function{} = fun} = closure, args, base_ctx), + do: invoke_target(closure, fun, args, %{}, base_ctx) + + def invoke(_, _, _), do: :error + + def invoke_with_receiver(%Bytecode.Function{} = fun, args, this_obj), + do: invoke_with_receiver(fun, args, this_obj, nil) + + def invoke_with_receiver({:closure, _, %Bytecode.Function{}} = closure, args, this_obj), + do: invoke_with_receiver(closure, args, this_obj, nil) + + def invoke_with_receiver(_, _, _), do: :error + + def invoke_with_receiver(%Bytecode.Function{} = fun, args, this_obj, base_ctx), + do: invoke_target(fun, fun, args, %{this: this_obj}, base_ctx) + + def invoke_with_receiver( + {:closure, _, %Bytecode.Function{} = fun} = closure, + args, + this_obj, + base_ctx + ), + do: invoke_target(closure, fun, args, %{this: this_obj}, base_ctx) + + def invoke_with_receiver(_, _, _, _), do: :error + + def invoke_constructor(%Bytecode.Function{} = fun, args, this_obj, new_target), + do: invoke_constructor(fun, args, this_obj, new_target, nil) + + def invoke_constructor( + {:closure, _, %Bytecode.Function{}} = closure, + args, + this_obj, + new_target + ), + do: invoke_constructor(closure, args, this_obj, new_target, nil) + + def invoke_constructor(_, _, _, _), do: :error + + def invoke_constructor(%Bytecode.Function{} = fun, args, this_obj, new_target, base_ctx), + do: invoke_target(fun, fun, args, %{this: this_obj, new_target: new_target}, base_ctx) + + def invoke_constructor( + {:closure, _, %Bytecode.Function{} = fun} = closure, + args, + this_obj, + new_target, + base_ctx + ), + do: invoke_target(closure, fun, args, %{this: this_obj, new_target: new_target}, base_ctx) + + def invoke_constructor(_, _, _, _, _), do: :error + + defp invoke_target(current_func, %Bytecode.Function{} = fun, args, ctx_overrides, base_ctx) do + key = {fun.byte_code, fun.arg_count} + normalized_args = normalize_args(args, fun.arg_count) + + case Heap.get_compiled(key) do + {:compiled, {mod, name}, atoms} -> + ctx = invocation_ctx(base_ctx, current_func, args, ctx_overrides, fun, atoms) + {:ok, invoke_compiled(fun, {mod, name}, ctx, normalized_args)} + + :unsupported -> + :error + + nil -> + compile_and_invoke(fun, current_func, args, normalized_args, ctx_overrides, base_ctx, key) + end + end + + defp compile_and_invoke(fun, current_func, args, normalized_args, ctx_overrides, base_ctx, key) do + case Compiler.compile(fun) do + {:ok, compiled} -> + atoms = Process.get({:qb_fn_atoms, fun.byte_code}, Heap.get_atoms()) + Heap.put_compiled(key, {:compiled, compiled, atoms}) + ctx = invocation_ctx(base_ctx, current_func, args, ctx_overrides, fun, atoms) + {:ok, invoke_compiled(fun, compiled, ctx, normalized_args)} + + {:error, _} -> + Heap.put_compiled(key, :unsupported) + :error + end + end + + defp invoke_compiled(%Bytecode.Function{func_kind: 1}, compiled, ctx, args) do + # Generator: wrap in yield/suspend protocol + compiled_gen_invoke(compiled, ctx, args) + end + + defp invoke_compiled(%Bytecode.Function{func_kind: 2}, compiled, ctx, args) do + # Async: wrap in promise + compiled_async_invoke(compiled, ctx, args) + end + + defp invoke_compiled(%Bytecode.Function{func_kind: 3}, compiled, ctx, args) do + # Async generator + compiled_async_gen_invoke(compiled, ctx, args) + end + + defp invoke_compiled(_fun, compiled, ctx, args) do + apply_compiled(compiled, ctx, args) + end + + defp compiled_gen_invoke(compiled, ctx, args) do + gen_ref = make_ref() + + try do + apply_compiled(compiled, ctx, args) + catch + {:generator_yield, _val, continuation} -> + Heap.put_obj(gen_ref, %{state: :suspended, continuation: continuation}) + end + + GeneratorIterator.build(gen_ref) + end + + defp compiled_async_invoke(compiled, ctx, args) do + result = apply_compiled(compiled, ctx, args) + Promise.resolved(result) + catch + {:generator_return, val} -> Promise.resolved(val) + {:js_throw, val} -> Promise.rejected(val) + end + + defp compiled_async_gen_invoke(compiled, ctx, args) do + gen_ref = make_ref() + + try do + apply_compiled(compiled, ctx, args) + catch + {:generator_yield, _val, continuation} -> + Heap.put_obj(gen_ref, %{state: :suspended, continuation: continuation}) + end + + GeneratorIterator.build_async(gen_ref) + end + + defp apply_compiled({mod, name}, ctx, args), do: apply(mod, name, [ctx | args]) + + defp invocation_ctx(base_ctx, current_func, args, %{} = ctx_overrides, fun, atoms) + when map_size(ctx_overrides) == 0 do + build_invocation_ctx(base_ctx(base_ctx), current_func, args, fun, atoms) + end + + defp invocation_ctx( + base_ctx, + current_func, + args, + %{this: this_obj, new_target: new_target}, + fun, + atoms + ) do + build_invocation_ctx(base_ctx(base_ctx), current_func, args, fun, atoms, + this: this_obj, + new_target: new_target + ) + end + + defp invocation_ctx(base_ctx, current_func, args, ctx_overrides, fun, atoms) do + ctx = build_invocation_ctx(base_ctx(base_ctx), current_func, args, fun, atoms) + + ctx + |> struct(Map.take(ctx_overrides, [:this, :new_target])) + |> Context.mark_dirty() + end + + defp build_invocation_ctx(%Context{} = base_ctx, current_func, args, fun, atoms), + do: build_invocation_ctx(base_ctx, current_func, args, fun, atoms, []) + + defp build_invocation_ctx(%Context{} = base_ctx, current_func, args, _fun, atoms, []) do + {home_object, super} = home_object_and_super(current_func) + + %Context{ + base_ctx + | atoms: atoms || current_atoms(base_ctx), + current_func: current_func, + arg_buf: List.to_tuple(args), + trace_enabled: trace_enabled(base_ctx), + home_object: home_object, + super: super + } + |> Context.mark_dirty() + end + + defp build_invocation_ctx( + %Context{} = base_ctx, + current_func, + args, + _fun, + atoms, + this: this_obj + ) do + {home_object, super} = home_object_and_super(current_func) + + %Context{ + base_ctx + | atoms: atoms || current_atoms(base_ctx), + current_func: current_func, + arg_buf: List.to_tuple(args), + trace_enabled: trace_enabled(base_ctx), + home_object: home_object, + super: super, + this: this_obj + } + |> Context.mark_dirty() + end + + defp build_invocation_ctx( + %Context{} = base_ctx, + current_func, + args, + _fun, + atoms, + this: this_obj, + new_target: new_target + ) do + {home_object, super} = home_object_and_super(current_func) + + %Context{ + base_ctx + | atoms: atoms || current_atoms(base_ctx), + current_func: current_func, + arg_buf: List.to_tuple(args), + trace_enabled: trace_enabled(base_ctx), + home_object: home_object, + super: super, + this: this_obj, + new_target: new_target + } + |> Context.mark_dirty() + end + + defp build_invocation_ctx(%Context{} = base_ctx, current_func, args, _fun, atoms, overrides) do + {home_object, super} = home_object_and_super(current_func) + + %Context{ + base_ctx + | atoms: atoms || current_atoms(base_ctx), + current_func: current_func, + arg_buf: List.to_tuple(args), + trace_enabled: trace_enabled(base_ctx), + home_object: home_object, + super: super, + this: Keyword.get(overrides, :this, base_ctx.this), + new_target: Keyword.get(overrides, :new_target, base_ctx.new_target) + } + |> Context.mark_dirty() + end + + defp base_ctx(%Context{} = ctx), do: ensure_globals(ctx) + + defp base_ctx(nil) do + %Context{atoms: Heap.get_atoms(), globals: base_globals(), trace_enabled: false} + end + + defp base_ctx(map) when is_map(map) do + map + |> then(&struct(Context, Map.merge(Map.from_struct(%Context{}), &1))) + |> ensure_globals() + end + + defp ensure_globals(%Context{globals: globals} = ctx) when globals == %{}, + do: %{ctx | globals: base_globals()} + + defp ensure_globals(%Context{} = ctx), do: ctx + + defp base_globals, do: GlobalEnv.base_globals() + + defp current_atoms(%Context{} = ctx), do: ctx.atoms + + defp trace_enabled(%Context{} = ctx), do: ctx.trace_enabled + + defp home_object_and_super(%Bytecode.Function{need_home_object: false}), + do: {:undefined, :undefined} + + defp home_object_and_super({:closure, _, %Bytecode.Function{need_home_object: false}}), + do: {:undefined, :undefined} + + defp home_object_and_super(current_func) do + home_object = Functions.current_home_object(current_func) + {home_object, current_super(home_object)} + end + + defp current_super(:undefined), do: :undefined + defp current_super(nil), do: :undefined + defp current_super(home_object), do: Class.get_super(home_object) + + def normalize_args(_args, 0), do: [] + def normalize_args([a0 | _], 1), do: [a0] + def normalize_args([], 1), do: [:undefined] + def normalize_args([a0, a1 | _], 2), do: [a0, a1] + def normalize_args([a0], 2), do: [a0, :undefined] + def normalize_args([], 2), do: [:undefined, :undefined] + def normalize_args([a0, a1, a2 | _], 3), do: [a0, a1, a2] + def normalize_args([a0, a1], 3), do: [a0, a1, :undefined] + def normalize_args([a0], 3), do: [a0, :undefined, :undefined] + def normalize_args([], 3), do: [:undefined, :undefined, :undefined] + + def normalize_args([a0, a1, a2, a3 | _], 4), do: [a0, a1, a2, a3] + def normalize_args([a0, a1, a2], 4), do: [a0, a1, a2, :undefined] + def normalize_args([a0, a1], 4), do: [a0, a1, :undefined, :undefined] + def normalize_args([a0], 4), do: [a0, :undefined, :undefined, :undefined] + def normalize_args([], 4), do: [:undefined, :undefined, :undefined, :undefined] + def normalize_args([a0, a1, a2, a3, a4 | _], 5), do: [a0, a1, a2, a3, a4] + def normalize_args([a0, a1, a2, a3], 5), do: [a0, a1, a2, a3, :undefined] + def normalize_args([a0, a1, a2], 5), do: [a0, a1, a2, :undefined, :undefined] + def normalize_args([a0, a1], 5), do: [a0, a1, :undefined, :undefined, :undefined] + def normalize_args([a0], 5), do: [a0, :undefined, :undefined, :undefined, :undefined] + def normalize_args([], 5), do: [:undefined, :undefined, :undefined, :undefined, :undefined] + + def normalize_args(args, arg_count) do + args + |> Enum.take(arg_count) + |> then(fn args -> args ++ List.duplicate(:undefined, arg_count - length(args)) end) + end +end diff --git a/lib/quickbeam/vm/compiler/runtime_helpers.ex b/lib/quickbeam/vm/compiler/runtime_helpers.ex new file mode 100644 index 00000000..33cb7916 --- /dev/null +++ b/lib/quickbeam/vm/compiler/runtime_helpers.ex @@ -0,0 +1,1058 @@ +defmodule QuickBEAM.VM.Compiler.RuntimeHelpers do + @moduledoc false + + import Bitwise, only: [bnot: 1] + import QuickBEAM.VM.Heap.Keys, only: [proto: 0] + + alias QuickBEAM.VM.{Builtin, Bytecode, GlobalEnv, Heap, Invocation, Names} + alias QuickBEAM.VM.Compiler.Runner + alias QuickBEAM.VM.Environment.Captures + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.Interpreter.{Closures, Context, Values} + alias QuickBEAM.VM.Invocation.Context, as: InvokeContext + alias QuickBEAM.VM.ObjectModel.{Class, Copy, Delete, Functions, Get, Methods, Private, Put} + alias QuickBEAM.VM.PromiseState, as: Promise + alias QuickBEAM.VM.Runtime + + @tdz :__tdz__ + + def entry_ctx do + case Heap.get_ctx() do + %Context{} = ctx -> + Context.mark_dirty(ctx) + + map when is_map(map) -> + map |> context_struct() |> Context.mark_dirty() + + _ -> + %Context{atoms: Heap.get_atoms(), globals: GlobalEnv.base_globals()} + |> Context.mark_dirty() + end + end + + def ensure_initialized_local!(_ctx, val), do: ensure_initialized_local!(val) + def strict_neq(_ctx, a, b), do: strict_neq(a, b) + def undefined?(_ctx, val), do: undefined?(val) + def null?(_ctx, val), do: null?(val) + def typeof_is_undefined(_ctx, val), do: typeof_is_undefined(val) + def typeof_is_function(_ctx, val), do: typeof_is_function(val) + def bit_not(_ctx, a), do: bit_not(a) + def lnot(_ctx, a), do: lnot(a) + def inc(_ctx, a), do: inc(a) + def dec(_ctx, a), do: dec(a) + def post_inc(_ctx, a), do: post_inc(a) + def post_dec(_ctx, a), do: post_dec(a) + + def get_var(ctx, name) when is_binary(name), do: fetch_ctx_var(ctx, name) + + def get_global(globals, name) do + case Map.fetch(globals, name) do + {:ok, val} -> val + :error -> throw({:js_throw, Heap.make_error("#{name} is not defined", "ReferenceError")}) + end + end + + def get_global_undef(globals, name), do: Map.get(globals, name, :undefined) + + def get_var(ctx, atom_idx), + do: fetch_ctx_var(ctx, Names.resolve_atom(context_atoms(ctx), atom_idx)) + + def get_var_undef(ctx, name) when is_binary(name), + do: GlobalEnv.get(context_globals(ctx), name, :undefined) + + def get_var_undef(ctx, atom_idx), + do: get_var_undef(ctx, Names.resolve_atom(context_atoms(ctx), atom_idx)) + + def push_atom_value(ctx, atom_idx), do: Names.resolve_atom(context_atoms(ctx), atom_idx) + + def private_symbol(_ctx, name) when is_binary(name), do: Private.private_symbol(name) + + def private_symbol(ctx, atom_idx), + do: Private.private_symbol(Names.resolve_atom(context_atoms(ctx), atom_idx)) + + def new_object(_ctx), do: new_object() + def array_from(_ctx, list), do: array_from(list) + + def get_var_ref(ctx, idx), do: read_var_ref(current_var_ref(ctx, idx)) + def get_var_ref_check(ctx, idx), do: checked_var_ref(ctx, idx) + + def get_capture(ctx, key) do + case context_current_func(ctx) do + {:closure, captured, _} -> read_var_ref(Map.get(captured, key, :undefined)) + _ -> :undefined + end + end + + def invoke_var_ref(ctx, idx, args), + do: Invocation.invoke_runtime(ctx, get_var_ref(ctx, idx), args) + + def invoke_var_ref0(ctx, idx), do: Invocation.invoke_runtime(ctx, get_var_ref(ctx, idx), []) + + def invoke_var_ref1(ctx, idx, arg0), + do: Invocation.invoke_runtime(ctx, get_var_ref(ctx, idx), [arg0]) + + def invoke_var_ref2(ctx, idx, arg0, arg1), + do: Invocation.invoke_runtime(ctx, get_var_ref(ctx, idx), [arg0, arg1]) + + def invoke_var_ref3(ctx, idx, arg0, arg1, arg2), + do: Invocation.invoke_runtime(ctx, get_var_ref(ctx, idx), [arg0, arg1, arg2]) + + def invoke_var_ref_check(ctx, idx, args), + do: Invocation.invoke_runtime(ctx, checked_var_ref(ctx, idx), args) + + def invoke_var_ref_check0(ctx, idx), + do: Invocation.invoke_runtime(ctx, checked_var_ref(ctx, idx), []) + + def invoke_var_ref_check1(ctx, idx, arg0), + do: Invocation.invoke_runtime(ctx, checked_var_ref(ctx, idx), [arg0]) + + def invoke_var_ref_check2(ctx, idx, arg0, arg1), + do: Invocation.invoke_runtime(ctx, checked_var_ref(ctx, idx), [arg0, arg1]) + + def invoke_var_ref_check3(ctx, idx, arg0, arg1, arg2), + do: Invocation.invoke_runtime(ctx, checked_var_ref(ctx, idx), [arg0, arg1, arg2]) + + def put_var_ref(ctx, idx, val) do + write_var_ref(current_var_ref(ctx, idx), val) + :ok + end + + def set_var_ref(ctx, idx, val) do + put_var_ref(ctx, idx, val) + val + end + + def put_capture(ctx, key, val) do + case context_current_func(ctx) do + {:closure, captured, _} -> write_var_ref(Map.get(captured, key, :undefined), val) + _ -> :ok + end + + :ok + end + + def set_capture(ctx, key, val) do + put_capture(ctx, key, val) + val + end + + def push_this(ctx) do + case context_this(ctx) do + this + when this == :uninitialized or + (is_tuple(this) and tuple_size(this) == 2 and elem(this, 0) == :uninitialized) -> + throw({:js_throw, Heap.make_error("this is not initialized", "ReferenceError")}) + + this -> + this + end + end + + def special_object(ctx, type) do + current_func = context_current_func(ctx) + arg_buf = context_arg_buf(ctx) + + case type do + 0 -> Heap.wrap(Tuple.to_list(arg_buf)) + 1 -> Heap.wrap(Tuple.to_list(arg_buf)) + 2 -> current_func + 3 -> context_new_target(ctx) + 4 -> context_home_object(ctx, current_func) + 5 -> Heap.wrap(%{}) + 6 -> Heap.wrap(%{}) + 7 -> Heap.wrap(%{"__proto__" => nil}) + _ -> :undefined + end + end + + def get_super(ctx, func) do + if context_home_object(ctx, context_current_func(ctx)) == func, + do: context_super(ctx), + else: Class.get_super(func) + end + + def get_array_el2(_ctx, obj, idx), do: get_array_el2(obj, idx) + def set_function_name(_ctx, fun, name), do: set_function_name(fun, name) + + def set_function_name_atom(ctx, fun, atom_idx), + do: Functions.set_name_atom(fun, atom_idx, context_atoms(ctx)) + + def set_function_name_computed(_ctx, fun, name_val), + do: set_function_name_computed(fun, name_val) + + def put_field(_ctx, obj, key, val) when is_binary(key) do + put_field(obj, key, val) + end + + def put_field(ctx, obj, atom_idx, val), + do: put_field(obj, Names.resolve_atom(context_atoms(ctx), atom_idx), val) + + def define_field(_ctx, obj, key, val) when is_binary(key), do: define_field(obj, key, val) + + def define_field(ctx, obj, atom_idx, val), + do: define_field(obj, Names.resolve_atom(context_atoms(ctx), atom_idx), val) + + def put_array_el(_ctx, obj, idx, val), do: put_array_el(obj, idx, val) + def define_array_el(_ctx, obj, idx, val), do: define_array_el(obj, idx, val) + + def define_method(_ctx, target, method, name, flags) when is_binary(name), + do: define_method(target, method, name, flags) + + def define_method(ctx, target, method, atom_idx, flags), + do: + Methods.define_method( + target, + method, + Names.resolve_atom(context_atoms(ctx), atom_idx), + flags + ) + + def define_method_computed(_ctx, target, method, field_name, flags), + do: define_method_computed(target, method, field_name, flags) + + def set_home_object(_ctx, method, target), do: set_home_object(method, target) + def add_brand(_ctx, target, brand), do: add_brand(target, brand) + def append_spread(_ctx, arr, idx, obj), do: append_spread(arr, idx, obj) + + def copy_data_properties(_ctx, target, source) do + copy_data_properties(target, source) + end + + def define_class(ctx, ctor, parent_ctor, atom_idx) do + ctor_closure = + case ctor do + %Bytecode.Function{} = fun -> {:closure, %{}, fun} + other -> other + end + + Class.define_class( + ctor_closure, + parent_ctor, + Names.resolve_atom(context_atoms(ctx), atom_idx) + ) + end + + def invoke_runtime(ctx, fun, args), do: Invocation.invoke_runtime(ctx, fun, args) + + def invoke_method_runtime(ctx, fun, this_obj, args), + do: Invocation.invoke_method_runtime(ctx, fun, this_obj, args) + + def construct_runtime(ctx, ctor, new_target, args), + do: Invocation.construct_runtime(ctx, ctor, new_target, args) + + def for_of_start(ctx, obj) do + case obj do + list when is_list(list) -> + {{:list_iter, list, 0}, :undefined} + + {:obj, ref} = obj_ref -> + case Heap.get_obj(ref) do + {:qb_arr, arr} -> + {{:list_iter, :array.to_list(arr), 0}, :undefined} + + list when is_list(list) -> + {{:list_iter, list, 0}, :undefined} + + map when is_map(map) -> + sym_iter = {:symbol, "Symbol.iterator"} + + cond do + Map.has_key?(map, sym_iter) -> + iter_fn = Map.get(map, sym_iter) + iter_obj = Invocation.call_callback(ctx, iter_fn, []) + {iter_obj, Get.get(iter_obj, "next")} + + Map.has_key?(map, "next") -> + {obj_ref, Get.get(obj_ref, "next")} + + true -> + {{:list_iter, [], 0}, :undefined} + end + + _ -> + {{:list_iter, [], 0}, :undefined} + end + + s when is_binary(s) -> + {{:list_iter, String.codepoints(s), 0}, :undefined} + + _ -> + {{:list_iter, [], 0}, :undefined} + end + end + + def for_in_start(_ctx, obj), do: for_in_start(obj) + def for_in_next(_ctx, iter), do: for_in_next(iter) + + def for_of_next(_ctx, _next_fn, :undefined), do: {true, :undefined, :undefined} + + def for_of_next(_ctx, _next_fn, {:list_iter, list, idx}) do + if idx < length(list) do + {false, Enum.at(list, idx), {:list_iter, list, idx + 1}} + else + {true, :undefined, :undefined} + end + end + + def for_of_next(ctx, next_fn, iter_obj) do + result = Invocation.call_callback(ctx, next_fn, []) + done = Get.get(result, "done") + value = Get.get(result, "value") + + if done == true do + {true, :undefined, :undefined} + else + {false, value, iter_obj} + end + end + + def iterator_close(_ctx, :undefined), do: :ok + def iterator_close(_ctx, {:list_iter, _, _}), do: :ok + + def iterator_close(ctx, iter_obj) do + return_fn = Get.get(iter_obj, "return") + + if return_fn != :undefined and return_fn != nil do + Invocation.call_callback(ctx, return_fn, []) + end + + :ok + end + + def delete_property(_ctx, obj, key), do: delete_property(obj, key) + def ensure_capture_cell(_ctx, cell, val), do: ensure_capture_cell(cell, val) + def close_capture_cell(_ctx, cell, val), do: close_capture_cell(cell, val) + def sync_capture_cell(_ctx, cell, val), do: sync_capture_cell(cell, val) + + def set_proto(_ctx, obj, proto), do: set_proto(obj, proto) + + def get_private_field(_ctx, obj, key) do + case Private.get_field(obj, key) do + :missing -> throw({:js_throw, Private.brand_error()}) + val -> val + end + end + + def put_private_field(_ctx, obj, key, val) do + case Private.put_field!(obj, key, val) do + :ok -> :ok + :error -> throw({:js_throw, Private.brand_error()}) + end + end + + def define_private_field(_ctx, obj, key, val) do + case Private.define_field!(obj, key, val) do + :ok -> :ok + :error -> throw({:js_throw, Private.brand_error()}) + end + end + + def check_brand(_ctx, obj, brand) do + case Private.ensure_brand(obj, brand) do + :ok -> :ok + :error -> throw({:js_throw, Private.brand_error()}) + end + end + + def init_ctor(ctx) do + current_func = context_current_func(ctx) + + raw = + case current_func do + {:closure, _, %Bytecode.Function{} = f} -> f + %Bytecode.Function{} = f -> f + other -> other + end + + parent = Heap.get_parent_ctor(raw) + args = Tuple.to_list(context_arg_buf(ctx)) + + pending_this = + case context_this(ctx) do + {:uninitialized, {:obj, _} = obj} -> obj + {:obj, _} = obj -> obj + other -> other + end + + parent_ctx = Context.mark_dirty(%{ensure_context(ctx) | this: pending_this}) + + result = + case parent do + nil -> + pending_this + + %Bytecode.Function{} = f -> + case Runner.invoke_constructor( + {:closure, %{}, f}, args, pending_this, context_new_target(ctx), parent_ctx + ) do + {:ok, val} -> val + :error -> Invocation.invoke_with_receiver({:closure, %{}, f}, args, context_gas(ctx), pending_this) + end + + {:closure, _, %Bytecode.Function{}} = closure -> + case Runner.invoke_constructor( + closure, args, pending_this, context_new_target(ctx), parent_ctx + ) do + {:ok, val} -> val + :error -> Invocation.invoke_with_receiver(closure, args, context_gas(ctx), pending_this) + end + + {:builtin, _name, cb} when is_function(cb, 2) -> + cb.(args, pending_this) + + {:builtin, _name, cb} when is_function(cb, 2) -> + cb.(args, pending_this) + + _ -> + pending_this + end + + result = + case result do + {:obj, _} = obj -> obj + _ -> pending_this + end + + Heap.put_ctx(Context.mark_dirty(%{parent_ctx | this: result})) + result + end + + def make_loc_ref(_ctx, idx), do: make_loc_ref(idx) + def make_arg_ref(_ctx, idx), do: make_arg_ref(idx) + + def make_var_ref_ref(ctx, idx) do + case current_var_ref(ctx, idx) do + {:cell, _} = cell -> cell + val -> + ref = make_ref() + Heap.put_cell(ref, val) + {:cell, ref} + end + end + + def get_ref_value(_ctx, ref), do: get_ref_value(ref) + def put_ref_value(_ctx, val, ref), do: put_ref_value(val, ref) + + def rest(ctx, start_idx) do + arg_buf = context_arg_buf(ctx) + + rest_args = + if start_idx < tuple_size(arg_buf) do + Tuple.to_list(arg_buf) |> Enum.drop(start_idx) + else + [] + end + + Heap.wrap(rest_args) + end + + def throw_error(ctx, atom_idx, reason) do + name = Names.resolve_atom(context_atoms(ctx), atom_idx) + {error_type, message} = throw_error_message(name, reason) + throw({:js_throw, Heap.make_error(message, error_type)}) + end + + def throw_error_message(name, reason) do + case reason do + 0 -> {"TypeError", "'#{name}' is read-only"} + 1 -> {"SyntaxError", "redeclaration of '#{name}'"} + 2 -> {"ReferenceError", "cannot access '#{name}' before initialization"} + 3 -> {"ReferenceError", "unsupported reference to 'super'"} + 4 -> {"TypeError", "iterator does not have a throw method"} + _ -> {"Error", name} + end + end + + def ensure_initialized_local!(val) do + if val == @tdz do + throw( + {:js_throw, + Heap.make_error("Cannot access variable before initialization", "ReferenceError")} + ) + end + + val + end + + def strict_neq(a, b), do: not Values.strict_eq(a, b) + + def undefined?(val), do: val == :undefined + def null?(val), do: val == nil + def typeof_is_undefined(val), do: val == :undefined or val == nil + def typeof_is_function(val), do: Builtin.callable?(val) + + def bit_not(a), do: Values.to_int32(bnot(Values.to_int32(a))) + def lnot(a), do: not Values.truthy?(a) + + def inc(a), do: Values.add(a, 1) + def dec(a), do: Values.sub(a, 1) + + def post_inc(a) do + num = Values.to_number(a) + {Values.add(num, 1), num} + end + + def post_dec(a) do + num = Values.to_number(a) + {Values.sub(num, 1), num} + end + + def get_var(name) when is_binary(name) do + case GlobalEnv.fetch(name) do + {:found, val} -> + val + + :not_found -> + throw({:js_throw, Heap.make_error("#{name} is not defined", "ReferenceError")}) + end + end + + def get_var(atom_idx), do: get_var(Names.resolve_atom(InvokeContext.current_atoms(), atom_idx)) + + def get_var_undef(name) when is_binary(name), do: GlobalEnv.get(name, :undefined) + + def get_var_undef(atom_idx), + do: get_var_undef(Names.resolve_atom(InvokeContext.current_atoms(), atom_idx)) + + def push_atom_value(atom_idx), do: Names.resolve_atom(InvokeContext.current_atoms(), atom_idx) + + def private_symbol(name) when is_binary(name), do: Private.private_symbol(name) + + def private_symbol(atom_idx), + do: Private.private_symbol(Names.resolve_atom(InvokeContext.current_atoms(), atom_idx)) + + def new_object do + object_proto = Heap.get_object_prototype() + init = if object_proto, do: %{proto() => object_proto}, else: %{} + Heap.wrap(init) + end + + def array_from(list), do: Heap.wrap(list) + + def get_field(obj, key) when is_binary(key), do: Get.get(obj, key) + + def get_field(obj, atom_idx), + do: Get.get(obj, Names.resolve_atom(InvokeContext.current_atoms(), atom_idx)) + + def get_var_ref(idx), do: read_var_ref(current_var_ref(idx)) + + def get_var_ref_check(idx), do: checked_var_ref(idx) + + def invoke_var_ref(idx, args), do: Invocation.invoke_runtime(get_var_ref(idx), args) + def invoke_var_ref0(idx), do: Invocation.invoke_runtime(get_var_ref(idx), []) + def invoke_var_ref1(idx, arg0), do: Invocation.invoke_runtime(get_var_ref(idx), [arg0]) + + def invoke_var_ref2(idx, arg0, arg1), + do: Invocation.invoke_runtime(get_var_ref(idx), [arg0, arg1]) + + def invoke_var_ref3(idx, arg0, arg1, arg2), + do: Invocation.invoke_runtime(get_var_ref(idx), [arg0, arg1, arg2]) + + def invoke_var_ref_check(idx, args), do: Invocation.invoke_runtime(checked_var_ref(idx), args) + def invoke_var_ref_check0(idx), do: Invocation.invoke_runtime(checked_var_ref(idx), []) + + def invoke_var_ref_check1(idx, arg0), + do: Invocation.invoke_runtime(checked_var_ref(idx), [arg0]) + + def invoke_var_ref_check2(idx, arg0, arg1), + do: Invocation.invoke_runtime(checked_var_ref(idx), [arg0, arg1]) + + def invoke_var_ref_check3(idx, arg0, arg1, arg2), + do: Invocation.invoke_runtime(checked_var_ref(idx), [arg0, arg1, arg2]) + + def put_var_ref(idx, val) do + write_var_ref(current_var_ref(idx), val) + :ok + end + + def set_var_ref(idx, val) do + put_var_ref(idx, val) + val + end + + def push_this do + case InvokeContext.current_this() do + this + when this == :uninitialized or + (is_tuple(this) and tuple_size(this) == 2 and elem(this, 0) == :uninitialized) -> + throw({:js_throw, Heap.make_error("this is not initialized", "ReferenceError")}) + + this -> + this + end + end + + def special_object(type) do + case InvokeContext.fast_ctx() do + {_atoms, _globals, current_func, arg_buf, _this, new_target, home_object, _super} -> + case type do + 0 -> Heap.wrap(Tuple.to_list(arg_buf)) + 1 -> Heap.wrap(Tuple.to_list(arg_buf)) + 2 -> current_func + 3 -> new_target + 4 -> home_object + 5 -> Heap.wrap(%{}) + 6 -> Heap.wrap(%{}) + 7 -> Heap.wrap(%{"__proto__" => nil}) + _ -> :undefined + end + + _ -> + current_func = InvokeContext.current_func() + arg_buf = InvokeContext.current_arg_buf() + + case type do + 0 -> Heap.wrap(Tuple.to_list(arg_buf)) + 1 -> Heap.wrap(Tuple.to_list(arg_buf)) + 2 -> current_func + 3 -> InvokeContext.current_new_target() + 4 -> InvokeContext.current_home_object(current_func) + 5 -> Heap.wrap(%{}) + 6 -> Heap.wrap(%{}) + 7 -> Heap.wrap(%{"__proto__" => nil}) + _ -> :undefined + end + end + end + + def get_super(func) do + case InvokeContext.fast_ctx() do + {_atoms, _globals, _current_func, _arg_buf, _this, _new_target, ^func, super} -> + super + + _ -> + if InvokeContext.current_home_object(InvokeContext.current_func()) == func, + do: InvokeContext.current_super(), + else: Class.get_super(func) + end + end + + def get_array_el2(obj, idx), do: {Get.get(obj, idx), obj} + + def set_function_name(fun, name), do: Functions.rename(fun, name) + + def set_function_name_atom(fun, atom_idx), + do: Functions.set_name_atom(fun, atom_idx, InvokeContext.current_atoms()) + + def set_function_name_computed(fun, name_val), do: Functions.set_name_computed(fun, name_val) + + def put_field(obj, key, val) when is_binary(key) do + Put.put(obj, key, val) + :ok + end + + def put_field(obj, atom_idx, val), + do: put_field(obj, Names.resolve_atom(InvokeContext.current_atoms(), atom_idx), val) + + def define_field(obj, key, val) when is_binary(key) do + Put.put(obj, key, val) + obj + end + + def define_field(obj, atom_idx, val), + do: define_field(obj, Names.resolve_atom(InvokeContext.current_atoms(), atom_idx), val) + + def put_array_el(obj, idx, val) do + Put.put_element(obj, idx, val) + :ok + end + + def define_array_el(obj, idx, val), do: Put.define_array_el(obj, idx, val) + + def define_method(target, method, name, flags) when is_binary(name), + do: Methods.define_method(target, method, name, flags) + + def define_method(target, method, atom_idx, flags), + do: + Methods.define_method( + target, + method, + Names.resolve_atom(InvokeContext.current_atoms(), atom_idx), + flags + ) + + def define_method_computed(target, method, field_name, flags), + do: Methods.define_method_computed(target, method, field_name, flags) + + def set_home_object(method, target), do: Methods.set_home_object(method, target) + + def add_brand(target, brand), do: Private.add_brand(target, brand) + + def append_spread(arr, idx, obj), do: Copy.append_spread(arr, idx, obj) + + def copy_data_properties(target, source) do + Copy.copy_data_properties(target, source) + target + end + + def construct_runtime(ctor, new_target, args), + do: Invocation.construct_runtime(ctor, new_target, args) + + def instanceof({:obj, _} = obj, ctor) do + ctor_proto = Get.get(ctor, "prototype") + prototype_chain_contains?(obj, ctor_proto) + end + + def instanceof(_obj, _ctor), do: false + + def delete_property(obj, key), do: Delete.delete_property(obj, key) + + def undefined_or_null?(val), do: val == :undefined or val == nil + + def ensure_capture_cell(cell, val), do: Captures.ensure(cell, val) + def close_capture_cell(cell, val), do: Captures.close(cell, val) + def sync_capture_cell(cell, val), do: Captures.sync(cell, val) + + def set_proto({:obj, ref} = _obj, proto) do + map = Heap.get_obj(ref, %{}) + if is_map(map), do: Heap.put_obj(ref, Map.put(map, proto(), proto)) + :ok + end + + def set_proto(_obj, _proto), do: :ok + + def make_loc_ref(_idx) do + ref = make_ref() + Heap.put_cell(ref, :undefined) + {:cell, ref} + end + + def make_arg_ref(idx) do + ref = make_ref() + val = elem(InvokeContext.current_arg_buf(), idx) + Heap.put_cell(ref, val) + {:cell, ref} + end + + def get_ref_value({:cell, _} = cell), do: Closures.read_cell(cell) + def get_ref_value(_), do: :undefined + + def put_ref_value(val, {:cell, _} = cell) do + Closures.write_cell(cell, val) + val + end + + def put_ref_value(val, _), do: val + + def define_class(ctor, parent_ctor, atom_idx) do + ctor_closure = + case ctor do + %Bytecode.Function{} = fun -> {:closure, %{}, fun} + other -> other + end + + Class.define_class( + ctor_closure, + parent_ctor, + Names.resolve_atom(InvokeContext.current_atoms(), atom_idx) + ) + end + + def invoke_runtime(fun, args), do: Invocation.invoke_runtime(fun, args) + + def invoke_method_runtime(fun, this_obj, args), + do: Invocation.invoke_method_runtime(fun, this_obj, args) + + def apply_super(ctx, fun, new_target, args) do + Invocation.construct_runtime(ctx, fun, new_target, args) + end + + def apply_super(fun, new_target, args) do + Invocation.construct_runtime(fun, new_target, args) + end + + def update_this(ctx, this_val) do + Context.mark_dirty(%{ctx | this: this_val}) + end + + def update_this(this_val) do + ctx = current_context() + Context.mark_dirty(%{ctx | this: this_val}) + end + + def await(_ctx, val), do: Interpreter.resolve_awaited(val) + def await(val), do: Interpreter.resolve_awaited(val) + + def import_module(ctx, specifier) do + if is_binary(specifier) and Map.get(ctx, :runtime_pid) != nil do + Promise.resolved(Runtime.new_object()) + else + Promise.rejected( + Heap.make_error("Cannot import #{specifier}", "TypeError") + ) + end + end + + def import_module(specifier) do + Promise.rejected( + Heap.make_error("Cannot import #{specifier}", "TypeError") + ) + end + + def get_length(obj), do: Get.length_of(obj) + + def for_of_start(obj) do + case obj do + list when is_list(list) -> + {{:list_iter, list, 0}, :undefined} + + {:obj, ref} = obj_ref -> + case Heap.get_obj(ref) do + {:qb_arr, arr} -> + {{:list_iter, :array.to_list(arr), 0}, :undefined} + + list when is_list(list) -> + {{:list_iter, list, 0}, :undefined} + + map when is_map(map) -> + sym_iter = {:symbol, "Symbol.iterator"} + + cond do + Map.has_key?(map, sym_iter) -> + iter_fn = Map.get(map, sym_iter) + iter_obj = Runtime.call_callback(iter_fn, []) + {iter_obj, Get.get(iter_obj, "next")} + + Map.has_key?(map, "next") -> + {obj_ref, Get.get(obj_ref, "next")} + + true -> + {{:list_iter, [], 0}, :undefined} + end + + _ -> + {{:list_iter, [], 0}, :undefined} + end + + s when is_binary(s) -> + {{:list_iter, String.codepoints(s), 0}, :undefined} + + _ -> + {{:list_iter, [], 0}, :undefined} + end + end + + def for_in_start(obj), do: {:for_in_iterator, enumerable_keys(obj)} + + def for_in_next({:for_in_iterator, [key | rest_keys]}) do + {false, key, {:for_in_iterator, rest_keys}} + end + + def for_in_next({:for_in_iterator, []} = iter) do + {true, :undefined, iter} + end + + def for_in_next(iter), do: {true, :undefined, iter} + + def for_of_next(_next_fn, :undefined), do: {true, :undefined, :undefined} + + def for_of_next(_next_fn, {:list_iter, list, idx}) do + if idx < length(list) do + {false, Enum.at(list, idx), {:list_iter, list, idx + 1}} + else + {true, :undefined, :undefined} + end + end + + def for_of_next(next_fn, iter_obj) do + result = Runtime.call_callback(next_fn, []) + done = Get.get(result, "done") + value = Get.get(result, "value") + + if done == true do + {true, :undefined, :undefined} + else + {false, value, iter_obj} + end + end + + def iterator_close(:undefined), do: :ok + def iterator_close({:list_iter, _, _}), do: :ok + + def iterator_close(iter_obj) do + return_fn = Get.get(iter_obj, "return") + + if return_fn != :undefined and return_fn != nil do + Runtime.call_callback(return_fn, []) + end + + :ok + end + + defp enumerable_keys(obj), do: Copy.enumerable_keys(obj) + + defp prototype_chain_contains?(_, :undefined), do: false + defp prototype_chain_contains?(_, nil), do: false + + defp prototype_chain_contains?({:obj, ref}, target) do + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> + case Map.get(map, proto()) do + ^target -> true + nil -> false + :undefined -> false + parent -> prototype_chain_contains?(parent, target) + end + + _ -> + false + end + end + + defp prototype_chain_contains?(_, _), do: false + + defp current_var_ref(idx), do: current_var_ref(current_context(), idx) + + defp current_var_ref(ctx, idx) do + case context_current_func(ctx) do + {:closure, captured, %Bytecode.Function{} = fun} -> + case capture_keys_tuple(fun) do + keys when idx >= 0 and idx < tuple_size(keys) -> + Map.get(captured, elem(keys, idx), :undefined) + + _ -> + :undefined + end + + _ -> + :undefined + end + end + + defp capture_keys_tuple(%Bytecode.Function{closure_vars: vars} = fun) do + key = {:qb_capture_keys, fun.byte_code} + + case Process.get(key) do + nil -> + tuple = vars |> Enum.map(&closure_capture_key/1) |> List.to_tuple() + Process.put(key, tuple) + tuple + + cached -> + cached + end + end + + defp read_var_ref({:cell, _} = cell), do: Closures.read_cell(cell) + defp read_var_ref(other), do: other + + defp checked_var_ref(idx), do: checked_var_ref(current_context(), idx) + + defp checked_var_ref(ctx, idx) do + case current_var_ref(ctx, idx) do + :__tdz__ -> + throw({:js_throw, Heap.make_error(var_ref_error_message(ctx, idx), "ReferenceError")}) + + {:cell, _} = cell -> + val = Closures.read_cell(cell) + + if val == :__tdz__ and var_ref_name(ctx, idx) == "this" and + derived_this_uninitialized?(ctx) do + throw({:js_throw, Heap.make_error("this is not initialized", "ReferenceError")}) + end + + val + + val -> + val + end + end + + defp write_var_ref({:cell, _} = cell, val), do: Closures.write_cell(cell, val) + defp write_var_ref(_, _), do: :ok + + defp var_ref_error_message(ctx, idx) do + if var_ref_name(ctx, idx) == "this" and derived_this_uninitialized?(ctx) do + "this is not initialized" + else + "Cannot access variable before initialization" + end + end + + defp var_ref_name(ctx, idx) do + case context_current_func(ctx) do + {:closure, _, %Bytecode.Function{closure_vars: vars}} + when idx >= 0 and idx < length(vars) -> + vars + |> Enum.at(idx) + |> Map.get(:name) + |> Names.resolve_display_name(context_atoms(ctx)) + + _ -> + nil + end + end + + defp closure_capture_key(%{closure_type: type, var_idx: idx}), do: {type, idx} + + defp derived_this_uninitialized?(ctx) do + case context_this(ctx) do + this + when this == :uninitialized or + (is_tuple(this) and tuple_size(this) == 2 and elem(this, 0) == :uninitialized) -> + true + + _ -> + false + end + end + + defp fetch_ctx_var(ctx, name) do + case GlobalEnv.fetch(context_globals(ctx), name) do + {:found, val} -> + val + + :not_found -> + throw({:js_throw, Heap.make_error("#{name} is not defined", "ReferenceError")}) + end + end + + defp current_context do + case Heap.get_ctx() do + %Context{} = ctx -> ctx + map when is_map(map) -> context_struct(map) + _ -> %Context{atoms: Heap.get_atoms(), globals: GlobalEnv.base_globals()} + end + end + + defp context_struct(%Context{} = ctx), do: ctx + + defp context_struct(map) when is_map(map) do + struct(Context, Map.merge(Map.from_struct(%Context{}), map)) + end + + defp context_atoms(%{atoms: atoms}), do: atoms + defp context_atoms(_), do: {} + defp context_globals(%{globals: globals}), do: globals + defp context_globals(_), do: GlobalEnv.base_globals() + defp context_current_func(%{current_func: current_func}), do: current_func + defp context_current_func(_), do: :undefined + defp context_arg_buf(%{arg_buf: arg_buf}), do: arg_buf + defp context_arg_buf(_), do: {} + defp context_this(%{this: this}), do: this + defp context_this(_), do: :undefined + defp context_new_target(%{new_target: new_target}), do: new_target + defp context_new_target(_), do: :undefined + defp context_gas(%{gas: gas}), do: gas + defp context_gas(_), do: Context.default_gas() + + defp ensure_context(%Context{} = ctx), do: ctx + defp ensure_context(map) when is_map(map), do: context_struct(map) + + defp ensure_context(_), + do: %Context{atoms: Heap.get_atoms(), globals: GlobalEnv.base_globals()} + + defp context_home_object(ctx, current_func) do + case Map.get(ctx, :home_object, :undefined) do + :undefined -> Functions.current_home_object(current_func) + home_object -> home_object + end + end + + defp context_super(ctx) do + case Map.get(ctx, :super, :undefined) do + :undefined -> Class.get_super(context_home_object(ctx, context_current_func(ctx))) + super -> super + end + end +end diff --git a/lib/quickbeam/vm/decoder.ex b/lib/quickbeam/vm/decoder.ex new file mode 100644 index 00000000..fcbdec10 --- /dev/null +++ b/lib/quickbeam/vm/decoder.ex @@ -0,0 +1,278 @@ +defmodule QuickBEAM.VM.Decoder do + @compile {:inline, + get_u8: 2, + get_i8: 2, + get_u16: 2, + get_i16: 2, + get_u32: 2, + get_i32: 2, + get_atom_u32: 2, + resolve_label: 2, + short_form_operands: 2} + @moduledoc """ + Decodes raw QuickJS bytecode bytes into instruction tuples. + + Returns a tuple of {opcode_integer, args} indexed by instruction position + (NOT byte offset). Labels are resolved to instruction indices via a + byte-offset-to-index map. Opcodes are raw integer tags for O(1) BEAM JIT + jump-table dispatch. + """ + + alias QuickBEAM.VM.Opcodes + import Bitwise + + @type instruction :: {non_neg_integer(), [term()]} + + @spec decode(binary()) :: {:ok, [instruction()]} | {:error, term()} + def decode(byte_code, arg_count \\ 0) when is_binary(byte_code) do + case build_offset_map(byte_code) do + {:ok, offset_map} -> + decode_pass2(byte_code, byte_size(byte_code), 0, 0, offset_map, [], arg_count) + + {:error, _} = err -> + err + end + end + + defp build_offset_map(bc) do + build_offset_map(bc, byte_size(bc), 0, 0, %{}) + end + + defp build_offset_map(_bc, len, pos, _idx, acc) when pos >= len do + {:ok, acc} + end + + defp build_offset_map(bc, len, pos, idx, acc) do + op = :binary.at(bc, pos) + + case Opcodes.info(op) do + nil -> + {:error, {:unknown_opcode, op, pos}} + + {_name, size, _n_pop, _n_push, _fmt} -> + if pos + size > len do + {:error, {:truncated_instruction, op, pos}} + else + build_offset_map(bc, len, pos + size, idx + 1, Map.put(acc, pos, idx)) + end + end + end + + defp decode_pass2(_bc, len, pos, _idx, _offset_map, acc, _ac) when pos >= len do + {:ok, Enum.reverse(acc)} + end + + defp decode_pass2(bc, len, pos, idx, offset_map, acc, ac) do + op = :binary.at(bc, pos) + + case Opcodes.info(op) do + nil -> + {:error, {:unknown_opcode, op, pos}} + + {_name, size, _n_pop, _n_push, fmt} -> + if pos + size > len do + {:error, {:truncated_instruction, op, pos}} + else + operands = + case fmt do + :none_loc -> short_form_operands(op, ac) + :none_arg -> short_form_operands(op, ac) + :none_var_ref -> short_form_operands(op, ac) + :none_int -> short_form_operands(op, ac) + :npopx -> short_form_operands(op, ac) + _ -> decode_operands(bc, pos + 1, fmt, offset_map, ac) + end + + decode_pass2( + bc, + len, + pos + size, + idx + 1, + offset_map, + [ + {op, operands} | acc + ], + ac + ) + end + end + end + + # Short-form opcodes with implicit operands + # loc variants add arg_count offset; arg/var_ref/call/push don't + + # get_loc0..3 (197-200) + defp short_form_operands(197, ac), do: [0 + ac] + defp short_form_operands(198, ac), do: [1 + ac] + defp short_form_operands(199, ac), do: [2 + ac] + defp short_form_operands(200, ac), do: [3 + ac] + # put_loc0..3 (201-204) + defp short_form_operands(201, ac), do: [0 + ac] + defp short_form_operands(202, ac), do: [1 + ac] + defp short_form_operands(203, ac), do: [2 + ac] + defp short_form_operands(204, ac), do: [3 + ac] + # set_loc0..3 (205-208) + defp short_form_operands(205, ac), do: [0 + ac] + defp short_form_operands(206, ac), do: [1 + ac] + defp short_form_operands(207, ac), do: [2 + ac] + defp short_form_operands(208, ac), do: [3 + ac] + # get_loc0_loc1 (196) + defp short_form_operands(196, ac), do: [0 + ac, 1 + ac] + # get_arg0..3 (209-212) + defp short_form_operands(209, _ac), do: [0] + defp short_form_operands(210, _ac), do: [1] + defp short_form_operands(211, _ac), do: [2] + defp short_form_operands(212, _ac), do: [3] + # put_arg0..3 (213-216) + defp short_form_operands(213, _ac), do: [0] + defp short_form_operands(214, _ac), do: [1] + defp short_form_operands(215, _ac), do: [2] + defp short_form_operands(216, _ac), do: [3] + # set_arg0..3 (217-220) + defp short_form_operands(217, _ac), do: [0] + defp short_form_operands(218, _ac), do: [1] + defp short_form_operands(219, _ac), do: [2] + defp short_form_operands(220, _ac), do: [3] + # get_var_ref0..3 (221-224) + defp short_form_operands(221, _ac), do: [0] + defp short_form_operands(222, _ac), do: [1] + defp short_form_operands(223, _ac), do: [2] + defp short_form_operands(224, _ac), do: [3] + # put_var_ref0..3 (225-228) + defp short_form_operands(225, _ac), do: [0] + defp short_form_operands(226, _ac), do: [1] + defp short_form_operands(227, _ac), do: [2] + defp short_form_operands(228, _ac), do: [3] + # set_var_ref0..3 (229-232) + defp short_form_operands(229, _ac), do: [0] + defp short_form_operands(230, _ac), do: [1] + defp short_form_operands(231, _ac), do: [2] + defp short_form_operands(232, _ac), do: [3] + # call0..3 (238-241) + defp short_form_operands(238, _ac), do: [0] + defp short_form_operands(239, _ac), do: [1] + defp short_form_operands(240, _ac), do: [2] + defp short_form_operands(241, _ac), do: [3] + # push_minus1 (179), push_0..7 (180-187) + defp short_form_operands(179, _ac), do: [-1] + defp short_form_operands(180, _ac), do: [0] + defp short_form_operands(181, _ac), do: [1] + defp short_form_operands(182, _ac), do: [2] + defp short_form_operands(183, _ac), do: [3] + defp short_form_operands(184, _ac), do: [4] + defp short_form_operands(185, _ac), do: [5] + defp short_form_operands(186, _ac), do: [6] + defp short_form_operands(187, _ac), do: [7] + # push_empty_string (192) — no operands + defp short_form_operands(192, _ac), do: [] + # Fallback + defp short_form_operands(_op, _ac), do: [] + + # ── Operand decoding ── + + defp decode_operands(bc, pos, :u8, _om, _ac), do: [get_u8(bc, pos)] + defp decode_operands(bc, pos, :i8, _om, _ac), do: [get_i8(bc, pos)] + defp decode_operands(bc, pos, :u16, _om, _ac), do: [get_u16(bc, pos)] + defp decode_operands(bc, pos, :i16, _om, _ac), do: [get_i16(bc, pos)] + defp decode_operands(bc, pos, :i32, _om, _ac), do: [get_i32(bc, pos)] + defp decode_operands(bc, pos, :u32, _om, _ac), do: [get_u32(bc, pos)] + + defp decode_operands(bc, pos, :u32x2, _om, _ac) do + [get_u32(bc, pos), get_u32(bc, pos + 4)] + end + + defp decode_operands(_bc, _pos, :none, _om, _ac), do: [] + defp decode_operands(bc, pos, :npop, _om, _ac), do: [get_u16(bc, pos)] + + defp decode_operands(bc, pos, :npop_u16, _om, _ac) do + [get_u16(bc, pos), get_u16(bc, pos + 2)] + end + + defp decode_operands(bc, pos, :loc8, _om, ac), do: [get_u8(bc, pos) + ac] + defp decode_operands(bc, pos, :const8, _om, _ac), do: [get_u8(bc, pos)] + defp decode_operands(bc, pos, :loc, _om, ac), do: [get_u16(bc, pos) + ac] + defp decode_operands(bc, pos, :arg, _om, _ac), do: [get_u16(bc, pos)] + defp decode_operands(bc, pos, :var_ref, _om, _ac), do: [get_u16(bc, pos)] + defp decode_operands(bc, pos, :const, _om, _ac), do: [get_u32(bc, pos)] + + defp decode_operands(bc, pos, :label8, om, _ac) do + target_byte = pos + get_i8(bc, pos) + [resolve_label(target_byte, om)] + end + + defp decode_operands(bc, pos, :label16, om, _ac) do + target_byte = pos + get_i16(bc, pos) + [resolve_label(target_byte, om)] + end + + defp decode_operands(bc, pos, :label, om, _ac) do + byte_off = pos + get_i32(bc, pos) + [resolve_label(byte_off, om)] + end + + defp decode_operands(bc, pos, :label_u16, om, _ac) do + byte_off = pos + get_i32(bc, pos) + [resolve_label(byte_off, om), get_u16(bc, pos + 4)] + end + + defp decode_operands(bc, pos, :atom, _om, _ac) do + [get_atom_u32(bc, pos)] + end + + defp decode_operands(bc, pos, :atom_u8, _om, _ac) do + [get_atom_u32(bc, pos), get_u8(bc, pos + 4)] + end + + defp decode_operands(bc, pos, :atom_u16, _om, _ac) do + [get_atom_u32(bc, pos), get_u16(bc, pos + 4)] + end + + defp decode_operands(bc, pos, :atom_label_u8, om, _ac) do + byte_off = pos + 4 + get_i32(bc, pos + 4) + [get_atom_u32(bc, pos), resolve_label(byte_off, om), get_u8(bc, pos + 8)] + end + + defp decode_operands(bc, pos, :atom_label_u16, om, _ac) do + byte_off = pos + 4 + get_i32(bc, pos + 4) + [get_atom_u32(bc, pos), resolve_label(byte_off, om), get_u16(bc, pos + 8)] + end + + defp resolve_label(byte_off, offset_map) do + Map.get(offset_map, byte_off, byte_off) + end + + # ── Byte accessors (little-endian) ── + + defp get_u8(bc, pos), do: :binary.at(bc, pos) + + defp get_i8(bc, pos) do + v = :binary.at(bc, pos) + if v >= 128, do: v - 256, else: v + end + + defp get_u16(bc, pos), do: :binary.decode_unsigned(:binary.part(bc, pos, 2), :little) + + defp get_i16(bc, pos) do + v = get_u16(bc, pos) + if v >= 0x8000, do: v - 0x10000, else: v + end + + defp get_u32(bc, pos), do: :binary.decode_unsigned(:binary.part(bc, pos, 4), :little) + + defp get_i32(bc, pos) do + v = get_u32(bc, pos) + if v >= 0x80000000, do: v - 0x100000000, else: v + end + + @js_atom_end Opcodes.js_atom_end() + defp get_atom_u32(bc, pos) do + v = get_u32(bc, pos) + + cond do + band(v, 0x80000000) != 0 -> {:tagged_int, band(v, 0x7FFFFFFF)} + v >= 1 and v < @js_atom_end -> {:predefined, v} + v >= @js_atom_end -> v - @js_atom_end + true -> {:predefined, v} + end + end +end diff --git a/lib/quickbeam/vm/environment/captures.ex b/lib/quickbeam/vm/environment/captures.ex new file mode 100644 index 00000000..1701d216 --- /dev/null +++ b/lib/quickbeam/vm/environment/captures.ex @@ -0,0 +1,34 @@ +defmodule QuickBEAM.VM.Environment.Captures do + @moduledoc false + + alias QuickBEAM.VM.Heap + + def ensure({:cell, _} = cell, _val), do: cell + + def ensure(_cell, val) do + ref = make_ref() + Heap.put_cell(ref, val) + {:cell, ref} + end + + def close({:cell, ref}, val) do + current = Heap.get_cell(ref) + next_val = if current == :undefined, do: val, else: current + new_ref = make_ref() + Heap.put_cell(new_ref, next_val) + {:cell, new_ref} + end + + def close(_cell, val) do + ref = make_ref() + Heap.put_cell(ref, val) + {:cell, ref} + end + + def sync({:cell, ref}, val) do + Heap.put_cell(ref, val) + :ok + end + + def sync(_, _), do: :ok +end diff --git a/lib/quickbeam/vm/execution/trace.ex b/lib/quickbeam/vm/execution/trace.ex new file mode 100644 index 00000000..0180df39 --- /dev/null +++ b/lib/quickbeam/vm/execution/trace.ex @@ -0,0 +1,23 @@ +defmodule QuickBEAM.VM.Execution.Trace do + @moduledoc false + + @key :qb_active_frames + + def push(fun) do + Process.put(@key, [%{fun: fun, pc: 0} | Process.get(@key, [])]) + end + + def pop do + case Process.get(@key, []) do + [_ | rest] -> Process.put(@key, rest) + [] -> :ok + end + end + + def update_pc(pc) do + case Process.get(@key, []) do + [frame | rest] -> Process.put(@key, [%{frame | pc: pc} | rest]) + [] -> :ok + end + end +end diff --git a/lib/quickbeam/vm/global_env.ex b/lib/quickbeam/vm/global_env.ex new file mode 100644 index 00000000..0101e815 --- /dev/null +++ b/lib/quickbeam/vm/global_env.ex @@ -0,0 +1,90 @@ +defmodule QuickBEAM.VM.GlobalEnv do + @moduledoc false + + alias QuickBEAM.VM.{Heap, Names, Runtime} + alias QuickBEAM.VM.Interpreter.Context + + def current do + case Heap.get_ctx() do + %Context{globals: globals} when globals != %{} -> globals + %Context{} -> base_globals() + _ -> base_globals() + end + end + + def base_globals do + case Heap.get_base_globals() do + nil -> + builtins = Runtime.global_bindings() + persistent = Heap.get_persistent_globals() || %{} + globals = Map.merge(builtins, Map.drop(persistent, Map.keys(builtins))) + Heap.put_base_globals(globals) + globals + + globals -> + globals + end + end + + def fetch(%Context{} = ctx, atom_idx), do: fetch(ctx.globals, atom_idx, ctx.atoms) + + def fetch(globals, atom_idx) when is_map(globals), + do: fetch(globals, atom_idx, Heap.get_atoms()) + + def fetch(atom_idx), do: fetch(current(), atom_idx, Heap.get_atoms()) + + def get(%Context{} = ctx, atom_idx, default), + do: get(ctx.globals, atom_idx, default, ctx.atoms) + + def get(globals, atom_idx, default) when is_map(globals), + do: get(globals, atom_idx, default, Heap.get_atoms()) + + def get(atom_idx, default), do: get(current(), atom_idx, default, Heap.get_atoms()) + + def put(%Context{} = ctx, atom_idx, val, opts \\ []) do + name = Names.resolve_atom(ctx, atom_idx) + globals = Map.put(ctx.globals, name, val) + + if Keyword.get(opts, :persist, true) do + Heap.put_persistent_globals(globals) + Heap.put_base_globals(globals) + end + + %{ctx | globals: globals} |> Context.mark_dirty() + end + + def define_var(%Context{} = ctx, atom_idx) do + Heap.put_var(Names.resolve_atom(ctx, atom_idx), :undefined) + Context.mark_dirty(ctx) + end + + def check_define_var(%Context{} = ctx, atom_idx) do + Heap.delete_var(Names.resolve_atom(ctx, atom_idx)) + Context.mark_dirty(ctx) + end + + def refresh(%Context{} = ctx) do + globals = Map.merge(ctx.globals, Heap.get_persistent_globals() || %{}) + Heap.put_base_globals(globals) + %{ctx | globals: globals} |> Context.mark_dirty() + end + + def current_name(atom_idx), do: Names.resolve_atom(Heap.get_atoms(), atom_idx) + + defp fetch(globals, atom_idx, atoms) do + name = resolve_name(atom_idx, atoms) + + case Map.fetch(globals, name) do + {:ok, val} -> {:found, val} + :error -> :not_found + end + end + + defp get(globals, atom_idx, default, atoms) do + name = resolve_name(atom_idx, atoms) + Map.get(globals, name, default) + end + + defp resolve_name(name, _atoms) when is_binary(name), do: name + defp resolve_name(name, atoms), do: Names.resolve_atom(atoms, name) +end diff --git a/lib/quickbeam/vm/heap.ex b/lib/quickbeam/vm/heap.ex new file mode 100644 index 00000000..c0971ad9 --- /dev/null +++ b/lib/quickbeam/vm/heap.ex @@ -0,0 +1,439 @@ +defmodule QuickBEAM.VM.Heap do + @moduledoc """ + Mutable heap storage for JS runtime values. + + All heap access goes through this module — callers never touch + the process dictionary directly. Current implementation uses the + process dictionary for single-process performance; the backing + store can be swapped to ETS for concurrent access. + + ## Storage keys + + - `integer_id` — JS object/array properties (raw integer keys) + - `{:qb_cell, ref}` — closure variable cells + - `{:qb_class_proto, ctor}` — class prototype objects + - `{:qb_parent_ctor, ctor}` — parent constructor references + - `{:qb_var, name}` — global variable bindings + """ + + alias QuickBEAM.VM.Heap.{Async, Caches, Context, Registry, Shapes, Store} + + @compile {:inline, + get_obj: 1, + get_obj: 2, + get_obj_raw: 1, + put_obj: 2, + put_obj_raw: 2, + update_obj: 3, + get_cell: 1, + put_cell: 2, + get_var: 1, + put_var: 2, + delete_var: 1, + get_ctx: 0, + put_ctx: 1, + frozen?: 1, + freeze: 1, + get_decoded: 1, + put_decoded: 2, + get_compiled: 1, + put_compiled: 2, + get_class_proto: 1, + put_class_proto: 2, + get_parent_ctor: 1, + put_parent_ctor: 2, + get_ctor_statics: 1, + wrap: 1, + to_list: 1, + obj_is_array?: 1, + obj_to_list: 1, + array_get: 2, + array_size: 1, + array_push: 2, + array_set: 3, + make_error: 2, + get_object_prototype: 0, + get_atoms: 0, + get_persistent_globals: 0} + + # ── Convenience constructors ── + + def wrap(data) when is_map(data) do + if is_map_key(data, "__proto__") do + {proto, rest} = Map.pop!(data, "__proto__") + wrap_map(rest, proto) + else + wrap_map(data, nil) + end + end + + def wrap(data) do + id = Store.next_id() + put_obj(id, data) + {:obj, id} + end + + defp wrap_map(map, proto) do + case Shapes.from_map(map) do + {:ok, shape_id, offsets, vals} -> + wrap_shaped(shape_id, offsets, vals, proto) + + :ineligible -> + id = Store.next_id() + data = if proto, do: Map.put(map, "__proto__", proto), else: map + put_obj(id, data) + {:obj, id} + end + end + + @doc "Create a shape-backed object with pre-sorted keys. Keys tuple is a compile-time constant." + def wrap_keyed(keys, vals) when is_tuple(keys) and is_tuple(vals) do + cache_key = {:qb_wrap_cache, keys} + + case Process.get(cache_key) do + {shape_id, offsets} -> + id = Store.next_id() + Store.put_obj_raw(id, {:shape, shape_id, offsets, vals, nil}) + {:obj, id} + + nil -> + map = :maps.from_list(:lists.zip(Tuple.to_list(keys), Tuple.to_list(vals))) + + case Shapes.from_map(map) do + {:ok, shape_id, offsets, _} -> + Process.put(cache_key, {shape_id, offsets}) + id = Store.next_id() + Store.put_obj_raw(id, {:shape, shape_id, offsets, vals, nil}) + {:obj, id} + + :ineligible -> + wrap(map) + end + end + end + + @doc "Fast allocation with a pre-resolved shape. Skips eligibility check and key sorting." + def wrap_shaped(shape_id, offsets, vals, proto) do + id = Store.next_id() + Store.put_obj_raw(id, {:shape, shape_id, offsets, vals, proto}) + {:obj, id} + end + + def to_list({:obj, ref}) do + case Process.get(ref, []) do + {:qb_arr, arr} -> + :array.to_list(arr) + + list when is_list(list) -> + list + + {:shape, _shape_id, _offsets, _vals, _proto} -> + [] + + map when is_map(map) -> + len = Map.get(map, "length", 0) + + if is_integer(len) and len > 0, + do: for(i <- 0..(len - 1), do: Map.get(map, Integer.to_string(i), :undefined)), + else: [] + + _ -> + [] + end + end + + def to_list({:qb_arr, arr}), do: :array.to_list(arr) + def to_list(list) when is_list(list), do: list + def to_list(_), do: [] + + def make_error(message, name) do + proto = + case find_error_proto(name) do + nil -> nil + ctor -> get_class_proto(ctor) + end + + base = %{"message" => message, "name" => name, "stack" => ""} + error = if proto, do: wrap(Map.put(base, "__proto__", proto)), else: wrap(base) + + if get_ctx() != nil, + do: QuickBEAM.VM.Stacktrace.attach_stack(error), + else: error + end + + defp find_error_proto(name) do + case get_global_cache() do + nil -> + case get_ctx() do + %{globals: globals} -> Map.get(globals, name) + _ -> nil + end + + cache -> + Map.get(cache, name) + end + end + + def get_or_create_prototype(ctor) do + case get_class_proto(ctor) do + nil -> + key = {:qb_func_proto, ctor} + + case Process.get(key) do + nil -> + proto = wrap(%{"constructor" => ctor}) + Process.put(key, proto) + proto + + existing -> + existing + end + + proto -> + proto + end + end + + # ── Objects ── + + defdelegate get_obj(ref), to: Store + defdelegate get_obj(ref, default), to: Store + defdelegate get_obj_raw(ref), to: Store + defdelegate put_obj(ref, value), to: Store + defdelegate put_obj_raw(ref, value), to: Store + defdelegate put_obj_key(ref, key, value), to: Store + defdelegate put_obj_key(ref, map, key, value), to: Store + defdelegate update_obj(ref, default, fun), to: Store + + # ── Array helpers ── + + defdelegate obj_is_array?(ref), to: Store + defdelegate obj_to_list(ref), to: Store + defdelegate array_get(ref, idx), to: Store + defdelegate array_size(ref), to: Store + defdelegate array_push(ref, values), to: Store + defdelegate array_set(ref, idx, value), to: Store + + # ── Closure cells ── + + defdelegate get_cell(ref), to: Store + defdelegate put_cell(ref, value), to: Store + + # ── Class metadata ── + + defdelegate get_class_proto(ctor), to: Store + defdelegate put_class_proto(ctor, proto), to: Store + defdelegate get_parent_ctor(ctor), to: Store + defdelegate put_parent_ctor(ctor, parent), to: Store + defdelegate delete_parent_ctor(ctor), to: Store + defdelegate get_ctor_statics(ctor), to: Store + defdelegate put_ctor_statics(ctor, statics), to: Store + defdelegate put_ctor_static(ctor, key, value), to: Store + defdelegate get_var(name), to: Store + defdelegate put_var(name, value), to: Store + defdelegate delete_var(name), to: Store + + # ── Interpreter context ── + + defdelegate get_ctx(), to: Context + defdelegate put_ctx(ctx), to: Context + defdelegate get_decoded(byte_code), to: Caches + defdelegate put_decoded(byte_code, instructions), to: Caches + defdelegate get_compiled(key), to: Caches + defdelegate put_compiled(key, compiled), to: Caches + defdelegate frozen?(ref), to: Store + defdelegate freeze(ref), to: Store + defdelegate get_prop_desc(ref, key), to: Store + defdelegate put_prop_desc(ref, key, desc), to: Store + defdelegate get_object_prototype(), to: Context + defdelegate put_object_prototype(proto), to: Context + defdelegate get_global_cache(), to: Context + defdelegate put_global_cache(bindings), to: Context + defdelegate get_base_globals(), to: Context + defdelegate put_base_globals(globals), to: Context + defdelegate get_atoms(), to: Context + defdelegate put_atoms(atoms), to: Context + defdelegate get_persistent_globals(), to: Context + defdelegate put_persistent_globals(globals), to: Context + defdelegate get_handler_globals(), to: Context + defdelegate put_handler_globals(globals), to: Context + defdelegate get_runtime_mode(runtime), to: Context + defdelegate put_runtime_mode(runtime, mode), to: Context + defdelegate enqueue_microtask(task), to: Async + defdelegate dequeue_microtask(), to: Async + defdelegate get_promise_waiters(ref), to: Async + defdelegate put_promise_waiters(ref, waiters), to: Async + defdelegate delete_promise_waiters(ref), to: Async + defdelegate register_module(name, exports), to: Registry + defdelegate get_module(name), to: Registry + defdelegate all_module_exports(), to: Registry + defdelegate get_symbol(key), to: Registry + defdelegate put_symbol(key, sym), to: Registry + + # ── Garbage collection ── + + @gc_initial_threshold 5_000 + def gc_initial_threshold, do: @gc_initial_threshold + + def gc_needed?, do: Process.get(:qb_gc_needed, false) + + def mark_and_sweep(roots) do + marked = mark(roots, MapSet.new()) + sweep_heap(marked) + live_count = MapSet.size(marked) + Process.put(:qb_alloc_count, live_count) + Process.put(:qb_gc_threshold, live_count + max(live_count, @gc_initial_threshold)) + Process.delete(:qb_gc_needed) + end + + @doc "Clear all heap state. Used in test setup." + def reset do + for key <- Process.get_keys() do + case key do + id when is_integer(id) and id > 0 -> Process.delete(key) + {:qb_cell, _} -> Process.delete(key) + {:qb_class_proto, _} -> Process.delete(key) + {:qb_func_proto, _} -> Process.delete(key) + {:qb_decoded, _} -> Process.delete(key) + {:qb_compiled, _} -> Process.delete(key) + {:qb_promise_waiters, _} -> Process.delete(key) + {:qb_module, _} -> Process.delete(key) + {:qb_prop_desc, _, _} -> Process.delete(key) + {:qb_frozen, _} -> Process.delete(key) + {:qb_var, _} -> Process.delete(key) + {:qb_key_order, _} -> Process.delete(key) + {:qb_runtime_mode, _} -> Process.delete(key) + {:qb_alloc_count, _} -> Process.delete(key) + {:qb_gc_threshold, _} -> Process.delete(key) + {:qb_symbol_registry, _} -> Process.delete(key) + {:qb_ctor_statics, _} -> Process.delete(key) + {:qb_parent_ctor, _} -> Process.delete(key) + :qb_persistent_globals -> Process.delete(key) + :qb_handler_globals -> Process.delete(key) + :qb_atoms -> Process.delete(key) + :qb_module_list -> Process.delete(key) + :qb_ctx -> Process.delete(key) + :qb_gc_needed -> Process.delete(key) + :qb_alloc_count -> Process.delete(key) + :qb_next_id -> Process.delete(key) + :qb_object_prototype -> Process.delete(key) + :qb_global_bindings_cache -> Process.delete(key) + :qb_base_globals_cache -> Process.delete(key) + :qb_microtask_queue -> Process.delete(key) + :qb_shape_table -> Process.delete(key) + :qb_shape_empty -> Process.delete(key) + :qb_shape_next_id -> Process.delete(key) + _ -> :ok + end + end + + :ok + end + + @doc "Full GC between independent eval() invocations." + def gc(extra_roots \\ []) do + module_roots = all_module_exports() + persistent_roots = get_persistent_globals() |> Map.values() + all_roots = List.wrap(extra_roots) ++ module_roots ++ persistent_roots + + marked = if all_roots == [], do: nil, else: mark(all_roots, MapSet.new()) + sweep_all(marked) + end + + # ── Mark phase ── + + defp mark([], visited), do: visited + + defp mark([{:obj, ref} | rest], visited) do + mark_ref(ref, rest, visited, fn + {:shape, _shape_id, _offsets, vals, proto} -> + Tuple.to_list(vals) ++ [proto] + + map when is_map(map) -> + Map.values(map) ++ Map.keys(map) + + {:qb_arr, arr} -> + :array.to_list(arr) + + list when is_list(list) -> + list + + _ -> + [] + end) + end + + defp mark([{:cell, ref} | rest], visited) do + mark_ref({:qb_cell, ref}, rest, visited, fn val -> [val] end) + end + + defp mark( + [{:closure, captured, %QuickBEAM.VM.Bytecode.Function{} = fun} = closure | rest], + visited + ) do + related = [get_class_proto(closure), get_class_proto(fun), get_parent_ctor(fun)] + statics = Map.values(get_ctor_statics(closure)) ++ Map.values(get_ctor_statics(fun)) + mark(Map.values(captured) ++ related ++ statics ++ rest, visited) + end + + defp mark([{:builtin, _, _} = builtin | rest], visited) do + related = [get_class_proto(builtin), get_parent_ctor(builtin)] + statics = Map.values(get_ctor_statics(builtin)) + mark(related ++ statics ++ rest, visited) + end + + defp mark([%QuickBEAM.VM.Bytecode.Function{} = fun | rest], visited) do + related = [get_class_proto(fun), get_parent_ctor(fun)] + statics = Map.values(get_ctor_statics(fun)) + mark(Map.values(Map.from_struct(fun)) ++ related ++ statics ++ rest, visited) + end + + defp mark([tuple | rest], visited) when is_tuple(tuple), + do: mark(Tuple.to_list(tuple) ++ rest, visited) + + defp mark([list | rest], visited) when is_list(list), + do: mark(list ++ rest, visited) + + defp mark([%{} = map | rest], visited), + do: mark(Map.values(map) ++ rest, visited) + + defp mark([_ | rest], visited), do: mark(rest, visited) + + defp mark_ref(key, rest, visited, children_fn) do + if MapSet.member?(visited, key) do + mark(rest, visited) + else + visited = MapSet.put(visited, key) + children = children_fn.(Process.get(key, :undefined)) + mark(children ++ rest, visited) + end + end + + # ── Sweep phase ── + + defp sweep_heap(marked) do + for key <- Process.get_keys(), heap_key?(key), not MapSet.member?(marked, key) do + Process.delete(key) + end + end + + defp sweep_all(marked) do + for key <- Process.get_keys() do + cond do + heap_key?(key) -> unless marked && MapSet.member?(marked, key), do: Process.delete(key) + ephemeral_key?(key) -> Process.delete(key) + true -> :ok + end + end + end + + defp heap_key?(key) when is_integer(key) and key > 0, do: true + defp heap_key?({:qb_cell, _}), do: true + defp heap_key?(_), do: false + + defp ephemeral_key?({:qb_prop_desc, _, _}), do: true + defp ephemeral_key?({:qb_frozen, _}), do: true + defp ephemeral_key?({:qb_var, _}), do: true + defp ephemeral_key?({:qb_key_order, _}), do: true + defp ephemeral_key?(_), do: false +end diff --git a/lib/quickbeam/vm/heap/async.ex b/lib/quickbeam/vm/heap/async.ex new file mode 100644 index 00000000..a9b2612c --- /dev/null +++ b/lib/quickbeam/vm/heap/async.ex @@ -0,0 +1,25 @@ +defmodule QuickBEAM.VM.Heap.Async do + @moduledoc false + + def enqueue_microtask(task) do + queue = Process.get(:qb_microtask_queue, :queue.new()) + Process.put(:qb_microtask_queue, :queue.in(task, queue)) + end + + def dequeue_microtask do + queue = Process.get(:qb_microtask_queue, :queue.new()) + + case :queue.out(queue) do + {{:value, task}, rest} -> + Process.put(:qb_microtask_queue, rest) + task + + {:empty, _} -> + nil + end + end + + def get_promise_waiters(ref), do: Process.get({:qb_promise_waiters, ref}, []) + def put_promise_waiters(ref, waiters), do: Process.put({:qb_promise_waiters, ref}, waiters) + def delete_promise_waiters(ref), do: Process.delete({:qb_promise_waiters, ref}) +end diff --git a/lib/quickbeam/vm/heap/caches.ex b/lib/quickbeam/vm/heap/caches.ex new file mode 100644 index 00000000..4bfcf327 --- /dev/null +++ b/lib/quickbeam/vm/heap/caches.ex @@ -0,0 +1,11 @@ +defmodule QuickBEAM.VM.Heap.Caches do + @moduledoc false + + def get_decoded(byte_code), do: Process.get({:qb_decoded, byte_code}) + + def put_decoded(byte_code, instructions), + do: Process.put({:qb_decoded, byte_code}, instructions) + + def get_compiled(key), do: Process.get({:qb_compiled, key}) + def put_compiled(key, compiled), do: Process.put({:qb_compiled, key}, compiled) +end diff --git a/lib/quickbeam/vm/heap/context.ex b/lib/quickbeam/vm/heap/context.ex new file mode 100644 index 00000000..bb928177 --- /dev/null +++ b/lib/quickbeam/vm/heap/context.ex @@ -0,0 +1,65 @@ +defmodule QuickBEAM.VM.Heap.Context do + @moduledoc false + + alias QuickBEAM.VM.Interpreter.Context + + def get_ctx do + case Process.get(:qb_ctx, :__qb_missing__) do + :__qb_missing__ -> + case Process.get(:qb_fast_ctx, :__qb_missing__) do + {atoms, globals, current_func, arg_buf, this, new_target, home_object, super} -> + %Context{ + atoms: atoms, + globals: globals, + current_func: current_func, + arg_buf: arg_buf, + this: this, + new_target: new_target, + home_object: home_object, + super: super + } + + _ -> + nil + end + + ctx -> + ctx + end + end + + def put_ctx(nil), do: Process.delete(:qb_ctx) + def put_ctx(ctx), do: Process.put(:qb_ctx, ctx) + + def get_object_prototype, do: Process.get(:qb_object_prototype) + def put_object_prototype(proto), do: Process.put(:qb_object_prototype, proto) + + def get_global_cache, do: Process.get(:qb_global_bindings_cache) + + def put_global_cache(bindings) do + Process.delete(:qb_base_globals_cache) + Process.put(:qb_global_bindings_cache, bindings) + end + + def get_base_globals, do: Process.get(:qb_base_globals_cache) + def put_base_globals(globals), do: Process.put(:qb_base_globals_cache, globals) + + def get_atoms, do: Process.get(:qb_atoms, {}) + def put_atoms(atoms), do: Process.put(:qb_atoms, atoms) + + def get_persistent_globals, do: Process.get(:qb_persistent_globals, %{}) + + def put_persistent_globals(globals) do + Process.put(:qb_persistent_globals, globals) + end + + def get_handler_globals, do: Process.get(:qb_handler_globals) + + def put_handler_globals(globals) do + Process.delete(:qb_base_globals_cache) + Process.put(:qb_handler_globals, globals) + end + + def get_runtime_mode(runtime), do: Process.get({:qb_runtime_mode, runtime}) + def put_runtime_mode(runtime, mode), do: Process.put({:qb_runtime_mode, runtime}, mode) +end diff --git a/lib/quickbeam/vm/heap/keys.ex b/lib/quickbeam/vm/heap/keys.ex new file mode 100644 index 00000000..ae5bb82a --- /dev/null +++ b/lib/quickbeam/vm/heap/keys.ex @@ -0,0 +1,38 @@ +defmodule QuickBEAM.VM.Heap.Keys do + @moduledoc false + + @proto "__proto__" + @promise_state "__promise_state__" + @promise_value "__promise_value__" + @map_data "__map_data__" + @set_data "__set_data__" + @typed_array "__typed_array__" + @date_ms "__date_ms__" + @proxy_target "__proxy_target__" + @proxy_handler "__proxy_handler__" + @buffer "__buffer__" + @key_order :__key_order__ + @primitive_value "__primitive_value__" + @type_key "__type__" + @offset "__offset__" + + defmacro proto, do: @proto + defmacro promise_state, do: @promise_state + defmacro promise_value, do: @promise_value + defmacro map_data, do: @map_data + defmacro set_data, do: @set_data + defmacro typed_array, do: @typed_array + defmacro date_ms, do: @date_ms + defmacro proxy_target, do: @proxy_target + defmacro proxy_handler, do: @proxy_handler + defmacro buffer, do: @buffer + defmacro key_order, do: @key_order + defmacro primitive_value, do: @primitive_value + defmacro type_key, do: @type_key + defmacro offset, do: @offset + + def internal?(key) when is_binary(key), + do: String.starts_with?(key, "__") and String.ends_with?(key, "__") + + def internal?(_), do: false +end diff --git a/lib/quickbeam/vm/heap/registry.ex b/lib/quickbeam/vm/heap/registry.ex new file mode 100644 index 00000000..c9f2fa7b --- /dev/null +++ b/lib/quickbeam/vm/heap/registry.ex @@ -0,0 +1,20 @@ +defmodule QuickBEAM.VM.Heap.Registry do + @moduledoc false + + def register_module(name, exports) do + Process.put({:qb_module, name}, exports) + existing = Process.get(:qb_module_list, []) + unless name in existing, do: Process.put(:qb_module_list, [name | existing]) + end + + def get_module(name), do: Process.get({:qb_module, name}) + + def all_module_exports do + Process.get(:qb_module_list, []) + |> Enum.map(&Process.get({:qb_module, &1})) + |> Enum.reject(&is_nil/1) + end + + def get_symbol(key), do: Process.get({:qb_symbol_registry, key}) + def put_symbol(key, sym), do: Process.put({:qb_symbol_registry, key}, sym) +end diff --git a/lib/quickbeam/vm/heap/shapes.ex b/lib/quickbeam/vm/heap/shapes.ex new file mode 100644 index 00000000..84b0625f --- /dev/null +++ b/lib/quickbeam/vm/heap/shapes.ex @@ -0,0 +1,204 @@ +defmodule QuickBEAM.VM.Heap.Shapes do + @moduledoc """ + Hidden-class shape tracking for plain JS objects. + + When objects are created with a consistent set of property names, + they share a "shape" that maps each property name to a fixed tuple + offset. Property access becomes O(1) tuple indexing instead of + O(log n) map lookup. + + Shape-backed objects are stored as + {:shape, shape_id, offsets_map, values_tuple, proto_ref} + in the process dictionary under the object's integer ID key. + + Objects that gain accessors, internal keys, or otherwise become + non-plain deopt back to regular maps. + """ + + @empty_shape 0 + + # ── Shape registry (per-process) ── + + defp shape_table do + case Process.get(:qb_shape_table) do + nil -> + empty = %{ + keys: [], + offsets: %{}, + parent_id: nil, + transitions: %{} + } + + table = {empty} + Process.put(:qb_shape_table, table) + table + + table -> + table + end + end + + defp next_shape_id do + tuple_size(shape_table()) + end + + def get_shape(id) do + elem(shape_table(), id) + end + + defp put_shape(id, shape) do + table = shape_table() + + new_table = + if id == tuple_size(table) do + :erlang.append_element(table, shape) + else + put_elem(table, id, shape) + end + + Process.put(:qb_shape_table, new_table) + end + + # ── Public API ── + + def empty_shape_id, do: @empty_shape + + @doc "Return the offset for `key` in `shape_id`, or `:error`." + def lookup(shape_id, key) do + shape = get_shape(shape_id) + Map.fetch(shape.offsets, key) + end + + @doc "Return the ordered list of keys for `shape_id`." + def keys(shape_id) do + get_shape(shape_id).keys + end + + @doc """ + Transition `shape_id` by adding `key`. + Returns `{new_shape_id, offset}`. + """ + def transition(shape_id, key) do + shape = get_shape(shape_id) + offset = map_size(shape.offsets) + + case Map.get(shape.transitions, key) do + nil -> + new_id = next_shape_id() + new_offsets = Map.put(shape.offsets, key, offset) + + new_shape = %{ + keys: shape.keys ++ [key], + offsets: new_offsets, + parent_id: shape_id, + transitions: %{} + } + + put_shape(new_id, new_shape) + put_shape(shape_id, %{shape | transitions: Map.put(shape.transitions, key, {new_id, new_offsets})}) + {new_id, new_offsets, offset} + + {child_id, child_offsets} -> + {child_id, child_offsets, offset} + end + end + + @doc """ + Convert a plain map (without `__proto__`) into a shape and values tuple. + Returns `{:ok, shape_id, values_tuple}` or `:ineligible`. + """ + def from_map(map) when is_map(map) and map_size(map) == 0 do + {:ok, @empty_shape, %{}, {}} + end + + def from_map(map) when is_map(map) do + case resolve_shape_for_map(map) do + {shape_id, offsets} -> + {:ok, shape_id, offsets, :erlang.list_to_tuple(:maps.values(map))} + + :ineligible -> + :ineligible + end + end + + def from_map(_), do: :ineligible + + defp resolve_shape_for_map(map) do + size = map_size(map) + cache_key = {:qb_shape_cache, size, :erlang.phash2(:maps.keys(map))} + + case Process.get(cache_key) do + nil -> + case build_shape(map) do + :ineligible -> + :ineligible + + shape_id -> + offsets = get_shape(shape_id).offsets + Process.put(cache_key, {shape_id, offsets}) + {shape_id, offsets} + end + + result -> + result + end + end + + defp build_shape(map) do + keys = :maps.keys(map) + + if Enum.all?(keys, &(is_binary(&1) and not internal_key?(&1))) and + Enum.all?(:maps.values(map), &simple_value?/1) do + # For flatmaps, keys are already sorted + {shape_id, _, _} = + Enum.reduce(keys, {@empty_shape, %{}, 0}, fn key, {sid, _, _} -> + transition(sid, key) + end) + + shape_id + else + :ineligible + end + end + + @doc "Reconstruct a plain map from a shape-backed representation." + def to_map(shape_id, vals, proto) do + keys = keys(shape_id) + map = keys_vals_to_map(keys, vals, 0, %{}) + if proto, do: Map.put(map, "__proto__", proto), else: map + end + + defp keys_vals_to_map([], _vals, _idx, acc), do: acc + defp keys_vals_to_map([k | ks], vals, idx, acc), do: keys_vals_to_map(ks, vals, idx + 1, Map.put(acc, k, elem(vals, idx))) + + @doc "Check whether a stored heap value is shape-backed." + def shape?({:shape, _, _, _, _}), do: true + def shape?(_), do: false + + @doc "Grow or update a values tuple at `offset`." + def put_val(vals, offset, val) when offset < tuple_size(vals) do + put_elem(vals, offset, val) + end + + def put_val(vals, offset, val) when offset == tuple_size(vals) do + :erlang.append_element(vals, val) + end + + def put_val(vals, offset, val) do + padding = offset - tuple_size(vals) + padded = Enum.reduce(1..padding, vals, fn _, acc -> :erlang.append_element(acc, :undefined) end) + :erlang.append_element(padded, val) + end + + # ── Eligibility ── + + defp internal_key?(key) when is_binary(key), + do: String.starts_with?(key, "__") and String.ends_with?(key, "__") and byte_size(key) > 2 + + defp internal_key?(_), do: false + + defp simple_value?({:accessor, _, _}), do: false + defp simple_value?({:symbol, _, _}), do: false + defp simple_value?({:symbol, _}), do: false + defp simple_value?(_), do: true +end diff --git a/lib/quickbeam/vm/heap/store.ex b/lib/quickbeam/vm/heap/store.ex new file mode 100644 index 00000000..2d911271 --- /dev/null +++ b/lib/quickbeam/vm/heap/store.ex @@ -0,0 +1,197 @@ +defmodule QuickBEAM.VM.Heap.Store do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + alias QuickBEAM.VM.Heap.Shapes + + # ── Raw storage (bypasses shape→map reconstruction) ── + + def get_obj_raw(ref), do: Process.get(ref) + def put_obj_raw(ref, val), do: Process.put(ref, val) + + # ── Object access (map-compatible, reconstructs shapes) ── + + def get_obj(ref) do + case Process.get(ref) do + {:shape, shape_id, _offsets, vals, proto} -> Shapes.to_map(shape_id, vals, proto) + other -> other + end + end + + def get_obj(ref, default) do + case Process.get(ref, default) do + {:shape, shape_id, _offsets, vals, proto} -> Shapes.to_map(shape_id, vals, proto) + other -> other + end + end + + def put_obj(ref, list) when is_list(list) do + Process.put(ref, {:qb_arr, :array.from_list(list, :undefined)}) + track_alloc() + end + + def put_obj(ref, val) do + Process.put(ref, val) + track_alloc() + end + + def put_obj_key(ref, key, val), do: put_obj_key(ref, get_obj_raw(ref), key, val) + + def put_obj_key(ref, {:shape, shape_id, offsets, vals, proto}, key, val) do + case Map.fetch(offsets, key) do + {:ok, offset} -> + new_vals = Shapes.put_val(vals, offset, val) + Process.put(ref, {:shape, shape_id, offsets, new_vals, proto}) + + :error -> + {new_shape_id, new_offsets, offset} = Shapes.transition(shape_id, key) + new_vals = Shapes.put_val(vals, offset, val) + Process.put(ref, {:shape, new_shape_id, new_offsets, new_vals, proto}) + end + end + + def put_obj_key(ref, map, key, val) when is_map(map) do + new_map = + if not Map.has_key?(map, key) and (is_binary(key) or is_integer(key)) do + order = Map.get(map, key_order(), []) + Map.put(Map.put(map, key, val), key_order(), [key | order]) + else + Map.put(map, key, val) + end + + Process.put(ref, new_map) + end + + def put_obj_key(ref, _other, key, val) do + Process.put(ref, %{key => val}) + end + + def update_obj(ref, default, fun) do + current = Process.get(ref, default) + + current_map = + case current do + {:shape, shape_id, _offsets, vals, proto} -> Shapes.to_map(shape_id, vals, proto) + other -> other + end + + result = fun.(current_map) + Process.put(ref, result) + end + + # ── Array helpers ── + + def obj_is_array?(ref) do + case Process.get(ref) do + {:qb_arr, _} -> true + _ -> false + end + end + + def obj_to_list(ref) do + case Process.get(ref) do + {:qb_arr, arr} -> :array.to_list(arr) + list when is_list(list) -> list + _ -> [] + end + end + + def array_get(ref, idx) do + case Process.get(ref) do + {:qb_arr, arr} when idx >= 0 -> + if idx < :array.size(arr), do: :array.get(idx, arr), else: :undefined + + _ -> + :undefined + end + end + + def array_size(ref) do + case Process.get(ref) do + {:qb_arr, arr} -> :array.size(arr) + list when is_list(list) -> length(list) + _ -> 0 + end + end + + def array_push(ref, values) do + case Process.get(ref) do + {:qb_arr, arr} -> + new_arr = + Enum.reduce(values, {:array.size(arr), arr}, fn value, {idx, array} -> + {idx + 1, :array.set(idx, value, array)} + end) + |> elem(1) + + Process.put(ref, {:qb_arr, new_arr}) + :array.size(new_arr) + + _ -> + 0 + end + end + + def array_set(ref, idx, val) do + case Process.get(ref) do + {:qb_arr, arr} -> Process.put(ref, {:qb_arr, :array.set(idx, val, arr)}) + _ -> :ok + end + end + + # ── Closure cells ── + + def get_cell(ref), do: Process.get({:qb_cell, ref}, :undefined) + def put_cell(ref, val), do: Process.put({:qb_cell, ref}, val) + + # ── Class metadata ── + + def get_class_proto({:closure, _, raw} = ctor), + do: Process.get({:qb_class_proto, ctor}) || Process.get({:qb_class_proto, raw}) + + def get_class_proto(ctor), do: Process.get({:qb_class_proto, ctor}) + def put_class_proto(ctor, proto), do: Process.put({:qb_class_proto, ctor}, proto) + + def get_parent_ctor({:closure, _, raw} = ctor), + do: Process.get({:qb_parent_ctor, ctor}) || Process.get({:qb_parent_ctor, raw}) + + def get_parent_ctor(ctor), do: Process.get({:qb_parent_ctor, ctor}) + def put_parent_ctor(ctor, parent), do: Process.put({:qb_parent_ctor, ctor}, parent) + def delete_parent_ctor(ctor), do: Process.delete({:qb_parent_ctor, ctor}) + + def get_ctor_statics(ctor), do: Process.get({:qb_ctor_statics, ctor}, %{}) + def put_ctor_statics(ctor, statics), do: Process.put({:qb_ctor_statics, ctor}, statics) + + def put_ctor_static(ctor, key, val) do + statics = get_ctor_statics(ctor) + put_ctor_statics(ctor, Map.put(statics, key, val)) + end + + def get_var(name), do: Process.get({:qb_var, name}) + def put_var(name, val), do: Process.put({:qb_var, name}, val) + def delete_var(name), do: Process.delete({:qb_var, name}) + + def frozen?(ref) do + Process.get(:qb_has_frozen, false) and Process.get({:qb_frozen, ref}, false) + end + + def freeze(ref) do + Process.put(:qb_has_frozen, true) + Process.put({:qb_frozen, ref}, true) + end + + def get_prop_desc(ref, key), do: Process.get({:qb_prop_desc, ref, key}) + def put_prop_desc(ref, key, desc), do: Process.put({:qb_prop_desc, ref, key}, desc) + + # ── Object ID allocation ── + + def next_id, do: :erlang.unique_integer([:positive, :monotonic]) + + defp track_alloc do + count = Process.get(:qb_alloc_count, 0) + 1 + Process.put(:qb_alloc_count, count) + + if count >= Process.get(:qb_gc_threshold, QuickBEAM.VM.Heap.gc_initial_threshold()) do + Process.put(:qb_gc_needed, true) + end + end +end diff --git a/lib/quickbeam/vm/interpreter.ex b/lib/quickbeam/vm/interpreter.ex new file mode 100644 index 00000000..052c4087 --- /dev/null +++ b/lib/quickbeam/vm/interpreter.ex @@ -0,0 +1,3282 @@ +defmodule QuickBEAM.VM.Interpreter do + import Bitwise, only: [bnot: 1, &&&: 2] + import QuickBEAM.VM.Builtin, only: [build_methods: 1, build_object: 1] + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.Execution.Trace + + alias QuickBEAM.VM.{ + Builtin, + Bytecode, + Decoder, + GlobalEnv, + Heap, + Invocation, + Names, + PredefinedAtoms, + Runtime, + Stacktrace + } + + alias QuickBEAM.JSError + alias QuickBEAM.VM.Compiler.RuntimeHelpers + alias QuickBEAM.VM.Invocation.Context, as: InvokeContext + alias QuickBEAM.VM.ObjectModel.{Class, Copy, Delete, Functions, Get, Methods, Private, Put} + alias QuickBEAM.VM.PromiseState, as: Promise + + alias __MODULE__.{ + ClosureBuilder, + Closures, + Context, + EvalEnv, + Frame, + Gas, + Generator, + Setup, + Values + } + + require Frame + + @moduledoc """ + Executes decoded QuickJS bytecode via multi-clause function dispatch. + + The interpreter pre-decodes bytecode into instruction tuples for O(1) indexed + access, then runs a tail-recursive dispatch loop with one `defp run/5` clause + per opcode family. + + ## JS value representation + + - number: Elixir integer or float + - string: Elixir binary + - boolean: `true` / `false` + - null: `nil` + - undefined: `:undefined` + - object: `{:obj, reference()}` + - function: `%Bytecode.Function{}` | `{:closure, map(), %Bytecode.Function{}}` + - symbol: `{:symbol, desc}` | `{:symbol, desc, ref}` + - bigint: `{:bigint, integer()}` + """ + + @compile {:inline, + put_local: 3, + list_iterator_next: 1, + make_list_iterator: 1, + with_has_property?: 2, + check_prototype_chain: 2} + + @op_invalid 0 + @op_push_i32 1 + @op_push_const 2 + @op_fclosure 3 + @op_push_atom_value 4 + @op_private_symbol 5 + @op_undefined 6 + @op_null 7 + @op_push_this 8 + @op_push_false 9 + @op_push_true 10 + @op_object 11 + @op_special_object 12 + @op_rest 13 + @op_drop 14 + @op_nip 15 + @op_nip1 16 + @op_dup 17 + @op_dup1 18 + @op_dup2 19 + @op_dup3 20 + @op_insert2 21 + @op_insert3 22 + @op_insert4 23 + @op_perm3 24 + @op_perm4 25 + @op_perm5 26 + @op_swap 27 + @op_swap2 28 + @op_rot3l 29 + @op_rot3r 30 + @op_rot4l 31 + @op_rot5l 32 + @op_call_constructor 33 + @op_call 34 + @op_tail_call 35 + @op_call_method 36 + @op_tail_call_method 37 + @op_array_from 38 + @op_apply 39 + @op_return 40 + @op_return_undef 41 + @op_check_ctor_return 42 + @op_check_ctor 43 + @op_init_ctor 44 + @op_check_brand 45 + @op_add_brand 46 + @op_return_async 47 + @op_throw 48 + @op_throw_error 49 + @op_eval 50 + @op_apply_eval 51 + @op_regexp 52 + @op_get_super 53 + @op_import 54 + @op_get_var_undef 55 + @op_get_var 56 + @op_put_var 57 + @op_put_var_init 58 + @op_get_ref_value 59 + @op_put_ref_value 60 + @op_define_var 61 + @op_check_define_var 62 + @op_define_func 63 + @op_get_field 64 + @op_get_field2 65 + @op_put_field 66 + @op_get_private_field 67 + @op_put_private_field 68 + @op_define_private_field 69 + @op_get_array_el 70 + @op_get_array_el2 71 + @op_put_array_el 72 + @op_get_super_value 73 + @op_put_super_value 74 + @op_define_field 75 + @op_set_name 76 + @op_set_name_computed 77 + @op_set_proto 78 + @op_set_home_object 79 + @op_define_array_el 80 + @op_append 81 + @op_copy_data_properties 82 + @op_define_method 83 + @op_define_method_computed 84 + @op_define_class 85 + @op_define_class_computed 86 + @op_get_loc 87 + @op_put_loc 88 + @op_set_loc 89 + @op_get_arg 90 + @op_put_arg 91 + @op_set_arg 92 + @op_get_var_ref 93 + @op_put_var_ref 94 + @op_set_var_ref 95 + @op_set_loc_uninitialized 96 + @op_get_loc_check 97 + @op_put_loc_check 98 + @op_put_loc_check_init 99 + @op_get_var_ref_check 100 + @op_put_var_ref_check 101 + @op_put_var_ref_check_init 102 + @op_close_loc 103 + @op_if_false 104 + @op_if_true 105 + @op_goto 106 + @op_catch 107 + @op_gosub 108 + @op_ret 109 + @op_nip_catch 110 + @op_to_object 111 + @op_to_propkey 112 + @op_to_propkey2 113 + @op_with_get_var 114 + @op_with_put_var 115 + @op_with_delete_var 116 + @op_with_make_ref 117 + @op_with_get_ref 118 + @op_with_get_ref_undef 119 + @op_make_loc_ref 120 + @op_make_arg_ref 121 + @op_make_var_ref 123 + @op_for_in_start 124 + @op_for_of_start 125 + @op_for_await_of_start 126 + @op_for_in_next 127 + @op_for_of_next 128 + @op_iterator_check_object 129 + @op_iterator_get_value_done 130 + @op_iterator_close 131 + @op_iterator_next 132 + @op_iterator_call 133 + @op_initial_yield 134 + @op_yield 135 + @op_yield_star 136 + @op_async_yield_star 137 + @op_await 138 + @op_neg 139 + @op_plus 140 + @op_dec 141 + @op_inc 142 + @op_post_dec 143 + @op_post_inc 144 + @op_dec_loc 145 + @op_inc_loc 146 + @op_add_loc 147 + @op_not 148 + @op_lnot 149 + @op_typeof 150 + @op_delete 151 + @op_delete_var 152 + @op_mul 153 + @op_div 154 + @op_mod 155 + @op_add 156 + @op_sub 157 + @op_shl 158 + @op_sar 159 + @op_shr 160 + @op_band 161 + @op_bxor 162 + @op_bor 163 + @op_pow 164 + @op_lt 165 + @op_lte 166 + @op_gt 167 + @op_gte 168 + @op_instanceof 169 + @op_in 170 + @op_eq 171 + @op_neq 172 + @op_strict_eq 173 + @op_strict_neq 174 + @op_is_undefined_or_null 175 + @op_private_in 176 + @op_push_bigint_i32 177 + @op_nop 178 + @op_push_minus1 179 + @op_push_0 180 + @op_push_1 181 + @op_push_2 182 + @op_push_3 183 + @op_push_4 184 + @op_push_5 185 + @op_push_6 186 + @op_push_7 187 + @op_push_i8 188 + @op_push_i16 189 + @op_push_const8 190 + @op_fclosure8 191 + @op_push_empty_string 192 + @op_get_loc8 193 + @op_put_loc8 194 + @op_set_loc8 195 + @op_get_loc0_loc1 196 + @op_get_loc0 197 + @op_get_loc1 198 + @op_get_loc2 199 + @op_get_loc3 200 + @op_put_loc0 201 + @op_put_loc1 202 + @op_put_loc2 203 + @op_put_loc3 204 + @op_set_loc0 205 + @op_set_loc1 206 + @op_set_loc2 207 + @op_set_loc3 208 + @op_get_arg0 209 + @op_get_arg1 210 + @op_get_arg2 211 + @op_get_arg3 212 + @op_put_arg0 213 + @op_put_arg1 214 + @op_put_arg2 215 + @op_put_arg3 216 + @op_set_arg0 217 + @op_set_arg1 218 + @op_set_arg2 219 + @op_set_arg3 220 + @op_get_var_ref0 221 + @op_get_var_ref1 222 + @op_get_var_ref2 223 + @op_get_var_ref3 224 + @op_put_var_ref0 225 + @op_put_var_ref1 226 + @op_put_var_ref2 227 + @op_put_var_ref3 228 + @op_set_var_ref0 229 + @op_set_var_ref1 230 + @op_set_var_ref2 231 + @op_set_var_ref3 232 + @op_get_length 233 + @op_if_false8 234 + @op_if_true8 235 + @op_goto8 236 + @op_goto16 237 + @op_call0 238 + @op_call1 239 + @op_call2 240 + @op_call3 241 + @op_is_undefined 242 + @op_is_null 243 + @op_typeof_is_undefined 244 + @op_typeof_is_function 245 + + @func_generator 1 + @func_async 2 + @func_async_generator 3 + @gc_check_interval 1000 + + defp check_gas(_pc, frame, stack, gas, ctx), + do: Gas.check(frame, stack, gas, ctx, @gc_check_interval) + + @spec eval(Bytecode.Function.t()) :: {:ok, term()} | {:error, term()} + def eval(%Bytecode.Function{} = fun), do: eval(fun, [], %{}) + + @spec eval(Bytecode.Function.t(), [term()], map()) :: {:ok, term()} | {:error, term()} + def eval(%Bytecode.Function{} = fun, args, opts), do: eval(fun, args, opts, {}) + + @spec eval(Bytecode.Function.t(), [term()], map(), tuple()) :: {:ok, term()} | {:error, term()} + def eval(%Bytecode.Function{} = fun, args, opts, atoms) do + case eval_with_ctx(fun, args, opts, atoms) do + {:ok, value, _ctx} -> {:ok, value} + {:error, _} = err -> err + end + end + + defp eval_with_ctx(%Bytecode.Function{} = fun, args, opts, atoms) do + gas = Map.get(opts, :gas, Context.default_gas()) + + ctx = Setup.build_eval_context(opts, atoms, gas) + + Heap.put_atoms(atoms) + Setup.store_function_atoms(fun, atoms) + prev_ctx = Heap.get_ctx() + Heap.put_ctx(ctx) + ctx = Context.mark_synced(ctx) + + try do + case Decoder.decode(fun.byte_code, fun.arg_count) do + {:ok, instructions} -> + instructions = List.to_tuple(instructions) + locals = :erlang.make_tuple(max(fun.arg_count + fun.var_count, 1), :undefined) + + frame = + Frame.new( + locals, + List.to_tuple(fun.constants), + {}, + fun.stack_size, + instructions, + %{} + ) + + if ctx.trace_enabled, do: Trace.push(fun) + + try do + result = run(0, frame, args, gas, ctx) + Promise.drain_microtasks() + {:ok, unwrap_promise(result), Heap.get_ctx()} + catch + {:js_throw, val} -> {:error, {:js_throw, val}} + {:error, _} = err -> err + after + if ctx.trace_enabled, do: Trace.pop() + end + + {:error, _} = err -> + err + end + after + if prev_ctx, do: Heap.put_ctx(prev_ctx), else: Heap.put_ctx(nil) + end + end + + @doc "Invoke a bytecode function or closure from external code." + def invoke(fun, args, gas), do: Invocation.invoke(fun, args, gas) + + @doc """ + Invokes a JS function with a specific `this` receiver. + """ + def invoke_with_receiver(fun, args, gas, this_obj), + do: Invocation.invoke_with_receiver(fun, args, gas, this_obj) + + def invoke_constructor(fun, args, gas, this_obj, new_target), + do: Invocation.invoke_constructor(fun, args, gas, this_obj, new_target) + + defp catch_js_throw(pc, frame, rest, gas, ctx, fun) do + result = fun.() + run(pc + 1, frame, [result | rest], gas, ctx) + catch + {:js_throw, val} -> throw_or_catch(frame, val, gas, ctx) + end + + defp catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fun) do + result = fun.() + persistent = Heap.get_persistent_globals() || %{} + + run( + pc + 1, + frame, + [result | rest], + gas, + Context.mark_dirty(%{ctx | globals: Map.merge(ctx.globals, persistent)}) + ) + catch + {:js_throw, val} -> throw_or_catch(frame, val, gas, ctx) + end + + # ── Helpers ── + + defp clean_eval_globals(pre_eval_globals) do + post = Heap.get_persistent_globals() || %{} + + cleaned = + Enum.reduce(post, post, fn {key, _val}, acc -> + case Map.fetch(pre_eval_globals, key) do + {:ok, old_val} -> Map.put(acc, key, old_val) + :error -> Map.delete(acc, key) + end + end) + + Heap.put_persistent_globals(cleaned) + end + + defp caller_is_strict?(%Context{current_func: func}) do + case func do + {:closure, _, %Bytecode.Function{is_strict_mode: s}} -> s + %Bytecode.Function{is_strict_mode: s} -> s + _ -> false + end + end + + defp uninitialized_this_local?(ctx, idx), do: EvalEnv.current_local_name(ctx, idx) == "this" + + defp derived_this_uninitialized?(%Context{ + this: this, + current_func: {:closure, _, %Bytecode.Function{is_derived_class_constructor: true}} + }) + when this == :uninitialized or + (is_tuple(this) and tuple_size(this) == 2 and elem(this, 0) == :uninitialized), + do: true + + defp derived_this_uninitialized?(%Context{ + this: this, + current_func: %Bytecode.Function{is_derived_class_constructor: true} + }) + when this == :uninitialized or + (is_tuple(this) and tuple_size(this) == 2 and elem(this, 0) == :uninitialized), + do: true + + defp derived_this_uninitialized?(_), do: false + + defp current_var_ref_name( + %Context{current_func: {:closure, _, %Bytecode.Function{closure_vars: vars}}}, + idx + ) + when idx >= 0 and idx < length(vars), + do: vars |> Enum.at(idx) |> Map.get(:name) |> Names.resolve_display_name() + + defp current_var_ref_name(%Context{current_func: %Bytecode.Function{closure_vars: vars}}, idx) + when idx >= 0 and idx < length(vars), + do: vars |> Enum.at(idx) |> Map.get(:name) |> Names.resolve_display_name() + + defp current_var_ref_name(_, _), do: nil + + defp put_local(f, idx, val), + do: put_elem(f, Frame.locals(), put_elem(elem(f, Frame.locals()), idx, val)) + + defp throw_or_catch(frame, error, gas, ctx) do + error = maybe_refresh_error_stack(error) + + case ctx.catch_stack do + [{target, saved_stack} | rest_catch] -> + run( + target, + frame, + [error | saved_stack], + gas, + Context.mark_dirty(%{ctx | catch_stack: rest_catch}) + ) + + [] -> + throw({:js_throw, error}) + end + end + + defp maybe_refresh_error_stack({:obj, ref} = error) do + case Heap.get_obj(ref, %{}) do + %{"name" => _, "message" => _} -> Stacktrace.attach_stack(error) + _ -> error + end + end + + defp maybe_refresh_error_stack(error), do: error + + defp get_arg_value(%Context{arg_buf: arg_buf}, idx) do + if idx < tuple_size(arg_buf), do: elem(arg_buf, idx), else: :undefined + end + + defp throw_null_property_error(frame, obj, atom_idx, gas, ctx) do + prop = Names.resolve_atom(ctx, atom_idx) + nullish = if obj == nil, do: "null", else: "undefined" + + error = + Heap.make_error("Cannot read properties of #{nullish} (reading '#{prop}')", "TypeError") + + throw_or_catch(frame, error, gas, ctx) + end + + defp unwrap_promise(val, depth \\ 0) + + defp unwrap_promise({:obj, ref}, depth) when depth < 10 do + case Heap.get_obj(ref, %{}) do + %{ + promise_state() => :resolved, + promise_value() => val + } -> + unwrap_promise(val, depth + 1) + + _ -> + {:obj, ref} + end + end + + defp unwrap_promise(val, _depth), do: val + + def resolve_awaited({:obj, ref} = obj) do + Promise.drain_microtasks() + + case Heap.get_obj(ref, %{}) do + %{ + promise_state() => :resolved, + promise_value() => val + } -> + val + + %{ + promise_state() => :rejected, + promise_value() => val + } -> + throw({:js_throw, val}) + + %{promise_state() => :pending} -> + # Drain again in case resolution was queued + Promise.drain_microtasks() + + case Heap.get_obj(ref, %{}) do + %{ + promise_state() => :resolved, + promise_value() => val + } -> + val + + %{ + promise_state() => :rejected, + promise_value() => val + } -> + throw({:js_throw, val}) + + _ -> + obj + end + + _ -> + obj + end + end + + def resolve_awaited(val), do: val + + defp list_iterator_next(pos_ref) do + state = Heap.get_obj(pos_ref, %{pos: 0, list: []}) + + if state.pos < length(state.list) do + val = Enum.at(state.list, state.pos) + Heap.put_obj(pos_ref, %{state | pos: state.pos + 1}) + Heap.wrap(%{"value" => val, "done" => false}) + else + Heap.wrap(%{"value" => :undefined, "done" => true}) + end + end + + defp make_list_iterator(items) do + pos_ref = make_ref() + Heap.put_obj(pos_ref, %{pos: 0, list: items}) + next_fn = {:builtin, "next", fn _, _ -> list_iterator_next(pos_ref) end} + {build_object(do: val("next", next_fn)), next_fn} + end + + defp eval_code(code, caller_frame, gas, ctx, var_objs, keep_declared?) do + with {:ok, bc} <- QuickBEAM.Runtime.compile(ctx.runtime_pid, code), + {:ok, parsed} <- Bytecode.decode(bc) do + declared_names = eval_declared_names(parsed.value, parsed.atoms) + eval_globals = collect_caller_locals(caller_frame, ctx) + captured_globals = collect_captured_globals(ctx.current_func) + + eval_scope_globals = + merge_var_object_globals(Map.merge(eval_globals, captured_globals), var_objs) + + base_globals = + if keep_declared?, + do: Map.drop(ctx.globals, MapSet.to_list(declared_names)), + else: ctx.globals + + scoped_globals = + if keep_declared?, + do: Map.drop(eval_scope_globals, MapSet.to_list(declared_names)), + else: eval_scope_globals + + eval_ctx_globals = + base_globals + |> Map.merge(scoped_globals) + |> Map.put("arguments", Heap.wrap(Tuple.to_list(ctx.arg_buf))) + + visible_declared_names = + base_globals + |> Map.merge(eval_scope_globals) + |> Map.put("arguments", :present) + |> Map.keys() + |> Enum.filter(&is_binary/1) + |> MapSet.new() + |> MapSet.intersection(declared_names) + + eval_opts = %{ + gas: gas, + runtime_pid: ctx.runtime_pid, + globals: eval_ctx_globals, + this: ctx.this, + arg_buf: ctx.arg_buf, + current_func: ctx.current_func, + new_target: ctx.new_target + } + + pre_eval_globals = Heap.get_persistent_globals() || %{} + + case eval_with_ctx(parsed.value, [], eval_opts, parsed.atoms) do + {:ok, val, final_ctx} -> + post_eval_globals = Heap.get_persistent_globals() || %{} + + transient_globals = + post_eval_globals + |> Map.merge(Map.get(final_ctx || %{}, :globals, %{})) + |> Map.take(MapSet.to_list(visible_declared_names)) + + apply_eval_transients(ctx.current_func, var_objs, transient_globals, keep_declared?) + write_back_eval_vars(caller_frame, ctx, pre_eval_globals, var_objs, declared_names) + + clean_eval_globals(pre_eval_globals) + {val, transient_globals} + + {:error, {:js_throw, val}} -> + write_back_eval_vars(caller_frame, ctx, pre_eval_globals, var_objs, declared_names) + + clean_eval_globals(pre_eval_globals) + throw({:js_throw, val}) + + _ -> + {:undefined, %{}} + end + else + {:error, msg} when is_binary(msg) -> + throw({:js_throw, Heap.make_error(msg, "SyntaxError")}) + + {:error, %JSError{name: name, message: msg}} -> + throw({:js_throw, Heap.make_error(msg, name)}) + + _ -> + {:undefined, %{}} + end + end + + defp merge_var_object_globals(globals, []), do: globals + + defp merge_var_object_globals(globals, var_objs) do + Enum.reduce(var_objs, globals, fn + {:obj, ref}, acc -> + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> Map.merge(acc, map) + _ -> acc + end + + _, acc -> + acc + end) + end + + defp captured_var_objects({:closure, captured, _}) do + captured + |> Map.values() + |> Enum.flat_map(fn + {:cell, ref} -> + case Heap.get_cell(ref) do + {:obj, _} = obj -> [obj] + _ -> [] + end + + _ -> + [] + end) + end + + defp captured_var_objects(_), do: [] + + defp collect_captured_globals({:closure, captured, %Bytecode.Function{closure_vars: cvs}}) do + Enum.reduce(cvs, %{}, fn cv, acc -> + case Names.resolve_display_name(cv.name) do + name when is_binary(name) -> + val = + case Map.get(captured, ClosureBuilder.capture_key(cv), :undefined) do + {:cell, ref} -> Heap.get_cell(ref) + other -> other + end + + Map.put(acc, name, val) + + _ -> + acc + end + end) + end + + defp collect_captured_globals(_), do: %{} + + defp write_back_eval_vars(caller_frame, ctx, original_globals, var_objs, declared_names) do + new_globals = Heap.get_persistent_globals() || %{} + + if caller_is_strict?(ctx) do + func_name = EvalEnv.current_func_name(ctx) + + if func_name && Map.has_key?(new_globals, func_name) do + old_val = + case ctx.current_func do + {:closure, _, %Bytecode.Function{} = f} -> Heap.get_parent_ctor(f) + _ -> nil + end + + new_val = Map.get(new_globals, func_name) + + if old_val == nil and new_val != ctx.current_func and new_val != :undefined do + throw({:js_throw, Heap.make_error("Assignment to constant variable.", "TypeError")}) + end + end + end + + vrefs = elem(caller_frame, Frame.var_refs()) + l2v = elem(caller_frame, Frame.l2v()) + + case ctx.current_func do + {:closure, _, %Bytecode.Function{locals: local_defs}} -> + do_write_back(local_defs, vrefs, l2v, new_globals, ctx, original_globals, declared_names) + + %Bytecode.Function{locals: local_defs} -> + do_write_back(local_defs, vrefs, l2v, new_globals, ctx, original_globals, declared_names) + + _ -> + :ok + end + + if match?({:closure, _, %Bytecode.Function{}}, ctx.current_func) do + write_back_captured_vars(ctx.current_func, new_globals, original_globals, declared_names) + end + + if var_objs != [] do + for {name, val} <- new_globals, + is_binary(name), + Map.has_key?(original_globals, name), + Map.get(original_globals, name) != val do + for var_obj <- var_objs, do: Put.put(var_obj, name, val) + end + end + end + + defp apply_eval_transients(current_func, var_objs, transient_globals, keep_declared?) do + if transient_globals != %{} do + if var_objs != [] do + for {name, val} <- transient_globals, var_obj <- var_objs do + Put.put(var_obj, name, val) + end + end + + if keep_declared? do + apply_transient_captured_vars( + current_func, + transient_globals, + MapSet.new(Map.keys(transient_globals)) + ) + end + end + end + + defp write_back_captured_vars( + {:closure, captured, %Bytecode.Function{closure_vars: cvs}}, + new_globals, + original_globals, + declared_names + ) do + for cv <- cvs, + name = Names.resolve_display_name(cv.name), + is_binary(name), + not MapSet.member?(declared_names, name), + Map.has_key?(new_globals, name), + Map.get(original_globals, name) != Map.get(new_globals, name) do + case Map.get(captured, ClosureBuilder.capture_key(cv)) do + {:cell, ref} -> Heap.put_cell(ref, Map.get(new_globals, name)) + _ -> :ok + end + end + end + + defp write_back_captured_vars(_, _, _, _), do: :ok + + defp apply_transient_captured_vars( + {:closure, captured, %Bytecode.Function{closure_vars: cvs}}, + new_globals, + declared_names + ) do + for cv <- cvs, + name = Names.resolve_display_name(cv.name), + is_binary(name), + MapSet.member?(declared_names, name), + Map.has_key?(new_globals, name) do + case Map.get(captured, ClosureBuilder.capture_key(cv)) do + {:cell, ref} -> + old_val = Heap.get_cell(ref) + restore_stack = Process.get(:qb_eval_restore_stack, []) + Process.put(:qb_eval_restore_stack, [{ref, old_val} | restore_stack]) + Heap.put_cell(ref, Map.get(new_globals, name)) + + _ -> + :ok + end + end + end + + defp apply_transient_captured_vars(_, _, _), do: :ok + + defp restore_eval_restores(mark) do + restores = Process.get(:qb_eval_restore_stack, []) + {to_restore, keep} = Enum.split(restores, length(restores) - mark) + + Enum.each(to_restore, fn {ref, old_val} -> + Heap.put_cell(ref, old_val) + end) + + Process.put(:qb_eval_restore_stack, keep) + end + + defp eval_declared_names(%Bytecode.Function{} = fun, atoms) do + local_names = + fun.locals + |> Enum.map(&Names.resolve_display_name(&1.name)) + |> Enum.filter(&is_binary/1) + + instruction_names = + case Decoder.decode(fun.byte_code, fun.arg_count) do + {:ok, insns} -> + insns + |> Enum.reduce([], fn + {op, [atom_ref, _scope]}, acc when op in [@op_define_var, @op_check_define_var] -> + case resolve_declared_atom(atom_ref, atoms) do + name when is_binary(name) -> [name | acc] + _ -> acc + end + + {op, [atom_ref, _flags]}, acc when op == @op_define_func -> + case resolve_declared_atom(atom_ref, atoms) do + name when is_binary(name) -> [name | acc] + _ -> acc + end + + _, acc -> + acc + end) + + _ -> + [] + end + + MapSet.new(local_names ++ instruction_names) + end + + defp eval_declared_names(_, _), do: MapSet.new() + + defp resolve_declared_atom({:predefined, idx}, _atoms), + do: PredefinedAtoms.lookup(idx) + + defp resolve_declared_atom(idx, atoms) + when is_integer(idx) and idx >= 0 and idx < tuple_size(atoms), do: elem(atoms, idx) + + defp resolve_declared_atom(name, _atoms) when is_binary(name), do: name + defp resolve_declared_atom(_, _atoms), do: nil + + defp do_write_back(local_defs, vrefs, l2v, new_globals, ctx, original_globals, declared_names) do + func_name = EvalEnv.current_func_name(ctx) + + for {vd, idx} <- Enum.with_index(local_defs), + name = Names.resolve_display_name(vd.name), + is_binary(name), + not MapSet.member?(declared_names, name), + name != func_name, + Map.has_key?(new_globals, name), + new_val = Map.get(new_globals, name), + Map.get(original_globals, name) != new_val do + case Map.get(l2v, idx) do + nil -> + :ok + + vref_idx when vref_idx < tuple_size(vrefs) -> + case elem(vrefs, vref_idx) do + {:cell, ref} -> Closures.write_cell({:cell, ref}, new_val) + _ -> :ok + end + + _ -> + :ok + end + end + end + + defp collect_caller_locals(frame, ctx) do + locals = elem(frame, Frame.locals()) + + case ctx.current_func do + {:closure, _, %Bytecode.Function{locals: local_defs, arg_count: ac}} -> + build_local_map(local_defs, ac, locals, ctx) + + %Bytecode.Function{locals: local_defs, arg_count: ac} -> + build_local_map(local_defs, ac, locals, ctx) + + _ -> + %{} + end + end + + defp build_local_map(local_defs, arg_count, locals, ctx) do + arg_buf = ctx.arg_buf + + local_defs + |> Enum.with_index() + |> Enum.reduce(%{}, fn {vd, idx}, acc -> + with name when is_binary(name) <- vd.name, + val when val != :undefined <- local_value(idx, arg_count, arg_buf, locals) do + Map.put(acc, name, val) + else + _ -> acc + end + end) + end + + defp local_value(idx, _arg_count, arg_buf, _locals) when idx < tuple_size(arg_buf) do + elem(arg_buf, idx) + end + + defp local_value(idx, _arg_count, _arg_buf, locals) do + if idx < tuple_size(locals), do: elem(locals, idx), else: :undefined + end + + defp collect_iterator(iter_obj, acc) do + next_fn = Get.get(iter_obj, "next") + + case Runtime.call_callback(next_fn, []) do + {:obj, ref} -> + result = Heap.get_obj(ref, %{}) + done = Map.get(result, "done", false) + + if done == true do + Enum.reverse(acc) + else + val = Map.get(result, "value", :undefined) + collect_iterator(iter_obj, [val | acc]) + end + + _ -> + Enum.reverse(acc) + end + end + + defp materialize_constant({:template_object, elems, raw}) when is_list(elems) do + raw_list = + case raw do + {:array, l} when is_list(l) -> l + l when is_list(l) -> l + :undefined -> elems + _ -> elems + end + + raw_ref = make_ref() + + raw_map = + raw_list + |> Enum.with_index() + |> Enum.reduce(%{"length" => length(raw_list)}, fn {v, i}, acc -> + Map.put(acc, Integer.to_string(i), v) + end) + + Heap.put_obj(raw_ref, raw_map) + + ref = make_ref() + + map = + elems + |> Enum.with_index() + |> Enum.reduce(%{"length" => length(elems), "raw" => {:obj, raw_ref}}, fn {v, i}, acc -> + Map.put(acc, Integer.to_string(i), v) + end) + + Heap.put_obj(ref, map) + {:obj, ref} + end + + defp materialize_constant({:template_object, {:array, elems}, raw}) do + materialize_constant({:template_object, elems, raw}) + end + + defp materialize_constant({:template_object, elems, raw}) when not is_list(elems) do + materialize_constant({:template_object, [elems], raw}) + end + + defp materialize_constant(val), do: val + + defp check_prototype_chain(_, :undefined), do: false + defp check_prototype_chain(_, nil), do: false + + defp check_prototype_chain({:obj, ref}, target) do + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> + case Map.get(map, proto()) do + ^target -> true + nil -> false + :undefined -> false + proto -> check_prototype_chain(proto, target) + end + + _ -> + false + end + end + + defp check_prototype_chain(_, _), do: false + + defp with_has_property?({:obj, _} = obj, key) do + Get.get(obj, key) != :undefined + end + + defp with_has_property?(_, _), do: false + + defp ensure_initialized_local!(ctx, idx, val) do + if val == :__tdz__ or + (val == :undefined and uninitialized_this_local?(ctx, idx) and + derived_this_uninitialized?(ctx)) do + message = + if uninitialized_this_local?(ctx, idx) and derived_this_uninitialized?(ctx), + do: "this is not initialized", + else: "Cannot access variable before initialization" + + throw({:js_throw, Heap.make_error(message, "ReferenceError")}) + end + end + + defp eval_scope_var_objects(frame, ctx, enabled?, scope_idx) do + if enabled? do + locals = elem(frame, Frame.locals()) + + obj_locals = + for i <- 0..(tuple_size(locals) - 1), + obj = elem(locals, i), + match?({:obj, _}, obj), + do: obj + + obj_locals = if scope_idx == 0, do: Enum.take(obj_locals, 1), else: obj_locals + Enum.uniq(obj_locals ++ captured_var_objects(ctx.current_func)) + else + [] + end + end + + defp run_eval_or_call(pc, frame, rest, gas, ctx, fun, args, scope_idx, var_objs) do + case eval_or_call( + fun, + List.first(args, :undefined), + args, + scope_idx, + frame, + gas, + ctx, + var_objs + ) do + {:ok, {result, new_ctx}} -> run(pc + 1, frame, [result | rest], gas, new_ctx) + {:error, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + defp eval_or_call(fun, code, args, scope_idx, frame, gas, ctx, var_objs) do + try do + {:ok, eval_or_call_result(fun, code, args, scope_idx, frame, gas, ctx, var_objs)} + catch + {:js_throw, error} -> {:error, error} + end + end + + defp eval_or_call_result(fun, code, args, scope_idx, frame, gas, ctx, var_objs) do + cond do + fun == ctx.globals["eval"] and is_binary(code) and ctx.runtime_pid != nil -> + keep_declared? = scope_idx > 0 + {value, transient_globals} = eval_code(code, frame, gas, ctx, var_objs, keep_declared?) + {value, Context.mark_dirty(%{ctx | globals: Map.merge(ctx.globals, transient_globals)})} + + callable?(fun) -> + persistent = Heap.get_persistent_globals() || %{} + + {dispatch_call(fun, args, gas, ctx, :undefined), + Context.mark_dirty(%{ctx | globals: Map.merge(ctx.globals, persistent)})} + + true -> + {:undefined, ctx} + end + end + + defp callable?(fun) do + is_function(fun) or match?({:fn, _, _}, fun) or match?({:bound, _, _}, fun) or + match?(%Bytecode.Function{}, fun) or match?({:closure, _, %Bytecode.Function{}}, fun) + end + + defp run_arg_update(pc, frame, stack, gas, %Context{arg_buf: arg_buf} = ctx, idx, val) do + locals = elem(frame, Frame.locals()) + + frame = + if idx < tuple_size(locals) do + Closures.write_captured_local( + elem(frame, Frame.l2v()), + idx, + val, + locals, + elem(frame, Frame.var_refs()) + ) + + put_local(frame, idx, val) + else + frame + end + + ctx = put_arg_value(ctx, idx, val, arg_buf) + run(pc + 1, frame, stack, gas, ctx) + end + + # ── Main dispatch loop ── + + defp run(pc, frame, stack, gas, %Context{pd_synced: true} = ctx) do + if ctx.trace_enabled, do: Trace.update_pc(pc) + run(elem(elem(frame, Frame.insns()), pc), pc, frame, stack, gas, ctx) + end + + defp run(pc, frame, stack, gas, %Context{pd_synced: false} = ctx) do + Heap.put_ctx(ctx) + ctx = Context.mark_synced(ctx) + + if ctx.trace_enabled, do: Trace.update_pc(pc) + run(elem(elem(frame, Frame.insns()), pc), pc, frame, stack, gas, ctx) + end + + defp run(pc, frame, stack, gas, ctx) do + Heap.put_ctx(ctx) + if Map.get(ctx, :trace_enabled, false), do: Trace.update_pc(pc) + run(elem(elem(frame, Frame.insns()), pc), pc, frame, stack, gas, ctx) + end + + # ── Push constants ── + + defp run({op, [val]}, pc, frame, stack, gas, ctx) + when op in [ + @op_push_i32, + @op_push_i8, + @op_push_i16, + @op_push_minus1, + @op_push_0, + @op_push_1, + @op_push_2, + @op_push_3, + @op_push_4, + @op_push_5, + @op_push_6, + @op_push_7 + ], + do: run(pc + 1, frame, [val | stack], gas, ctx) + + defp run({op, [idx]}, pc, frame, stack, gas, ctx) + when op in [@op_push_const, @op_push_const8] do + val = Names.resolve_const(elem(frame, Frame.constants()), idx) + val = materialize_constant(val) + run(pc + 1, frame, [val | stack], gas, ctx) + end + + defp run({@op_push_atom_value, [atom_idx]}, pc, frame, stack, gas, ctx) do + run(pc + 1, frame, [Names.resolve_atom(ctx, atom_idx) | stack], gas, ctx) + end + + defp run({@op_undefined, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, [:undefined | stack], gas, ctx) + + defp run({@op_null, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, [nil | stack], gas, ctx) + + defp run({@op_push_false, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, [false | stack], gas, ctx) + + defp run({@op_push_true, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, [true | stack], gas, ctx) + + defp run({@op_push_empty_string, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, ["" | stack], gas, ctx) + + defp run({@op_push_bigint_i32, [val]}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, [{:bigint, val} | stack], gas, ctx) + + # ── Stack manipulation ── + + defp run({@op_drop, []}, pc, frame, [_ | rest], gas, ctx), + do: run(pc + 1, frame, rest, gas, ctx) + + defp run({@op_nip, []}, pc, frame, [a, _b | rest], gas, ctx), + do: run(pc + 1, frame, [a | rest], gas, ctx) + + defp run({@op_nip1, []}, pc, frame, [a, b, _c | rest], gas, ctx), + do: run(pc + 1, frame, [a, b | rest], gas, ctx) + + defp run({@op_dup, []}, pc, frame, [a | _] = stack, gas, ctx), + do: run(pc + 1, frame, [a | stack], gas, ctx) + + defp run({@op_dup1, []}, pc, frame, [a, b | _] = stack, gas, ctx) do + run(pc + 1, frame, [a, b | stack], gas, ctx) + end + + defp run({@op_dup2, []}, pc, frame, [a, b | _] = stack, gas, ctx) do + run(pc + 1, frame, [a, b, a, b | stack], gas, ctx) + end + + defp run({@op_dup3, []}, pc, frame, [a, b, c | _] = stack, gas, ctx) do + run(pc + 1, frame, [a, b, c, a, b, c | stack], gas, ctx) + end + + defp run({@op_insert2, []}, pc, frame, [a, b | rest], gas, ctx), + do: run(pc + 1, frame, [a, b, a | rest], gas, ctx) + + defp run({@op_insert3, []}, pc, frame, [a, b, c | rest], gas, ctx), + do: run(pc + 1, frame, [a, b, c, a | rest], gas, ctx) + + defp run({@op_insert4, []}, pc, frame, [a, b, c, d | rest], gas, ctx), + do: run(pc + 1, frame, [a, b, c, d, a | rest], gas, ctx) + + defp run({@op_perm3, []}, pc, frame, [a, b, c | rest], gas, ctx), + do: run(pc + 1, frame, [a, c, b | rest], gas, ctx) + + defp run({@op_perm4, []}, pc, frame, [a, b, c, d | rest], gas, ctx), + do: run(pc + 1, frame, [a, c, d, b | rest], gas, ctx) + + defp run({@op_perm5, []}, pc, frame, [a, b, c, d, e | rest], gas, ctx), + do: run(pc + 1, frame, [a, c, d, e, b | rest], gas, ctx) + + defp run({@op_swap, []}, pc, frame, [a, b | rest], gas, ctx), + do: run(pc + 1, frame, [b, a | rest], gas, ctx) + + defp run({@op_swap2, []}, pc, frame, [a, b, c, d | rest], gas, ctx), + do: run(pc + 1, frame, [c, d, a, b | rest], gas, ctx) + + defp run({@op_rot3l, []}, pc, frame, [a, b, c | rest], gas, ctx), + do: run(pc + 1, frame, [c, a, b | rest], gas, ctx) + + defp run({@op_rot3r, []}, pc, frame, [a, b, c | rest], gas, ctx), + do: run(pc + 1, frame, [b, c, a | rest], gas, ctx) + + defp run({@op_rot4l, []}, pc, frame, [a, b, c, d | rest], gas, ctx), + do: run(pc + 1, frame, [d, a, b, c | rest], gas, ctx) + + defp run({@op_rot5l, []}, pc, frame, [a, b, c, d, e | rest], gas, ctx), + do: run(pc + 1, frame, [e, a, b, c, d | rest], gas, ctx) + + # ── Args ── + + defp run({op, [idx]}, pc, frame, stack, gas, ctx) + when op in [@op_get_arg, @op_get_arg0, @op_get_arg1, @op_get_arg2, @op_get_arg3], + do: run(pc + 1, frame, [get_arg_value(ctx, idx) | stack], gas, ctx) + + # ── Locals ── + + defp run({op, [idx]}, pc, frame, stack, gas, ctx) + when op in [ + @op_get_loc, + @op_get_loc0, + @op_get_loc1, + @op_get_loc2, + @op_get_loc3, + @op_get_loc8 + ] do + run( + pc + 1, + frame, + [ + Closures.read_captured_local( + elem(frame, Frame.l2v()), + idx, + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()) + ) + | stack + ], + gas, + ctx + ) + end + + defp run({op, [idx]}, pc, frame, [val | rest], gas, ctx) + when op in [ + @op_put_loc, + @op_put_loc0, + @op_put_loc1, + @op_put_loc2, + @op_put_loc3, + @op_put_loc8 + ] do + Closures.write_captured_local( + elem(frame, Frame.l2v()), + idx, + val, + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()) + ) + + run(pc + 1, put_local(frame, idx, val), rest, gas, ctx) + end + + defp run({op, [idx]}, pc, frame, [val | rest], gas, ctx) + when op in [ + @op_set_loc, + @op_set_loc0, + @op_set_loc1, + @op_set_loc2, + @op_set_loc3, + @op_set_loc8 + ] do + Closures.write_captured_local( + elem(frame, Frame.l2v()), + idx, + val, + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()) + ) + + run(pc + 1, put_local(frame, idx, val), [val | rest], gas, ctx) + end + + defp run({@op_set_loc_uninitialized, [idx]}, pc, frame, stack, gas, ctx) do + Closures.write_captured_local( + elem(frame, Frame.l2v()), + idx, + :__tdz__, + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()) + ) + + run(pc + 1, put_local(frame, idx, :__tdz__), stack, gas, ctx) + end + + defp run({@op_get_loc_check, [idx]}, pc, frame, stack, gas, ctx) do + val = elem(elem(frame, Frame.locals()), idx) + ensure_initialized_local!(ctx, idx, val) + run(pc + 1, frame, [val | stack], gas, ctx) + end + + defp run({@op_put_loc_check, [idx]}, pc, frame, [val | rest], gas, ctx) do + ensure_initialized_local!(ctx, idx, val) + + Closures.write_captured_local( + elem(frame, Frame.l2v()), + idx, + val, + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()) + ) + + run(pc + 1, put_local(frame, idx, val), rest, gas, ctx) + end + + defp run({@op_put_loc_check_init, [idx]}, pc, frame, [val | rest], gas, ctx) do + run(pc + 1, put_local(frame, idx, val), rest, gas, ctx) + end + + defp run({@op_get_loc0_loc1, [idx0, idx1]}, pc, frame, stack, gas, ctx) do + locals = elem(frame, Frame.locals()) + run(pc + 1, frame, [elem(locals, idx1), elem(locals, idx0) | stack], gas, ctx) + end + + # ── Variable references (closures) ── + + defp run({op, [idx]}, pc, frame, stack, gas, ctx) + when op in [ + @op_get_var_ref, + @op_get_var_ref0, + @op_get_var_ref1, + @op_get_var_ref2, + @op_get_var_ref3 + ] do + val = + case elem(elem(frame, Frame.var_refs()), idx) do + {:cell, _} = cell -> Closures.read_cell(cell) + other -> other + end + + run(pc + 1, frame, [val | stack], gas, ctx) + end + + defp run({op, [idx]}, pc, frame, [val | rest], gas, ctx) + when op in [ + @op_put_var_ref, + @op_put_var_ref0, + @op_put_var_ref1, + @op_put_var_ref2, + @op_put_var_ref3 + ] do + case elem(elem(frame, Frame.var_refs()), idx) do + {:cell, ref} -> Closures.write_cell({:cell, ref}, val) + _ -> :ok + end + + run(pc + 1, frame, rest, gas, ctx) + end + + defp run({op, [idx]}, pc, frame, [val | rest], gas, ctx) + when op in [ + @op_set_var_ref, + @op_set_var_ref0, + @op_set_var_ref1, + @op_set_var_ref2, + @op_set_var_ref3 + ] do + case elem(elem(frame, Frame.var_refs()), idx) do + {:cell, ref} -> Closures.write_cell({:cell, ref}, val) + _ -> :ok + end + + run(pc + 1, frame, [val | rest], gas, ctx) + end + + defp run({@op_close_loc, [idx]}, pc, frame, stack, gas, ctx) do + case Map.get(elem(frame, Frame.l2v()), idx) do + nil -> + run(pc + 1, frame, stack, gas, ctx) + + vref_idx -> + vrefs = elem(frame, Frame.var_refs()) + old_cell = elem(vrefs, vref_idx) + val = Closures.read_cell(old_cell) + new_ref = make_ref() + Heap.put_cell(new_ref, val) + frame = put_elem(frame, Frame.var_refs(), put_elem(vrefs, vref_idx, {:cell, new_ref})) + run(pc + 1, frame, stack, gas, ctx) + end + end + + # ── Control flow ── + + defp run({op, [target]}, pc, frame, [val | rest], gas, ctx) + when op in [@op_if_false, @op_if_false8] do + if Values.falsy?(val) do + gas = if target <= pc, do: check_gas(pc, frame, rest, gas, ctx), else: gas + run(target, frame, rest, gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run({op, [target]}, pc, frame, [val | rest], gas, ctx) + when op in [@op_if_true, @op_if_true8] do + if Values.truthy?(val) do + gas = if target <= pc, do: check_gas(pc, frame, rest, gas, ctx), else: gas + run(target, frame, rest, gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run({op, [target]}, __pc, frame, stack, gas, ctx) + when op in [@op_goto, @op_goto8, @op_goto16] do + run(target, frame, stack, gas, ctx) + end + + defp run({@op_return, []}, _pc, _frame, [val | _], _gas, _ctx), do: val + + defp run({@op_return_undef, []}, _pc, _frame, _stack, _gas, _ctx), do: :undefined + + # ── Arithmetic ── + + defp run({@op_add, []}, pc, frame, [b, a | rest], gas, %Context{catch_stack: [_ | _]} = ctx) do + run(pc + 1, frame, [Values.add(a, b) | rest], gas, ctx) + catch + {:js_throw, val} -> throw_or_catch(frame, val, gas, ctx) + end + + defp run({@op_add, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.add(a, b) | rest], gas, ctx) + + defp run({@op_sub, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.sub(a, b) | rest], gas, ctx) + + defp run({@op_mul, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.mul(a, b) | rest], gas, ctx) + + defp run({@op_div, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.div(a, b) | rest], gas, ctx) + + defp run({@op_mod, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.mod(a, b) | rest], gas, ctx) + + defp run({@op_pow, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.pow(a, b) | rest], gas, ctx) + + # ── Bitwise ── + + defp run({@op_band, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.band(a, b) | rest], gas, ctx) + + defp run({@op_bor, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.bor(a, b) | rest], gas, ctx) + + defp run({@op_bxor, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.bxor(a, b) | rest], gas, ctx) + + defp run({@op_shl, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.shl(a, b) | rest], gas, ctx) + + defp run({@op_sar, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.sar(a, b) | rest], gas, ctx) + + defp run({@op_shr, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.shr(a, b) | rest], gas, ctx) + + # ── Comparison ── + + defp run({@op_lt, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.lt(a, b) | rest], gas, ctx) + + defp run({@op_lte, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.lte(a, b) | rest], gas, ctx) + + defp run({@op_gt, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.gt(a, b) | rest], gas, ctx) + + defp run({@op_gte, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.gte(a, b) | rest], gas, ctx) + + defp run({@op_eq, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.eq(a, b) | rest], gas, ctx) + + defp run({@op_neq, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.neq(a, b) | rest], gas, ctx) + + defp run({@op_strict_eq, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.strict_eq(a, b) | rest], gas, ctx) + + defp run({@op_strict_neq, []}, pc, frame, [b, a | rest], gas, ctx), + do: run(pc + 1, frame, [not Values.strict_eq(a, b) | rest], gas, ctx) + + # ── Unary ── + + defp run({@op_neg, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.neg(a) | rest], gas, ctx) + + defp run({@op_plus, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.to_number(a) | rest], gas, ctx) + + defp run({@op_inc, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.add(a, 1) | rest], gas, ctx) + + defp run({@op_dec, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.sub(a, 1) | rest], gas, ctx) + + defp run({@op_post_inc, []}, pc, frame, [a | rest], gas, ctx) do + num = Values.to_number(a) + run(pc + 1, frame, [Values.add(num, 1), num | rest], gas, ctx) + end + + defp run({@op_post_dec, []}, pc, frame, [a | rest], gas, ctx) do + num = Values.to_number(a) + run(pc + 1, frame, [Values.sub(num, 1), num | rest], gas, ctx) + end + + defp run({@op_inc_loc, [idx]}, pc, frame, stack, gas, ctx) do + locals = elem(frame, Frame.locals()) + vrefs = elem(frame, Frame.var_refs()) + l2v = elem(frame, Frame.l2v()) + new_val = Values.add(elem(locals, idx), 1) + Closures.write_captured_local(l2v, idx, new_val, locals, vrefs) + run(pc + 1, put_local(frame, idx, new_val), stack, gas, ctx) + end + + defp run({@op_dec_loc, [idx]}, pc, frame, stack, gas, ctx) do + locals = elem(frame, Frame.locals()) + vrefs = elem(frame, Frame.var_refs()) + l2v = elem(frame, Frame.l2v()) + new_val = Values.sub(elem(locals, idx), 1) + Closures.write_captured_local(l2v, idx, new_val, locals, vrefs) + run(pc + 1, put_local(frame, idx, new_val), stack, gas, ctx) + end + + defp run({@op_add_loc, [idx]}, pc, frame, [val | rest], gas, ctx) do + locals = elem(frame, Frame.locals()) + vrefs = elem(frame, Frame.var_refs()) + l2v = elem(frame, Frame.l2v()) + new_val = Values.add(elem(locals, idx), val) + Closures.write_captured_local(l2v, idx, new_val, locals, vrefs) + run(pc + 1, put_local(frame, idx, new_val), rest, gas, ctx) + end + + defp run({@op_not, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.to_int32(bnot(Values.to_int32(a))) | rest], gas, ctx) + + defp run({@op_lnot, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [not Values.truthy?(a) | rest], gas, ctx) + + defp run({@op_typeof, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [Values.typeof(a) | rest], gas, ctx) + + # ── Function creation / calls ── + + defp run({op, [idx]}, pc, frame, stack, gas, ctx) when op in [@op_fclosure, @op_fclosure8] do + fun = Names.resolve_const(elem(frame, Frame.constants()), idx) + vrefs = elem(frame, Frame.var_refs()) + + closure = + build_closure( + fun, + elem(frame, Frame.locals()), + vrefs, + elem(frame, Frame.l2v()), + ctx + ) + + run(pc + 1, frame, [closure | stack], gas, ctx) + end + + defp run({op, [argc]}, pc, frame, stack, gas, ctx) + when op in [@op_call, @op_call0, @op_call1, @op_call2, @op_call3], + do: call_function(pc, frame, stack, argc, gas, ctx) + + defp run({@op_tail_call, [argc]}, _pc, _frame, stack, gas, ctx), + do: tail_call(stack, argc, gas, ctx) + + defp run({@op_call_method, [argc]}, pc, frame, stack, gas, ctx), + do: call_method(pc, frame, stack, argc, gas, ctx) + + defp run({@op_tail_call_method, [argc]}, _pc, _frame, stack, gas, ctx), + do: tail_call_method(stack, argc, gas, ctx) + + # ── Objects ── + + defp run({@op_object, []}, pc, frame, stack, gas, ctx) do + ref = make_ref() + proto = Heap.get_object_prototype() + init = if proto, do: %{proto() => proto}, else: %{} + Heap.put_obj(ref, init) + run(pc + 1, frame, [{:obj, ref} | stack], gas, ctx) + end + + defp run({@op_get_field, [atom_idx]}, __pc, frame, [obj | _rest], gas, ctx) + when obj == nil or obj == :undefined do + throw_null_property_error(frame, obj, atom_idx, gas, ctx) + end + + defp run({@op_get_field, [atom_idx]}, pc, frame, [obj | rest], gas, ctx) do + run( + pc + 1, + frame, + [Get.get(obj, Names.resolve_atom(ctx, atom_idx)) | rest], + gas, + ctx + ) + end + + defp run({@op_put_field, [atom_idx]}, pc, frame, [val, obj | rest], gas, ctx) do + try do + Put.put(obj, Names.resolve_atom(ctx, atom_idx), val) + run(pc + 1, frame, rest, gas, ctx) + catch + {:js_throw, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + defp run({@op_define_field, [atom_idx]}, pc, frame, [val, obj | rest], gas, ctx) do + try do + Put.put(obj, Names.resolve_atom(ctx, atom_idx), val) + run(pc + 1, frame, [obj | rest], gas, ctx) + catch + {:js_throw, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + defp run({@op_get_array_el, []}, pc, frame, [idx, obj | rest], gas, ctx) do + run(pc + 1, frame, [Put.get_element(obj, idx) | rest], gas, ctx) + end + + defp run({@op_put_array_el, []}, pc, frame, [val, idx, obj | rest], gas, ctx) do + try do + Put.put_element(obj, idx, val) + run(pc + 1, frame, rest, gas, ctx) + catch + {:js_throw, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + defp run({@op_get_super_value, []}, pc, frame, [key, proto, this_obj | rest], gas, ctx) do + val = Class.get_super_value(proto, this_obj, key) + run(pc + 1, frame, [val | rest], gas, ctx) + end + + defp run({@op_put_super_value, []}, pc, frame, [val, key, proto_obj, this_obj | rest], gas, ctx) do + try do + Class.put_super_value(proto_obj, this_obj, key, val) + run(pc + 1, frame, rest, gas, ctx) + catch + {:js_throw, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + defp run({@op_get_private_field, []}, pc, frame, [key, obj | rest], gas, ctx) do + case Private.get_field(obj, key) do + :missing -> throw_or_catch(frame, Private.brand_error(), gas, ctx) + val -> run(pc + 1, frame, [val | rest], gas, ctx) + end + end + + defp run({@op_put_private_field, []}, pc, frame, [key, val, obj | rest], gas, ctx) do + case Private.put_field!(obj, key, val) do + :ok -> run(pc + 1, frame, rest, gas, ctx) + :error -> throw_or_catch(frame, Private.brand_error(), gas, ctx) + end + end + + defp run({@op_define_private_field, []}, pc, frame, [val, key, obj | rest], gas, ctx) do + case Private.define_field!(obj, key, val) do + :ok -> run(pc + 1, frame, rest, gas, ctx) + :error -> throw_or_catch(frame, Private.brand_error(), gas, ctx) + end + end + + defp run({@op_private_in, []}, pc, frame, [key, obj | rest], gas, ctx) do + run(pc + 1, frame, [Private.has_field?(obj, key) | rest], gas, ctx) + end + + defp run({@op_get_length, []}, pc, frame, [obj | rest], gas, ctx) do + run(pc + 1, frame, [Get.length_of(obj) | rest], gas, ctx) + end + + defp run({@op_array_from, [argc]}, pc, frame, stack, gas, ctx) do + {elems, rest} = Enum.split(stack, argc) + ref = make_ref() + Heap.put_obj(ref, Enum.reverse(elems)) + run(pc + 1, frame, [{:obj, ref} | rest], gas, ctx) + end + + # ── Misc / no-op ── + + defp run({@op_nop, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run({@op_to_object, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run({@op_to_propkey, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run({@op_to_propkey2, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run({@op_check_ctor, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run({@op_check_ctor_return, []}, pc, frame, [val | rest], gas, ctx) do + case Class.check_ctor_return(val) do + {replace_with_this?, checked_val} -> + run(pc + 1, frame, [replace_with_this?, checked_val | rest], gas, ctx) + + :error -> + throw_or_catch( + frame, + Heap.make_error( + "Derived constructors may only return object or undefined", + "TypeError" + ), + gas, + ctx + ) + end + end + + defp run({@op_set_name, [atom_idx]}, pc, frame, [fun | rest], gas, ctx) do + named = Functions.set_name_atom(fun, atom_idx, ctx.atoms) + run(pc + 1, frame, [named | rest], gas, ctx) + end + + defp run({@op_throw, []}, __pc, frame, [val | _], gas, ctx) do + throw_or_catch(frame, val, gas, ctx) + end + + defp run({@op_is_undefined, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [a == :undefined | rest], gas, ctx) + + defp run({@op_is_null, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [a == nil | rest], gas, ctx) + + defp run({@op_is_undefined_or_null, []}, pc, frame, [a | rest], gas, ctx), + do: run(pc + 1, frame, [a == :undefined or a == nil | rest], gas, ctx) + + defp run({@op_invalid, []}, _pc, _frame, _stack, _gas, _ctx), + do: throw({:error, :invalid_opcode}) + + defp run({@op_get_var_undef, [atom_idx]}, pc, frame, stack, gas, ctx) do + run(pc + 1, frame, [GlobalEnv.get(ctx, atom_idx, :undefined) | stack], gas, ctx) + end + + defp run({@op_get_var, [atom_idx]}, pc, frame, stack, gas, ctx) do + case GlobalEnv.fetch(ctx, atom_idx) do + {:found, val} -> + run(pc + 1, frame, [val | stack], gas, ctx) + + :not_found -> + error = + Heap.make_error("#{Names.resolve_atom(ctx, atom_idx)} is not defined", "ReferenceError") + + throw_or_catch(frame, error, gas, ctx) + end + end + + defp run({op, [atom_idx]}, pc, frame, [val | rest], gas, ctx) + when op in [@op_put_var, @op_put_var_init] do + new_ctx = GlobalEnv.put(ctx, atom_idx, val) + run(pc + 1, frame, rest, gas, new_ctx) + end + + defp run({@op_define_func, [atom_idx, _flags]}, pc, frame, [fun | rest], gas, ctx) do + next_ctx = GlobalEnv.put(ctx, atom_idx, fun) + run(pc + 1, frame, rest, gas, next_ctx) + end + + defp run({@op_define_var, [atom_idx, _scope]}, pc, frame, stack, gas, ctx) do + GlobalEnv.define_var(ctx, atom_idx) + run(pc + 1, frame, stack, gas, ctx) + end + + defp run({@op_check_define_var, [atom_idx, _scope]}, pc, frame, stack, gas, ctx) do + GlobalEnv.check_define_var(ctx, atom_idx) + run(pc + 1, frame, stack, gas, ctx) + end + + defp run({@op_get_field2, [atom_idx]}, __pc, frame, [obj | _rest], gas, ctx) + when obj == nil or obj == :undefined do + throw_null_property_error(frame, obj, atom_idx, gas, ctx) + end + + defp run({@op_get_field2, [atom_idx]}, pc, frame, [obj | rest], gas, ctx) do + val = Get.get(obj, Names.resolve_atom(ctx, atom_idx)) + run(pc + 1, frame, [val, obj | rest], gas, ctx) + end + + # ── try/catch ── + + defp run({@op_catch, [target]}, pc, frame, stack, gas, %Context{catch_stack: catch_stack} = ctx) do + ctx = Context.mark_dirty(%{ctx | catch_stack: [{target, stack} | catch_stack]}) + run(pc + 1, frame, [target | stack], gas, ctx) + end + + defp run( + {@op_nip_catch, []}, + pc, + frame, + [a, _catch_offset | rest], + gas, + %Context{catch_stack: [_ | rest_catch]} = ctx + ) do + run(pc + 1, frame, [a | rest], gas, Context.mark_dirty(%{ctx | catch_stack: rest_catch})) + end + + # ── for-in ── + + defp run({@op_for_in_start, []}, _pc, frame, [obj | _rest], gas, ctx) + when obj == :uninitialized do + throw_or_catch(frame, Heap.make_error("this is not initialized", "ReferenceError"), gas, ctx) + end + + defp run({@op_for_in_start, []}, pc, frame, [obj | rest], gas, ctx) do + keys = Copy.enumerable_keys(obj) + run(pc + 1, frame, [{:for_in_iterator, keys} | rest], gas, ctx) + end + + defp run( + {@op_for_in_next, []}, + pc, + frame, + [{:for_in_iterator, [key | rest_keys]} | rest], + gas, + ctx + ) do + run(pc + 1, frame, [false, key, {:for_in_iterator, rest_keys} | rest], gas, ctx) + end + + defp run({@op_for_in_next, []}, pc, frame, [iter | rest], gas, ctx) do + run(pc + 1, frame, [true, :undefined, iter | rest], gas, ctx) + end + + # ── new / constructor ── + + defp run({@op_call_constructor, [argc]}, pc, frame, stack, gas, ctx) do + {args, [new_target, ctor | rest]} = Enum.split(stack, argc) + + gas = check_gas(pc, frame, rest, gas, ctx) + + catch_js_throw(pc, frame, rest, gas, ctx, fn -> + rev_args = Enum.reverse(args) + + raw_ctor = + case ctor do + {:closure, _, %Bytecode.Function{} = f} -> f + {:bound, _, inner, _, _} -> inner + other -> other + end + + # Generators and async generators cannot be constructors + case raw_ctor do + %Bytecode.Function{func_kind: fk} when fk in [@func_generator, @func_async_generator] -> + name = raw_ctor.name || "anonymous" + throw({:js_throw, Heap.make_error("#{name} is not a constructor", "TypeError")}) + + _ -> + :ok + end + + this_ref = make_ref() + + raw_new_target = + case new_target do + {:closure, _, %Bytecode.Function{} = f} -> f + %Bytecode.Function{} = f -> f + _ -> nil + end + + proto = + if raw_new_target != nil and raw_new_target != raw_ctor do + Heap.get_class_proto(raw_new_target) || Heap.get_class_proto(raw_ctor) || + Heap.get_or_create_prototype(ctor) + else + Heap.get_class_proto(raw_ctor) || Heap.get_or_create_prototype(ctor) + end + + init = if proto, do: %{proto() => proto}, else: %{} + Heap.put_obj(this_ref, init) + fresh_this = {:obj, this_ref} + + this_obj = + case raw_ctor do + %Bytecode.Function{is_derived_class_constructor: true} -> + {:uninitialized, fresh_this} + + _ -> + fresh_this + end + + ctor_ctx = Context.mark_dirty(%{ctx | this: this_obj, new_target: new_target}) + + result = + case ctor do + %Bytecode.Function{} = f -> + do_invoke( + f, + {:closure, %{}, f}, + rev_args, + ClosureBuilder.ctor_var_refs(f), + gas, + ctor_ctx + ) + + {:closure, captured, %Bytecode.Function{} = f} -> + do_invoke( + f, + {:closure, captured, f}, + rev_args, + ClosureBuilder.ctor_var_refs(f, captured), + gas, + ctor_ctx + ) + + {:bound, _, _, orig_fun, bound_args} -> + all_args = bound_args ++ rev_args + + case orig_fun do + %Bytecode.Function{} = f -> + do_invoke( + f, + {:closure, %{}, f}, + all_args, + ClosureBuilder.ctor_var_refs(f), + gas, + ctor_ctx + ) + + {:closure, captured, %Bytecode.Function{} = f} -> + do_invoke( + f, + {:closure, captured, f}, + all_args, + ClosureBuilder.ctor_var_refs(f, captured), + gas, + ctor_ctx + ) + + {:builtin, _, cb} when is_function(cb, 2) -> + cb.(all_args, this_obj) + + _ -> + this_obj + end + + {:builtin, name, cb} when is_function(cb, 2) -> + obj = cb.(rev_args, nil) + + if name in ~w(Number String Boolean) do + # Store primitive value for valueOf() on wrapper objects + existing = Heap.get_obj(this_ref, %{}) + val_fn = {:builtin, "valueOf", fn _, _ -> obj end} + + to_str_fn = + {:builtin, "toString", fn _, _ -> Values.stringify(obj) end} + + Heap.put_obj( + this_ref, + existing + |> Map.merge( + build_methods do + val("valueOf", val_fn) + val("toString", to_str_fn) + end + ) + |> Map.put(primitive_value(), obj) + ) + end + + if name in ~w(Error TypeError RangeError SyntaxError ReferenceError URIError EvalError) do + case obj do + {:obj, ref} -> + existing = Heap.get_obj(ref, %{}) + + if is_map(existing) and not Map.has_key?(existing, "name") do + Heap.put_obj(ref, Map.put(existing, "name", name)) + end + + _ -> + :ok + end + end + + obj + + _ -> + this_obj + end + + result = Class.coalesce_this_result(result, this_obj) + + if match?({:uninitialized, _}, result) do + throw({:js_throw, Heap.make_error("this is not initialized", "ReferenceError")}) + end + + case {result, Heap.get_class_proto(raw_ctor)} do + {{:obj, rref}, {:obj, _} = proto2} -> + rmap = Heap.get_obj(rref, %{}) + + unless Map.has_key?(rmap, proto()) do + Heap.put_obj(rref, Map.put(rmap, proto(), proto2)) + end + + _ -> + :ok + end + + result + end) + end + + defp run({@op_init_ctor, []}, pc, frame, stack, gas, %Context{arg_buf: arg_buf} = ctx) do + raw = + case ctx.current_func do + {:closure, _, %Bytecode.Function{} = f} -> f + %Bytecode.Function{} = f -> f + other -> other + end + + parent = Heap.get_parent_ctor(raw) + args = Tuple.to_list(arg_buf) + + pending_this = + case ctx.this do + {:uninitialized, {:obj, _} = obj} -> obj + {:obj, _} = obj -> obj + _ -> ctx.this + end + + parent_ctx = Context.mark_dirty(%{ctx | this: pending_this}) + + result = + case parent do + nil -> + pending_this + + %Bytecode.Function{} = f -> + do_invoke(f, {:closure, %{}, f}, args, ClosureBuilder.ctor_var_refs(f), gas, parent_ctx) + + {:closure, captured, %Bytecode.Function{} = f} -> + do_invoke( + f, + {:closure, captured, f}, + args, + ClosureBuilder.ctor_var_refs(f, captured), + gas, + parent_ctx + ) + + {:builtin, _name, cb} when is_function(cb, 2) -> + cb.(args, pending_this) + + _ -> + pending_this + end + + result = + case result do + {:obj, _} = obj -> obj + _ -> pending_this + end + + run(pc + 1, frame, [result | stack], gas, Context.mark_dirty(%{ctx | this: result})) + end + + # ── instanceof ── + + defp run({@op_instanceof, []}, pc, frame, [ctor, obj | rest], gas, ctx) do + result = + case obj do + {:obj, _} -> + ctor_proto = Get.get(ctor, "prototype") + check_prototype_chain(obj, ctor_proto) + + _ -> + false + end + + run(pc + 1, frame, [result | rest], gas, ctx) + end + + # ── delete ── + + defp run({@op_delete, []}, __pc, frame, [key, obj | _rest], gas, ctx) + when obj == nil or obj == :undefined do + nullish = if obj == nil, do: "null", else: "undefined" + + error = + Heap.make_error("Cannot delete properties of #{nullish} (deleting '#{key}')", "TypeError") + + throw_or_catch(frame, error, gas, ctx) + end + + defp run({@op_delete, []}, pc, frame, [key, obj | rest], gas, ctx) do + result = + case obj do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + + if is_map(map) do + desc = Heap.get_prop_desc(ref, key) + + if match?(%{configurable: false}, desc) do + false + else + new_map = Map.delete(map, key) + Heap.put_obj(ref, new_map) + true + end + else + true + end + + _ -> + true + end + + run(pc + 1, frame, [result | rest], gas, ctx) + end + + defp run({@op_delete_var, [_atom_idx]}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, [true | stack], gas, ctx) + + # ── in operator ── + + defp run({@op_in, []}, pc, frame, [obj, key | rest], gas, ctx) do + run(pc + 1, frame, [Put.has_property(obj, key) | rest], gas, ctx) + end + + # ── regexp literal ── + + defp run({@op_regexp, []}, pc, frame, [pattern, flags | rest], gas, ctx) do + run(pc + 1, frame, [{:regexp, pattern, flags} | rest], gas, ctx) + end + + # ── spread / array construction ── + + defp run({@op_append, []}, pc, frame, [obj, idx, arr | rest], gas, ctx) do + src_list = + case obj do + {:qb_arr, arr} -> + :array.to_list(arr) + + list when is_list(list) -> + list + + {:obj, ref} -> + stored = Heap.get_obj(ref) + + cond do + match?({:qb_arr, _}, stored) -> + Heap.to_list({:obj, ref}) + + is_list(stored) -> + stored + + is_map(stored) and Map.has_key?(stored, {:symbol, "Symbol.iterator"}) -> + iter_fn = Map.get(stored, {:symbol, "Symbol.iterator"}) + iter_obj = Runtime.call_callback(iter_fn, []) + collect_iterator(iter_obj, []) + + is_map(stored) and Map.has_key?(stored, set_data()) -> + Map.get(stored, set_data(), []) + + is_map(stored) and Map.has_key?(stored, map_data()) -> + Map.get(stored, map_data(), []) + + true -> + [] + end + + _ -> + [] + end + + arr_list = + case arr do + {:qb_arr, arr_data} -> :array.to_list(arr_data) + list when is_list(list) -> list + {:obj, ref} -> Heap.to_list({:obj, ref}) + _ -> [] + end + + merged = arr_list ++ src_list + new_idx = if(is_integer(idx), do: idx, else: Runtime.to_int(idx)) + length(src_list) + + merged_obj = + case arr do + {:obj, ref} -> + Heap.put_obj(ref, merged) + {:obj, ref} + + _ -> + merged + end + + run(pc + 1, frame, [new_idx, merged_obj | rest], gas, ctx) + end + + defp run({@op_define_array_el, []}, pc, frame, [val, idx, obj | rest], gas, ctx) do + {_idx, obj2} = Put.define_array_el(obj, idx, val) + run(pc + 1, frame, [idx, obj2 | rest], gas, ctx) + end + + # ── Closure variable refs (mutable) ── + + defp run({op, [idx]}, pc, frame, stack, gas, ctx) + when op in [@op_make_var_ref, @op_make_loc_ref] do + ref = make_ref() + Heap.put_cell(ref, elem(elem(frame, Frame.locals()), idx)) + run(pc + 1, frame, [{:cell, ref} | stack], gas, ctx) + end + + defp run({@op_make_arg_ref, [idx]}, pc, frame, stack, gas, ctx) do + ref = make_ref() + Heap.put_cell(ref, get_arg_value(ctx, idx)) + run(pc + 1, frame, [{:cell, ref} | stack], gas, ctx) + end + + defp run({@op_get_var_ref_check, [idx]}, pc, frame, stack, gas, ctx) do + case elem(elem(frame, Frame.var_refs()), idx) do + :__tdz__ -> + message = + if current_var_ref_name(ctx, idx) == "this", + do: "this is not initialized", + else: "Cannot access variable before initialization" + + throw({:js_throw, Heap.make_error(message, "ReferenceError")}) + + {:cell, _} = cell -> + val = Closures.read_cell(cell) + + if val == :__tdz__ and current_var_ref_name(ctx, idx) == "this" and + derived_this_uninitialized?(ctx) do + throw({:js_throw, Heap.make_error("this is not initialized", "ReferenceError")}) + end + + run(pc + 1, frame, [val | stack], gas, ctx) + + val -> + run(pc + 1, frame, [val | stack], gas, ctx) + end + end + + defp run({op, [idx]}, pc, frame, [val | rest], gas, ctx) + when op in [@op_put_var_ref_check, @op_put_var_ref_check_init] do + case elem(elem(frame, Frame.var_refs()), idx) do + {:cell, ref} -> Closures.write_cell({:cell, ref}, val) + _ -> :ok + end + + run(pc + 1, frame, rest, gas, ctx) + end + + defp run({@op_get_ref_value, []}, pc, frame, [ref | rest], gas, ctx) do + run(pc + 1, frame, [Closures.read_cell(ref) | rest], gas, ctx) + end + + defp run({@op_put_ref_value, []}, pc, frame, [val, {:cell, _} = ref | rest], gas, ctx) do + Closures.write_cell(ref, val) + run(pc + 1, frame, [val | rest], gas, ctx) + end + + defp run({@op_put_ref_value, []}, pc, frame, [val, key, obj | rest], gas, ctx) + when is_binary(key) do + try do + Put.put(obj, key, val) + run(pc + 1, frame, rest, gas, ctx) + catch + {:js_throw, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + # ── gosub/ret (finally blocks) ── + + defp run({@op_gosub, [target]}, pc, frame, stack, gas, ctx) do + run(target, frame, [{:return_addr, pc + 1} | stack], gas, ctx) + end + + defp run({@op_ret, []}, __pc, frame, [{:return_addr, ret_pc} | rest], gas, ctx) do + run(ret_pc, frame, rest, gas, ctx) + end + + # ── eval ── + + defp run({@op_import, []}, pc, frame, [specifier, _import_meta | rest], gas, ctx) do + result = + if is_binary(specifier) and ctx.runtime_pid != nil do + case QuickBEAM.Runtime.load_module(ctx.runtime_pid, specifier, "") do + :ok -> + # Module loaded — create a module namespace object + # For now, return an empty object (module exports would need linking) + Promise.resolved(Runtime.new_object()) + + {:error, _} -> + Promise.rejected(Heap.make_error("Cannot find module '#{specifier}'", "TypeError")) + end + else + Promise.rejected(Heap.make_error("Invalid module specifier", "TypeError")) + end + + run(pc + 1, frame, [result | rest], gas, ctx) + end + + defp run({@op_eval, [argc | scope_args]}, pc, frame, stack, gas, ctx) do + {args, rest} = Enum.split(stack, argc + 1) + eval_ref = List.last(args) + call_args = Enum.take(args, argc) |> Enum.reverse() + scope_depth = List.first(scope_args, -1) + var_objs = eval_scope_var_objects(frame, ctx, scope_args != [], scope_depth) + + run_eval_or_call(pc, frame, rest, gas, ctx, eval_ref, call_args, scope_depth, var_objs) + end + + defp run({@op_apply_eval, [scope_idx_raw]}, pc, frame, [arg_array, fun | rest], gas, ctx) do + args = Heap.to_list(arg_array) + scope_idx = scope_idx_raw - 1 + var_objs = eval_scope_var_objects(frame, ctx, scope_idx >= 0, scope_idx) + + run_eval_or_call(pc, frame, rest, gas, ctx, fun, args, scope_idx, var_objs) + end + + # ── Iterators ── + + defp run({@op_for_of_start, []}, pc, frame, [obj | rest], gas, ctx) do + {iter_obj, next_fn} = + case obj do + list when is_list(list) -> + make_list_iterator(list) + + {:obj, ref} -> + stored = Heap.get_obj(ref) + + case stored do + {:qb_arr, arr} -> + make_list_iterator(:array.to_list(arr)) + + list when is_list(list) -> + make_list_iterator(list) + + map when is_map(map) -> + sym_iter = {:symbol, "Symbol.iterator"} + + cond do + Map.has_key?(map, sym_iter) -> + iter_fn = Map.get(map, sym_iter) + iter_obj = Runtime.call_callback(iter_fn, []) + {iter_obj, Get.get(iter_obj, "next")} + + Map.has_key?(map, "next") -> + {obj, Get.get(obj, "next")} + + true -> + make_list_iterator([]) + end + + _ -> + make_list_iterator([]) + end + + s when is_binary(s) -> + make_list_iterator(String.codepoints(s)) + + _ -> + make_list_iterator([]) + end + + run(pc + 1, frame, [0, next_fn, iter_obj | rest], gas, ctx) + end + + defp run({@op_for_of_next, [idx]}, pc, frame, stack, gas, ctx) do + offset = 3 + idx + iter_obj = Enum.at(stack, offset - 1) + next_fn = Enum.at(stack, offset - 2) + + if iter_obj == :undefined do + run(pc + 1, frame, [true, :undefined | stack], gas, ctx) + else + result = Runtime.call_callback(next_fn, []) + done = Get.get(result, "done") + value = Get.get(result, "value") + + if done == true do + cleared = List.replace_at(stack, offset - 1, :undefined) + run(pc + 1, frame, [true, :undefined | cleared], gas, ctx) + else + run(pc + 1, frame, [false, value | stack], gas, ctx) + end + end + end + + # iterator_next: stack is [val, catch_offset, next_fn, iter_obj | rest] + # Calls next_fn(iter_obj, val), replaces val (top) with raw result object + defp run( + {@op_iterator_next, []}, + pc, + frame, + [val, catch_offset, next_fn, iter_obj | rest], + gas, + ctx + ) do + result = Runtime.call_callback(next_fn, [val]) + run(pc + 1, frame, [result, catch_offset, next_fn, iter_obj | rest], gas, ctx) + end + + defp run({@op_iterator_get_value_done, []}, pc, frame, [result | rest], gas, ctx) do + done = Get.get(result, "done") + value = Get.get(result, "value") + + if done == true do + run(pc + 1, frame, [true, :undefined | rest], gas, ctx) + else + run(pc + 1, frame, [false, value | rest], gas, ctx) + end + end + + defp run( + {@op_iterator_close, []}, + pc, + frame, + [_catch_offset, _next_fn, iter_obj | rest], + gas, + ctx + ) do + if iter_obj != :undefined do + return_fn = Get.get(iter_obj, "return") + + if return_fn != :undefined and return_fn != nil do + Runtime.call_callback(return_fn, []) + end + end + + run(pc + 1, frame, rest, gas, ctx) + end + + defp run({@op_iterator_check_object, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run({@op_iterator_call, [flags]}, pc, frame, stack, gas, ctx) do + [_val, _catch_offset, _next_fn, iter_obj | _] = stack + method_name = if Bitwise.band(flags, 1) == 1, do: "throw", else: "return" + method = Get.get(iter_obj, method_name) + + if method == :undefined or method == nil do + run(pc + 1, frame, [true | stack], gas, ctx) + else + result = + if Bitwise.band(flags, 2) == 2 do + Runtime.call_callback(method, []) + else + [val | _] = stack + Runtime.call_callback(method, [val]) + end + + [_ | rest] = stack + run(pc + 1, frame, [false, result | rest], gas, ctx) + end + end + + defp run({@op_iterator_call, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + # ── Misc stubs ── + + defp run({op, [idx]}, pc, frame, [val | rest], gas, %Context{} = ctx) + when op in [@op_put_arg, @op_put_arg0, @op_put_arg1, @op_put_arg2, @op_put_arg3] do + run_arg_update(pc, frame, rest, gas, ctx, idx, val) + end + + defp run({@op_set_home_object, []}, pc, frame, [method, target | _] = stack, gas, ctx) do + key = {:qb_home_object, Functions.home_object_key(method)} + if key != {:qb_home_object, nil}, do: Process.put(key, target) + run(pc + 1, frame, stack, gas, ctx) + end + + defp run({@op_set_proto, []}, pc, frame, [proto, obj | rest], gas, ctx) do + case obj do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + + if is_map(map) do + Heap.put_obj(ref, Map.put(map, proto(), proto)) + end + + _ -> + :ok + end + + run(pc + 1, frame, [obj | rest], gas, ctx) + end + + defp run( + {@op_special_object, [type]}, + pc, + frame, + stack, + gas, + %Context{arg_buf: arg_buf, current_func: current_func, home_object: home_object} = ctx + ) do + val = + case type do + 0 -> + args_list = Tuple.to_list(arg_buf) + Heap.wrap(args_list) + + 1 -> + args_list = Tuple.to_list(arg_buf) + Heap.wrap(args_list) + + 2 -> + current_func + + 3 -> + ctx.new_target + + 4 -> + if home_object == :undefined do + key = {:qb_home_object, Functions.home_object_key(current_func)} + Process.get(key, :undefined) + else + home_object + end + + 5 -> + Heap.wrap(%{}) + + 6 -> + Heap.wrap(%{}) + + 7 -> + Heap.wrap(%{"__proto__" => nil}) + + _ -> + :undefined + end + + run(pc + 1, frame, [val | stack], gas, ctx) + end + + defp run({@op_rest, [start_idx]}, pc, frame, stack, gas, %Context{arg_buf: arg_buf} = ctx) do + rest_args = + if start_idx < tuple_size(arg_buf) do + Tuple.to_list(arg_buf) |> Enum.drop(start_idx) + else + [] + end + + ref = make_ref() + Heap.put_obj(ref, rest_args) + run(pc + 1, frame, [{:obj, ref} | stack], gas, ctx) + end + + defp run({@op_typeof_is_function, []}, pc, frame, [val | rest], gas, ctx) do + result = Builtin.callable?(val) + + run(pc + 1, frame, [result | rest], gas, ctx) + end + + defp run({@op_typeof_is_undefined, []}, pc, frame, [val | rest], gas, ctx) do + result = val == :undefined or val == nil + run(pc + 1, frame, [result | rest], gas, ctx) + end + + defp run({@op_throw_error, []}, _pc, _frame, [val | _], _gas, _ctx), do: throw({:js_throw, val}) + + defp run({@op_throw_error, [atom_idx, reason]}, __pc, frame, _stack, gas, ctx) do + name = Names.resolve_atom(ctx, atom_idx) + {error_type, message} = RuntimeHelpers.throw_error_message(name, reason) + throw_or_catch(frame, Heap.make_error(message, error_type), gas, ctx) + end + + defp run({@op_set_name_computed, []}, pc, frame, [fun, name_val | rest], gas, ctx) do + named = Functions.set_name_computed(fun, name_val) + run(pc + 1, frame, [named, name_val | rest], gas, ctx) + end + + defp run({@op_copy_data_properties, []}, pc, frame, stack, gas, ctx), + do: run(pc + 1, frame, stack, gas, ctx) + + defp run( + {@op_get_super, []}, + pc, + frame, + [func | rest], + gas, + %Context{home_object: home_object, super: super} = ctx + ) do + val = if func == home_object, do: super, else: Class.get_super(func) + run(pc + 1, frame, [val | rest], gas, ctx) + end + + defp run({@op_push_this, []}, _pc, frame, _stack, gas, %Context{this: this} = ctx) + when this == :uninitialized or + (is_tuple(this) and tuple_size(this) == 2 and elem(this, 0) == :uninitialized) do + throw_or_catch(frame, Heap.make_error("this is not initialized", "ReferenceError"), gas, ctx) + end + + defp run({@op_push_this, []}, pc, frame, stack, gas, %Context{this: this} = ctx) do + run(pc + 1, frame, [this | stack], gas, ctx) + end + + defp run({@op_private_symbol, [atom_idx]}, pc, frame, stack, gas, ctx) do + name = Names.resolve_atom(ctx, atom_idx) + run(pc + 1, frame, [Private.private_symbol(name) | stack], gas, ctx) + end + + # ── Argument mutation ── + + defp run({op, [idx]}, pc, frame, [val | rest], gas, %Context{} = ctx) + when op in [@op_set_arg, @op_set_arg0, @op_set_arg1, @op_set_arg2, @op_set_arg3] do + run_arg_update(pc, frame, [val | rest], gas, ctx, idx, val) + end + + # ── Array element access (2-element push) ── + + defp run({@op_get_array_el2, []}, pc, frame, [idx, obj | rest], gas, ctx) do + run(pc + 1, frame, [Get.get(obj, idx), obj | rest], gas, ctx) + end + + # ── Spread/rest via apply ── + + defp run({@op_apply, [1]}, pc, frame, [arg_array, new_target, fun | rest], gas, ctx) do + result = invoke_super_constructor(fun, new_target, apply_args(arg_array), gas, ctx) + run(pc + 1, frame, [result | rest], gas, Context.mark_dirty(%{ctx | this: result})) + end + + defp run({@op_apply, [_magic]}, pc, frame, [arg_array, this_obj, fun | rest], gas, ctx) do + args = apply_args(arg_array) + apply_ctx = Context.mark_dirty(%{ctx | this: this_obj}) + + result = dispatch_call(fun, args, gas, apply_ctx, this_obj) + + run(pc + 1, frame, [result | rest], gas, ctx) + end + + # ── Object spread (copy_data_properties with mask) ── + + defp run({@op_copy_data_properties, [mask]}, pc, frame, stack, gas, ctx) do + target_idx = mask &&& 3 + source_idx = Bitwise.bsr(mask, 2) &&& 7 + target = Enum.at(stack, target_idx) + source = Enum.at(stack, source_idx) + + try do + Copy.copy_data_properties(target, source) + run(pc + 1, frame, stack, gas, ctx) + catch + {:js_throw, error} -> throw_or_catch(frame, error, gas, ctx) + end + end + + # ── Class definitions ── + + defp run( + {@op_define_class, [atom_idx, _flags]}, + pc, + frame, + [ctor, parent_ctor | rest], + gas, + ctx + ) do + locals = elem(frame, Frame.locals()) + vrefs = elem(frame, Frame.var_refs()) + l2v = elem(frame, Frame.l2v()) + + ctor_closure = + case ctor do + %Bytecode.Function{} = f -> + base = build_closure(f, locals, vrefs, l2v, ctx) + inherit_parent_vrefs(base, vrefs) + + already_closure -> + already_closure + end + + class_name = Names.resolve_atom(ctx, atom_idx) + {proto, ctor_closure} = Class.define_class(ctor_closure, parent_ctor, class_name) + + frame = EvalEnv.seed_class_binding(frame, ctx, atom_idx, ctor_closure) + + run(pc + 1, frame, [proto, ctor_closure | rest], gas, ctx) + end + + defp run({@op_add_brand, []}, pc, frame, [obj, brand | rest], gas, ctx) do + Private.add_brand(obj, brand) + run(pc + 1, frame, rest, gas, ctx) + end + + defp run({@op_check_brand, []}, pc, frame, [brand, obj | _] = stack, gas, ctx) do + case Private.ensure_brand(obj, brand) do + :ok -> run(pc + 1, frame, stack, gas, ctx) + :error -> throw_or_catch(frame, Private.brand_error(), gas, ctx) + end + end + + defp run( + {@op_define_class_computed, [atom_idx, flags]}, + pc, + frame, + [ctor, parent_ctor, _computed_name | rest], + gas, + ctx + ) do + run({@op_define_class, [atom_idx, flags]}, pc, frame, [ctor, parent_ctor | rest], gas, ctx) + end + + defp run( + {@op_define_method, [atom_idx, flags]}, + pc, + frame, + [method_closure, target | rest], + gas, + ctx + ) do + Methods.define_method(target, method_closure, Names.resolve_atom(ctx, atom_idx), flags) + run(pc + 1, frame, [target | rest], gas, ctx) + end + + defp run( + {@op_define_method_computed, [flags]}, + pc, + frame, + [method_closure, field_name, target | rest], + gas, + ctx + ) do + Methods.define_method_computed(target, method_closure, field_name, flags) + run(pc + 1, frame, [target | rest], gas, ctx) + end + + # ── Generators ── + + defp run({@op_initial_yield, []}, pc, frame, stack, gas, ctx) do + throw({:generator_yield, :undefined, pc + 1, frame, stack, gas, ctx}) + end + + defp run({@op_yield, []}, pc, frame, [val | rest], gas, ctx) do + throw({:generator_yield, val, pc + 1, frame, rest, gas, ctx}) + end + + defp run({@op_yield_star, []}, pc, frame, [val | rest], gas, ctx) do + throw({:generator_yield_star, val, pc + 1, frame, rest, gas, ctx}) + end + + defp run({@op_async_yield_star, []}, pc, frame, [val | rest], gas, ctx) do + throw({:generator_yield_star, val, pc + 1, frame, rest, gas, ctx}) + end + + defp run({@op_await, []}, pc, frame, [val | rest], gas, ctx) do + resolved = resolve_awaited(val) + run(pc + 1, frame, [resolved | rest], gas, ctx) + end + + defp run({@op_return_async, []}, _pc, _frame, [val | _], _gas, _ctx) do + throw({:generator_return, val}) + end + + # ── with statement ── + + defp run({@op_with_get_var, [atom_idx, target, _is_with]}, pc, frame, [obj | rest], gas, ctx) do + key = Names.resolve_atom(ctx, atom_idx) + + if with_has_property?(obj, key) do + run(target, frame, [Get.get(obj, key) | rest], gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run( + {@op_with_put_var, [atom_idx, target, _is_with]}, + pc, + frame, + [obj, val | rest], + gas, + ctx + ) do + key = Names.resolve_atom(ctx, atom_idx) + + if with_has_property?(obj, key) do + Put.put(obj, key, val) + run(target, frame, rest, gas, ctx) + else + run(pc + 1, frame, [val | rest], gas, ctx) + end + end + + defp run({@op_with_delete_var, [atom_idx, target, _is_with]}, pc, frame, [obj | rest], gas, ctx) do + key = Names.resolve_atom(ctx, atom_idx) + + if with_has_property?(obj, key) do + Delete.delete_property(obj, key) + run(target, frame, [true | rest], gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run({@op_with_make_ref, [atom_idx, target, _is_with]}, pc, frame, [obj | rest], gas, ctx) do + key = Names.resolve_atom(ctx, atom_idx) + + if with_has_property?(obj, key) do + run(target, frame, [key, obj | rest], gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run({@op_with_get_ref, [atom_idx, target, _is_with]}, pc, frame, [obj | rest], gas, ctx) do + key = Names.resolve_atom(ctx, atom_idx) + + if with_has_property?(obj, key) do + run(target, frame, [Get.get(obj, key), obj | rest], gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run( + {@op_with_get_ref_undef, [atom_idx, target, _is_with]}, + pc, + frame, + [obj | rest], + gas, + ctx + ) do + key = Names.resolve_atom(ctx, atom_idx) + + if with_has_property?(obj, key) do + run(target, frame, [Get.get(obj, key), :undefined | rest], gas, ctx) + else + run(pc + 1, frame, rest, gas, ctx) + end + end + + defp run({@op_for_await_of_start, []}, pc, frame, [obj | rest], gas, ctx) do + {iter_obj, next_fn} = + case obj do + {:obj, ref} -> + stored = Heap.get_obj(ref, []) + + cond do + match?({:qb_arr, _}, stored) -> + make_list_iterator(Heap.to_list({:obj, ref})) + + match?({:qb_arr, _}, stored) -> + make_list_iterator(Heap.to_list({:obj, ref})) + + is_list(stored) -> + make_list_iterator(stored) + + is_map(stored) and Map.has_key?(stored, "next") -> + {obj, Get.get(obj, "next")} + + true -> + {obj, :undefined} + end + + _ -> + {obj, :undefined} + end + + run(pc + 1, frame, [0, next_fn, iter_obj | rest], gas, ctx) + end + + # ── Catch-all for unimplemented opcodes ── + + defp run({op, args}, _pc, _frame, _stack, _gas, _ctx) do + throw({:error, {:unimplemented_opcode, op, args}}) + end + + defp apply_args(arg_array) do + case arg_array do + {:qb_arr, arr} -> :array.to_list(arr) + list when is_list(list) -> list + {:obj, ref} -> Heap.to_list({:obj, ref}) + _ -> [] + end + end + + defp invoke_super_constructor(fun, new_target, args, gas, ctx) do + pending_this = pending_constructor_this(ctx.this) + + ctor_ctx = + Context.mark_dirty(%{ + ctx + | this: super_constructor_this(fun, pending_this), + new_target: new_target + }) + + result = + case fun do + %Bytecode.Function{} = f -> + do_invoke(f, {:closure, %{}, f}, args, ClosureBuilder.ctor_var_refs(f), gas, ctor_ctx) + + {:closure, captured, %Bytecode.Function{} = f} -> + do_invoke( + f, + {:closure, captured, f}, + args, + ClosureBuilder.ctor_var_refs(f, captured), + gas, + ctor_ctx + ) + + {:bound, _, _, orig_fun, bound_args} -> + invoke_super_constructor(orig_fun, new_target, bound_args ++ args, gas, ctx) + + {:builtin, _name, cb} when is_function(cb, 2) -> + cb.(args, pending_this) + + _ -> + pending_this + end + + result = Class.coalesce_this_result(result, ctor_ctx.this) + + case result do + {:uninitialized, _} -> + throw({:js_throw, Heap.make_error("this is not initialized", "ReferenceError")}) + + other -> + other + end + end + + defp pending_constructor_this({:uninitialized, {:obj, _} = obj}), do: obj + defp pending_constructor_this({:obj, _} = obj), do: obj + defp pending_constructor_this(other), do: other + + defp super_constructor_this(fun, pending_this) do + case unwrap_constructor_target(fun) do + %Bytecode.Function{is_derived_class_constructor: true} -> {:uninitialized, pending_this} + _ -> pending_this + end + end + + defp unwrap_constructor_target({:closure, _, %Bytecode.Function{} = f}), do: f + defp unwrap_constructor_target({:bound, _, inner, _, _}), do: unwrap_constructor_target(inner) + defp unwrap_constructor_target(other), do: other + + defp put_arg_value(ctx, idx, val, arg_buf) do + padded = Tuple.to_list(arg_buf) + + padded = + if idx < length(padded), + do: padded, + else: padded ++ List.duplicate(:undefined, idx + 1 - length(padded)) + + Context.mark_dirty(%{ctx | arg_buf: List.to_tuple(List.replace_at(padded, idx, val))}) + end + + defp dispatch_call(fun, args, gas, ctx, this), + do: Invocation.dispatch(fun, args, gas, ctx, this) + + # ── Tail calls ── + + defp tail_call([fun | _rest], 0, gas, ctx) do + dispatch_call(fun, [], gas, ctx, nil) + end + + defp tail_call([a0, fun | _], 1, gas, ctx) do + dispatch_call(fun, [a0], gas, ctx, nil) + end + + defp tail_call([a1, a0, fun | _], 2, gas, ctx) do + dispatch_call(fun, [a0, a1], gas, ctx, nil) + end + + defp tail_call(stack, argc, gas, ctx) do + {args, [fun | _]} = Enum.split(stack, argc) + dispatch_call(fun, Enum.reverse(args), gas, ctx, nil) + end + + defp tail_call_method([fun, obj | _], 0, gas, ctx) do + dispatch_call(fun, [], gas, Context.mark_dirty(%{ctx | this: obj}), obj) + end + + defp tail_call_method([a0, fun, obj | _], 1, gas, ctx) do + dispatch_call(fun, [a0], gas, Context.mark_dirty(%{ctx | this: obj}), obj) + end + + defp tail_call_method(stack, argc, gas, ctx) do + {args, [fun, obj | _]} = Enum.split(stack, argc) + dispatch_call(fun, Enum.reverse(args), gas, Context.mark_dirty(%{ctx | this: obj}), obj) + end + + # ── Closure construction ── + + defp build_closure(fun, locals, vrefs, l2v, ctx), + do: ClosureBuilder.build(fun, locals, vrefs, l2v, ctx) + + defp inherit_parent_vrefs(closure, parent_vrefs), + do: ClosureBuilder.inherit_parent_vrefs(closure, parent_vrefs) + + # ── Function calls ── + + defp call_function(pc, frame, stack, 0, gas, ctx) do + [fun | rest] = stack + gas = check_gas(pc, frame, rest, gas, ctx) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [], gas, ctx, nil) + end) + end + + defp call_function(pc, frame, [a0, fun | rest], 1, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [a0], gas, ctx, nil) + end) + end + + defp call_function(pc, frame, [a1, a0, fun | rest], 2, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [a0, a1], gas, ctx, nil) + end) + end + + defp call_function(pc, frame, [a2, a1, a0, fun | rest], 3, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [a0, a1, a2], gas, ctx, nil) + end) + end + + defp call_function(pc, frame, stack, argc, gas, ctx) do + {args, [fun | rest]} = Enum.split(stack, argc) + gas = check_gas(pc, frame, rest, gas, ctx) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, Enum.reverse(args), gas, ctx, nil) + end) + end + + defp call_method(pc, frame, [fun, obj | rest], 0, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + method_ctx = Context.mark_dirty(%{ctx | this: obj}) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [], gas, method_ctx, obj) + end) + end + + defp call_method(pc, frame, [a0, fun, obj | rest], 1, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + method_ctx = Context.mark_dirty(%{ctx | this: obj}) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [a0], gas, method_ctx, obj) + end) + end + + defp call_method(pc, frame, [a1, a0, fun, obj | rest], 2, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + method_ctx = Context.mark_dirty(%{ctx | this: obj}) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [a0, a1], gas, method_ctx, obj) + end) + end + + defp call_method(pc, frame, [a2, a1, a0, fun, obj | rest], 3, gas, ctx) do + gas = check_gas(pc, frame, rest, gas, ctx) + method_ctx = Context.mark_dirty(%{ctx | this: obj}) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, [a0, a1, a2], gas, method_ctx, obj) + end) + end + + defp call_method(pc, frame, stack, argc, gas, ctx) do + gas = check_gas(pc, frame, stack, gas, ctx) + {args, [fun, obj | rest]} = Enum.split(stack, argc) + method_ctx = Context.mark_dirty(%{ctx | this: obj}) + + catch_js_throw_refresh_globals(pc, frame, rest, gas, ctx, fn -> + dispatch_call(fun, Enum.reverse(args), gas, method_ctx, obj) + end) + end + + def invoke_function_fallback(%Bytecode.Function{} = fun, args, gas, ctx) do + invoke_function(fun, args, gas, ctx) + end + + def invoke_function_fallback(other, args, _gas, _ctx) + when not is_tuple(other) or elem(other, 0) != :bound, + do: Builtin.call(other, args, nil) + + def invoke_function_fallback({:bound, _, inner, _, _}, args, gas, _ctx), + do: Invocation.invoke(inner, args, gas) + + def invoke_closure_fallback({:closure, _, %Bytecode.Function{}} = closure, args, gas, ctx) do + invoke_closure(closure, args, gas, ctx) + end + + def invoke_closure_fallback(other, args, gas, ctx), + do: invoke_function_fallback(other, args, gas, ctx) + + defp invoke_function(%Bytecode.Function{} = fun, args, gas, ctx) do + do_invoke(fun, {:closure, %{}, fun}, args, [], gas, ctx) + end + + defp invoke_closure({:closure, captured, %Bytecode.Function{} = fun} = self, args, gas, ctx) do + var_refs = + for cv <- fun.closure_vars do + Map.get(captured, ClosureBuilder.capture_key(cv), :undefined) + end + + do_invoke(fun, self, args, var_refs, gas, ctx) + end + + defp do_invoke(%Bytecode.Function{} = fun, self_ref, args, var_refs, gas, ctx) do + cache_key = {fun.byte_code, fun.arg_count} + + insns = + case Heap.get_decoded(cache_key) do + nil -> + case Decoder.decode(fun.byte_code, fun.arg_count) do + {:ok, instructions} -> + t = List.to_tuple(instructions) + Heap.put_decoded(cache_key, t) + t + + {:error, _} = err -> + throw(err) + end + + cached -> + cached + end + + case insns do + insns when is_tuple(insns) -> + locals = :erlang.make_tuple(max(fun.arg_count + fun.var_count, 1), :undefined) + + {locals, var_refs_tuple, l2v} = + Closures.setup_captured_locals(fun, locals, var_refs, args) + + frame = + Frame.new( + locals, + List.to_tuple(fun.constants), + var_refs_tuple, + fun.stack_size, + insns, + l2v + ) + + fn_atoms = Process.get({:qb_fn_atoms, fun.byte_code}, Heap.get_atoms()) + Heap.put_atoms(fn_atoms) + + inner_ctx = + %{ + ctx + | current_func: self_ref, + arg_buf: List.to_tuple(args), + catch_stack: [], + atoms: fn_atoms + } + |> InvokeContext.attach_method_state() + + prev_ctx = Heap.get_ctx() + Heap.put_ctx(inner_ctx) + inner_ctx = Context.mark_synced(inner_ctx) + + if inner_ctx.trace_enabled, do: Trace.push(self_ref) + restore_mark = length(Process.get(:qb_eval_restore_stack, [])) + + try do + case fun.func_kind do + @func_generator -> Generator.invoke(frame, gas, inner_ctx) + @func_async -> Generator.invoke_async(frame, gas, inner_ctx) + @func_async_generator -> Generator.invoke_async_generator(frame, gas, inner_ctx) + _ -> run(0, frame, [], gas, inner_ctx) + end + after + restore_eval_restores(restore_mark) + if inner_ctx.trace_enabled, do: Trace.pop() + if prev_ctx, do: Heap.put_ctx(prev_ctx) + end + end + end + + @doc """ + Runs a bytecode frame — entry point for external callers. + """ + def run_frame(frame, stack, gas, ctx), do: run(0, frame, stack, gas, ctx) + def run_frame(pc, frame, stack, gas, ctx), do: run(pc, frame, stack, gas, ctx) + + @doc """ + Invokes a callback function from built-in code (e.g. Array.prototype.map). + """ + def invoke_callback(fun, args), do: Invocation.invoke_callback(fun, args) +end diff --git a/lib/quickbeam/vm/interpreter/closure_builder.ex b/lib/quickbeam/vm/interpreter/closure_builder.ex new file mode 100644 index 00000000..b63f2863 --- /dev/null +++ b/lib/quickbeam/vm/interpreter/closure_builder.ex @@ -0,0 +1,104 @@ +defmodule QuickBEAM.VM.Interpreter.ClosureBuilder do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.Interpreter.Context + + def build(%Bytecode.Function{} = fun, locals, vrefs, l2v, %Context{} = ctx) do + parent_arg_count = current_function_arg_count(ctx) + + captured = + for cv <- fun.closure_vars, into: %{} do + {capture_key(cv), capture_var(cv, locals, vrefs, l2v, parent_arg_count)} + end + + {:closure, captured, fun} + end + + def build(other, _locals, _vrefs, _l2v, _ctx), do: other + + def inherit_parent_vrefs({:closure, captured, %Bytecode.Function{} = fun}, parent_vrefs) + when is_tuple(parent_vrefs) do + extra = + if tuple_size(parent_vrefs) == 0 do + %{} + else + for i <- 0..(tuple_size(parent_vrefs) - 1), + not Map.has_key?(captured, capture_key(2, i)), + into: %{} do + {capture_key(2, i), elem(parent_vrefs, i)} + end + end + + {:closure, Map.merge(extra, captured), fun} + end + + def inherit_parent_vrefs(closure, _parent_vrefs), do: closure + + def ctor_var_refs(%Bytecode.Function{} = fun, captured \\ %{}) do + cell_ref = make_ref() + Heap.put_cell(cell_ref, false) + + case fun.closure_vars do + [] -> + [{:cell, cell_ref}] + + closure_vars -> + Enum.map(closure_vars, &Map.get(captured, capture_key(&1), {:cell, cell_ref})) + end + end + + def capture_key(%{closure_type: type, var_idx: idx}), do: capture_key(type, idx) + def capture_key(type, idx), do: {type, idx} + + defp capture_var(%{closure_type: 2, var_idx: idx}, _locals, vrefs, _l2v, _arg_count) + when idx < tuple_size(vrefs) do + case elem(vrefs, idx) do + {:cell, _} = existing -> + existing + + val -> + ref = make_ref() + Heap.put_cell(ref, val) + {:cell, ref} + end + end + + defp capture_var(%{closure_type: 0, var_idx: idx}, locals, vrefs, l2v, arg_count) do + capture_local_var(idx + arg_count, locals, vrefs, l2v) + end + + defp capture_var(%{var_idx: idx}, locals, vrefs, l2v, _arg_count) do + capture_local_var(idx, locals, vrefs, l2v) + end + + defp capture_local_var(idx, locals, vrefs, l2v) do + case Map.get(l2v, idx) do + nil -> + val = if idx < tuple_size(locals), do: elem(locals, idx), else: :undefined + ref = make_ref() + Heap.put_cell(ref, val) + {:cell, ref} + + vref_idx -> + case elem(vrefs, vref_idx) do + {:cell, _} = existing -> + existing + + _ -> + val = elem(locals, idx) + ref = make_ref() + Heap.put_cell(ref, val) + {:cell, ref} + end + end + end + + defp current_function_arg_count(%Context{ + current_func: {:closure, _, %Bytecode.Function{arg_count: n}} + }), + do: n + + defp current_function_arg_count(%Context{current_func: %Bytecode.Function{arg_count: n}}), do: n + defp current_function_arg_count(%Context{arg_buf: arg_buf}), do: tuple_size(arg_buf) +end diff --git a/lib/quickbeam/vm/interpreter/closures.ex b/lib/quickbeam/vm/interpreter/closures.ex new file mode 100644 index 00000000..8d75df74 --- /dev/null +++ b/lib/quickbeam/vm/interpreter/closures.ex @@ -0,0 +1,95 @@ +defmodule QuickBEAM.VM.Interpreter.Closures do + @compile {:inline, read_cell: 1, write_cell: 2, read_captured_local: 4, write_captured_local: 5} + @moduledoc false + + alias QuickBEAM.VM.Heap + + def read_cell({:cell, ref}), do: Heap.get_cell(ref) + def read_cell(_), do: :undefined + + def write_cell({:cell, ref}, val), do: Heap.put_cell(ref, val) + def write_cell(_, _), do: :ok + + def read_captured_local(l2v, idx, locals, var_refs) do + case Map.get(l2v, idx) do + nil -> + elem(locals, idx) + + vref_idx -> + case elem(var_refs, vref_idx) do + {:cell, ref} -> Heap.get_cell(ref) + val -> val + end + end + end + + def write_captured_local(l2v, idx, val, _locals, var_refs) do + case Map.get(l2v, idx) do + nil -> + :ok + + vref_idx -> + case elem(var_refs, vref_idx) do + {:cell, ref} -> Heap.put_cell(ref, val) + _ -> :ok + end + end + end + + def setup_captured_locals(%{locals: []}, locals, var_refs, _args) do + vrefs = if is_tuple(var_refs), do: var_refs, else: List.to_tuple(var_refs) + {locals, vrefs, %{}} + end + + def setup_captured_locals(fun, locals, var_refs, args) do + arg_buf = List.to_tuple(args) + vrefs = if is_tuple(var_refs), do: var_refs, else: List.to_tuple(var_refs) + + setup_captured_locals(fun.locals, 0, locals, vrefs, tuple_size(vrefs), arg_buf, %{}) + end + + defp setup_captured_locals([], _idx, locals, vrefs, _closure_ref_count, _arg_buf, l2v), + do: {locals, vrefs, l2v} + + defp setup_captured_locals( + [%{is_captured: true, var_ref_idx: var_ref_idx} | rest], + idx, + locals, + vrefs, + closure_ref_count, + arg_buf, + l2v + ) do + val = + if idx < tuple_size(arg_buf), + do: elem(arg_buf, idx), + else: elem(locals, idx) + + ref = make_ref() + Heap.put_cell(ref, val) + local_ref_idx = closure_ref_count + var_ref_idx + + setup_captured_locals( + rest, + idx + 1, + put_elem(locals, idx, val), + put_vref(vrefs, local_ref_idx, {:cell, ref}), + closure_ref_count, + arg_buf, + Map.put(l2v, idx, local_ref_idx) + ) + end + + defp setup_captured_locals([_ | rest], idx, locals, vrefs, closure_ref_count, arg_buf, l2v), + do: setup_captured_locals(rest, idx + 1, locals, vrefs, closure_ref_count, arg_buf, l2v) + + defp put_vref(vrefs, idx, val) when idx < tuple_size(vrefs), do: put_elem(vrefs, idx, val) + + defp put_vref(vrefs, idx, val) do + vrefs + |> Tuple.to_list() + |> Kernel.++(List.duplicate(:undefined, idx + 1 - tuple_size(vrefs))) + |> List.replace_at(idx, val) + |> List.to_tuple() + end +end diff --git a/lib/quickbeam/vm/interpreter/context.ex b/lib/quickbeam/vm/interpreter/context.ex new file mode 100644 index 00000000..f181dc70 --- /dev/null +++ b/lib/quickbeam/vm/interpreter/context.ex @@ -0,0 +1,40 @@ +defmodule QuickBEAM.VM.Interpreter.Context do + @moduledoc false + @type t :: %__MODULE__{ + this: term(), + arg_buf: tuple(), + current_func: term(), + home_object: term(), + super: term(), + catch_stack: [{non_neg_integer(), [term()]}], + atoms: tuple(), + globals: map(), + runtime_pid: pid() | nil, + new_target: term(), + gas: pos_integer(), + trace_enabled: boolean(), + pd_synced: boolean() + } + + @default_gas 1_000_000_000 + + def default_gas, do: @default_gas + + defstruct this: :undefined, + arg_buf: {}, + current_func: :undefined, + home_object: :undefined, + super: :undefined, + catch_stack: [], + atoms: {}, + globals: %{}, + runtime_pid: nil, + new_target: :undefined, + gas: @default_gas, + trace_enabled: false, + pd_synced: false + + def mark_dirty(%__MODULE__{} = ctx), do: %{ctx | pd_synced: false} + def mark_synced(%__MODULE__{} = ctx), do: %{ctx | pd_synced: true} + def synced?(%__MODULE__{pd_synced: synced?}), do: synced? +end diff --git a/lib/quickbeam/vm/interpreter/eval_env.ex b/lib/quickbeam/vm/interpreter/eval_env.ex new file mode 100644 index 00000000..6dfd9236 --- /dev/null +++ b/lib/quickbeam/vm/interpreter/eval_env.ex @@ -0,0 +1,81 @@ +defmodule QuickBEAM.VM.Interpreter.EvalEnv do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Names} + alias QuickBEAM.VM.Interpreter.{Closures, Context, Frame} + + require Frame + + def resolve_local_name(name), do: Names.resolve_display_name(name) + + def seed_class_binding(frame, ctx, atom_idx, ctor_closure) do + case class_binding_local_index(ctx, atom_idx) do + nil -> + frame + + idx -> + Closures.write_captured_local( + elem(frame, Frame.l2v()), + idx, + ctor_closure, + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()) + ) + + put_local(frame, idx, ctor_closure) + end + end + + def current_func_name(%Context{current_func: func}) do + case func do + {:closure, _, %Bytecode.Function{name: name}} -> name + %Bytecode.Function{name: name} -> name + _ -> nil + end + end + + def current_local_name( + %Context{current_func: {:closure, _, %Bytecode.Function{locals: locals}}}, + idx + ) + when idx >= 0 and idx < length(locals), + do: locals |> Enum.at(idx) |> Map.get(:name) |> resolve_local_name() + + def current_local_name(%Context{current_func: %Bytecode.Function{locals: locals}}, idx) + when idx >= 0 and idx < length(locals), + do: locals |> Enum.at(idx) |> Map.get(:name) |> resolve_local_name() + + def current_local_name(_, _), do: nil + + defp class_binding_local_index(%Context{current_func: current_func}, atom_idx) do + class_name = resolve_local_name(atom_idx) + + current_func + |> current_bytecode_function() + |> case do + %Bytecode.Function{locals: locals} -> + locals + |> Enum.with_index() + |> Enum.filter(fn {%{name: name, scope_level: scope_level, is_lexical: is_lexical}, _idx} -> + is_lexical and scope_level > 1 and resolve_local_name(name) == class_name + end) + |> Enum.max_by(fn {%{scope_level: scope_level}, _idx} -> scope_level end, fn -> nil end) + |> case do + nil -> nil + {_local, idx} -> idx + end + + _ -> + nil + end + end + + defp class_binding_local_index(_, _), do: nil + + defp current_bytecode_function({:closure, _, %Bytecode.Function{} = fun}), do: fun + defp current_bytecode_function(%Bytecode.Function{} = fun), do: fun + defp current_bytecode_function(_), do: nil + + defp put_local(frame, idx, val), + do: put_elem(frame, Frame.locals(), put_elem(elem(frame, Frame.locals()), idx, val)) +end diff --git a/lib/quickbeam/vm/interpreter/frame.ex b/lib/quickbeam/vm/interpreter/frame.ex new file mode 100644 index 00000000..53ffb5dc --- /dev/null +++ b/lib/quickbeam/vm/interpreter/frame.ex @@ -0,0 +1,21 @@ +defmodule QuickBEAM.VM.Interpreter.Frame do + @moduledoc false + @type t :: {tuple(), tuple(), tuple(), non_neg_integer(), tuple(), map()} + + # Tuple layout: {locals, constants, var_refs, _stack_size (unused), instructions, local_to_vref} + @locals 0 + @constants 1 + @var_refs 2 + @insns 4 + @l2v 5 + + defmacro locals, do: @locals + defmacro constants, do: @constants + defmacro var_refs, do: @var_refs + defmacro insns, do: @insns + defmacro l2v, do: @l2v + + def new(locals, constants, var_refs, stack_size, instructions, local_to_vref) do + {locals, constants, var_refs, stack_size, instructions, local_to_vref} + end +end diff --git a/lib/quickbeam/vm/interpreter/gas.ex b/lib/quickbeam/vm/interpreter/gas.ex new file mode 100644 index 00000000..5f375b2f --- /dev/null +++ b/lib/quickbeam/vm/interpreter/gas.ex @@ -0,0 +1,35 @@ +defmodule QuickBEAM.VM.Interpreter.Gas do + @moduledoc false + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter.Frame + + require Frame + + def check(frame, stack, gas, ctx, interval) do + gas = gas - 1 + + if gas <= 0 do + throw({:error, {:out_of_gas, gas}}) + end + + if rem(gas, interval) == 0 and Heap.gc_needed?() do + roots = + [ + elem(frame, Frame.locals()), + elem(frame, Frame.var_refs()), + elem(frame, Frame.constants()), + ctx.this, + ctx.current_func, + ctx.arg_buf, + ctx.catch_stack, + ctx.globals + | stack + ] ++ Heap.all_module_exports() + + Heap.mark_and_sweep(roots) + end + + gas + end +end diff --git a/lib/quickbeam/vm/interpreter/generator.ex b/lib/quickbeam/vm/interpreter/generator.ex new file mode 100644 index 00000000..a06c2298 --- /dev/null +++ b/lib/quickbeam/vm/interpreter/generator.ex @@ -0,0 +1,151 @@ +defmodule QuickBEAM.VM.Interpreter.Generator do + @moduledoc false + + import QuickBEAM.VM.Builtin, only: [build_object: 1] + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.PromiseState, as: Promise + + def invoke(frame, gas, ctx) do + gen_ref = make_ref() + suspend(gen_ref, frame, gas, ctx) + build_iterator(gen_ref, &next/2, &return_value/2) + end + + def invoke_async(frame, gas, ctx) do + result = Interpreter.run_frame(frame, [], gas, ctx) + Promise.resolved(result) + catch + {:generator_return, val} -> Promise.resolved(val) + {:js_throw, val} -> Promise.rejected(val) + end + + def invoke_async_generator(frame, gas, ctx) do + gen_ref = make_ref() + suspend(gen_ref, frame, gas, ctx) + + build_iterator(gen_ref, &async_next/2, fn _ref, val -> + Promise.resolved(done_result(val)) + end) + end + + # ── Sync generator ── + + defp next(gen_ref, arg) do + case Heap.get_obj(gen_ref) do + %{state: :suspended} = s -> + Heap.put_ctx(s.ctx) + resume_sync(gen_ref, s, arg) + + _ -> + done_result(:undefined) + end + end + + defp resume_sync(gen_ref, s, arg) do + result = Interpreter.run_frame(s.pc, s.frame, [false, arg | s.stack], s.gas, s.ctx) + complete(gen_ref) + done_result(result) + catch + {:generator_yield, val, sp, sf, ss, sg, sc} -> + save_suspended(gen_ref, sp, sf, ss, sg, sc) + yield_result(val) + + {:generator_yield_star, val, sp, sf, ss, sg, sc} -> + save_suspended(gen_ref, sp, sf, ss, sg, sc) + val + + {:generator_return, val} -> + complete(gen_ref) + done_result(val) + + {:js_throw, _} = thrown -> + complete(gen_ref) + throw(thrown) + end + + # ── Async generator ── + + defp async_next(gen_ref, arg) do + case Heap.get_obj(gen_ref) do + %{state: :suspended} = s -> + prev_ctx = Heap.get_ctx() + Heap.put_ctx(s.ctx) + + try do + resume_async(gen_ref, s, arg) + after + if prev_ctx, do: Heap.put_ctx(prev_ctx) + end + + _ -> + Promise.resolved(done_result(:undefined)) + end + end + + defp resume_async(gen_ref, s, arg) do + result = Interpreter.run_frame(s.pc, s.frame, [false, arg | s.stack], s.gas, s.ctx) + complete(gen_ref) + Promise.resolved(done_result(result)) + catch + {:generator_yield, val, sp, sf, ss, sg, sc} -> + save_suspended(gen_ref, sp, sf, ss, sg, sc) + Promise.resolved(yield_result(val)) + + {:generator_return, val} -> + complete(gen_ref) + Promise.resolved(done_result(val)) + + {:js_throw, _} = thrown -> + complete(gen_ref) + throw(thrown) + end + + # ── Shared helpers ── + + defp return_value(gen_ref, val) do + complete(gen_ref) + done_result(val) + end + + defp suspend(gen_ref, frame, gas, ctx) do + Interpreter.run_frame(frame, [], gas, ctx) + catch + {:generator_yield, _val, sp, sf, ss, sg, sc} -> + save_suspended(gen_ref, sp, sf, ss, sg, sc) + + {:generator_yield_star, _val, sp, sf, ss, sg, sc} -> + save_suspended(gen_ref, sp, sf, ss, sg, sc) + end + + defp save_suspended(ref, pc, frame, stack, gas, ctx) do + Heap.put_obj(ref, %{state: :suspended, pc: pc, frame: frame, stack: stack, gas: gas, ctx: ctx}) + end + + defp complete(ref), do: Heap.put_obj(ref, %{state: :completed}) + + defp yield_result(val), do: Heap.wrap(%{"value" => val, "done" => false}) + defp done_result(val), do: Heap.wrap(%{"value" => val, "done" => true}) + + defp build_iterator(gen_ref, next_impl, return_impl) do + next_fn = + {:builtin, "next", + fn + [arg | _], _this -> next_impl.(gen_ref, arg) + [], _this -> next_impl.(gen_ref, :undefined) + end} + + return_fn = + {:builtin, "return", + fn + [val | _], _this -> return_impl.(gen_ref, val) + [], _this -> return_impl.(gen_ref, :undefined) + end} + + build_object do + val("next", next_fn) + val("return", return_fn) + end + end +end diff --git a/lib/quickbeam/vm/interpreter/setup.ex b/lib/quickbeam/vm/interpreter/setup.ex new file mode 100644 index 00000000..36252adf --- /dev/null +++ b/lib/quickbeam/vm/interpreter/setup.ex @@ -0,0 +1,38 @@ +defmodule QuickBEAM.VM.Interpreter.Setup do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Heap, Runtime} + alias QuickBEAM.VM.Interpreter.Context + alias QuickBEAM.VM.Invocation.Context, as: InvokeContext + + def build_eval_context(opts, atoms, gas) do + base_globals = Runtime.global_bindings() + persistent = Heap.get_persistent_globals() |> Map.drop(Map.keys(base_globals)) + + %Context{ + atoms: atoms, + gas: gas, + globals: + base_globals + |> Map.merge(persistent) + |> Map.merge(Map.get(opts, :globals, %{})), + runtime_pid: Map.get(opts, :runtime_pid), + this: Map.get(opts, :this, :undefined), + arg_buf: Map.get(opts, :arg_buf, {}), + current_func: Map.get(opts, :current_func, :undefined), + new_target: Map.get(opts, :new_target, :undefined), + trace_enabled: Map.get(opts, :trace_enabled, true) + } + |> InvokeContext.attach_method_state() + end + + def store_function_atoms(%Bytecode.Function{} = fun, atoms) do + Process.put({:qb_fn_atoms, fun.byte_code}, atoms) + + for %Bytecode.Function{} = inner <- fun.constants do + store_function_atoms(inner, atoms) + end + + :ok + end +end diff --git a/lib/quickbeam/vm/interpreter/values.ex b/lib/quickbeam/vm/interpreter/values.ex new file mode 100644 index 00000000..5b9a5d59 --- /dev/null +++ b/lib/quickbeam/vm/interpreter/values.ex @@ -0,0 +1,604 @@ +defmodule QuickBEAM.VM.Interpreter.Values do + @moduledoc false + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.Runtime + + @compile {:inline, + truthy?: 1, + falsy?: 1, + to_int32: 1, + strict_eq: 2, + add: 2, + sub: 2, + mul: 2, + neg: 1, + typeof: 1, + to_number: 1, + stringify: 1, + lt: 2, + lte: 2, + gt: 2, + gte: 2, + eq: 2, + neq: 2, + band: 2, + bor: 2, + bxor: 2, + shl: 2, + sar: 2, + shr: 2} + + alias QuickBEAM.VM.Bytecode + import Bitwise + + def truthy?(nil), do: false + def truthy?(:undefined), do: false + def truthy?(false), do: false + def truthy?(0), do: false + def truthy?(+0.0), do: false + def truthy?(""), do: false + def truthy?({:bigint, 0}), do: false + def truthy?({:bigint, _}), do: true + def truthy?(_), do: true + + def falsy?(val), do: not truthy?(val) + + def to_number(val) when is_number(val), do: val + def to_number(true), do: 1 + def to_number(false), do: 0 + def to_number(nil), do: 0 + def to_number(:undefined), do: :nan + def to_number(:infinity), do: :infinity + def to_number(:neg_infinity), do: :neg_infinity + def to_number(:nan), do: :nan + + def to_number(s) when is_binary(s), do: parse_numeric(String.trim(s)) + + def to_number({:bigint, _}), + do: + throw( + {:js_throw, + %{"message" => "Cannot convert a BigInt value to a number", "name" => "TypeError"}} + ) + + def to_number({:obj, ref} = obj) do + map = Heap.get_obj(ref, %{}) + + case Map.get(map, "valueOf") do + fun when fun != nil and fun != :undefined -> + to_number(Interpreter.invoke_with_receiver(fun, [], Runtime.gas_budget(), obj)) + + _ -> + :nan + end + end + + def to_number(_), do: :nan + + defp parse_numeric(""), do: 0 + defp parse_numeric("0x" <> rest), do: parse_int_or_nan(rest, 16) + defp parse_numeric("0X" <> rest), do: parse_int_or_nan(rest, 16) + defp parse_numeric("0o" <> rest), do: parse_int_or_nan(rest, 8) + defp parse_numeric("0O" <> rest), do: parse_int_or_nan(rest, 8) + defp parse_numeric("0b" <> rest), do: parse_int_or_nan(rest, 2) + defp parse_numeric("0B" <> rest), do: parse_int_or_nan(rest, 2) + defp parse_numeric("Infinity" <> _), do: :infinity + defp parse_numeric("+Infinity" <> _), do: :infinity + defp parse_numeric("-Infinity" <> _), do: :neg_infinity + + defp parse_numeric(s) do + case Integer.parse(s) do + {i, ""} -> + i + + _ -> + case Float.parse(s) do + {f, ""} -> f + _ -> :nan + end + end + end + + defp parse_int_or_nan(s, base) do + case Integer.parse(s, base) do + {i, ""} -> i + _ -> :nan + end + end + + def to_int32(val) when is_integer(val), do: wrap_int32(val) + def to_int32(val) when is_float(val), do: wrap_int32(trunc(val)) + def to_int32(true), do: 1 + def to_int32(false), do: 0 + def to_int32(nil), do: 0 + def to_int32(:undefined), do: 0 + + def to_int32(val) when is_binary(val) do + case to_number(val) do + n when is_integer(n) -> wrap_int32(n) + n when is_float(n) -> wrap_int32(trunc(n)) + _ -> 0 + end + end + + def to_int32(_), do: 0 + + def to_uint32(val) when is_integer(val), do: Bitwise.band(val, 0xFFFFFFFF) + def to_uint32(val) when is_float(val), do: Bitwise.band(trunc(val), 0xFFFFFFFF) + def to_uint32(true), do: 1 + def to_uint32(false), do: 0 + def to_uint32(nil), do: 0 + def to_uint32(:undefined), do: 0 + + def to_uint32(val) when is_binary(val) do + case to_number(val) do + n when is_integer(n) -> Bitwise.band(n, 0xFFFFFFFF) + n when is_float(n) -> Bitwise.band(trunc(n), 0xFFFFFFFF) + _ -> 0 + end + end + + def to_uint32(_), do: 0 + + defp wrap_int32(n) do + n = Bitwise.band(n, 0xFFFFFFFF) + if n >= 0x80000000, do: n - 0x100000000, else: n + end + + def stringify(:undefined), do: "undefined" + def stringify(nil), do: "null" + def stringify(true), do: "true" + def stringify(false), do: "false" + def stringify(:nan), do: "NaN" + def stringify(:infinity), do: "Infinity" + def stringify(:neg_infinity), do: "-Infinity" + def stringify(n) when is_integer(n), do: Integer.to_string(n) + def stringify(n) when is_float(n) and n == 0.0, do: "0" + def stringify(n) when is_float(n), do: format_float(n) + def stringify({:bigint, n}), do: Integer.to_string(n) + def stringify({:symbol, desc}), do: "Symbol(#{desc})" + def stringify({:symbol, desc, _ref}), do: "Symbol(#{desc})" + def stringify(s) when is_binary(s), do: s + + def stringify({:obj, ref} = obj) do + data = Heap.get_obj(ref, %{}) + + case data do + {:qb_arr, arr} -> + :array.to_list(arr) + |> Enum.map(&stringify/1) + |> Enum.join(",") + + list when is_list(list) -> + Enum.map_join(list, ",", fn + :undefined -> "" + nil -> "" + v -> stringify(v) + end) + + map when is_map(map) -> + case Map.get(map, "toString") do + fun when fun != nil and fun != :undefined -> + stringify( + Interpreter.invoke_with_receiver( + fun, + [], + Runtime.gas_budget(), + obj + ) + ) + + _ -> + "[object Object]" + end + + _ -> + "[object Object]" + end + end + + def stringify(_), do: "[object]" + + def typeof(:undefined), do: "undefined" + def typeof(:nan), do: "number" + def typeof(:infinity), do: "number" + def typeof(nil), do: "object" + def typeof(true), do: "boolean" + def typeof(false), do: "boolean" + def typeof(val) when is_number(val), do: "number" + def typeof(val) when is_binary(val), do: "string" + def typeof(%Bytecode.Function{}), do: "function" + def typeof({:closure, _, %Bytecode.Function{}}), do: "function" + def typeof({:symbol, _}), do: "symbol" + def typeof({:symbol, _, _}), do: "symbol" + def typeof({:bound, _, _, _, _}), do: "function" + def typeof({:bigint, _}), do: "bigint" + def typeof({:builtin, _, _}), do: "function" + def typeof(_), do: "object" + + def strict_eq(:nan, :nan), do: false + def strict_eq(:infinity, :infinity), do: true + def strict_eq(:neg_infinity, :neg_infinity), do: true + def strict_eq({:bigint, a}, {:bigint, b}), do: a == b + def strict_eq({:symbol, _, ref1}, {:symbol, _, ref2}), do: ref1 === ref2 + def strict_eq(a, b) when is_number(a) and is_number(b), do: a == b + def strict_eq(a, b), do: a === b + + def add({:bigint, a}, {:bigint, b}), do: {:bigint, a + b} + + def add({:symbol, _}, _), + do: + throw( + {:js_throw, + Heap.make_error( + "Cannot convert a Symbol value to a string", + "TypeError" + )} + ) + + def add(_, {:symbol, _}), + do: + throw( + {:js_throw, + Heap.make_error( + "Cannot convert a Symbol value to a string", + "TypeError" + )} + ) + + def add({:symbol, _, _}, _), + do: + throw( + {:js_throw, + Heap.make_error( + "Cannot convert a Symbol value to a string", + "TypeError" + )} + ) + + def add(_, {:symbol, _, _}), + do: + throw( + {:js_throw, + Heap.make_error( + "Cannot convert a Symbol value to a string", + "TypeError" + )} + ) + + def add(a, b) when is_binary(a) or is_binary(b), do: stringify(a) <> stringify(b) + def add(a, b) when is_number(a) and is_number(b), do: a + b + def add(a, b), do: numeric_add(to_number(a), to_number(b)) + + defp numeric_add(a, b) when is_number(a) and is_number(b), do: a + b + defp numeric_add(:nan, _), do: :nan + defp numeric_add(_, :nan), do: :nan + defp numeric_add(:infinity, :neg_infinity), do: :nan + defp numeric_add(:neg_infinity, :infinity), do: :nan + defp numeric_add(:infinity, _), do: :infinity + defp numeric_add(:neg_infinity, _), do: :neg_infinity + defp numeric_add(_, :infinity), do: :infinity + defp numeric_add(_, :neg_infinity), do: :neg_infinity + defp numeric_add(_, _), do: :nan + + def sub({:bigint, a}, {:bigint, b}), do: {:bigint, a - b} + def sub(a, b) when is_number(a) and is_number(b), do: a - b + def sub(a, b), do: numeric_add(to_number(a), neg(to_number(b))) + + def mul({:bigint, a}, {:bigint, b}), do: {:bigint, a * b} + def mul(a, b) when is_number(a) and is_number(b), do: a * b + + def mul(a, b) do + na = to_number(a) + nb = to_number(b) + + cond do + na == :nan or nb == :nan -> + :nan + + na in [:infinity, :neg_infinity] or nb in [:infinity, :neg_infinity] -> + if na == 0 or nb == 0, do: :nan, else: mul_inf_sign(na, nb) + + # Reached when one or both args were non-numeric but to_number made them numeric (e.g. booleans) + is_number(na) and is_number(nb) -> + na * nb + + true -> + :nan + end + end + + defp mul_inf_sign(a, b) do + sign_a = if a == :neg_infinity or (is_number(a) and a < 0), do: -1, else: 1 + sign_b = if b == :neg_infinity or (is_number(b) and b < 0), do: -1, else: 1 + if sign_a * sign_b > 0, do: :infinity, else: :neg_infinity + end + + def div({:bigint, a}, {:bigint, b}) when b != 0, do: {:bigint, Kernel.div(a, b)} + + def div({:bigint, _}, {:bigint, 0}), + do: throw({:js_throw, %{"message" => "Division by zero", "name" => "RangeError"}}) + + def div(a, b) when is_number(a) and is_number(b), do: div_numbers(a, b) + + def div(a, b) do + na = to_number(a) + nb = to_number(b) + + cond do + na == :nan or nb == :nan -> + :nan + + na in [:infinity, :neg_infinity] or nb in [:infinity, :neg_infinity] -> + div_inf(na, nb) + + is_number(na) and is_number(nb) -> + div_numbers(na, nb) + + true -> + :nan + end + end + + defp div_inf(:infinity, :infinity), do: :nan + defp div_inf(:infinity, :neg_infinity), do: :nan + defp div_inf(:neg_infinity, :infinity), do: :nan + defp div_inf(:neg_infinity, :neg_infinity), do: :nan + defp div_inf(:infinity, n) when is_number(n) and n > 0, do: :infinity + defp div_inf(:infinity, n) when is_number(n) and n < 0, do: :neg_infinity + defp div_inf(:neg_infinity, n) when is_number(n) and n > 0, do: :neg_infinity + defp div_inf(:neg_infinity, n) when is_number(n) and n < 0, do: :infinity + defp div_inf(n, :infinity) when is_number(n), do: 0.0 + defp div_inf(n, :neg_infinity) when is_number(n), do: -0.0 + defp div_inf(_, _), do: :nan + + defp div_numbers(a, b) when b == 0, + do: if(neg_zero?(b), do: div_by_neg_zero(a), else: inf_or_nan(a)) + + defp div_numbers(a, b), do: a / b + + defp div_by_neg_zero(a) when a > 0, do: :neg_infinity + defp div_by_neg_zero(a) when a < 0, do: :infinity + defp div_by_neg_zero(_), do: :nan + + def mod({:bigint, a}, {:bigint, b}) when b != 0, do: {:bigint, rem(a, b)} + + def mod({:bigint, _}, {:bigint, 0}), + do: throw({:js_throw, %{"message" => "Division by zero", "name" => "RangeError"}}) + + def mod(a, b) when is_integer(a) and is_integer(b) and b != 0, do: rem(a, b) + def mod(a, b) when is_number(a) and is_number(b) and b != 0, do: a - Float.floor(a / b) * b + def mod(_, b) when is_number(b), do: :nan + + def mod(_, _), do: :nan + + def pow({:bigint, a}, {:bigint, b}) when b >= 0, do: {:bigint, Integer.pow(a, b)} + def pow(a, b) when is_number(a) and is_number(b), do: :math.pow(a, b) + def pow(_, _), do: :nan + + def neg({:bigint, a}), do: {:bigint, -a} + def neg(0), do: -0.0 + def neg(:infinity), do: :neg_infinity + def neg(:neg_infinity), do: :infinity + def neg(:nan), do: :nan + def neg(a) when is_number(a), do: -a + def neg(a), do: neg(to_number(a)) + + def neg_zero?(b), do: is_float(b) and b == 0.0 and hd(:erlang.float_to_list(b)) == ?- + + defp format_float(n) do + short = :erlang.float_to_binary(n, [:short]) + + cond do + String.contains?(short, "e") or String.contains?(short, "E") -> + format_js_exponential(short, n) + + String.ends_with?(short, ".0") -> + String.trim_trailing(short, ".0") + + true -> + short + end + end + + defp format_js_exponential(short, _n) do + {mantissa, exp} = + case String.split(short, ~r/[eE]/) do + [m, e] -> {m, String.to_integer(e)} + _ -> {short, 0} + end + + mantissa = + if String.ends_with?(mantissa, ".0"), + do: String.trim_trailing(mantissa, ".0"), + else: mantissa + + expand_exponential(mantissa, exp) + end + + defp expand_exponential(mantissa, exp) when exp >= 0 and exp <= 20 do + {prefix, digits, decimal_pos} = split_mantissa(mantissa) + total_pos = decimal_pos + exp + + if total_pos >= String.length(digits) do + prefix <> digits <> String.duplicate("0", total_pos - String.length(digits)) + else + prefix <> + String.slice(digits, 0, total_pos) <> "." <> String.slice(digits, total_pos..-1//1) + end + end + + defp expand_exponential(mantissa, exp) when exp < 0 and exp >= -6 do + {prefix, digits, _} = split_mantissa(mantissa) + prefix <> "0." <> String.duplicate("0", abs(exp) - 1) <> digits + end + + defp expand_exponential(mantissa, exp) do + sign = if exp >= 0, do: "+", else: "" + mantissa <> "e" <> sign <> Integer.to_string(exp) + end + + defp split_mantissa(mantissa) do + {prefix, abs_mantissa} = + case mantissa do + "-" <> rest -> {"-", rest} + other -> {"", other} + end + + digits = String.replace(abs_mantissa, ".", "") + + decimal_pos = + case String.split(abs_mantissa, ".") do + [int, _] -> String.length(int) + _ -> String.length(digits) + end + + {prefix, digits, decimal_pos} + end + + defp inf_or_nan(a) when a > 0, do: :infinity + defp inf_or_nan(a) when a < 0, do: :neg_infinity + defp inf_or_nan(_), do: :nan + + def band({:bigint, a}, {:bigint, b}), do: {:bigint, Bitwise.band(a, b)} + def band(a, b), do: Bitwise.band(to_int32(a), to_int32(b)) + def bor({:bigint, a}, {:bigint, b}), do: {:bigint, Bitwise.bor(a, b)} + def bor(a, b), do: Bitwise.bor(to_int32(a), to_int32(b)) + def bxor({:bigint, a}, {:bigint, b}), do: {:bigint, Bitwise.bxor(a, b)} + def bxor(a, b), do: Bitwise.bxor(to_int32(a), to_int32(b)) + + def shl({:bigint, a}, {:bigint, b}) when b >= 0 and b <= 1_000_000, + do: {:bigint, Bitwise.bsl(a, b)} + + def shl({:bigint, _}, {:bigint, _}), + do: throw({:js_throw, %{"message" => "Maximum BigInt size exceeded", "name" => "RangeError"}}) + + def shl(a, b), do: to_int32(Bitwise.bsl(to_int32(a), Bitwise.band(to_int32(b), 31))) + def sar({:bigint, a}, {:bigint, b}), do: {:bigint, Bitwise.bsr(a, b)} + def sar(a, b), do: Bitwise.bsr(to_int32(a), Bitwise.band(to_int32(b), 31)) + + def shr(a, b) do + ua = to_int32(a) &&& 0xFFFFFFFF + Bitwise.bsr(ua, Bitwise.band(to_int32(b), 31)) + end + + def lt({:bigint, a}, {:bigint, b}), do: a < b + def lt(a, b) when is_number(a) and is_number(b), do: a < b + def lt(a, b) when is_binary(a) and is_binary(b), do: a < b + def lt(a, b), do: numeric_compare(to_number(a), to_number(b), &Kernel. b + def gt(a, b) when is_number(a) and is_number(b), do: a > b + def gt(a, b) when is_binary(a) and is_binary(b), do: a > b + def gt(a, b), do: numeric_compare(to_number(a), to_number(b), &Kernel.>/2) + + def gte({:bigint, a}, {:bigint, b}), do: a >= b + def gte(a, b) when is_number(a) and is_number(b), do: a >= b + def gte(a, b) when is_binary(a) and is_binary(b), do: a >= b + def gte(a, b), do: numeric_compare(to_number(a), to_number(b), &Kernel.>=/2) + + defp numeric_compare(:nan, _, _), do: false + defp numeric_compare(_, :nan, _), do: false + defp numeric_compare(a, b, op) when is_number(a) and is_number(b), do: op.(a, b) + defp numeric_compare(_, _, _), do: false + + def eq({:bigint, a}, {:bigint, b}), do: a == b + def eq(a, b), do: abstract_eq(a, b) + def neq(a, b), do: not abstract_eq(a, b) + + defp abstract_eq(nil, nil), do: true + defp abstract_eq(nil, :undefined), do: true + defp abstract_eq(:undefined, nil), do: true + defp abstract_eq(:undefined, :undefined), do: true + defp abstract_eq(a, b) when is_number(a) and is_number(b), do: a == b + defp abstract_eq(a, b) when is_binary(a) and is_binary(b), do: a == b + defp abstract_eq(a, b) when is_boolean(a) and is_boolean(b), do: a == b + defp abstract_eq(true, b), do: abstract_eq(1, b) + defp abstract_eq(a, true), do: abstract_eq(a, 1) + defp abstract_eq(false, b), do: abstract_eq(0, b) + defp abstract_eq(a, false), do: abstract_eq(a, 0) + defp abstract_eq(a, b) when is_number(a) and is_binary(b), do: a == to_number(b) + defp abstract_eq(a, b) when is_binary(a) and is_number(b), do: to_number(a) == b + defp abstract_eq({:bigint, a}, b) when is_integer(b), do: a == b + defp abstract_eq({:bigint, a}, b) when is_float(b), do: a == b + + defp abstract_eq({:bigint, a}, b) when is_binary(b) do + case Integer.parse(b) do + {n, ""} -> a == n + _ -> false + end + end + + defp abstract_eq(a, {:bigint, b}) when is_binary(a) do + case Integer.parse(a) do + {n, ""} -> n == b + _ -> false + end + end + + defp abstract_eq(a, {:bigint, b}) when is_integer(a), do: a == b + defp abstract_eq(a, {:bigint, b}) when is_float(a), do: a == b + + defp abstract_eq({:obj, _} = obj, b) when is_number(b) or is_binary(b) do + prim = to_primitive(obj) + if match?({:obj, _}, prim), do: false, else: abstract_eq(prim, b) + end + + defp abstract_eq(a, {:obj, _} = obj) when is_number(a) or is_binary(a) do + prim = to_primitive(obj) + if match?({:obj, _}, prim), do: false, else: abstract_eq(a, prim) + end + + defp abstract_eq({:obj, ref1}, {:obj, ref2}), do: ref1 === ref2 + defp abstract_eq({:symbol, _, ref1}, {:symbol, _, ref2}), do: ref1 === ref2 + defp abstract_eq(_, _), do: false + + defp to_primitive({:obj, ref} = obj) do + data = Heap.get_obj(ref, %{}) + + if is_map(data) do + call_to_primitive(data, obj, "valueOf") || + proto_to_primitive(data, obj, "valueOf") || + call_to_primitive(data, obj, "toString") || + proto_to_primitive(data, obj, "toString") || + obj + else + obj + end + end + + defp call_to_primitive(map, obj, method) do + case Map.get(map, method) do + {:builtin, _, cb} -> + unwrap_primitive(cb.([], obj)) + + fun when fun != nil and fun != :undefined -> + unwrap_primitive(Interpreter.invoke_with_receiver(fun, [], Runtime.gas_budget(), obj)) + + _ -> + nil + end + end + + defp proto_to_primitive(map, obj, method) do + case Map.get(map, proto()) do + {:obj, pref} -> + pmap = Heap.get_obj(pref, %{}) + if is_map(pmap), do: call_to_primitive(pmap, obj, method) + + _ -> + nil + end + end + + defp unwrap_primitive({:obj, _}), do: nil + defp unwrap_primitive(val), do: val +end diff --git a/lib/quickbeam/vm/invocation.ex b/lib/quickbeam/vm/invocation.ex new file mode 100644 index 00000000..742e6d03 --- /dev/null +++ b/lib/quickbeam/vm/invocation.ex @@ -0,0 +1,407 @@ +defmodule QuickBEAM.VM.Invocation do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys, only: [proto: 0] + + alias QuickBEAM.VM.{Builtin, Bytecode, Compiler, GlobalEnv, Heap, Runtime} + alias QuickBEAM.VM.Compiler.Runner + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.Interpreter.Context + alias QuickBEAM.VM.Invocation.Context, as: InvokeContext + alias QuickBEAM.VM.ObjectModel.{Class, Get} + + def invoke(fun, args, gas \\ Runtime.gas_budget()) + + def invoke(%Bytecode.Function{} = fun, args, gas) do + track_invoke_depth() + + result = + case Compiler.invoke(fun, args) do + {:ok, result} -> result + :error -> Interpreter.invoke_function_fallback(fun, args, gas, active_ctx()) + end + + maybe_gc(result, [fun | args]) + end + + def invoke({:closure, _, %Bytecode.Function{} = inner} = closure, args, gas) do + track_invoke_depth() + + result = + if compiled_closure_callable?(inner) do + case Runner.invoke(closure, args) do + {:ok, result} -> result + :error -> Interpreter.invoke_closure_fallback(closure, args, gas, active_ctx()) + end + else + Interpreter.invoke_closure_fallback(closure, args, gas, active_ctx()) + end + + maybe_gc(result, [closure | args]) + end + + def invoke(other, args, _gas) when not is_tuple(other) or elem(other, 0) != :bound, + do: Builtin.call(other, args, nil) + + def invoke({:bound, _, inner, _, _}, args, gas), do: invoke(inner, args, gas) + + def invoke_with_receiver(fun, args, this_obj), + do: invoke_with_receiver(fun, args, Runtime.gas_budget(), this_obj) + + def invoke_with_receiver(fun, args, gas, this_obj) do + prev = Heap.get_ctx() + Heap.put_ctx(%{active_ctx() | this: this_obj} |> InvokeContext.attach_method_state()) + + try do + invoke_receiver_target(fun, args, gas, this_obj) + after + if prev, do: Heap.put_ctx(prev), else: Heap.put_ctx(nil) + end + end + + def invoke_constructor(fun, args, this_obj, new_target), + do: invoke_constructor(fun, args, Runtime.gas_budget(), this_obj, new_target) + + def invoke_constructor(fun, args, gas, this_obj, new_target) do + prev = Heap.get_ctx() + + ctor_ctx = + %{active_ctx() | this: this_obj, new_target: new_target} + |> InvokeContext.attach_method_state() + + Heap.put_ctx(ctor_ctx) + + try do + dispatch(fun, args, gas, ctor_ctx, this_obj) + after + if prev, do: Heap.put_ctx(prev), else: Heap.put_ctx(nil) + end + end + + def dispatch(fun, args, gas, ctx, this) do + case fun do + %Bytecode.Function{} = bytecode_fun -> + Interpreter.invoke_function_fallback(bytecode_fun, args, gas, ctx) + + {:closure, _, %Bytecode.Function{}} = closure -> + Interpreter.invoke_closure_fallback(closure, args, gas, ctx) + + {:bound, _, inner, _, _} -> + invoke(inner, args, gas) + + other -> + Builtin.call(other, args, this) + end + end + + def call_callback(fun, args), do: call_callback(active_ctx(), fun, args) + + def call_callback(ctx, fun, args) do + case fun do + %Bytecode.Function{} = bytecode_fun -> + callback_invoke(bytecode_fun, args, ctx) + + {:closure, _, %Bytecode.Function{}} = closure -> + callback_invoke(closure, args, ctx) + + other -> + try do + Builtin.call(other, args, nil) + catch + {:js_throw, _} -> :undefined + end + end + end + + def invoke_callback(fun, args), do: invoke_callback(active_ctx(), fun, args) + + def invoke_callback(ctx, fun, args) do + case fun do + %Bytecode.Function{} = bytecode_fun -> + callback_invoke(bytecode_fun, args, ctx, fn -> List.first(args, :undefined) end) + + {:closure, _, %Bytecode.Function{}} = closure -> + callback_invoke(closure, args, ctx, fn -> List.first(args, :undefined) end) + + _ -> + try do + Builtin.call(fun, args, nil) + catch + {:js_throw, _} -> List.first(args, :undefined) + end + end + end + + def invoke_runtime(fun, args), do: invoke_runtime(active_ctx(), fun, args) + + def invoke_runtime(%Context{} = ctx, {:closure, _, %Bytecode.Function{need_home_object: false} = inner} = closure, args) do + key = {inner.byte_code, inner.arg_count} + + case Heap.get_compiled(key) do + {:compiled, {mod, name}, atoms} -> + nargs = Runner.normalize_args(args, inner.arg_count) + + fast_ctx = %{ + ctx + | current_func: closure, + arg_buf: List.to_tuple(nargs), + atoms: atoms || ctx.atoms, + pd_synced: false + } + + apply(mod, name, [fast_ctx | nargs]) + + _ -> + invoke_runtime_full(ctx, closure, args) + end + end + + def invoke_runtime(ctx, fun, args), do: invoke_runtime_full(ctx, fun, args) + + defp invoke_runtime_full(ctx, fun, args) do + case fun do + %Bytecode.Function{} = bytecode_fun -> + case Runner.invoke(bytecode_fun, args, ctx) do + {:ok, value} -> value + :error -> Interpreter.invoke_function_fallback(bytecode_fun, args, ctx.gas, ctx) + end + + {:closure, _, %Bytecode.Function{} = inner} = closure -> + invoke_closure(closure, inner, args, ctx) + + {:bound, _, inner, _, _} -> + invoke_runtime(ctx, inner, args) + + other -> + Builtin.call(other, args, nil) + end + end + + def invoke_method_runtime(fun, this_obj, args), + do: invoke_method_runtime(active_ctx(), fun, this_obj, args) + + def invoke_method_runtime(ctx, fun, this_obj, args) do + case fun do + %Bytecode.Function{} = bytecode_fun -> + if compiled_method_callable?(bytecode_fun, this_obj) do + case Runner.invoke_with_receiver(bytecode_fun, args, this_obj, ctx) do + {:ok, value} -> + value + + :error -> + Interpreter.invoke_function_fallback( + bytecode_fun, + args, + ctx.gas, + Context.mark_dirty(%{ctx | this: this_obj}) + ) + end + else + Interpreter.invoke_function_fallback( + bytecode_fun, + args, + ctx.gas, + Context.mark_dirty(%{ctx | this: this_obj}) + ) + end + + {:closure, _, %Bytecode.Function{} = inner} = closure -> + if compiled_method_callable?(inner, this_obj) do + case Runner.invoke_with_receiver(closure, args, this_obj, ctx) do + {:ok, value} -> + value + + :error -> + Interpreter.invoke_closure_fallback( + closure, + args, + ctx.gas, + Context.mark_dirty(%{ctx | this: this_obj}) + ) + end + else + Interpreter.invoke_closure_fallback( + closure, + args, + ctx.gas, + Context.mark_dirty(%{ctx | this: this_obj}) + ) + end + + {:bound, _, inner, _, _} -> + invoke_method_runtime(ctx, inner, this_obj, args) + + other -> + Builtin.call(other, args, this_obj) + end + end + + def construct_runtime(ctor, new_target, args), + do: construct_runtime(active_ctx(), ctor, new_target, args) + + def construct_runtime(ctx, ctor, new_target, args) do + raw_ctor = unwrap_constructor_target(ctor) + raw_new_target = unwrap_new_target(new_target) + + ctor_proto = + constructor_prototype(raw_new_target) || constructor_prototype(raw_ctor) || + Heap.get_object_prototype() + + init = if ctor_proto, do: %{proto() => ctor_proto}, else: %{} + this_obj = Heap.wrap(init) + + result = + case ctor do + %Bytecode.Function{} = fun -> + case Runner.invoke_constructor(fun, args, this_obj, new_target, ctx) do + {:ok, value} -> value + :error -> invoke_constructor(fun, args, ctx.gas, this_obj, new_target) + end + + {:closure, _, %Bytecode.Function{}} = closure -> + case Runner.invoke_constructor(closure, args, this_obj, new_target, ctx) do + {:ok, value} -> + value + + :error -> + invoke_constructor(closure, args, ctx.gas, this_obj, new_target) + end + + {:bound, _, _inner, orig_fun, bound_args} -> + construct_runtime(orig_fun, new_target, bound_args ++ args) + + {:builtin, _name, cb} when is_function(cb, 2) -> + cb.(args, this_obj) + + _ -> + this_obj + end + + Class.coalesce_this_result(result, this_obj) + end + + defp maybe_gc(result, extra_roots) do + depth = Process.get(:qb_invoke_depth, 0) - 1 + Process.put(:qb_invoke_depth, depth) + + if depth == 0 and Heap.gc_needed?() do + Heap.gc([result | extra_roots]) + end + + result + end + + defp track_invoke_depth do + depth = Process.get(:qb_invoke_depth, 0) + 1 + Process.put(:qb_invoke_depth, depth) + end + + defp active_ctx do + base_globals = GlobalEnv.base_globals() + + case Heap.get_ctx() do + %Context{} = ctx when ctx.globals == %{} -> + Context.mark_dirty(%{ctx | globals: base_globals}) + + %Context{} = ctx -> + ctx + + nil -> + %Context{atoms: Heap.get_atoms(), globals: base_globals} + + map -> + struct( + Context, + Map.merge(Map.from_struct(%Context{}), Map.put(map, :globals, base_globals)) + ) + end + end + + defp invoke_receiver_target(%Bytecode.Function{} = fun, args, gas, this_obj) do + if compiled_method_callable?(fun, this_obj) do + case Runner.invoke_with_receiver(fun, args, this_obj) do + {:ok, value} -> value + :error -> Interpreter.invoke_function_fallback(fun, args, gas, Heap.get_ctx()) + end + else + Interpreter.invoke_function_fallback(fun, args, gas, Heap.get_ctx()) + end + end + + defp invoke_receiver_target( + {:closure, _, %Bytecode.Function{} = inner} = closure, + args, + gas, + this_obj + ) do + if compiled_method_callable?(inner, this_obj) do + case Runner.invoke_with_receiver(closure, args, this_obj) do + {:ok, value} -> value + :error -> Interpreter.invoke_closure_fallback(closure, args, gas, Heap.get_ctx()) + end + else + Interpreter.invoke_closure_fallback(closure, args, gas, Heap.get_ctx()) + end + end + + defp invoke_receiver_target(other, args, gas, this_obj), + do: dispatch(other, args, gas, Heap.get_ctx(), this_obj) + + defp callback_invoke(fun, args, ctx, on_throw \\ fn -> :undefined end) + + defp callback_invoke(%Bytecode.Function{} = fun, args, ctx, on_throw) do + try do + case Runner.invoke(fun, args, ctx) do + {:ok, value} -> value + :error -> Interpreter.invoke_function_fallback(fun, args, ctx.gas, ctx) + end + catch + {:js_throw, _} -> on_throw.() + end + end + + defp callback_invoke({:closure, _, %Bytecode.Function{} = inner} = closure, args, ctx, on_throw) do + try do + invoke_closure(closure, inner, args, ctx) + catch + {:js_throw, _} -> on_throw.() + end + end + + defp invoke_closure(closure, %Bytecode.Function{need_home_object: false}, args, ctx) do + case Runner.invoke(closure, args, ctx) do + {:ok, value} -> value + :error -> Interpreter.invoke_closure_fallback(closure, args, ctx.gas, ctx) + end + end + + defp invoke_closure(closure, _inner, args, ctx) do + Interpreter.invoke_closure_fallback(closure, args, ctx.gas, ctx) + end + + defp compiled_closure_callable?(%Bytecode.Function{need_home_object: false}), do: true + defp compiled_closure_callable?(_), do: false + + defp compiled_method_callable?(%Bytecode.Function{need_home_object: false}, {:obj, _}), do: true + defp compiled_method_callable?(_, _), do: false + + defp unwrap_constructor_target({:closure, _, %Bytecode.Function{} = fun}), do: fun + defp unwrap_constructor_target({:bound, _, inner, _, _}), do: unwrap_constructor_target(inner) + defp unwrap_constructor_target(other), do: other + + defp unwrap_new_target({:closure, _, %Bytecode.Function{} = fun}), do: fun + defp unwrap_new_target(%Bytecode.Function{} = fun), do: fun + defp unwrap_new_target(_), do: nil + + defp constructor_prototype(nil), do: nil + + defp constructor_prototype(target) do + case Heap.get_class_proto(target) do + {:obj, _} = proto_obj -> proto_obj + _ -> normalize_constructor_prototype(Get.get(target, "prototype")) + end + end + + defp normalize_constructor_prototype({:obj, _} = object_proto), do: object_proto + defp normalize_constructor_prototype(_), do: nil +end diff --git a/lib/quickbeam/vm/invocation/context.ex b/lib/quickbeam/vm/invocation/context.ex new file mode 100644 index 00000000..e522c36f --- /dev/null +++ b/lib/quickbeam/vm/invocation/context.ex @@ -0,0 +1,165 @@ +defmodule QuickBEAM.VM.Invocation.Context do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Heap, Runtime} + alias QuickBEAM.VM.Interpreter.Context + alias QuickBEAM.VM.ObjectModel.{Class, Functions} + + @fast_ctx_key :qb_fast_ctx + @missing :__qb_missing__ + + def snapshot_fast_ctx, do: Process.get(@fast_ctx_key, @missing) + + def restore_fast_ctx(@missing), do: Process.delete(@fast_ctx_key) + def restore_fast_ctx(snapshot), do: Process.put(@fast_ctx_key, snapshot) + + def put_fast_ctx(ctx) do + current_func = Map.get(ctx, :current_func, :undefined) + home_object = Functions.current_home_object(current_func) + + Process.put( + @fast_ctx_key, + { + Map.get(ctx, :atoms, {}), + Map.get(ctx, :globals, %{}), + current_func, + Map.get(ctx, :arg_buf, {}), + Map.get(ctx, :this, :undefined), + Map.get(ctx, :new_target, :undefined), + home_object, + current_super(home_object) + } + ) + end + + def fast_ctx, do: Process.get(@fast_ctx_key, @missing) + + def attach_method_state( + %Context{current_func: %Bytecode.Function{need_home_object: false}} = ctx + ), do: ctx + + def attach_method_state( + %Context{current_func: {:closure, _, %Bytecode.Function{need_home_object: false}}} = ctx + ), + do: ctx + + def attach_method_state(%Context{current_func: current_func} = ctx) do + home_object = Functions.current_home_object(current_func) + + ctx + |> Map.merge(%{home_object: home_object, super: current_super(home_object)}) + |> Context.mark_dirty() + end + + def current_atoms do + case fast_ctx() do + {atoms, _globals, _current_func, _arg_buf, _this, _new_target, _home_object, _super} -> + atoms + + _ -> + case Heap.get_ctx() do + %{atoms: atoms} -> atoms + _ -> Heap.get_atoms() + end + end + end + + def current_globals do + case fast_ctx() do + {_atoms, globals, _current_func, _arg_buf, _this, _new_target, _home_object, _super} -> + globals + + _ -> + case Heap.get_ctx() do + %{globals: globals} -> globals + _ -> Runtime.global_bindings() + end + end + end + + def current_func do + case fast_ctx() do + {_atoms, _globals, current_func, _arg_buf, _this, _new_target, _home_object, _super} -> + current_func + + _ -> + case Heap.get_ctx() do + %{current_func: current_func} -> current_func + _ -> :undefined + end + end + end + + def current_arg_buf do + case fast_ctx() do + {_atoms, _globals, _current_func, arg_buf, _this, _new_target, _home_object, _super} -> + arg_buf + + _ -> + case Heap.get_ctx() do + %{arg_buf: arg_buf} -> arg_buf + _ -> {} + end + end + end + + def current_this do + case fast_ctx() do + {_atoms, _globals, _current_func, _arg_buf, this, _new_target, _home_object, _super} -> + this + + _ -> + case Heap.get_ctx() do + %{this: this} -> this + _ -> :undefined + end + end + end + + def current_new_target do + case fast_ctx() do + {_atoms, _globals, _current_func, _arg_buf, _this, new_target, _home_object, _super} -> + new_target + + _ -> + case Heap.get_ctx() do + %{new_target: new_target} -> new_target + _ -> :undefined + end + end + end + + def current_home_object(current_func \\ current_func()) + + def current_home_object(%Bytecode.Function{need_home_object: false}), do: :undefined + + def current_home_object({:closure, _, %Bytecode.Function{need_home_object: false}}), + do: :undefined + + def current_home_object(current_func) do + case fast_ctx() do + {_atoms, _globals, _current_func, _arg_buf, _this, _new_target, home_object, _super} -> + home_object + + _ -> + Functions.current_home_object(current_func) + end + end + + def current_super(home_object \\ current_home_object()) + def current_super(:undefined), do: :undefined + def current_super(nil), do: :undefined + + def current_super(home_object) do + case fast_ctx() do + {_atoms, _globals, _current_func, _arg_buf, _this, _new_target, cached_home_object, super} + when cached_home_object == home_object -> + super + + _ -> + Class.get_super(home_object) + end + end + + def missing, do: @missing +end diff --git a/lib/quickbeam/vm/leb128.ex b/lib/quickbeam/vm/leb128.ex new file mode 100644 index 00000000..8bcfe50e --- /dev/null +++ b/lib/quickbeam/vm/leb128.ex @@ -0,0 +1,59 @@ +defmodule QuickBEAM.VM.LEB128 do + @moduledoc false + import Bitwise + + @spec read_unsigned(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, :bad_leb128} + def read_unsigned(<>), do: read_unsigned(rest, 0, 0) + + defp read_unsigned(<<1::1, value::7, rest::binary>>, acc, shift) when shift < 64 do + read_unsigned(rest, acc + (value <<< shift), shift + 7) + end + + defp read_unsigned(<<0::1, value::7, rest::binary>>, acc, shift) do + {:ok, acc + (value <<< shift), rest} + end + + defp read_unsigned(_, _, _), do: {:error, :bad_leb128} + + @spec read_signed(binary()) :: {:ok, integer(), binary()} | {:error, :bad_sleb128} + def read_signed(<>), do: read_signed(rest, 0, 0) + + defp read_signed(<<1::1, value::7, rest::binary>>, acc, shift) when shift < 64 do + read_signed(rest, acc + (value <<< shift), shift + 7) + end + + defp read_signed(<<0::1, value::7, rest::binary>>, acc, shift) do + result = acc + (value <<< shift) + # Sign extend if the last byte's high bit is set + size = shift + 7 + # Sign-extend: shift left to put sign bit at position 63, then arithmetic shift right + {:ok, (result <<< (64 - size)) >>> (64 - size), rest} + end + + defp read_signed(_, _, _), do: {:error, :bad_sleb128} + + @spec read_u16(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()} + def read_u16(bin) do + with {:ok, val, rest} <- read_unsigned(bin) do + {:ok, band(val, 0xFFFF), rest} + end + end + + @spec read_u8(binary()) :: {:ok, byte(), binary()} | {:error, :unexpected_end} + def read_u8(<>), do: {:ok, val, rest} + def read_u8(_), do: {:error, :unexpected_end} + + @spec read_u32(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()} + def read_u32(bin) do + with {:ok, val, rest} <- read_unsigned(bin) do + {:ok, band(val, 0xFFFFFFFF), rest} + end + end + + @spec read_u64(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()} + def read_u64(<>), do: {:ok, val, rest} + def read_u64(_), do: {:error, :unexpected_end} + + @spec read_i32(binary()) :: {:ok, integer(), binary()} | {:error, term()} + def read_i32(bin), do: read_signed(bin) +end diff --git a/lib/quickbeam/vm/names.ex b/lib/quickbeam/vm/names.ex new file mode 100644 index 00000000..5bc402ac --- /dev/null +++ b/lib/quickbeam/vm/names.ex @@ -0,0 +1,76 @@ +defmodule QuickBEAM.VM.Names do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Heap, PredefinedAtoms} + alias QuickBEAM.VM.Interpreter.Context + alias QuickBEAM.VM.Interpreter.Values + + @js_atom_end QuickBEAM.VM.Opcodes.js_atom_end() + + def resolve_const(cpool, idx) when is_tuple(cpool) and idx < tuple_size(cpool) do + case elem(cpool, idx) do + {:array, list} when is_list(list) -> + ref = make_ref() + Heap.put_obj(ref, list) + {:obj, ref} + + other -> + other + end + end + + def resolve_const(_cpool, idx), do: {:const_ref, idx} + + def resolve_atom(%Context{atoms: atoms}, idx), do: resolve_atom(atoms, idx) + + def resolve_atom(_atoms, :empty_string), do: "" + + def resolve_atom(_atoms, {:predefined, idx}) when idx < @js_atom_end do + PredefinedAtoms.lookup(idx) || "atom_#{idx}" + end + + def resolve_atom(_atoms, {:tagged_int, val}), do: val + + def resolve_atom(atoms, idx) when is_integer(idx) and idx >= 0 and is_tuple(atoms) do + if idx < tuple_size(atoms), do: elem(atoms, idx), else: {:atom, idx} + end + + def resolve_atom(_atoms, other) when is_binary(other), do: other + def resolve_atom(_atoms, other) when is_integer(other), do: Integer.to_string(other) + def resolve_atom(_atoms, {:atom, n}), do: "atom_#{n}" + def resolve_atom(_atoms, other), do: inspect(other) + + def resolve_display_name(name, atoms \\ Heap.get_atoms()) + + def resolve_display_name(name, _atoms) when is_binary(name), do: name + def resolve_display_name({:predefined, idx}, _atoms), do: PredefinedAtoms.lookup(idx) + def resolve_display_name(idx, atoms) when is_integer(idx), do: resolve_atom(atoms, idx) + def resolve_display_name(_name, _atoms), do: nil + + def function_name(name_val) do + case name_val do + s when is_binary(s) -> s + n when is_number(n) -> Values.stringify(n) + {:symbol, desc, _} -> "[" <> desc <> "]" + {:symbol, desc} -> "[" <> desc <> "]" + _ -> "" + end + end + + def rename_function({:closure, captured, %Bytecode.Function{} = fun}, name), + do: {:closure, captured, %{fun | name: name}} + + def rename_function(%Bytecode.Function{} = fun, name), do: %{fun | name: name} + def rename_function({:builtin, _, cb}, name), do: {:builtin, name, cb} + def rename_function(other, _name), do: other + + def normalize_property_key(idx) do + case idx do + i when is_integer(i) -> Integer.to_string(i) + {:symbol, _} = sym -> sym + {:symbol, _, _} = sym -> sym + s when is_binary(s) -> s + other -> Kernel.to_string(other) + end + end +end diff --git a/lib/quickbeam/vm/object_model/class.ex b/lib/quickbeam/vm/object_model/class.ex new file mode 100644 index 00000000..f532c76c --- /dev/null +++ b/lib/quickbeam/vm/object_model/class.ex @@ -0,0 +1,183 @@ +defmodule QuickBEAM.VM.ObjectModel.Class do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys, only: [proto: 0] + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.Invocation + alias QuickBEAM.VM.Names + alias QuickBEAM.VM.ObjectModel.{Functions, Get, Put} + + def get_super(func) do + case func do + {:obj, ref} -> + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> Map.get(map, proto(), :undefined) + _ -> :undefined + end + + {:closure, _, %Bytecode.Function{} = fun} -> + Heap.get_parent_ctor(fun) || :undefined + + %Bytecode.Function{} = fun -> + Heap.get_parent_ctor(fun) || :undefined + + {:builtin, _, _} = builtin -> + Map.get(Heap.get_ctor_statics(builtin), "__proto__", :undefined) + + _ -> + :undefined + end + end + + def coalesce_this_result(result, this_obj) do + case result do + {:obj, _} = obj -> obj + %Bytecode.Function{} = fun -> fun + {:closure, _, %Bytecode.Function{}} = closure -> closure + _ -> this_obj + end + end + + def raw_function({:closure, _, %Bytecode.Function{} = fun}), do: fun + def raw_function(%Bytecode.Function{} = fun), do: fun + def raw_function(other), do: other + + def define_class(ctor_closure, parent_ctor, class_name \\ nil) do + ctor_closure = + if is_binary(class_name) and class_name != "" do + Functions.rename(ctor_closure, class_name) + else + ctor_closure + end + + raw = raw_function(ctor_closure) + proto_ref = make_ref() + proto_map = %{"constructor" => ctor_closure} + parent_proto = Heap.get_class_proto(parent_ctor) + base_proto = parent_proto || Heap.get_object_prototype() + proto_map = if base_proto, do: Map.put(proto_map, proto(), base_proto), else: proto_map + + Heap.put_obj(proto_ref, proto_map) + + Heap.put_prop_desc(proto_ref, "constructor", %{ + writable: true, + enumerable: false, + configurable: true + }) + + proto_obj = {:obj, proto_ref} + Heap.put_class_proto(raw, proto_obj) + Heap.put_ctor_statics(ctor_closure, %{"prototype" => proto_obj}) + + if parent_ctor != :undefined do + Heap.put_parent_ctor(raw, parent_ctor) + else + Heap.delete_parent_ctor(raw) + end + + {proto_obj, ctor_closure} + end + + def check_ctor_return(val) do + cond do + val == :undefined -> {true, val} + object_like?(val) -> {false, val} + true -> :error + end + end + + def get_super_value(proto_obj, this_obj, key) do + case find_super_property(proto_obj, key) do + {:accessor, getter, _} when getter != nil -> + Invocation.invoke_with_receiver(getter, [], this_obj) + + :undefined -> + :undefined + + val -> + val + end + end + + def put_super_value(proto_obj, this_obj, key, val) do + case find_super_setter(proto_obj, key) do + nil -> Put.put(this_obj, key, val) + setter -> Invocation.invoke_with_receiver(setter, [val], this_obj) + end + + :ok + end + + def define_class_name(ctor_closure, atom_idx, atoms \\ Heap.get_atoms()) do + define_class(ctor_closure, :undefined, Names.resolve_atom(atoms, atom_idx)) + end + + defp object_like?({:obj, _}), do: true + defp object_like?(%Bytecode.Function{}), do: true + defp object_like?({:closure, _, %Bytecode.Function{}}), do: true + defp object_like?({:builtin, _, _}), do: true + defp object_like?({:bound, _, _, _, _}), do: true + defp object_like?(_), do: false + + defp find_super_setter(proto_obj, key) do + case find_super_property(proto_obj, key) do + {:accessor, _, setter} when setter != nil -> setter + _ -> nil + end + end + + defp find_super_property({:obj, ref}, key) do + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> + case Map.fetch(map, key) do + {:ok, val} -> val + :error -> find_super_property(Map.get(map, proto(), :undefined), key) + end + + _ -> + Get.get({:obj, ref}, key) + end + end + + defp find_super_property({:closure, _, %Bytecode.Function{} = fun} = ctor, key) do + statics = Heap.get_ctor_statics(ctor) + + case Map.fetch(statics, key) do + {:ok, val} -> + val + + :error -> + find_super_property( + Heap.get_parent_ctor(fun) || Map.get(statics, "__proto__", :undefined), + key + ) + end + end + + defp find_super_property(%Bytecode.Function{} = fun, key) do + statics = Heap.get_ctor_statics(fun) + + case Map.fetch(statics, key) do + {:ok, val} -> + val + + :error -> + find_super_property( + Heap.get_parent_ctor(fun) || Map.get(statics, "__proto__", :undefined), + key + ) + end + end + + defp find_super_property({:builtin, _, _} = ctor, key) do + statics = Heap.get_ctor_statics(ctor) + + case Map.fetch(statics, key) do + {:ok, val} -> val + :error -> find_super_property(Map.get(statics, "__proto__", :undefined), key) + end + end + + defp find_super_property(value, key), do: Get.get(value, key) +end diff --git a/lib/quickbeam/vm/object_model/copy.ex b/lib/quickbeam/vm/object_model/copy.ex new file mode 100644 index 00000000..e3542e32 --- /dev/null +++ b/lib/quickbeam/vm/object_model/copy.ex @@ -0,0 +1,216 @@ +defmodule QuickBEAM.VM.ObjectModel.Copy do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys, + only: [key_order: 0, map_data: 0, proto: 0, proxy_handler: 0, proxy_target: 0, set_data: 0] + + alias QuickBEAM.VM.{Heap, Runtime} + alias QuickBEAM.VM.ObjectModel.Get + + def append_spread(arr, idx, obj) do + src_list = spread_source_to_list(obj) + arr_list = spread_target_to_list(arr) + new_idx = if(is_integer(idx), do: idx, else: Runtime.to_int(idx)) + length(src_list) + merged = arr_list ++ src_list + + merged_obj = + case arr do + {:obj, ref} -> + Heap.put_obj(ref, merged) + {:obj, ref} + + _ -> + merged + end + + {new_idx, merged_obj} + end + + def copy_data_properties(target, source) do + src_props = enumerable_string_props(source) + + case target do + {:obj, ref} -> + existing = Heap.get_obj(ref, %{}) + existing = if is_map(existing), do: existing, else: %{} + Heap.put_obj(ref, Map.merge(existing, src_props)) + + _ -> + :ok + end + + :ok + end + + def enumerable_string_props({:obj, ref} = source_obj) do + case Heap.get_obj_raw(ref) do + {:shape, shape_id, _offsets, vals, _proto} -> + Heap.Shapes.to_map(shape_id, vals, nil) + + {:qb_arr, _} -> + Enum.reduce(0..max(Heap.array_size(ref) - 1, 0), %{}, fn i, acc -> + Map.put(acc, Integer.to_string(i), Get.get(source_obj, Integer.to_string(i))) + end) + + list when is_list(list) -> + Enum.reduce(0..max(length(list) - 1, 0), %{}, fn i, acc -> + Map.put(acc, Integer.to_string(i), Get.get(source_obj, Integer.to_string(i))) + end) + + map when is_map(map) -> + map + |> Map.keys() + |> Enum.filter(&is_binary/1) + |> Enum.reject(fn key -> + String.starts_with?(key, "__") and String.ends_with?(key, "__") + end) + |> Enum.reduce(%{}, fn key, acc -> Map.put(acc, key, Get.get(source_obj, key)) end) + + _ -> + %{} + end + end + + def enumerable_string_props(map) when is_map(map), do: map + def enumerable_string_props(_), do: %{} + + def enumerable_keys({:obj, ref} = obj) do + case Heap.get_obj_raw(ref) do + {:shape, shape_id, _offsets, _vals, proto} -> + own_keys = Heap.Shapes.keys(shape_id) |> Enum.filter(&enumerable_key_candidate?/1) + proto_keys = enumerable_proto_keys(proto) + Runtime.sort_numeric_keys(own_keys ++ Enum.reject(proto_keys, &(&1 in own_keys))) + + raw -> + enumerable_keys_from_raw(obj, ref, raw) + end + end + + defp enumerable_keys_from_raw(obj, ref, raw) do + case raw || %{} do + %{proxy_target() => _target, proxy_handler() => handler} -> + own_keys_fn = Get.get(handler, "ownKeys") + + if own_keys_fn != :undefined and own_keys_fn != nil do + result = Runtime.call_callback(own_keys_fn, [obj]) + Heap.to_list(result) |> Enum.map(&to_string/1) + else + [] + end + + {:qb_arr, arr} -> + numeric_index_keys(:array.size(arr)) + + list when is_list(list) -> + numeric_index_keys(length(list)) + + map when is_map(map) -> + own_keys = enumerable_object_keys(map, ref) + proto_keys = enumerable_proto_keys(Map.get(map, proto())) + Runtime.sort_numeric_keys(own_keys ++ Enum.reject(proto_keys, &(&1 in own_keys))) + + _ -> + [] + end + end + + def enumerable_keys(map) when is_map(map) do + map + |> Map.keys() + |> Enum.filter(&is_binary/1) + |> Enum.reject(fn key -> String.starts_with?(key, "__") and String.ends_with?(key, "__") end) + |> Runtime.sort_numeric_keys() + end + + def enumerable_keys(list) when is_list(list), do: numeric_index_keys(length(list)) + + def enumerable_keys(string) when is_binary(string), + do: numeric_index_keys(Get.string_length(string)) + + def enumerable_keys(_), do: [] + + def spread_source_to_list({:qb_arr, arr}), do: :array.to_list(arr) + def spread_source_to_list(list) when is_list(list), do: list + + def spread_source_to_list({:obj, ref}) do + case Heap.get_obj(ref) do + {:qb_arr, arr} -> + :array.to_list(arr) + + list when is_list(list) -> + list + + map when is_map(map) -> + cond do + Map.has_key?(map, {:symbol, "Symbol.iterator"}) -> + iter_fn = Map.get(map, {:symbol, "Symbol.iterator"}) + iter_obj = Runtime.call_callback(iter_fn, []) + collect_iterator_values(iter_obj, []) + + Map.has_key?(map, set_data()) -> + Map.get(map, set_data(), []) + + Map.has_key?(map, map_data()) -> + Map.get(map, map_data(), []) + + true -> + [] + end + + _ -> + [] + end + end + + def spread_source_to_list(_), do: [] + + def spread_target_to_list({:qb_arr, arr}), do: :array.to_list(arr) + def spread_target_to_list(list) when is_list(list), do: list + def spread_target_to_list({:obj, _} = obj), do: Heap.to_list(obj) + def spread_target_to_list(_), do: [] + + defp collect_iterator_values(iter_obj, acc) do + next_fn = Get.get(iter_obj, "next") + result = Runtime.call_callback(next_fn, []) + + if Get.get(result, "done") do + Enum.reverse(acc) + else + collect_iterator_values(iter_obj, [Get.get(result, "value") | acc]) + end + end + + defp enumerable_object_keys(map, ref) do + raw_keys = + case Map.get(map, key_order()) do + order when is_list(order) -> Enum.reverse(order) + _ -> Map.keys(map) + end + + raw_keys + |> Enum.filter(&enumerable_key_candidate?/1) + |> Enum.reject(fn key -> match?(%{enumerable: false}, Heap.get_prop_desc(ref, key)) end) + end + + defp enumerable_proto_keys({:obj, ref}) do + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> + own_keys = enumerable_object_keys(map, ref) + parent_keys = enumerable_proto_keys(Map.get(map, proto())) + own_keys ++ Enum.reject(parent_keys, &(&1 in own_keys)) + + _ -> + [] + end + end + + defp enumerable_proto_keys(_), do: [] + + defp enumerable_key_candidate?(key) when is_binary(key), + do: not (String.starts_with?(key, "__") and String.ends_with?(key, "__")) + + defp enumerable_key_candidate?(_), do: false + + defp numeric_index_keys(size) when size <= 0, do: [] + defp numeric_index_keys(size), do: Enum.map(0..(size - 1), &Integer.to_string/1) +end diff --git a/lib/quickbeam/vm/object_model/delete.ex b/lib/quickbeam/vm/object_model/delete.ex new file mode 100644 index 00000000..ab30d254 --- /dev/null +++ b/lib/quickbeam/vm/object_model/delete.ex @@ -0,0 +1,41 @@ +defmodule QuickBEAM.VM.ObjectModel.Delete do + @moduledoc false + + alias QuickBEAM.VM.Heap + + def delete_property(nil, key) do + throw( + {:js_throw, + Heap.make_error("Cannot delete properties of null (deleting '#{key}')", "TypeError")} + ) + end + + def delete_property(:undefined, key) do + throw( + {:js_throw, + Heap.make_error( + "Cannot delete properties of undefined (deleting '#{key}')", + "TypeError" + )} + ) + end + + def delete_property({:obj, ref}, key) do + map = Heap.get_obj(ref, %{}) + + if is_map(map) do + desc = Heap.get_prop_desc(ref, key) + + if match?(%{configurable: false}, desc) do + false + else + Heap.put_obj(ref, Map.delete(map, key)) + true + end + else + true + end + end + + def delete_property(_obj, _key), do: true +end diff --git a/lib/quickbeam/vm/object_model/functions.ex b/lib/quickbeam/vm/object_model/functions.ex new file mode 100644 index 00000000..6f704b0f --- /dev/null +++ b/lib/quickbeam/vm/object_model/functions.ex @@ -0,0 +1,35 @@ +defmodule QuickBEAM.VM.ObjectModel.Functions do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Heap, Names} + + def function_name(name_val), do: Names.function_name(name_val) + def rename(fun, name), do: Names.rename_function(fun, name) + + def set_name_atom(fun, atom_idx, atoms \\ Heap.get_atoms()) do + rename(fun, Names.resolve_atom(atoms, atom_idx)) + end + + def set_name_computed(fun, name_val), do: rename(fun, function_name(name_val)) + + def put_home_object(method, target) do + if needs_home_object?(method) do + key = {:qb_home_object, home_object_key(method)} + if key != {:qb_home_object, nil}, do: Process.put(key, target) + end + + method + end + + def current_home_object(current_func) do + Process.get({:qb_home_object, home_object_key(current_func)}, :undefined) + end + + def home_object_key({:closure, _, %Bytecode.Function{byte_code: byte_code}}), do: byte_code + def home_object_key(%Bytecode.Function{byte_code: byte_code}), do: byte_code + def home_object_key(_), do: nil + + defp needs_home_object?({:closure, _, %Bytecode.Function{need_home_object: true}}), do: true + defp needs_home_object?(%Bytecode.Function{need_home_object: true}), do: true + defp needs_home_object?(_), do: false +end diff --git a/lib/quickbeam/vm/object_model/get.ex b/lib/quickbeam/vm/object_model/get.ex new file mode 100644 index 00000000..7dc7972d --- /dev/null +++ b/lib/quickbeam/vm/object_model/get.ex @@ -0,0 +1,421 @@ +defmodule QuickBEAM.VM.ObjectModel.Get do + @moduledoc "JS property resolution: own properties, prototype chain, getters." + + import Bitwise, only: [band: 2] + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.Invocation + alias QuickBEAM.VM.Runtime + + alias QuickBEAM.VM.Runtime.{ + Array, + Boolean, + Function, + Number, + Object, + RegExp, + TypedArray + } + + alias QuickBEAM.VM.Runtime.Map, as: JSMap + alias QuickBEAM.VM.Runtime.Set, as: JSSet + + alias QuickBEAM.VM.Runtime.ArrayBuffer + alias QuickBEAM.VM.Runtime.Date, as: JSDate + alias QuickBEAM.VM.Runtime.String, as: JSString + + def get(value, key) when is_binary(key) do + case get_own(value, key) do + :undefined -> + result = get_prototype_raw(value, key) + + case result do + {:accessor, getter, _} when getter != nil -> call_getter(getter, value) + _ -> result + end + + {:accessor, getter, _} when getter != nil -> + call_getter(getter, value) + + val -> + val + end + end + + def get(value, key) when is_integer(key), + do: get(value, Integer.to_string(key)) + + def get(_, _), do: :undefined + + def call_getter(fun, this_obj) do + Invocation.invoke_with_receiver(fun, [], this_obj) + end + + def regexp_flags(<>) do + [{1, "g"}, {2, "i"}, {4, "m"}, {8, "s"}, {16, "u"}, {32, "y"}] + |> Enum.reduce("", fn {bit, ch}, acc -> + if band(flags_byte, bit) != 0, do: acc <> ch, else: acc + end) + end + + def regexp_flags(_), do: "" + + def string_length(s) do + if byte_size(s) == String.length(s) do + byte_size(s) + else + s + |> String.to_charlist() + |> Enum.reduce(0, fn cp, acc -> + if cp > 0xFFFF, do: acc + 2, else: acc + 1 + end) + end + end + + def length_of(obj) do + case obj do + {:obj, ref} -> + case Heap.get_obj_raw(ref) do + {:shape, _, offsets, vals, _} -> + case Map.fetch(offsets, "length") do + {:ok, off} -> elem(vals, off) + :error -> map_size(offsets) + end + {:qb_arr, arr} -> :array.size(arr) + list when is_list(list) -> length(list) + map when is_map(map) -> Map.get(map, "length", map_size(map)) + _ -> 0 + end + + {:qb_arr, arr} -> + :array.size(arr) + + list when is_list(list) -> + length(list) + + string when is_binary(string) -> + string_length(string) + + %Bytecode.Function{} = fun -> + fun.defined_arg_count + + {:closure, _, %Bytecode.Function{} = fun} -> + fun.defined_arg_count + + {:bound, len, _, _, _} -> + len + + _ -> + :undefined + end + end + + # ── Own property lookup ── + + defp get_own({:obj, ref}, key) do + case Heap.get_obj_raw(ref) do + {:shape, _shape_id, _offsets, _vals, proto} when key == "__proto__" -> + if proto, do: proto, else: :undefined + + {:shape, _shape_id, offsets, vals, _proto} -> + case Map.fetch(offsets, key) do + {:ok, offset} -> elem(vals, offset) + :error -> :undefined + end + + nil -> + :undefined + + %{ + proxy_target() => target, + proxy_handler() => handler + } -> + get_trap = get_own(handler, "get") + + if get_trap != :undefined do + Runtime.call_callback(get_trap, [target, key]) + else + get_own(target, key) + end + + {:qb_arr, _} = arr -> + case Process.get({:qb_regexp_result, ref}) do + %{^key => val} -> val + _ -> get_own(arr, key) + end + + list when is_list(list) -> + case Process.get({:qb_regexp_result, ref}) do + %{^key => val} -> val + _ -> get_own(list, key) + end + + %{date_ms() => _} = map -> + case Map.get(map, key) do + nil -> JSDate.proto_property(key) + val -> val + end + + %{buffer() => _} = map -> + case Map.get(map, key) do + nil -> ArrayBuffer.proto_property(key) + val -> val + end + + map when is_map(map) -> + case Map.fetch(map, key) do + {:ok, {:accessor, getter, _setter}} when getter != nil -> + call_getter(getter, {:obj, ref}) + + {:ok, val} -> + val + + :error -> + case Map.get(map, "__wrapped_symbol__") do + sym when sym != nil -> get_own(sym, key) + _ -> :undefined + end + end + end + end + + defp get_own({:qb_arr, arr}, "length"), do: :array.size(arr) + + defp get_own({:qb_arr, arr}, key) when is_binary(key) do + case Integer.parse(key) do + {idx, ""} when idx >= 0 -> + if idx < :array.size(arr), do: :array.get(idx, arr), else: :undefined + + _ -> + :undefined + end + end + + defp get_own(list, "length") when is_list(list), do: length(list) + + defp get_own(list, key) when is_list(list) and is_binary(key) do + case Integer.parse(key) do + {idx, ""} when idx >= 0 -> Enum.at(list, idx, :undefined) + _ -> :undefined + end + end + + defp get_own(s, "length") when is_binary(s), do: string_length(s) + defp get_own(s, key) when is_binary(s), do: JSString.proto_property(key) + + defp get_own(n, _) when is_number(n), do: :undefined + defp get_own(true, _), do: :undefined + defp get_own(false, _), do: :undefined + defp get_own(nil, _), do: :undefined + defp get_own(:undefined, _), do: :undefined + + defp get_own({:builtin, _name, map}, key) when is_map(map) do + Map.get(map, key, :undefined) + end + + defp get_own({:builtin, name, _}, "from") + when name in ~w(Uint8Array Int8Array Uint8ClampedArray Uint16Array Int16Array Uint32Array Int32Array Float32Array Float64Array) do + type = Map.get(TypedArray.types(), name, :uint8) + + {:builtin, "from", + fn [source | _], _this -> + list = Heap.to_list(source) + TypedArray.constructor(type).(list, nil) + end} + end + + defp get_own({:builtin, _, _} = b, key) do + statics = Heap.get_ctor_statics(b) + + case Map.get(statics, :__module__) do + nil -> + Map.get(statics, key, :undefined) + + mod -> + case mod.static_property(key) do + :undefined -> Map.get(statics, key, :undefined) + val -> val + end + end + end + + defp get_own({:regexp, bytecode, _source}, "flags"), do: regexp_flags(bytecode) + defp get_own({:regexp, _bytecode, source}, "source") when is_binary(source), do: source + + defp get_own({:regexp, _, _}, key), do: RegExp.proto_property(key) + + defp get_own(%Bytecode.Function{} = f, "prototype") do + Heap.get_or_create_prototype(f) + end + + defp get_own(%Bytecode.Function{} = f, key) do + Map.get(Heap.get_ctor_statics(f), key, :undefined) + end + + defp get_own({:closure, _, %Bytecode.Function{}} = c, "prototype") do + Heap.get_or_create_prototype(c) + end + + defp get_own({:closure, _, %Bytecode.Function{} = f} = c, key) do + case Map.get(Heap.get_ctor_statics(c), key, :undefined) do + :undefined -> Map.get(Heap.get_ctor_statics(f), key, :undefined) + val -> val + end + end + + defp get_own({:symbol, desc}, "toString"), + do: {:builtin, "toString", fn _, _ -> "Symbol(#{desc})" end} + + defp get_own({:symbol, desc, _}, "toString"), + do: {:builtin, "toString", fn _, _ -> "Symbol(#{desc})" end} + + defp get_own({:symbol, _} = s, "valueOf"), do: {:builtin, "valueOf", fn _, _ -> s end} + defp get_own({:symbol, _, _} = s, "valueOf"), do: {:builtin, "valueOf", fn _, _ -> s end} + defp get_own({:symbol, desc}, "description"), do: desc + defp get_own({:symbol, desc, _}, "description"), do: desc + defp get_own({:bound, _, _, _, _} = b, key), do: Function.proto_property(b, key) + defp get_own(_, _), do: :undefined + + # ── Prototype chain ── + + defp get_prototype_raw({:obj, ref}, key) do + case Heap.get_obj_raw(ref) do + {:shape, _shape_id, _offsets, _vals, proto} -> + case proto do + {:obj, pref} -> + case Heap.get_obj_raw(pref) do + {:shape, _proto_shape_id, proto_offsets, proto_vals, _proto_next} -> + case Map.fetch(proto_offsets, key) do + {:ok, offset} -> elem(proto_vals, offset) + :error -> get_prototype_raw(proto, key) + end + + pmap when is_map(pmap) -> + case Map.fetch(pmap, key) do + {:ok, val} -> val + :error -> get_prototype_raw(proto, key) + end + + _ -> + get_prototype_raw(proto, key) + end + + _ -> + get_from_prototype(proto, key) + end + + map when is_map(map) and is_map_key(map, proto()) -> + proto = Map.get(map, proto()) + + case proto do + {:obj, pref} -> + pmap = Heap.get_obj(pref, %{}) + + if is_map(pmap) do + case Map.get(pmap, key, :undefined) do + :undefined -> get_prototype_raw(proto, key) + val -> val + end + else + get_from_prototype(proto, key) + end + + _ -> + get_from_prototype(proto, key) + end + + _ -> + get_from_prototype({:obj, ref}, key) + end + end + + defp get_prototype_raw(value, key), do: get_from_prototype(value, key) + + defp get_from_prototype({:obj, ref}, key) do + case Heap.get_obj(ref) do + {:qb_arr, _} -> + Array.proto_property(key) + + list when is_list(list) -> + Array.proto_property(key) + + map when is_map(map) -> + cond do + Map.has_key?(map, map_data()) -> + JSMap.proto_property(key) + + Map.has_key?(map, set_data()) -> + JSSet.proto_property(key) + + Map.has_key?(map, proto()) -> + get(Map.get(map, proto()), key) + + true -> + :undefined + end + + _ -> + :undefined + end + end + + defp get_from_prototype({:qb_arr, _}, "constructor") do + Map.get(Runtime.global_bindings(), "Array", :undefined) + end + + defp get_from_prototype({:qb_arr, _}, key), do: Array.proto_property(key) + + defp get_from_prototype(list, "constructor") when is_list(list) do + Map.get(Runtime.global_bindings(), "Array", :undefined) + end + + defp get_from_prototype(list, key) when is_list(list), do: Array.proto_property(key) + defp get_from_prototype(s, key) when is_binary(s), do: JSString.proto_property(key) + defp get_from_prototype(n, key) when is_number(n), do: Number.proto_property(key) + defp get_from_prototype(true, key), do: Boolean.proto_property(key) + defp get_from_prototype(false, key), do: Boolean.proto_property(key) + + defp get_from_prototype(%Bytecode.Function{} = f, key) when key in ["length", "name"], + do: Function.proto_property(f, key) + + defp get_from_prototype(%Bytecode.Function{} = f, key) do + case Heap.get_parent_ctor(f) do + nil -> Function.proto_property(f, key) + parent -> fallback_to_function_proto(get(parent, key), f, key) + end + end + + defp get_from_prototype({:closure, _, %Bytecode.Function{}} = c, key) + when key in ["length", "name"], + do: Function.proto_property(c, key) + + defp get_from_prototype({:closure, _, %Bytecode.Function{} = f} = c, key) do + case Heap.get_parent_ctor(f) do + nil -> Function.proto_property(c, key) + parent -> fallback_to_function_proto(get(parent, key), c, key) + end + end + + defp get_from_prototype({:builtin, "Error", _}, _key), + do: :undefined + + defp get_from_prototype({:builtin, "Array", _}, key), do: Array.static_property(key) + defp get_from_prototype({:builtin, "Object", _}, key), do: Object.static_property(key) + defp get_from_prototype({:builtin, "Map", _}, _key), do: :undefined + defp get_from_prototype({:builtin, "Set", _}, _key), do: :undefined + + defp get_from_prototype({:builtin, "Number", _}, key), + do: Number.static_property(key) + + defp get_from_prototype({:builtin, "String", _}, key), + do: JSString.static_property(key) + + defp get_from_prototype({:builtin, name, _} = fun, key) when is_binary(name), + do: Function.proto_property(fun, key) + + defp get_from_prototype(_, _), do: :undefined + + defp fallback_to_function_proto(:undefined, fun, key), do: Function.proto_property(fun, key) + defp fallback_to_function_proto(val, _fun, _key), do: val +end diff --git a/lib/quickbeam/vm/object_model/methods.ex b/lib/quickbeam/vm/object_model/methods.ex new file mode 100644 index 00000000..1b61a022 --- /dev/null +++ b/lib/quickbeam/vm/object_model/methods.ex @@ -0,0 +1,63 @@ +defmodule QuickBEAM.VM.ObjectModel.Methods do + @moduledoc false + + import Bitwise, only: [band: 2] + + alias QuickBEAM.VM.{Heap, Names} + alias QuickBEAM.VM.ObjectModel.{Functions, Put} + + def define_method(target, method, name, flags) when is_binary(name) do + method_type = band(flags, 3) + enumerable = band(flags, 4) != 0 + + named_method = + Functions.rename( + method, + case method_type do + 1 -> "get " <> name + 2 -> "set " <> name + _ -> name + end + ) + + Functions.put_home_object(named_method, target) + + case method_type do + 1 -> Put.put_getter(target, name, named_method, enumerable) + 2 -> Put.put_setter(target, name, named_method, enumerable) + _ -> Put.put(target, name, named_method, enumerable) + end + + target + end + + def define_method(target, method, atom_idx, flags), + do: define_method(target, method, Names.resolve_atom(Heap.get_atoms(), atom_idx), flags) + + def define_method_computed(target, method, field_name, flags) do + method_type = band(flags, 3) + enumerable = band(flags, 4) != 0 + + named_method = + Functions.rename( + method, + case method_type do + 1 -> "get " <> Functions.function_name(field_name) + 2 -> "set " <> Functions.function_name(field_name) + _ -> Functions.function_name(field_name) + end + ) + + Functions.put_home_object(named_method, target) + + case method_type do + 1 -> Put.put_getter(target, field_name, named_method, enumerable) + 2 -> Put.put_setter(target, field_name, named_method, enumerable) + _ -> Put.put(target, field_name, named_method, enumerable) + end + + target + end + + def set_home_object(method, target), do: Functions.put_home_object(method, target) +end diff --git a/lib/quickbeam/vm/object_model/private.ex b/lib/quickbeam/vm/object_model/private.ex new file mode 100644 index 00000000..4a81cc6d --- /dev/null +++ b/lib/quickbeam/vm/object_model/private.ex @@ -0,0 +1,123 @@ +defmodule QuickBEAM.VM.ObjectModel.Private do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys, only: [proto: 0] + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.ObjectModel.Functions + + def private_symbol(name) when is_binary(name), do: {:private_symbol, name, make_ref()} + + def get_field({:obj, ref}, key) do + Map.get(Heap.get_obj(ref, %{}), {:private, key}, :missing) + end + + def get_field({:closure, _, %Bytecode.Function{}} = ctor, key), + do: Map.get(Heap.get_ctor_statics(ctor), {:private, key}, :missing) + + def get_field(%Bytecode.Function{} = ctor, key), + do: Map.get(Heap.get_ctor_statics(ctor), {:private, key}, :missing) + + def get_field({:builtin, _, _} = ctor, key), + do: Map.get(Heap.get_ctor_statics(ctor), {:private, key}, :missing) + + def get_field(_, _key), do: :missing + + def has_field?(target, key), do: get_field(target, key) != :missing + + def put_field!(target, key, val) do + if has_field?(target, key) do + define_field!(target, key, val) + else + :error + end + end + + def define_field!({:obj, ref}, key, val) do + Heap.update_obj(ref, %{}, &Map.put(&1, {:private, key}, val)) + :ok + end + + def define_field!({:closure, _, %Bytecode.Function{}} = ctor, key, val) do + Heap.put_ctor_static(ctor, {:private, key}, val) + :ok + end + + def define_field!(%Bytecode.Function{} = ctor, key, val) do + Heap.put_ctor_static(ctor, {:private, key}, val) + :ok + end + + def define_field!({:builtin, _, _} = ctor, key, val) do + Heap.put_ctor_static(ctor, {:private, key}, val) + :ok + end + + def define_field!(_, _key, _val), do: :error + + def brands({:obj, ref}), do: Map.get(Heap.get_obj(ref, %{}), :__brands__, []) + + def brands({:closure, _, %Bytecode.Function{}} = ctor), + do: Map.get(Heap.get_ctor_statics(ctor), :__brands__, []) + + def brands(%Bytecode.Function{} = ctor), + do: Map.get(Heap.get_ctor_statics(ctor), :__brands__, []) + + def brands({:builtin, _, _} = ctor), do: Map.get(Heap.get_ctor_statics(ctor), :__brands__, []) + def brands(_), do: [] + + def add_brand({:obj, ref}, brand) do + Heap.update_obj(ref, %{}, fn map -> + existing = Map.get(map, :__brands__, []) + Map.put(map, :__brands__, [brand | existing]) + end) + + :ok + end + + def add_brand({:closure, _, %Bytecode.Function{}} = ctor, brand) do + add_ctor_brand(ctor, brand) + :ok + end + + def add_brand(%Bytecode.Function{} = ctor, brand) do + add_ctor_brand(ctor, brand) + :ok + end + + def add_brand({:builtin, _, _} = ctor, brand) do + add_ctor_brand(ctor, brand) + :ok + end + + def add_brand(_obj, _brand), do: :ok + + def ensure_brand(target, brand) do + if brand_match?(target, brand), do: :ok, else: :error + end + + def brand_error, do: Heap.make_error("invalid brand on object", "TypeError") + + defp brand_match?(target, brand) do + target_brands = brands(target) + home_object = Process.get({:qb_home_object, Functions.home_object_key(brand)}) + + brand in target_brands or + (home_object not in [nil, :undefined] and + (home_object in target_brands or brand_home_match?(target, home_object))) + end + + defp brand_home_match?({:obj, ref}, home_object) do + parent = Map.get(Heap.get_obj(ref, %{}), proto(), :undefined) + parent == home_object or brand_home_match?(parent, home_object) + end + + defp brand_home_match?(:undefined, _home_object), do: false + defp brand_home_match?(nil, _home_object), do: false + defp brand_home_match?(_, _home_object), do: false + + defp add_ctor_brand(ctor, brand) do + existing = Map.get(Heap.get_ctor_statics(ctor), :__brands__, []) + Heap.put_ctor_static(ctor, :__brands__, [brand | existing]) + end +end diff --git a/lib/quickbeam/vm/object_model/put.ex b/lib/quickbeam/vm/object_model/put.ex new file mode 100644 index 00000000..147fd19f --- /dev/null +++ b/lib/quickbeam/vm/object_model/put.ex @@ -0,0 +1,472 @@ +defmodule QuickBEAM.VM.ObjectModel.Put do + @moduledoc false + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.{Bytecode, Heap, Names, Runtime} + alias QuickBEAM.VM.Interpreter.Values + alias QuickBEAM.VM.Invocation + alias QuickBEAM.VM.ObjectModel.Get + + @compile {:inline, has_property: 2, get_element: 2, set_list_at: 3} + + defp shape_put(ref, shape_id, offsets, vals, proto, key, val) do + case Map.fetch(offsets, key) do + {:ok, offset} when offset < tuple_size(vals) -> + Process.put(ref, {:shape, shape_id, offsets, put_elem(vals, offset, val), proto}) + + {:ok, offset} -> + Process.put(ref, {:shape, shape_id, offsets, Heap.Shapes.put_val(vals, offset, val), proto}) + + :error -> + {new_shape_id, new_offsets, offset} = Heap.Shapes.transition(shape_id, key) + new_vals = + if offset == tuple_size(vals), + do: :erlang.append_element(vals, val), + else: Heap.Shapes.put_val(vals, offset, val) + + Process.put(ref, {:shape, new_shape_id, new_offsets, new_vals, proto}) + end + end + + def put({:obj, ref} = _obj, "length", val) do + case Heap.get_obj_raw(ref) do + {:shape, shape_id, offsets, vals, proto} -> + case Map.fetch(offsets, "length") do + {:ok, offset} -> + new_vals = Heap.Shapes.put_val(vals, offset, val) + Heap.put_obj_raw(ref, {:shape, shape_id, offsets, new_vals, proto}) + + :error -> + {new_shape_id, new_offsets, offset} = Heap.Shapes.transition(shape_id, "length") + new_vals = Heap.Shapes.put_val(vals, offset, val) + Heap.put_obj_raw(ref, {:shape, new_shape_id, new_offsets, new_vals, proto}) + end + + data -> + if is_list(data) or match?({:qb_arr, _}, data) do + new_len = Runtime.to_int(val) + list = if is_list(data), do: data, else: Heap.obj_to_list(ref) + old_len = length(list) + + if new_len < old_len do + non_configurable_idx = + Enum.find(new_len..(old_len - 1), fn i -> + match?(%{configurable: false}, Heap.get_prop_desc(ref, Integer.to_string(i))) + end) + + if non_configurable_idx do + Heap.put_obj(ref, Enum.take(list, non_configurable_idx + 1)) + throw({:js_throw, Heap.make_error("Cannot delete property", "TypeError")}) + end + + Heap.put_obj(ref, Enum.take(list, new_len)) + else + padded = list ++ List.duplicate(:undefined, new_len - old_len) + Heap.put_obj(ref, padded) + end + end + end + end + + def put_field({:obj, ref}, key, val) do + case Process.get(ref) do + {:shape, shape_id, offsets, vals, proto} -> + shape_put(ref, shape_id, offsets, vals, proto, key, val) + + _ -> + put({:obj, ref}, key, val) + end + end + + def put({:obj, ref} = obj, key, val) do + key = normalize_key(key) + + case Heap.get_obj_raw(ref) do + {:shape, shape_id, offsets, vals, proto} -> + cond do + Heap.frozen?(ref) -> + :ok + + key == "__proto__" -> + Heap.put_obj_raw(ref, {:shape, shape_id, offsets, vals, val}) + + true -> + shape_put(ref, shape_id, offsets, vals, proto, key, val) + end + + %{ + proxy_target() => target, + proxy_handler() => handler + } -> + set_trap = Get.get(handler, "set") + + if set_trap != :undefined do + Runtime.call_callback(set_trap, [target, key, val]) + else + put(target, key, val) + end + + {:qb_arr, _} -> + put_array_key(ref, key, val) + + list when is_list(list) -> + put_array_key(ref, key, val) + + map when is_map(map) -> + cond do + Heap.frozen?(ref) -> + :ok + + not Map.has_key?(map, key) -> + Heap.put_obj_key(ref, map, key, val) + + match?({:accessor, _, setter} when setter != nil, Map.get(map, key)) -> + {:accessor, _, setter} = Map.get(map, key) + invoke_setter(setter, val, obj) + + match?(%{writable: false}, Heap.get_prop_desc(ref, key)) -> + :ok + + true -> + Heap.put_obj_key(ref, map, key, val) + end + + _ -> + :ok + end + end + + def put(%Bytecode.Function{} = f, key, val), do: Heap.put_ctor_static(f, key, val) + + def put({:closure, _, %Bytecode.Function{}} = c, key, val), + do: Heap.put_ctor_static(c, key, val) + + def put({:builtin, _, _} = b, key, val), do: Heap.put_ctor_static(b, key, val) + + def put(_, _, _), do: :ok + + def put(target, key, val, true), do: put(target, key, val) + + def put({:obj, ref}, key, val, false) do + case Heap.get_obj_raw(ref) do + {:shape, shape_id, offsets, vals, proto} -> + if not Heap.frozen?(ref) do + shape_put(ref, shape_id, offsets, vals, proto, key, val) + + Heap.put_prop_desc(ref, key, %{writable: true, enumerable: false, configurable: true}) + end + + :ok + + map when is_map(map) -> + if not Heap.frozen?(ref) do + Heap.put_obj(ref, Map.put(map, key, val)) + Heap.put_prop_desc(ref, key, %{writable: true, enumerable: false, configurable: true}) + end + + :ok + + _ -> + :ok + end + end + + def put(%Bytecode.Function{} = f, key, val, _enumerable), do: Heap.put_ctor_static(f, key, val) + + def put({:closure, _, %Bytecode.Function{}} = c, key, val, _enumerable), + do: Heap.put_ctor_static(c, key, val) + + def put({:builtin, _, _} = b, key, val, _enumerable), do: Heap.put_ctor_static(b, key, val) + + def put(_, _, _, _), do: :ok + + defp normalize_key(k) when is_float(k) and k == trunc(k) and k >= 0, + do: k |> trunc() |> Integer.to_string() + + defp normalize_key(k) when is_float(k), do: Values.stringify(k) + defp normalize_key({:tagged_int, n}), do: Integer.to_string(n) + defp normalize_key(k) when is_integer(k) and k >= 0, do: Integer.to_string(k) + defp normalize_key(k), do: k + + defp put_array_key(ref, key, val) do + case key do + k when is_binary(k) -> + case Integer.parse(k) do + {idx, ""} when idx >= 0 -> put_element({:obj, ref}, idx, val) + _ -> :ok + end + + k when is_integer(k) and k >= 0 -> + put_element({:obj, ref}, k, val) + + _ -> + :ok + end + end + + def put_getter({:obj, ref}, key, fun) do + update_getter(ref, key, fun) + end + + def put_getter(target, key, fun), do: Heap.put_ctor_static(target, key, {:accessor, fun, nil}) + + def put_getter(target, key, fun, true), do: put_getter(target, key, fun) + + def put_getter({:obj, ref}, key, fun, false) do + update_getter(ref, key, fun) + Heap.put_prop_desc(ref, key, %{enumerable: false, configurable: true}) + end + + def put_getter(target, key, fun, _enumerable), + do: Heap.put_ctor_static(target, key, {:accessor, fun, nil}) + + def put_setter({:obj, ref}, key, fun) do + update_setter(ref, key, fun) + end + + def put_setter(target, key, fun), do: Heap.put_ctor_static(target, key, {:accessor, nil, fun}) + + def put_setter(target, key, fun, true), do: put_setter(target, key, fun) + + def put_setter({:obj, ref}, key, fun, false) do + update_setter(ref, key, fun) + Heap.put_prop_desc(ref, key, %{enumerable: false, configurable: true}) + end + + def put_setter(target, key, fun, _enumerable), + do: Heap.put_ctor_static(target, key, {:accessor, nil, fun}) + + defp update_getter(ref, key, fun) do + Heap.update_obj(ref, %{}, fn map -> + desc = + case Map.get(map, key) do + {:accessor, _get, set} -> {:accessor, fun, set} + _ -> {:accessor, fun, nil} + end + + Map.put(map, key, desc) + end) + end + + defp update_setter(ref, key, fun) do + Heap.update_obj(ref, %{}, fn map -> + desc = + case Map.get(map, key) do + {:accessor, get, _set} -> {:accessor, get, fun} + _ -> {:accessor, nil, fun} + end + + Map.put(map, key, desc) + end) + end + + defp invoke_setter(fun, val, this_obj) do + Invocation.invoke_with_receiver(fun, [val], this_obj) + end + + def has_property({:obj, ref}, key) do + map = Heap.get_obj(ref, %{}) + + case map do + %{ + proxy_target() => target, + proxy_handler() => handler + } -> + has_trap = Get.get(handler, "has") + + if has_trap != :undefined do + Runtime.call_callback(has_trap, [target, key]) + else + has_property(target, key) + end + + _ when is_map(map) -> + Map.has_key?(map, key) + + _ -> + false + end + end + + def has_property(obj, key) when is_map(obj), do: Map.has_key?(obj, key) + + def has_property({:qb_arr, arr}, key) when is_integer(key), + do: key >= 0 and key < :array.size(arr) + + def has_property(obj, key) when is_list(obj) and is_integer(key), + do: key >= 0 and key < length(obj) + + def has_property(_, _), do: false + + def get_element({:obj, ref} = obj, idx) do + case Heap.get_obj(ref) do + %{typed_array() => true} when is_integer(idx) -> + Runtime.TypedArray.get_element(obj, idx) + + {:qb_arr, arr} when is_integer(idx) -> + if idx >= 0 and idx < :array.size(arr), + do: :array.get(idx, arr), + else: :undefined + + list when is_list(list) and is_integer(idx) -> + Enum.at(list, idx, :undefined) + + map when is_map(map) -> + key = if is_integer(idx), do: Integer.to_string(idx), else: idx + Map.get(map, key, Map.get(map, idx, :undefined)) + + _ -> + :undefined + end + end + + def get_element({:qb_arr, arr}, idx) when is_integer(idx) do + if idx >= 0 and idx < :array.size(arr), + do: :array.get(idx, arr), + else: :undefined + end + + def get_element(obj, idx) when is_list(obj) and is_integer(idx), + do: Enum.at(obj, idx, :undefined) + + def get_element(obj, idx) when is_map(obj), do: Map.get(obj, idx, :undefined) + + def get_element(s, idx) when is_binary(s) and is_integer(idx) and idx >= 0, + do: String.at(s, idx) || :undefined + + def get_element(s, key) when is_binary(s) and is_binary(key), + do: Get.get(s, key) + + def get_element(obj, key) when is_binary(key) do + Get.get(obj, key) + end + + def get_element({:builtin, _, _} = b, {:symbol, _} = sym_key) do + case Map.get(Heap.get_ctor_statics(b), sym_key) do + {:accessor, getter, _} when getter != nil -> + Runtime.call_callback(getter, []) + + nil -> + :undefined + + val -> + val + end + end + + def get_element({:obj, ref}, {:symbol, _} = sym_key) do + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> + case Map.get(map, sym_key) do + {:accessor, getter, _} when getter != nil -> + Runtime.call_callback(getter, []) + + nil -> + :undefined + + val -> + val + end + + _ -> + :undefined + end + end + + def get_element(_, _), do: :undefined + + def put_element({:obj, ref} = obj, key, val) do + case Heap.get_obj(ref) do + %{typed_array() => true} when is_integer(key) -> + Runtime.TypedArray.set_element(obj, key, val) + + {:qb_arr, _} -> + case key do + i when is_integer(i) and i >= 0 -> Heap.array_set(ref, i, val) + _ -> :ok + end + + list when is_list(list) -> + case key do + i when is_integer(i) and i >= 0 and i < length(list) -> + Heap.put_obj(ref, List.replace_at(list, i, val)) + + i when is_integer(i) and i >= 0 -> + padded = list ++ List.duplicate(:undefined, i - length(list)) ++ [val] + Heap.put_obj(ref, padded) + + _ -> + :ok + end + + map when is_map(map) -> + str_key = + case key do + {:symbol, _, _} -> key + {:symbol, _} -> key + k when is_float(k) and k == trunc(k) and k >= 0 -> Integer.to_string(trunc(k)) + _ -> Kernel.to_string(key) + end + + Heap.put_obj_key(ref, map, str_key, val) + + nil -> + :ok + end + end + + def put_element(_, _, _), do: :ok + + def define_array_el(obj, idx, val) do + obj2 = + case obj do + list when is_list(list) -> + i = if is_integer(idx), do: idx, else: Runtime.to_int(idx) + set_list_at(list, i, val) + + {:obj, ref} -> + stored = Heap.get_obj(ref, []) + + cond do + match?({:qb_arr, _}, stored) -> + i = if is_integer(idx), do: idx, else: Runtime.to_int(idx) + Heap.array_set(ref, i, val) + + is_list(stored) -> + i = if is_integer(idx), do: idx, else: Runtime.to_int(idx) + Heap.put_obj(ref, set_list_at(stored, i, val)) + + is_map(stored) -> + Heap.put_obj_key(ref, stored, Names.normalize_property_key(idx), val) + + true -> + :ok + end + + {:obj, ref} + + %Bytecode.Function{} = ctor -> + Heap.put_ctor_static(ctor, Names.normalize_property_key(idx), val) + ctor + + {:closure, _, %Bytecode.Function{}} = ctor -> + Heap.put_ctor_static(ctor, Names.normalize_property_key(idx), val) + ctor + + {:builtin, _, _} = ctor -> + Heap.put_ctor_static(ctor, Names.normalize_property_key(idx), val) + ctor + + _ -> + obj + end + + {idx, obj2} + end + + def set_list_at(list, i, val) when is_integer(i) and i >= 0 and i < length(list), + do: List.replace_at(list, i, val) + + def set_list_at(list, i, val) when is_integer(i) and i >= 0, + do: list ++ List.duplicate(:undefined, max(0, i - length(list))) ++ [val] +end diff --git a/lib/quickbeam/vm/opcodes.ex b/lib/quickbeam/vm/opcodes.ex new file mode 100644 index 00000000..d58f4778 --- /dev/null +++ b/lib/quickbeam/vm/opcodes.ex @@ -0,0 +1,436 @@ +defmodule QuickBEAM.VM.Opcodes do + @moduledoc false + # Generated from quickjs-opcode.h + # Each entry: {name, byte_size, n_pop, n_push, format} + + # BC_TAG values (top-level serialization tags, not opcodes) + + @bc_tags %{ + null: 1, + undefined: 2, + bool_false: 3, + bool_true: 4, + int32: 5, + float64: 6, + string: 7, + object: 8, + array: 9, + big_int: 10, + template_object: 11, + function_bytecode: 12, + module: 13, + typed_array: 14, + array_buffer: 15, + shared_array_buffer: 16, + regexp: 17, + date: 18, + object_value: 19, + object_reference: 20, + map: 21, + set: 22, + symbol: 23 + } + + for {name, val} <- @bc_tags do + @doc false + def unquote(:"bc_tag_#{name}")(), do: unquote(val) + end + + @bc_version 25 + def bc_version, do: @bc_version + + @js_atom_end QuickBEAM.VM.PredefinedAtoms.count() + 1 + def js_atom_end, do: @js_atom_end + + # Opcode format types — determine how operand bytes are decoded + # :none / :none_int / :none_loc / :none_arg / :none_var_ref → 0 extra bytes + # :u8 / :i8 / :loc8 / :const8 / :label8 → 1 byte + # :u16 / :i16 / :label16 → 2 bytes + # :npop / :npopx → 1 byte (argc) + # :npop_u16 → 1 byte + 2 bytes + # :loc / :arg / :var_ref → LEB128 + # :u32 → LEB128 + # :u32x2 → LEB128 + LEB128 + # :i32 → SLEB128 + # :const → LEB128 + # :label → LEB128 + # :atom → LEB128 (atom table index) + # :atom_u8 → LEB128 + 1 byte + # :atom_u16 → LEB128 + 2 bytes + # :atom_label_u8 → LEB128 + LEB128 + 1 byte + # :atom_label_u16 → LEB128 + LEB128 + 2 bytes + # :label_u16 → LEB128 + 2 bytes + + # Format → :zero | {:bytes, pos_integer} | :leb128 | :mixed + @format_info %{ + none: :zero, + none_int: :zero, + none_loc: :zero, + none_arg: :zero, + none_var_ref: :zero, + u8: {:bytes, 1}, + i8: {:bytes, 1}, + loc8: {:bytes, 1}, + const8: {:bytes, 1}, + label8: {:bytes, 1}, + u16: {:bytes, 2}, + i16: {:bytes, 2}, + label16: {:bytes, 2}, + npop: {:bytes, 1}, + npopx: :zero, + npop_u16: :npop_u16, + loc: :leb128, + arg: :leb128, + var_ref: :leb128, + u32: :leb128, + u32x2: :leb128_leb128, + i32: :sleb128, + const: :leb128, + label: :leb128, + atom: :leb128, + atom_u8: :atom_u8, + atom_u16: :atom_u16, + atom_label_u8: :atom_label_u8, + atom_label_u16: :atom_label_u16, + label_u16: :label_u16 + } + + # Full opcode table: {opcode_number, name, byte_size, n_pop, n_push, format} + # Parsed from quickjs-opcode.h — order matters, index = opcode number + @opcodes %{ + 0 => {:invalid, 1, 0, 0, :none}, + 1 => {:push_i32, 5, 0, 1, :i32}, + 2 => {:push_const, 5, 0, 1, :const}, + 3 => {:fclosure, 5, 0, 1, :const}, + 4 => {:push_atom_value, 5, 0, 1, :atom}, + 5 => {:private_symbol, 5, 0, 1, :atom}, + 6 => {:undefined, 1, 0, 1, :none}, + 7 => {:null, 1, 0, 1, :none}, + 8 => {:push_this, 1, 0, 1, :none}, + 9 => {:push_false, 1, 0, 1, :none}, + 10 => {:push_true, 1, 0, 1, :none}, + 11 => {:object, 1, 0, 1, :none}, + 12 => {:special_object, 2, 0, 1, :u8}, + 13 => {:rest, 3, 0, 1, :u16}, + 14 => {:drop, 1, 1, 0, :none}, + 15 => {:nip, 1, 2, 1, :none}, + 16 => {:nip1, 1, 3, 2, :none}, + 17 => {:dup, 1, 1, 2, :none}, + 18 => {:dup1, 1, 2, 3, :none}, + 19 => {:dup2, 1, 2, 4, :none}, + 20 => {:dup3, 1, 3, 6, :none}, + 21 => {:insert2, 1, 2, 3, :none}, + 22 => {:insert3, 1, 3, 4, :none}, + 23 => {:insert4, 1, 4, 5, :none}, + 24 => {:perm3, 1, 3, 3, :none}, + 25 => {:perm4, 1, 4, 4, :none}, + 26 => {:perm5, 1, 5, 5, :none}, + 27 => {:swap, 1, 2, 2, :none}, + 28 => {:swap2, 1, 4, 4, :none}, + 29 => {:rot3l, 1, 3, 3, :none}, + 30 => {:rot3r, 1, 3, 3, :none}, + 31 => {:rot4l, 1, 4, 4, :none}, + 32 => {:rot5l, 1, 5, 5, :none}, + 33 => {:call_constructor, 3, 2, 1, :npop}, + 34 => {:call, 3, 1, 1, :npop}, + 35 => {:tail_call, 3, 1, 0, :npop}, + 36 => {:call_method, 3, 2, 1, :npop}, + 37 => {:tail_call_method, 3, 2, 0, :npop}, + 38 => {:array_from, 3, 0, 1, :npop}, + 39 => {:apply, 3, 3, 1, :u16}, + 40 => {:return, 1, 1, 0, :none}, + 41 => {:return_undef, 1, 0, 0, :none}, + 42 => {:check_ctor_return, 1, 1, 2, :none}, + 43 => {:check_ctor, 1, 0, 0, :none}, + 44 => {:init_ctor, 1, 0, 1, :none}, + 45 => {:check_brand, 1, 2, 2, :none}, + 46 => {:add_brand, 1, 2, 0, :none}, + 47 => {:return_async, 1, 1, 0, :none}, + 48 => {:throw, 1, 1, 0, :none}, + 49 => {:throw_error, 6, 0, 0, :atom_u8}, + 50 => {:eval, 5, 1, 1, :npop_u16}, + 51 => {:apply_eval, 3, 2, 1, :u16}, + 52 => {:regexp, 1, 2, 1, :none}, + 53 => {:get_super, 1, 1, 1, :none}, + 54 => {:import, 1, 2, 1, :none}, + 55 => {:get_var_undef, 5, 0, 1, :atom}, + 56 => {:get_var, 5, 0, 1, :atom}, + 57 => {:put_var, 5, 1, 0, :atom}, + 58 => {:put_var_init, 5, 1, 0, :atom}, + 59 => {:get_ref_value, 1, 2, 3, :none}, + 60 => {:put_ref_value, 1, 3, 0, :none}, + 61 => {:define_var, 6, 0, 0, :atom_u8}, + 62 => {:check_define_var, 6, 0, 0, :atom_u8}, + 63 => {:define_func, 6, 1, 0, :atom_u8}, + 64 => {:get_field, 5, 1, 1, :atom}, + 65 => {:get_field2, 5, 1, 2, :atom}, + 66 => {:put_field, 5, 2, 0, :atom}, + 67 => {:get_private_field, 1, 2, 1, :none}, + 68 => {:put_private_field, 1, 3, 0, :none}, + 69 => {:define_private_field, 1, 3, 1, :none}, + 70 => {:get_array_el, 1, 2, 1, :none}, + 71 => {:get_array_el2, 1, 2, 2, :none}, + 72 => {:put_array_el, 1, 3, 0, :none}, + 73 => {:get_super_value, 1, 3, 1, :none}, + 74 => {:put_super_value, 1, 4, 0, :none}, + 75 => {:define_field, 5, 2, 1, :atom}, + 76 => {:set_name, 5, 1, 1, :atom}, + 77 => {:set_name_computed, 1, 2, 2, :none}, + 78 => {:set_proto, 1, 2, 1, :none}, + 79 => {:set_home_object, 1, 2, 2, :none}, + 80 => {:define_array_el, 1, 3, 2, :none}, + 81 => {:append, 1, 3, 2, :none}, + 82 => {:copy_data_properties, 2, 3, 3, :u8}, + 83 => {:define_method, 6, 2, 1, :atom_u8}, + 84 => {:define_method_computed, 2, 3, 1, :u8}, + 85 => {:define_class, 6, 2, 2, :atom_u8}, + 86 => {:define_class_computed, 6, 3, 3, :atom_u8}, + 87 => {:get_loc, 3, 0, 1, :loc}, + 88 => {:put_loc, 3, 1, 0, :loc}, + 89 => {:set_loc, 3, 1, 1, :loc}, + 90 => {:get_arg, 3, 0, 1, :arg}, + 91 => {:put_arg, 3, 1, 0, :arg}, + 92 => {:set_arg, 3, 1, 1, :arg}, + 93 => {:get_var_ref, 3, 0, 1, :var_ref}, + 94 => {:put_var_ref, 3, 1, 0, :var_ref}, + 95 => {:set_var_ref, 3, 1, 1, :var_ref}, + 96 => {:set_loc_uninitialized, 3, 0, 0, :loc}, + 97 => {:get_loc_check, 3, 0, 1, :loc}, + 98 => {:put_loc_check, 3, 1, 0, :loc}, + 99 => {:put_loc_check_init, 3, 1, 0, :loc}, + 100 => {:get_var_ref_check, 3, 0, 1, :var_ref}, + 101 => {:put_var_ref_check, 3, 1, 0, :var_ref}, + 102 => {:put_var_ref_check_init, 3, 1, 0, :var_ref}, + 103 => {:close_loc, 3, 0, 0, :loc}, + 104 => {:if_false, 5, 1, 0, :label}, + 105 => {:if_true, 5, 1, 0, :label}, + 106 => {:goto, 5, 0, 0, :label}, + 107 => {:catch, 5, 0, 1, :label}, + 108 => {:gosub, 5, 0, 0, :label}, + 109 => {:ret, 1, 1, 0, :none}, + 110 => {:nip_catch, 1, 2, 1, :none}, + 111 => {:to_object, 1, 1, 1, :none}, + 112 => {:to_propkey, 1, 1, 1, :none}, + 113 => {:to_propkey2, 1, 2, 2, :none}, + 114 => {:with_get_var, 10, 1, 0, :atom_label_u8}, + 115 => {:with_put_var, 10, 2, 1, :atom_label_u8}, + 116 => {:with_delete_var, 10, 1, 0, :atom_label_u8}, + 117 => {:with_make_ref, 10, 1, 0, :atom_label_u8}, + 118 => {:with_get_ref, 10, 1, 0, :atom_label_u8}, + 119 => {:with_get_ref_undef, 10, 1, 0, :atom_label_u8}, + 120 => {:make_loc_ref, 7, 0, 2, :atom_u16}, + 121 => {:make_arg_ref, 7, 0, 2, :atom_u16}, + 122 => {:make_var_ref_ref, 7, 0, 2, :atom_u16}, + 123 => {:make_var_ref, 5, 0, 2, :atom}, + 124 => {:for_in_start, 1, 1, 1, :none}, + 125 => {:for_of_start, 1, 1, 3, :none}, + 126 => {:for_await_of_start, 1, 1, 3, :none}, + 127 => {:for_in_next, 1, 1, 3, :none}, + 128 => {:for_of_next, 2, 3, 5, :u8}, + 129 => {:iterator_check_object, 1, 1, 1, :none}, + 130 => {:iterator_get_value_done, 1, 1, 2, :none}, + 131 => {:iterator_close, 1, 3, 0, :none}, + 132 => {:iterator_next, 1, 4, 4, :none}, + 133 => {:iterator_call, 2, 4, 5, :u8}, + 134 => {:initial_yield, 1, 0, 0, :none}, + 135 => {:yield, 1, 1, 2, :none}, + 136 => {:yield_star, 1, 1, 2, :none}, + 137 => {:async_yield_star, 1, 1, 2, :none}, + 138 => {:await, 1, 1, 1, :none}, + 139 => {:neg, 1, 1, 1, :none}, + 140 => {:plus, 1, 1, 1, :none}, + 141 => {:dec, 1, 1, 1, :none}, + 142 => {:inc, 1, 1, 1, :none}, + 143 => {:post_dec, 1, 1, 2, :none}, + 144 => {:post_inc, 1, 1, 2, :none}, + 145 => {:dec_loc, 2, 0, 0, :loc8}, + 146 => {:inc_loc, 2, 0, 0, :loc8}, + 147 => {:add_loc, 2, 1, 0, :loc8}, + 148 => {:not, 1, 1, 1, :none}, + 149 => {:lnot, 1, 1, 1, :none}, + 150 => {:typeof, 1, 1, 1, :none}, + 151 => {:delete, 1, 2, 1, :none}, + 152 => {:delete_var, 5, 0, 1, :atom}, + 153 => {:mul, 1, 2, 1, :none}, + 154 => {:div, 1, 2, 1, :none}, + 155 => {:mod, 1, 2, 1, :none}, + 156 => {:add, 1, 2, 1, :none}, + 157 => {:sub, 1, 2, 1, :none}, + 158 => {:shl, 1, 2, 1, :none}, + 159 => {:sar, 1, 2, 1, :none}, + 160 => {:shr, 1, 2, 1, :none}, + 161 => {:band, 1, 2, 1, :none}, + 162 => {:bxor, 1, 2, 1, :none}, + 163 => {:bor, 1, 2, 1, :none}, + 164 => {:pow, 1, 2, 1, :none}, + 165 => {:lt, 1, 2, 1, :none}, + 166 => {:lte, 1, 2, 1, :none}, + 167 => {:gt, 1, 2, 1, :none}, + 168 => {:gte, 1, 2, 1, :none}, + 169 => {:instanceof, 1, 2, 1, :none}, + 170 => {:in, 1, 2, 1, :none}, + 171 => {:eq, 1, 2, 1, :none}, + 172 => {:neq, 1, 2, 1, :none}, + 173 => {:strict_eq, 1, 2, 1, :none}, + 174 => {:strict_neq, 1, 2, 1, :none}, + 175 => {:is_undefined_or_null, 1, 1, 1, :none}, + 176 => {:private_in, 1, 2, 1, :none}, + 177 => {:push_bigint_i32, 5, 0, 1, :i32}, + 178 => {:nop, 1, 0, 0, :none}, + 179 => {:push_minus1, 1, 0, 1, :none_int}, + 180 => {:push_0, 1, 0, 1, :none_int}, + 181 => {:push_1, 1, 0, 1, :none_int}, + 182 => {:push_2, 1, 0, 1, :none_int}, + 183 => {:push_3, 1, 0, 1, :none_int}, + 184 => {:push_4, 1, 0, 1, :none_int}, + 185 => {:push_5, 1, 0, 1, :none_int}, + 186 => {:push_6, 1, 0, 1, :none_int}, + 187 => {:push_7, 1, 0, 1, :none_int}, + 188 => {:push_i8, 2, 0, 1, :i8}, + 189 => {:push_i16, 3, 0, 1, :i16}, + 190 => {:push_const8, 2, 0, 1, :const8}, + 191 => {:fclosure8, 2, 0, 1, :const8}, + 192 => {:push_empty_string, 1, 0, 1, :none}, + 193 => {:get_loc8, 2, 0, 1, :loc8}, + 194 => {:put_loc8, 2, 1, 0, :loc8}, + 195 => {:set_loc8, 2, 1, 1, :loc8}, + 196 => {:get_loc0_loc1, 1, 0, 2, :none_loc}, + 197 => {:get_loc0, 1, 0, 1, :none_loc}, + 198 => {:get_loc1, 1, 0, 1, :none_loc}, + 199 => {:get_loc2, 1, 0, 1, :none_loc}, + 200 => {:get_loc3, 1, 0, 1, :none_loc}, + 201 => {:put_loc0, 1, 1, 0, :none_loc}, + 202 => {:put_loc1, 1, 1, 0, :none_loc}, + 203 => {:put_loc2, 1, 1, 0, :none_loc}, + 204 => {:put_loc3, 1, 1, 0, :none_loc}, + 205 => {:set_loc0, 1, 1, 1, :none_loc}, + 206 => {:set_loc1, 1, 1, 1, :none_loc}, + 207 => {:set_loc2, 1, 1, 1, :none_loc}, + 208 => {:set_loc3, 1, 1, 1, :none_loc}, + 209 => {:get_arg0, 1, 0, 1, :none_arg}, + 210 => {:get_arg1, 1, 0, 1, :none_arg}, + 211 => {:get_arg2, 1, 0, 1, :none_arg}, + 212 => {:get_arg3, 1, 0, 1, :none_arg}, + 213 => {:put_arg0, 1, 1, 0, :none_arg}, + 214 => {:put_arg1, 1, 1, 0, :none_arg}, + 215 => {:put_arg2, 1, 1, 0, :none_arg}, + 216 => {:put_arg3, 1, 1, 0, :none_arg}, + 217 => {:set_arg0, 1, 1, 1, :none_arg}, + 218 => {:set_arg1, 1, 1, 1, :none_arg}, + 219 => {:set_arg2, 1, 1, 1, :none_arg}, + 220 => {:set_arg3, 1, 1, 1, :none_arg}, + 221 => {:get_var_ref0, 1, 0, 1, :none_var_ref}, + 222 => {:get_var_ref1, 1, 0, 1, :none_var_ref}, + 223 => {:get_var_ref2, 1, 0, 1, :none_var_ref}, + 224 => {:get_var_ref3, 1, 0, 1, :none_var_ref}, + 225 => {:put_var_ref0, 1, 1, 0, :none_var_ref}, + 226 => {:put_var_ref1, 1, 1, 0, :none_var_ref}, + 227 => {:put_var_ref2, 1, 1, 0, :none_var_ref}, + 228 => {:put_var_ref3, 1, 1, 0, :none_var_ref}, + 229 => {:set_var_ref0, 1, 1, 1, :none_var_ref}, + 230 => {:set_var_ref1, 1, 1, 1, :none_var_ref}, + 231 => {:set_var_ref2, 1, 1, 1, :none_var_ref}, + 232 => {:set_var_ref3, 1, 1, 1, :none_var_ref}, + 233 => {:get_length, 1, 1, 1, :none}, + 234 => {:if_false8, 2, 1, 0, :label8}, + 235 => {:if_true8, 2, 1, 0, :label8}, + 236 => {:goto8, 2, 0, 0, :label8}, + 237 => {:goto16, 3, 0, 0, :label16}, + 238 => {:call0, 1, 1, 1, :npopx}, + 239 => {:call1, 1, 1, 1, :npopx}, + 240 => {:call2, 1, 1, 1, :npopx}, + 241 => {:call3, 1, 1, 1, :npopx}, + 242 => {:is_undefined, 1, 1, 1, :none}, + 243 => {:is_null, 1, 1, 1, :none}, + 244 => {:typeof_is_undefined, 1, 1, 1, :none}, + 245 => {:typeof_is_function, 1, 1, 1, :none} + } + + @name_to_num for {num, {name, _, _, _, _}} <- @opcodes, into: %{}, do: {name, num} + + def info(num) when is_integer(num), do: Map.get(@opcodes, num) + def num(name) when is_atom(name), do: Map.get(@name_to_num, name) + + def format_info(fmt), do: Map.get(@format_info, fmt) + + # Short-form opcodes expand to their canonical form + @short_forms %{ + push_minus1: {:push_i32, [-1]}, + push_0: {:push_i32, [0]}, + push_1: {:push_i32, [1]}, + push_2: {:push_i32, [2]}, + push_3: {:push_i32, [3]}, + push_4: {:push_i32, [4]}, + push_5: {:push_i32, [5]}, + push_6: {:push_i32, [6]}, + push_7: {:push_i32, [7]}, + get_loc0: {:get_loc, [0]}, + get_loc1: {:get_loc, [1]}, + get_loc2: {:get_loc, [2]}, + get_loc3: {:get_loc, [3]}, + put_loc0: {:put_loc, [0]}, + put_loc1: {:put_loc, [1]}, + put_loc2: {:put_loc, [2]}, + put_loc3: {:put_loc, [3]}, + set_loc0: {:set_loc, [0]}, + set_loc1: {:set_loc, [1]}, + set_loc2: {:set_loc, [2]}, + set_loc3: {:set_loc, [3]}, + get_arg0: {:get_arg, [0]}, + get_arg1: {:get_arg, [1]}, + get_arg2: {:get_arg, [2]}, + get_arg3: {:get_arg, [3]}, + put_arg0: {:put_arg, [0]}, + put_arg1: {:put_arg, [1]}, + put_arg2: {:put_arg, [2]}, + put_arg3: {:put_arg, [3]}, + set_arg0: {:set_arg, [0]}, + set_arg1: {:set_arg, [1]}, + set_arg2: {:set_arg, [2]}, + set_arg3: {:set_arg, [3]}, + get_var_ref0: {:get_var_ref, [0]}, + get_var_ref1: {:get_var_ref, [1]}, + get_var_ref2: {:get_var_ref, [2]}, + get_var_ref3: {:get_var_ref, [3]}, + put_var_ref0: {:put_var_ref, [0]}, + put_var_ref1: {:put_var_ref, [1]}, + put_var_ref2: {:put_var_ref, [2]}, + put_var_ref3: {:put_var_ref, [3]}, + set_var_ref0: {:set_var_ref, [0]}, + set_var_ref1: {:set_var_ref, [1]}, + set_var_ref2: {:set_var_ref, [2]}, + set_var_ref3: {:set_var_ref, [3]}, + call0: {:call, [0]}, + call1: {:call, [1]}, + call2: {:call, [2]}, + call3: {:call, [3]}, + push_empty_string: {:push_atom_value, [:empty_string]}, + get_loc0_loc1: {:get_loc0_loc1, []} + } + + @passthrough_aliases %{ + get_loc8: :get_loc, + put_loc8: :put_loc, + set_loc8: :set_loc, + get_loc_check8: :get_loc_check, + put_loc_check8: :put_loc_check + } + + def expand_short_form(name, args, arg_count \\ 0) do + case Map.get(@short_forms, name) do + nil -> + case Map.get(@passthrough_aliases, name) do + nil -> {name, args} + canonical -> {canonical, args} + end + + {canonical, const_args} -> + if canonical in [:get_loc, :put_loc, :set_loc] do + [idx] = const_args + {canonical, [idx + arg_count]} + else + {canonical, const_args} + end + end + end +end diff --git a/lib/quickbeam/vm/predefined_atoms.ex b/lib/quickbeam/vm/predefined_atoms.ex new file mode 100644 index 00000000..899d61bb --- /dev/null +++ b/lib/quickbeam/vm/predefined_atoms.ex @@ -0,0 +1,19 @@ +defmodule QuickBEAM.VM.PredefinedAtoms do + @moduledoc "QuickJS predefined atom table, generated at compile time from quickjs-atom.h" + + @header_path Application.app_dir(:quickbeam, "priv/c_src/quickjs-atom.h") + @external_resource @header_path + + @table @header_path + |> File.stream!() + |> Stream.filter(&match?("DEF(" <> _, &1)) + |> Stream.with_index(1) + |> Map.new(fn {line, idx} -> + {idx, line |> String.split("\"") |> Enum.at(1)} + end) + + def lookup(idx) when is_map_key(@table, idx), do: Map.fetch!(@table, idx) + def lookup(_), do: nil + + def count, do: map_size(@table) +end diff --git a/lib/quickbeam/vm/promise_state.ex b/lib/quickbeam/vm/promise_state.ex new file mode 100644 index 00000000..5c5588b2 --- /dev/null +++ b/lib/quickbeam/vm/promise_state.ex @@ -0,0 +1,185 @@ +defmodule QuickBEAM.VM.PromiseState do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter + + def resolved(val), do: make_promise(:resolved, val) + def rejected(val), do: make_promise(:rejected, val) + + def promise_then(args, {:obj, promise_ref}), do: then_impl(args, promise_ref) + def promise_then(_args, _this), do: resolved(:undefined) + + def promise_catch(args, this), do: promise_then([nil, List.first(args)], this) + + def promise_finally([callback | _], {:obj, promise_ref}) do + then_impl( + [ + fn value -> + run_finally(callback) + value + end, + fn reason -> + run_finally(callback) + throw({:js_throw, reason}) + end + ], + promise_ref + ) + end + + def promise_finally(_args, _this), do: resolved(:undefined) + + def resolve(ref, state, val) do + Heap.put_obj(ref, promise_obj(state, val, ref)) + + for {on_fulfilled, on_rejected, child_ref} <- pop_waiters(ref) do + handler = + case state do + :resolved -> on_fulfilled + :rejected -> on_rejected + end + + handler = if callable?(handler), do: handler, else: fn v -> v end + Heap.enqueue_microtask({:resolve, child_ref, handler, val}) + end + end + + def drain_microtasks do + case Heap.dequeue_microtask() do + nil -> + :ok + + {:resolve, child_ref, callback, val} -> + result = + try do + Interpreter.invoke_callback(callback, [val]) + catch + {:js_throw, err} -> {:rejected, err} + end + + case result do + {:rejected, err} -> resolve(child_ref, :rejected, err) + result_val -> resolve_or_chain(child_ref, result_val) + end + + drain_microtasks() + end + end + + # ── Internal ── + + defp make_promise(state, val) do + ref = make_ref() + Heap.put_obj(ref, promise_obj(state, val, ref)) + {:obj, ref} + end + + defp promise_obj(state, val, ref) do + base = %{ + promise_state() => state, + promise_value() => val, + "then" => then_fn(ref), + "catch" => catch_fn(ref) + } + + case promise_proto() do + nil -> base + proto -> Map.put(base, "__proto__", proto) + end + end + + defp pending_child do + ref = make_ref() + Heap.put_obj(ref, promise_obj(:pending, nil, ref)) + ref + end + + defp then_fn(promise_ref) do + {:builtin, "then", fn args, _this -> then_impl(args, promise_ref) end} + end + + defp catch_fn(promise_ref) do + {:builtin, "catch", fn args, _this -> then_impl([nil, List.first(args)], promise_ref) end} + end + + defp then_impl(args, promise_ref) do + on_fulfilled = Enum.at(args, 0) + on_rejected = Enum.at(args, 1) + + case Heap.get_obj(promise_ref, %{}) do + %{promise_state() => state, promise_value() => val} when state in [:resolved, :rejected] -> + handler = if state == :resolved, do: on_fulfilled, else: on_rejected + + if callable?(handler) do + child_ref = pending_child() + Heap.enqueue_microtask({:resolve, child_ref, handler, val}) + {:obj, child_ref} + else + make_promise(state, val) + end + + %{promise_state() => :pending} -> + child_ref = pending_child() + waiters = Heap.get_promise_waiters(promise_ref) + + Heap.put_promise_waiters(promise_ref, [ + {on_fulfilled, on_rejected, child_ref} | waiters + ]) + + {:obj, child_ref} + + _ -> + resolved(:undefined) + end + end + + defp run_finally(callback) do + if callable?(callback) do + Interpreter.invoke_callback(callback, []) + else + :undefined + end + end + + defp promise_proto do + case Heap.get_global_cache() do + %{"Promise" => ctor} -> Heap.get_class_proto(ctor) + _ -> nil + end + end + + defp resolve_or_chain(child_ref, {:obj, r}) do + case Heap.get_obj(r, %{}) do + %{promise_state() => :resolved, promise_value() => v} -> + resolve(child_ref, :resolved, v) + + %{promise_state() => :rejected, promise_value() => v} -> + resolve(child_ref, :rejected, v) + + %{promise_state() => :pending} -> + waiters = Heap.get_promise_waiters(r) + + Heap.put_promise_waiters(r, [ + {fn v -> resolve(child_ref, :resolved, v) end, nil, child_ref} | waiters + ]) + + _ -> + resolve(child_ref, :resolved, {:obj, r}) + end + end + + defp resolve_or_chain(child_ref, val), do: resolve(child_ref, :resolved, val) + + defp callable?(nil), do: false + defp callable?(:undefined), do: false + defp callable?(_), do: true + + defp pop_waiters(ref) do + waiters = Heap.get_promise_waiters(ref) + Heap.delete_promise_waiters(ref) + waiters + end +end diff --git a/lib/quickbeam/vm/runtime.ex b/lib/quickbeam/vm/runtime.ex new file mode 100644 index 00000000..93b394c9 --- /dev/null +++ b/lib/quickbeam/vm/runtime.ex @@ -0,0 +1,82 @@ +defmodule QuickBEAM.VM.Runtime do + @moduledoc "Shared helpers for the BEAM JS runtime: coercion, callbacks, object creation." + + alias QuickBEAM.VM.{Heap, Invocation} + alias QuickBEAM.VM.Interpreter.{Context, Values} + alias QuickBEAM.VM.Runtime.Globals + + def global_bindings do + case Heap.get_global_cache() do + nil -> Globals.build() + cached -> cached + end + end + + # ── Callback dispatch (used by higher-order array methods) ── + + def call_callback(fun, args), do: Invocation.call_callback(fun, args) + + def gas_budget do + case Heap.get_ctx() do + %{gas: gas} -> gas + _ -> Context.default_gas() + end + end + + # ── Shared helpers (public for cross-module use) ── + + def new_object do + Heap.wrap(%{}) + end + + defdelegate truthy?(val), to: Values + + def strict_equal?(a, b), do: a === b + + def stringify(val), do: Values.stringify(val) + + def to_int(n) when is_integer(n), do: n + def to_int(n) when is_float(n), do: trunc(n) + def to_int(_), do: 0 + + def to_float(n) when is_float(n), do: n + def to_float(n) when is_integer(n), do: n * 1.0 + def to_float(_), do: 0.0 + + def to_number({:bigint, n}), do: n + def to_number(val), do: Values.to_number(val) + + def normalize_index(idx, len) when idx < 0, do: max(len + idx, 0) + def normalize_index(idx, len), do: min(idx, len) + + @max_array_index 4_294_967_294 + + def sort_numeric_keys(keys) do + {numeric, strings} = + Enum.split_with(keys, fn + k when is_integer(k) and k >= 0 and k <= @max_array_index -> + true + + k when is_binary(k) -> + case Integer.parse(k) do + {n, ""} when n >= 0 and n <= @max_array_index -> true + _ -> false + end + + _ -> + false + end) + + sorted = + Enum.sort_by(numeric, fn + k when is_integer(k) -> k + k when is_binary(k) -> elem(Integer.parse(k), 0) + end) + |> Enum.map(fn + k when is_integer(k) -> Integer.to_string(k) + k -> k + end) + + sorted ++ Enum.filter(strings, &is_binary/1) + end +end diff --git a/lib/quickbeam/vm/runtime/array.ex b/lib/quickbeam/vm/runtime/array.ex new file mode 100644 index 00000000..a6b83dcc --- /dev/null +++ b/lib/quickbeam/vm/runtime/array.ex @@ -0,0 +1,833 @@ +defmodule QuickBEAM.VM.Runtime.Array do + @moduledoc "Array.prototype and Array static methods." + + use QuickBEAM.VM.Builtin + + import QuickBEAM.VM.Heap.Keys + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Runtime + + # ── Array.prototype dispatch ── + + proto "push" do + push(this, args) + end + + proto "pop" do + pop(this, args) + end + + proto "shift" do + shift(this, args) + end + + proto "unshift" do + unshift(this, args) + end + + proto "map" do + map(this, args) + end + + proto "filter" do + filter(this, args) + end + + proto "reduce" do + reduce(this, args) + end + + proto "forEach" do + for_each(this, args) + end + + proto "indexOf" do + index_of(this, args) + end + + proto "lastIndexOf" do + last_index_of(this, args) + end + + proto "toString" do + join(this, [","]) + end + + proto "includes" do + includes(this, args) + end + + proto "slice" do + slice(this, args) + end + + proto "splice" do + splice(this, args) + end + + proto "join" do + join(this, args) + end + + proto "concat" do + concat(this, args) + end + + proto "reverse" do + reverse(this, args) + end + + proto "sort" do + sort(this, args) + end + + proto "flat" do + flat(this, args) + end + + proto "find" do + find(this, args) + end + + proto "findIndex" do + find_index(this, args) + end + + proto "every" do + every(this, args) + end + + proto "some" do + some(this, args) + end + + proto "flatMap" do + flat_map(this, args) + end + + proto "fill" do + fill(this, args) + end + + proto "copyWithin" do + copy_within(this, args) + end + + proto "at" do + array_at(this, args) + end + + proto "findLast" do + find_last(this, args) + end + + proto "findLastIndex" do + find_last_index(this, args) + end + + proto "toReversed" do + to_reversed(this) + end + + proto "toSorted" do + to_sorted(this) + end + + proto "values" do + make_array_iterator(this, :values) + end + + proto "keys" do + make_array_iterator(this, :keys) + end + + proto "entries" do + make_array_iterator(this, :entries) + end + + def proto_property("constructor") do + Runtime.global_bindings() |> Map.get("Array", :undefined) + end + + # ── Array static dispatch ── + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + static "isArray" do + is_array(hd(args)) + end + + @max_proxy_depth 1_000_000 + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + defp is_array(val, depth \\ 0) + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + defp is_array(_, depth) when depth > @max_proxy_depth do + throw({:js_throw, Heap.make_error("Maximum call stack size exceeded", "RangeError")}) + end + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + defp is_array({:qb_arr, _}, _), do: true + defp is_array(list, _) when is_list(list), do: true + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + defp is_array({:obj, ref}, depth) do + case Heap.get_obj(ref) do + {:qb_arr, _} -> true + list when is_list(list) -> true + %{proxy_target() => target} -> is_array(target, depth + 1) + _ -> false + end + end + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + defp is_array(_, _), do: false + + static "from" do + from(args) + end + + static "of" do + args + end + + # ── Mutation helpers ── + + defp push({:obj, ref}, args) do + Heap.array_push(ref, args) + end + + defp push({:qb_arr, arr}, args), do: :array.size(arr) + length(args) + defp push(list, args) when is_list(list), do: length(list ++ args) + + defp pop({:obj, ref}, _) do + list = Heap.obj_to_list(ref) + + case List.pop_at(list, -1) do + {nil, _} -> + :undefined + + {last, rest} -> + Heap.put_obj(ref, rest) + last + end + end + + defp pop([_ | _] = list, _), do: List.last(list) + defp pop(_, _), do: :undefined + + defp shift({:obj, ref}, _) do + list = Heap.obj_to_list(ref) + + case list do + [first | rest] -> + Heap.put_obj(ref, rest) + first + + _ -> + :undefined + end + end + + defp shift(_, _), do: :undefined + + defp unshift({:obj, ref}, args) do + list = Heap.obj_to_list(ref) + new_list = args ++ list + Heap.put_obj(ref, new_list) + length(new_list) + end + + defp unshift(_, _), do: 0 + + # ── Higher-order ── + + defp map({:obj, ref}, [fun | _]) do + list = Heap.obj_to_list(ref) + + result = + Enum.map(Enum.with_index(list), fn {val, idx} -> + Runtime.call_callback(fun, [val, idx, list]) + end) + + Heap.wrap(result) + end + + defp map([_ | _] = list, [fun | _]) do + Enum.map(Enum.with_index(list), fn {val, idx} -> + Runtime.call_callback(fun, [val, idx, list]) + end) + end + + defp map(list, _), do: list + + defp filter({:obj, ref}, [fun | _]) do + list = Heap.obj_to_list(ref) + + result = + Enum.filter(Enum.with_index(list), fn {val, idx} -> + Runtime.truthy?(Runtime.call_callback(fun, [val, idx, list])) + end) + |> Enum.map(fn {val, _} -> val end) + + Heap.wrap(result) + end + + defp filter({:qb_arr, arr}, args), do: filter(:array.to_list(arr), args) + + defp filter(list, [fun | _]) when is_list(list) do + Enum.filter(Enum.with_index(list), fn {val, idx} -> + Runtime.truthy?(Runtime.call_callback(fun, [val, idx, list])) + end) + |> Enum.map(fn {val, _} -> val end) + end + + defp filter(list, _), do: list + + defp reduce({:obj, ref}, [fun | rest]) do + list = Heap.obj_to_list(ref) + reduce_impl(list, fun, rest) + end + + defp reduce({:qb_arr, arr}, args), do: reduce(:array.to_list(arr), args) + + defp reduce(list, [fun | rest]) when is_list(list), + do: reduce_impl(list, fun, rest) + + defp reduce([], [_, init | _]), do: init + defp reduce([val], _), do: val + + defp reduce_impl(list, fun, rest) do + {acc, items} = + case rest do + [init] -> {init, list} + _ -> {hd(list), tl(list)} + end + + Enum.reduce(Enum.with_index(items), acc, fn {val, idx}, a -> + Runtime.call_callback(fun, [a, val, idx, list]) + end) + end + + defp for_each({:obj, ref}, [fun | _]) do + list = Heap.obj_to_list(ref) + + Enum.each(Enum.with_index(list), fn {val, idx} -> + Runtime.call_callback(fun, [val, idx, list]) + end) + + :undefined + end + + defp for_each({:qb_arr, arr}, args), do: for_each(:array.to_list(arr), args) + + defp for_each(list, [fun | _]) when is_list(list) do + Enum.each(Enum.with_index(list), fn {val, idx} -> + Runtime.call_callback(fun, [val, idx, list]) + end) + + :undefined + end + + defp for_each(_, _), do: :undefined + + # ── Search ── + + defp index_of({:obj, ref}, args), do: index_of(Heap.obj_to_list(ref), args) + + defp index_of({:qb_arr, arr}, args), do: index_of(:array.to_list(arr), args) + + defp index_of(list, [val | rest]) when is_list(list) do + from = + case rest do + [f] when is_integer(f) and f >= 0 -> f + _ -> 0 + end + + list + |> Enum.drop(from) + |> Enum.find_index(&Runtime.strict_equal?(&1, val)) + |> then(fn + nil -> -1 + idx -> idx + from + end) + end + + defp index_of(_, _), do: -1 + + defp last_index_of({:obj, ref}, args), do: last_index_of(Heap.obj_to_list(ref), args) + + defp last_index_of({:qb_arr, arr}, args), do: last_index_of(:array.to_list(arr), args) + + defp last_index_of(list, [val | _]) when is_list(list) do + list + |> Enum.with_index() + |> Enum.reverse() + |> Enum.find_value(-1, fn {el, i} -> if Runtime.strict_equal?(el, val), do: i end) + end + + defp last_index_of(_, _), do: -1 + + defp includes({:obj, ref}, args), do: includes(Heap.obj_to_list(ref), args) + + defp includes({:qb_arr, arr}, args), do: includes(:array.to_list(arr), args) + + defp includes(list, [val | rest]) when is_list(list) do + from = + case rest do + [f] when is_integer(f) and f >= 0 -> f + _ -> 0 + end + + list |> Enum.drop(from) |> Enum.any?(&Runtime.strict_equal?(&1, val)) + end + + defp includes(_, _), do: false + + # ── Slice / splice ── + + defp slice({:obj, ref}, args), do: slice(Heap.obj_to_list(ref), args) + + defp slice({:qb_arr, arr}, args), do: slice(:array.to_list(arr), args) + + defp slice(list, args) when is_list(list) do + {start_idx, end_idx} = slice_args(list, args) + list |> Enum.slice(start_idx, max(end_idx - start_idx, 0)) + end + + defp slice(_, _), do: [] + + defp splice({:obj, ref}, args) do + list = Heap.obj_to_list(ref) + {removed, new_list} = do_splice(list, args) + Heap.put_obj(ref, new_list) + removed + end + + defp splice({:qb_arr, arr}, args), do: splice(:array.to_list(arr), args) + + defp splice(list, args) when is_list(list) do + {removed, _} = do_splice(list, args) + removed + end + + defp splice(_, _), do: [] + + defp do_splice(list, [start | rest]) do + s = Runtime.normalize_index(start, length(list)) + + {delete_count, insert} = + case rest do + [] -> {length(list) - s, []} + [dc | ins] -> {max(min(Runtime.to_int(dc), length(list) - s), 0), ins} + end + + {before, after_start} = Enum.split(list, s) + {removed, remaining} = Enum.split(after_start, delete_count) + {removed, before ++ insert ++ remaining} + end + + defp do_splice(list, _), do: {[], list} + + # ── Transform ── + + defp join({:obj, ref}, args), do: join(Heap.obj_to_list(ref), args) + + defp join({:qb_arr, arr}, args), do: join(:array.to_list(arr), args) + + defp join(list, [sep | _]) when is_list(list), + do: Enum.map_join(list, Runtime.stringify(sep), &array_element_to_string/1) + + defp join(list, []) when is_list(list), do: Enum.map_join(list, ",", &array_element_to_string/1) + defp join(_, _), do: "" + + defp array_element_to_string(:undefined), do: "" + defp array_element_to_string(nil), do: "" + defp array_element_to_string(val), do: Runtime.stringify(val) + + defp concat({:obj, ref}, args) do + list = Heap.obj_to_list(ref) + result = Enum.reduce(args, list, &concat_item(&1, &2)) + Heap.wrap(result) + end + + defp concat({:qb_arr, arr}, args), do: concat(:array.to_list(arr), args) + + defp concat(list, args) when is_list(list), do: Enum.reduce(args, list, &concat_item(&1, &2)) + + defp concat_item({:obj, r}, acc), do: acc ++ Heap.obj_to_list(r) + defp concat_item({:qb_arr, arr}, acc), do: acc ++ :array.to_list(arr) + defp concat_item(a, acc) when is_list(a), do: acc ++ a + defp concat_item(val, acc), do: acc ++ [val] + + defp reverse({:obj, ref}, _) do + list = Heap.obj_to_list(ref) + Heap.put_obj(ref, Enum.reverse(list)) + {:obj, ref} + end + + defp reverse({:qb_arr, arr}, args), do: reverse(:array.to_list(arr), args) + + defp reverse(list, _) when is_list(list), do: Enum.reverse(list) + defp reverse(_, _), do: [] + + defp sort({:obj, ref}, [_compare_fn | _] = args) do + list = Heap.obj_to_list(ref) + # Comparator fn returns negative (ab) + # Fall back to string sort if comparator can't be invoked + sorted = + try do + compare_fn = hd(args) + + Enum.sort(list, fn a, b -> + result = Runtime.call_callback(compare_fn, [a, b]) + + case result do + n when is_number(n) -> n < 0 + _ -> Runtime.stringify(a) < Runtime.stringify(b) + end + end) + catch + _ -> Enum.sort(list, fn a, b -> Runtime.stringify(a) < Runtime.stringify(b) end) + end + + Heap.put_obj(ref, sorted) + {:obj, ref} + end + + defp sort({:obj, ref}, []) do + list = Heap.obj_to_list(ref) + + Heap.put_obj( + ref, + Enum.sort(list, fn a, b -> + Runtime.stringify(a) < Runtime.stringify(b) + end) + ) + + {:obj, ref} + end + + defp sort({:qb_arr, arr}, args), do: sort(:array.to_list(arr), args) + + defp sort(list, [_ | _]) when is_list(list) do + Enum.sort(list, fn a, b -> Runtime.stringify(a) < Runtime.stringify(b) end) + end + + defp sort(list, []) when is_list(list), + do: + Enum.sort(list, fn a, b -> + Runtime.stringify(a) < Runtime.stringify(b) + end) + + defp flat({:obj, ref}, args), do: flat(Heap.obj_to_list(ref), args) + + defp flat({:qb_arr, arr}, args), do: flat(:array.to_list(arr), args) + + defp flat(list, _) when is_list(list) do + Enum.flat_map(list, fn + {:qb_arr, arr} -> + :array.to_list(arr) + + a when is_list(a) -> + a + + {:obj, ref} = obj -> + case Heap.get_obj(ref) do + {:qb_arr, arr} -> :array.to_list(arr) + a when is_list(a) -> a + _ -> [obj] + end + + val -> + [val] + end) + end + + defp flat(_, _), do: [] + + defp flat_map({:obj, ref}, args), do: flat_map(Heap.obj_to_list(ref), args) + + defp flat_map({:qb_arr, arr}, args), do: flat_map(:array.to_list(arr), args) + + defp flat_map(list, [cb | _]) when is_list(list) do + result = + Enum.flat_map(Enum.with_index(list), fn {item, idx} -> + val = Runtime.call_callback(cb, [item, idx, list]) + + case val do + {:obj, r} -> Heap.obj_to_list(r) + {:qb_arr, arr2} -> :array.to_list(arr2) + l when is_list(l) -> l + _ -> [val] + end + end) + + Heap.wrap(result) + end + + defp flat_map(_, _), do: :undefined + + defp fill({:obj, ref}, args) do + list = Heap.obj_to_list(ref) + val = Enum.at(args, 0, :undefined) + start_idx = Enum.at(args, 1) || 0 + end_idx = Enum.at(args, 2) || length(list) + + new_list = + Enum.with_index(list, fn item, idx -> + if idx >= start_idx and idx < end_idx, do: val, else: item + end) + + Heap.put_obj(ref, new_list) + {:obj, ref} + end + + defp fill({:qb_arr, arr}, args), do: fill(:array.to_list(arr), args) + + defp fill(list, args) when is_list(list) do + val = Enum.at(args, 0, :undefined) + List.duplicate(val, length(list)) + end + + defp fill(_, _), do: :undefined + + # ── Predicates ── + + defp find({:obj, ref}, args), do: find(Heap.obj_to_list(ref), args) + + defp find({:qb_arr, arr}, args), do: find(:array.to_list(arr), args) + + defp find(list, [fun | _]) when is_list(list) do + Enum.find_value(Enum.with_index(list), :undefined, fn {val, idx} -> + if Runtime.truthy?(Runtime.call_callback(fun, [val, idx, list])), do: val + end) + end + + defp find(_, _), do: :undefined + + defp find_index({:obj, ref}, args), do: find_index(Heap.obj_to_list(ref), args) + + defp find_index({:qb_arr, arr}, args), do: find_index(:array.to_list(arr), args) + + defp find_index(list, [fun | _]) when is_list(list) do + Enum.find_value(Enum.with_index(list), -1, fn {val, idx} -> + if Runtime.truthy?(Runtime.call_callback(fun, [val, idx, list])), do: idx + end) + end + + defp find_index(_, _), do: -1 + + defp every({:obj, ref}, args), do: every(Heap.obj_to_list(ref), args) + + defp every({:qb_arr, arr}, args), do: every(:array.to_list(arr), args) + + defp every(list, [fun | _]) when is_list(list) do + Enum.all?(Enum.with_index(list), fn {val, idx} -> + Runtime.truthy?(Runtime.call_callback(fun, [val, idx, list])) + end) + end + + defp every(_, _), do: true + + defp some({:obj, ref}, args), do: some(Heap.obj_to_list(ref), args) + + defp some({:qb_arr, arr}, args), do: some(:array.to_list(arr), args) + + defp some(list, [fun | _]) when is_list(list) do + Enum.any?(Enum.with_index(list), fn {val, idx} -> + Runtime.truthy?(Runtime.call_callback(fun, [val, idx, list])) + end) + end + + defp some(_, _), do: false + + # ── Array.from ── + + defp from(args) do + {source, map_fn} = + case args do + [s, f | _] -> {s, f} + [s] -> {s, nil} + _ -> {nil, nil} + end + + list = coerce_to_list(source) + + if map_fn do + Enum.map(Enum.with_index(list), fn {val, idx} -> + Runtime.call_callback(map_fn, [val, idx]) + end) + else + list + end + end + + defp coerce_to_list({:obj, ref}) do + case Heap.get_obj(ref) do + {:qb_arr, arr} -> :array.to_list(arr) + l when is_list(l) -> l + map when is_map(map) -> Heap.to_list({:obj, ref}) + _ -> [] + end + end + + defp coerce_to_list({:qb_arr, arr}), do: :array.to_list(arr) + defp coerce_to_list(l) when is_list(l), do: l + defp coerce_to_list(s) when is_binary(s), do: String.codepoints(s) + defp coerce_to_list(_), do: [] + + defp copy_within({:obj, ref}, args) do + list = Heap.obj_to_list(ref) + len = length(list) + target = Runtime.normalize_index(Runtime.to_int(Enum.at(args, 0, 0)), len) + start_idx = Runtime.normalize_index(Runtime.to_int(Enum.at(args, 1, 0)), len) + end_idx = Runtime.normalize_index(Runtime.to_int(Enum.at(args, 2) || len), len) + slice = Enum.slice(list, start_idx, end_idx - start_idx) + + new_list = + list + |> Enum.with_index() + |> Enum.map(fn {item, i} -> + offset = i - target + if i >= target and offset < length(slice), do: Enum.at(slice, offset), else: item + end) + + Heap.put_obj(ref, new_list) + {:obj, ref} + end + + defp copy_within(_, _), do: :undefined + + defp array_at({:obj, ref}, [idx | _]) do + list = Heap.obj_to_list(ref) + array_at(list, [idx]) + end + + defp array_at({:qb_arr, arr}, args), do: array_at(:array.to_list(arr), args) + + defp array_at(list, [idx | _]) when is_list(list) do + i = if is_number(idx), do: trunc(idx), else: 0 + i = if i < 0, do: length(list) + i, else: i + if i >= 0 and i < length(list), do: Enum.at(list, i), else: :undefined + end + + defp array_at(_, _), do: :undefined + + defp find_last({:obj, ref}, args), do: find_last(Heap.obj_to_list(ref), args) + + defp find_last({:qb_arr, arr}, args), do: find_last(:array.to_list(arr), args) + + defp find_last(list, [cb | _]) when is_list(list) do + list + |> Enum.reverse() + |> Enum.find(:undefined, fn item -> + Runtime.call_callback(cb, [item]) |> Runtime.truthy?() + end) + end + + defp find_last(_, _), do: :undefined + + defp find_last_index({:obj, ref}, args), + do: find_last_index(Heap.obj_to_list(ref), args) + + defp find_last_index({:qb_arr, arr}, args), do: find_last_index(:array.to_list(arr), args) + + defp find_last_index(list, [cb | _]) when is_list(list) do + list + |> Enum.with_index() + |> Enum.reverse() + |> Enum.find_value(-1, fn {item, idx} -> + if Runtime.call_callback(cb, [item, idx]) |> Runtime.truthy?(), do: idx + end) + end + + defp find_last_index(_, _), do: -1 + + defp to_reversed({:obj, ref}) do + list = Heap.obj_to_list(ref) + Heap.wrap(Enum.reverse(list)) + end + + defp to_reversed(_), do: :undefined + + defp to_sorted({:obj, ref}) do + list = Heap.obj_to_list(ref) + new_ref = make_ref() + + Heap.put_obj( + new_ref, + Enum.sort(list, fn a, b -> Runtime.stringify(a) <= Runtime.stringify(b) end) + ) + + {:obj, new_ref} + end + + defp to_sorted(_), do: :undefined + + defp make_array_iterator(arr, mode) do + list = + case arr do + {:obj, ref} -> + Heap.obj_to_list(ref) + + {:qb_arr, arr} -> + :array.to_list(arr) + + l when is_list(l) -> + l + + s when is_binary(s) -> + String.codepoints(s) + + _ -> + [] + end + + idx_ref = :atomics.new(1, signed: false) + + next_fn = + {:builtin, "next", + fn _args, _this -> + i = :atomics.get(idx_ref, 1) + + if i >= length(list) do + Heap.wrap(%{"value" => :undefined, "done" => true}) + else + :atomics.put(idx_ref, 1, i + 1) + + value = + case mode do + :values -> Enum.at(list, i, :undefined) + :keys -> i + :entries -> Heap.wrap([i, Enum.at(list, i, :undefined)]) + end + + Heap.wrap(%{"value" => value, "done" => false}) + end + end} + + build_object do + val("next", next_fn) + end + end + + # ── Internal ── + + defp slice_args(list, [start, end_]) do + s = Runtime.normalize_index(start, length(list)) + + e = + if end_ < 0, do: max(length(list) + end_, 0), else: min(Runtime.to_int(end_), length(list)) + + {s, e} + end + + defp slice_args(list, [start]) do + {Runtime.normalize_index(start, length(list)), length(list)} + end + + defp slice_args(list, []) do + {0, length(list)} + end +end diff --git a/lib/quickbeam/vm/runtime/array_buffer.ex b/lib/quickbeam/vm/runtime/array_buffer.ex new file mode 100644 index 00000000..63f98ce5 --- /dev/null +++ b/lib/quickbeam/vm/runtime/array_buffer.ex @@ -0,0 +1,177 @@ +defmodule QuickBEAM.VM.Runtime.ArrayBuffer do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Runtime + + def constructor(args, _this \\ nil) do + {byte_length, max_byte_length} = + case args do + [n, opts | _] when is_integer(n) -> {n, max_byte_length_option(opts)} + [n | _] when is_integer(n) -> {n, nil} + _ -> {0, nil} + end + + map = %{buffer() => :binary.copy(<<0>>, byte_length), "byteLength" => byte_length} + map = if max_byte_length, do: Map.put(map, "maxByteLength", max_byte_length), else: map + Heap.wrap(map) + end + + proto "transfer" do + case this do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + + if is_map(map) do + new_buf = Map.get(map, buffer(), <<>>) + + Heap.put_obj( + ref, + Map.merge(map, %{buffer() => <<>>, "byteLength" => 0, "__detached__" => true}) + ) + + Heap.wrap(%{buffer() => new_buf, "byteLength" => byte_size(new_buf)}) + else + :undefined + end + + _ -> + :undefined + end + end + + proto "resize" do + case this do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + + new_size = + case args do + [n | _] when is_number(n) -> trunc(n) + _ -> 0 + end + + if is_map(map) do + old_buf = Map.get(map, buffer(), <<>>) + + new_buf = + if new_size <= byte_size(old_buf) do + binary_part(old_buf, 0, new_size) + else + old_buf <> :binary.copy(<<0>>, new_size - byte_size(old_buf)) + end + + Heap.put_obj(ref, Map.merge(map, %{buffer() => new_buf, "byteLength" => new_size})) + end + + :undefined + + _ -> + :undefined + end + end + + proto "slice" do + do_slice(this, args) + end + + proto "sliceToImmutable" do + case this do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + buf = Map.get(map, buffer(), <<>>) + len = byte_size(buf) + + s = + case args do + [n | _] when is_number(n) -> normalize_idx(trunc(n), len) + _ -> 0 + end + + e = + case args do + [_, n | _] when is_number(n) -> normalize_idx(trunc(n), len) + _ -> len + end + + new_len = max(0, e - s) + new_buf = if new_len > 0, do: binary_part(buf, s, new_len), else: <<>> + Heap.wrap(%{buffer() => new_buf, "byteLength" => new_len, "__immutable__" => true}) + + _ -> + :undefined + end + end + + defp do_slice(this, args) do + case this do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + + if is_map(map) and Map.get(map, "__detached__") do + throw({:js_throw, Heap.make_error("ArrayBuffer is detached", "TypeError")}) + end + + buf = Map.get(map, buffer(), <<>>) + len = byte_size(buf) + + s = + case args do + [n | _] when is_number(n) -> normalize_idx(trunc(n), len) + _ -> 0 + end + + e = + case args do + [_, n | _] when is_number(n) -> normalize_idx(trunc(n), len) + _ -> len + end + + new_len = max(0, e - s) + + read_array_buffer_species() + + # After species getter, re-check the buffer (it may have been resized/detached) + map2 = Heap.get_obj(ref, %{}) + buf2 = Map.get(map2, buffer(), <<>>) + + if byte_size(buf2) < s + new_len do + throw({:js_throw, Heap.make_error("ArrayBuffer is detached", "TypeError")}) + end + + new_buf = if new_len > 0, do: binary_part(buf2, s, new_len), else: <<>> + Heap.wrap(%{buffer() => new_buf, "byteLength" => new_len}) + + _ -> + :undefined + end + end + + defp normalize_idx(n, len) when n < 0, do: max(0, len + n) + defp normalize_idx(n, len), do: min(n, len) + + defp max_byte_length_option({:obj, ref}) do + case Heap.get_obj(ref, %{}) do + map when is_map(map) -> Map.get(map, "maxByteLength") + _ -> nil + end + end + + defp max_byte_length_option(_), do: nil + + defp read_array_buffer_species do + case Runtime.global_bindings()["ArrayBuffer"] do + {:builtin, _, _} = ctor -> + case Map.get(Heap.get_ctor_statics(ctor), {:symbol, "Symbol.species"}) do + {:accessor, getter, _} when getter != nil -> Runtime.call_callback(getter, []) + _ -> nil + end + + _ -> + nil + end + end +end diff --git a/lib/quickbeam/vm/runtime/boolean.ex b/lib/quickbeam/vm/runtime/boolean.ex new file mode 100644 index 00000000..1326f00c --- /dev/null +++ b/lib/quickbeam/vm/runtime/boolean.ex @@ -0,0 +1,17 @@ +defmodule QuickBEAM.VM.Runtime.Boolean do + @moduledoc false + + use QuickBEAM.VM.Builtin + alias QuickBEAM.VM.Runtime + + proto "toString" do + Atom.to_string(this) + end + + proto "valueOf" do + this + end + + def constructor, + do: fn args, _this -> Runtime.truthy?(List.first(args, false)) end +end diff --git a/lib/quickbeam/vm/runtime/console.ex b/lib/quickbeam/vm/runtime/console.ex new file mode 100644 index 00000000..5c05ff80 --- /dev/null +++ b/lib/quickbeam/vm/runtime/console.ex @@ -0,0 +1,34 @@ +defmodule QuickBEAM.VM.Runtime.Console do + @moduledoc false + + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Runtime + + js_object "console" do + method "log" do + IO.puts(Enum.map_join(args, " ", &Runtime.stringify/1)) + :undefined + end + + method "warn" do + IO.puts(:stderr, Enum.map_join(args, " ", &Runtime.stringify/1)) + :undefined + end + + method "error" do + IO.puts(:stderr, Enum.map_join(args, " ", &Runtime.stringify/1)) + :undefined + end + + method "info" do + IO.puts(Enum.map_join(args, " ", &Runtime.stringify/1)) + :undefined + end + + method "debug" do + IO.puts(Enum.map_join(args, " ", &Runtime.stringify/1)) + :undefined + end + end +end diff --git a/lib/quickbeam/vm/runtime/date.ex b/lib/quickbeam/vm/runtime/date.ex new file mode 100644 index 00000000..4b6c0293 --- /dev/null +++ b/lib/quickbeam/vm/runtime/date.ex @@ -0,0 +1,546 @@ +defmodule QuickBEAM.VM.Runtime.Date do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + use QuickBEAM.VM.Builtin + alias QuickBEAM.VM.Heap + + @epoch_gs 719_528 * 86_400 + + # ── Constructor ── + + def constructor(args, _this) do + ms = + case args do + [] -> System.system_time(:millisecond) + [val] when is_number(val) -> trunc(val) + [s] when is_binary(s) -> parse_date_string(s) + [_ | _] when length(args) >= 2 -> local_from_components(args) + _ -> System.system_time(:millisecond) + end + + Heap.wrap(%{date_ms() => ms}) + end + + # ── Statics ── + + static("now", do: System.system_time(:millisecond)) + static("parse", do: parse_date_string(to_string(hd(args)))) + static("UTC", do: utc_from_components(args)) + + # ── Getters ── + + proto("getTime", do: get_ms(this)) + proto("valueOf", do: get_ms(this)) + proto("getFullYear", do: dt_field(this, :year)) + proto("getMonth", do: dt_field(this, :month, &(&1 - 1))) + proto("getDate", do: dt_field(this, :day)) + proto("getHours", do: dt_field(this, :hour)) + proto("getMinutes", do: dt_field(this, :minute)) + proto("getSeconds", do: dt_field(this, :second)) + proto("getMilliseconds", do: with_ms(this, &rem(&1, 1000))) + proto("getUTCFullYear", do: dt_field(this, :year)) + proto("getDay", do: with_dt(this, &(Date.day_of_week(&1) |> rem(7)))) + proto("getTimezoneOffset", do: tz_offset_minutes()) + + # ── Setters ── + + proto("setTime", do: put_ms(this, hd(args))) + proto("setFullYear", do: set_fields(this, [:year], args)) + proto("setMonth", do: set_field(this, :month, trunc(hd(args)) + 1)) + proto("setDate", do: set_fields(this, [:day], args)) + proto("setHours", do: set_fields(this, [:hour, :minute, :second], args)) + proto("setMinutes", do: set_fields(this, [:minute, :second], args)) + proto("setSeconds", do: set_fields(this, [:second], args)) + proto("setMilliseconds", do: set_ms_field(this, args)) + proto("setUTCHours", do: set_fields(this, [:hour, :minute, :second], args)) + proto("setUTCMinutes", do: set_fields(this, [:minute, :second], args)) + proto("setUTCSeconds", do: set_fields(this, [:second], args)) + proto("setUTCMilliseconds", do: set_ms_field(this, args)) + proto("setUTCFullYear", do: set_fields(this, [:year], args)) + proto("setUTCMonth", do: set_field(this, :month, trunc(hd(args)) + 1)) + proto("setUTCDate", do: set_fields(this, [:day], args)) + + # ── Formatting ── + + proto("toISOString", do: fmt_dt(this, &DateTime.to_iso8601/1)) + proto("toJSON", do: fmt_dt(this, &DateTime.to_iso8601/1)) + + proto("toString", + do: fmt_dt(this, &Calendar.strftime(&1, "%a %b %d %Y %H:%M:%S GMT+0000 (UTC)")) + ) + + proto("toDateString", do: fmt_dt(this, &Calendar.strftime(&1, "%a %b %d %Y"))) + proto("toTimeString", do: fmt_dt(this, &Calendar.strftime(&1, "%H:%M:%S GMT+0000"))) + proto("toUTCString", do: fmt_dt(this, &Calendar.strftime(&1, "%a, %d %b %Y %H:%M:%S GMT"))) + proto("toLocaleTimeString", do: fmt_local(this, "%I:%M:%S %p")) + proto("toLocaleDateString", do: fmt_local(this, "%m/%d/%Y")) + proto("toLocaleString", do: fmt_local(this, "%m/%d/%Y, %I:%M:%S %p")) + + # ── Internal: ms ↔ DateTime ── + + defp get_ms({:obj, ref}) do + case Heap.get_obj(ref, %{}) do + %{date_ms() => ms} -> ms + _ -> :nan + end + end + + defp get_ms(_), do: :nan + + defp ms_to_dt(ms) when is_number(ms) do + ms = trunc(ms) + DateTime.from_gregorian_seconds(div(ms, 1000) + @epoch_gs, {rem(abs(ms), 1000) * 1000, 3}) + rescue + _ -> nil + end + + defp ms_to_dt(_), do: nil + + defp dt_field(this, field, transform \\ & &1) do + case ms_to_dt(get_ms(this)) do + nil -> :nan + dt -> transform.(Map.get(dt, field)) + end + end + + defp with_dt(this, fun) do + case ms_to_dt(get_ms(this)) do + nil -> :nan + dt -> fun.(dt) + end + end + + defp with_ms(this, fun) do + case get_ms(this) do + ms when is_number(ms) -> fun.(trunc(ms)) + _ -> :nan + end + end + + defp fmt_dt(this, fun) do + case ms_to_dt(get_ms(this)) do + nil -> "Invalid Date" + dt -> fun.(dt) + end + end + + defp fmt_local(this, pattern) do + case ms_to_dt(get_ms(this)) do + nil -> + "Invalid Date" + + dt -> + local_erl = + :calendar.universal_time_to_local_time( + {{dt.year, dt.month, dt.day}, {dt.hour, dt.minute, dt.second}} + ) + + Calendar.strftime(NaiveDateTime.from_erl!(local_erl), pattern) + end + end + + defp put_ms({:obj, ref}, ms) when is_number(ms) do + Heap.put_obj(ref, Map.put(Heap.get_obj(ref, %{}), date_ms(), trunc(ms))) + trunc(ms) + end + + defp put_ms(_, _), do: :nan + + defp set_field(this, field, value) do + case ms_to_dt(get_ms(this)) do + nil -> :nan + dt -> put_ms(this, DateTime.to_unix(Map.put(dt, field, trunc(value)), :millisecond)) + end + rescue + _ -> :nan + end + + defp set_fields(this, fields, values) do + case ms_to_dt(get_ms(this)) do + nil -> + :nan + + dt -> + new_dt = + Enum.zip(fields, values) + |> Enum.reduce(dt, fn {field, val}, acc -> + if is_number(val), do: Map.put(acc, field, trunc(val)), else: acc + end) + + put_ms(this, DateTime.to_unix(new_dt, :millisecond)) + end + rescue + _ -> :nan + end + + defp set_ms_field(this, args) do + with_ms(this, &put_ms(this, div(&1, 1000) * 1000 + trunc(hd(args)))) + end + + defp tz_offset_minutes do + {utc, local} = {:calendar.universal_time(), :calendar.local_time()} + + div( + :calendar.datetime_to_gregorian_seconds(utc) - + :calendar.datetime_to_gregorian_seconds(local), + 60 + ) + end + + # ── Date component → ms ── + + defp utc_from_components(args) do + with {:ok, components} <- extract_components(args) do + utc_ms(components) + end + end + + defp local_from_components(args) do + with {:ok, {year, month, day, hour, minute, second, ms_part}} <- extract_components(args) do + local_erl = {{year, month, max(day, 1)}, {hour, minute, second}} + + case :calendar.local_time_to_universal_time_dst(local_erl) do + [utc_erl | _] -> + local_ndt = NaiveDateTime.from_erl!(local_erl) + utc_ndt = NaiveDateTime.from_erl!(utc_erl) + offset_min = div(NaiveDateTime.diff(local_ndt, utc_ndt, :second) + 30, 60) + + DateTime.to_unix(DateTime.from_naive!(local_ndt, "Etc/UTC"), :millisecond) - + offset_min * 60_000 + ms_part + + [] -> + utc_ms({year, month, max(day, 1), hour, minute, second, ms_part}) - + tz_offset_minutes() * -60_000 + end + end + rescue + _ -> :nan + end + + defp extract_components(args) do + padded = args ++ List.duplicate(0, 7) + count = min(length(args), 7) + + vals = + padded + |> Enum.take(count) + |> Enum.map(fn + v when v in [:nan, :NaN, :infinity, :neg_infinity] -> :nan + v when is_number(v) -> v + _ -> :nan + end) + + if Enum.any?(vals, &(&1 == :nan)) do + :nan + else + y = Enum.at(vals, 0, 0) + year = if y >= 0 and y <= 99, do: 1900 + trunc(y), else: trunc(y) + + {:ok, + {year, trunc(Enum.at(vals, 1, 0)) + 1, trunc(Enum.at(vals, 2, 1)), + trunc(Enum.at(vals, 3, 0)), trunc(Enum.at(vals, 4, 0)), trunc(Enum.at(vals, 5, 0)), + trunc(Enum.at(vals, 6, 0))}} + end + end + + defp utc_ms({year, month, day, hour, minute, second, ms_part}) do + year = year + div(month - 1, 12) + month = rem(rem(month - 1, 12) + 12, 12) + 1 + + case make_day(year, month) do + :nan -> + :nan + + base_days -> + day_f = (day - 1 + base_days) * 1.0 + + time_ms = + ((day_f * 24 + hour * 1.0) * 60 + minute * 1.0) * 60_000 + + second * 1000.0 + ms_part * 1.0 + + time_ms = trunc(time_ms) + if abs(time_ms) > 8_640_000_000_000_000, do: :nan, else: time_ms + end + end + + defp make_day(year, month) when year >= 0 do + :calendar.date_to_gregorian_days(year, month, 1) - 719_528 + rescue + _ -> :nan + end + + defp make_day(year, month) do + y = if month <= 2, do: year - 1, else: year + era = div(y - 399, 400) + yoe = y - era * 400 + doy = div(153 * (month + if(month > 2, do: -3, else: 9)) + 2, 5) + doe = yoe * 365 + div(yoe, 4) - div(yoe, 100) + doy + era * 146_097 + doe - 719_468 + end + + # ── Date.parse ── + + def parse_date_string(s) when is_binary(s) do + s = String.trim(s) + if s == "", do: :nan, else: do_parse(s) + end + + def parse_date_string(_), do: :nan + + defp do_parse(s) do + s_expanded = expand_short_iso(s) + has_explicit_tz = String.contains?(s, "Z") or has_tz_suffix?(s) + has_time = String.contains?(s_expanded, "T") + + with :miss <- try_rfc3339(s_expanded, has_explicit_tz, has_time), + :miss <- try_iso_date(s), + :miss <- try_informal(s), + :miss <- try_partial(s) do + :nan + end + end + + defp has_tz_suffix?(s) when byte_size(s) >= 6, + do: String.at(s, -6) in ["+", "-"] and String.at(s, -3) == ":" + + defp has_tz_suffix?(_), do: false + + defp try_rfc3339(s, has_explicit_tz, has_time) do + with_tz = + cond do + String.contains?(s, "Z") or has_tz_suffix?(s) -> s + String.contains?(s, "T") -> s <> "Z" + true -> s + end + + case safe_rfc3339_parse(with_tz) do + {:ok, ms} -> + if has_time and not has_explicit_tz, + do: ms + tz_offset_minutes() * 60_000, + else: ms + + :error -> + :miss + end + end + + defp safe_rfc3339_parse(s) do + us = :calendar.rfc3339_to_system_time(String.to_charlist(s), unit: :microsecond) + {:ok, div(us, 1000)} + rescue + _ -> :error + catch + _, _ -> :error + end + + defp try_iso_date(s) do + case Date.from_iso8601(s) do + {:ok, d} -> utc_ms({d.year, d.month, d.day, 0, 0, 0, 0}) + _ -> :miss + end + end + + defp try_partial(s) do + {sign, digits, has_sign} = + case s do + "+" <> r -> {1, r, true} + "-" <> r -> {-1, r, true} + r -> {1, r, false} + end + + valid_year_len? = &(byte_size(&1) == 4 or (byte_size(&1) == 6 and has_sign)) + + case String.split(digits, "-", parts: 3) do + [y] -> + if valid_year_len?.(y) do + case Integer.parse(y) do + {year, ""} -> utc_ms({sign * year, 1, 1, 0, 0, 0, 0}) + _ -> :miss + end + else + :miss + end + + [y, m] -> + if valid_year_len?.(y) do + with {year, ""} <- Integer.parse(y), + {month, ""} <- Integer.parse(m), + do: utc_ms({sign * year, month, 1, 0, 0, 0, 0}), + else: (_ -> :miss) + else + :miss + end + + _ -> + :miss + end + end + + # ── Informal date parsing ── + + @month_names %{ + "jan" => 1, + "feb" => 2, + "mar" => 3, + "apr" => 4, + "may" => 5, + "jun" => 6, + "jul" => 7, + "aug" => 8, + "sep" => 9, + "oct" => 10, + "nov" => 11, + "dec" => 12 + } + + @day_names ~w(sun mon tue wed thu fri sat) + + defp try_informal(s) do + s = strip_day_name(String.trim(s)) + + case String.split(s, " ", parts: 4) do + [a, b, c | rest] -> + time_tz = String.trim(Enum.join(rest, " ")) + + result = + if byte_size(a) == 4, do: parse_ymd(a, b, c), else: parse_mdy(a, b, c) + + case result do + {:ok, year, month, day} -> + {hour, minute, second, tz_offset} = parse_informal_time(time_tz) + + if tz_offset != nil do + utc_ms({year, month, day, hour, minute, second, 0}) - tz_offset * 60_000 + else + local_from_components([year, month - 1, day, hour, minute, second, 0]) + end + + :miss -> + :miss + end + + _ -> + :miss + end + end + + defp strip_day_name(s) do + case String.split(s, " ", parts: 2) do + [w, rest] -> + if String.downcase(String.slice(w, 0..2)) in @day_names, do: rest, else: s + + _ -> + s + end + end + + defp parse_ymd(year_str, month_str, day_str) do + with {year, ""} <- Integer.parse(year_str), + month when is_integer(month) <- + Map.get(@month_names, String.downcase(String.slice(month_str, 0..2))), + {day, ""} <- Integer.parse(day_str) do + {:ok, year, month, day} + else + _ -> :miss + end + end + + defp parse_mdy(month_str, day_str, year_str) do + with month when is_integer(month) <- + Map.get(@month_names, String.downcase(String.slice(month_str, 0..2))), + {day, ""} <- Integer.parse(day_str), + {year, ""} <- Integer.parse(year_str) do + {:ok, year, month, day} + else + _ -> :miss + end + end + + defp parse_informal_time(""), do: {0, 0, 0, nil} + + defp parse_informal_time(s) do + parts = String.split(s, " ") + {time_part, rest} = List.pop_at(parts, 0, "") + + {ampm, tz_parts} = + case rest do + [p | r] when p in ~w(AM PM am pm) -> {String.downcase(p), r} + r -> {nil, r} + end + + {h, m, sec} = + case String.split(time_part, ":") do + [hh, mm, ss] -> {String.to_integer(hh), String.to_integer(mm), String.to_integer(ss)} + [hh, mm] -> {String.to_integer(hh), String.to_integer(mm), 0} + _ -> {0, 0, 0} + end + + h = + case ampm do + "am" -> if h == 12, do: 0, else: h + "pm" -> if h == 12, do: 12, else: h + 12 + nil -> h + end + + tz_str = String.trim(Enum.join(tz_parts, " ")) + {h, m, sec, if(tz_str == "", do: nil, else: parse_tz_offset(tz_str))} + end + + defp parse_tz_offset(""), do: 0 + defp parse_tz_offset("Z"), do: 0 + defp parse_tz_offset("GMT" <> rest), do: parse_tz_offset(rest) + defp parse_tz_offset("UTC" <> rest), do: parse_tz_offset(rest) + defp parse_tz_offset("+" <> o), do: parse_tz_minutes(o) + defp parse_tz_offset("-" <> o), do: -parse_tz_minutes(o) + defp parse_tz_offset(_), do: 0 + + defp parse_tz_minutes(<>), + do: String.to_integer(h) * 60 + String.to_integer(m) + + defp parse_tz_minutes(s) do + case Integer.parse(s) do + {n, ""} -> n * 60 + _ -> 0 + end + end + + # ── ISO helpers ── + + defp expand_short_iso(<>) + when y1 in ?0..?9 and y2 in ?0..?9 and y3 in ?0..?9 and y4 in ?0..?9, + do: pad_seconds(<>) + + defp expand_short_iso(<>) + when y1 in ?0..?9 and y2 in ?0..?9 and y3 in ?0..?9 and y4 in ?0..?9 and + m1 in ?0..?9 and m2 in ?0..?9, + do: pad_seconds(<>) + + defp expand_short_iso(s), do: pad_seconds(s) + + defp pad_seconds(s) do + case String.split(s, "T", parts: 2) do + [date, time] -> + {time_part, tz} = split_time_tz(time) + + padded = + case String.split(time_part, ":") do + [h, m] -> h <> ":" <> m <> ":00" + _ -> time_part + end + + date <> "T" <> padded <> tz + + _ -> + s + end + end + + defp split_time_tz(time) do + cond do + String.ends_with?(time, "Z") -> String.split_at(time, -1) + byte_size(time) >= 6 and String.at(time, -6) in ["+", "-"] -> String.split_at(time, -6) + true -> {time, ""} + end + end +end diff --git a/lib/quickbeam/vm/runtime/errors.ex b/lib/quickbeam/vm/runtime/errors.ex new file mode 100644 index 00000000..b7fb069a --- /dev/null +++ b/lib/quickbeam/vm/runtime/errors.ex @@ -0,0 +1,80 @@ +defmodule QuickBEAM.VM.Runtime.Errors do + @moduledoc false + + import QuickBEAM.VM.Builtin, only: [build_methods: 1] + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Runtime + alias QuickBEAM.VM.Stacktrace + + @error_types ~w(Error TypeError RangeError SyntaxError ReferenceError URIError EvalError) + + def bindings do + error_proto_ref = make_ref() + error_ctor = {:builtin, "Error", fn args, _this -> error_constructor("Error", args) end} + + Heap.put_obj( + error_proto_ref, + build_methods do + val("name", "Error") + val("message", "") + val("constructor", error_ctor) + end + ) + + Heap.put_class_proto(error_ctor, {:obj, error_proto_ref}) + Heap.put_ctor_static(error_ctor, "prototype", {:obj, error_proto_ref}) + + Heap.put_ctor_static( + error_ctor, + "captureStackTrace", + {:builtin, "captureStackTrace", + fn + [], _ -> + throw({:js_throw, Heap.make_error("Cannot convert undefined to object", "TypeError")}) + + [obj | rest], _ -> + filter_fun = List.first(rest) + + case obj do + {:obj, _} -> Stacktrace.attach_stack(obj, filter_fun) + _ -> :ok + end + + :undefined + end} + ) + + Heap.put_ctor_static(error_ctor, "prepareStackTrace", :undefined) + Heap.put_ctor_static(error_ctor, "stackTraceLimit", 10) + + derived = + for name <- Enum.reject(@error_types, &(&1 == "Error")), into: %{} do + proto_ref = make_ref() + ctor = {:builtin, name, fn args, _this -> error_constructor(name, args) end} + + Heap.put_obj( + proto_ref, + build_methods do + val("__proto__", {:obj, error_proto_ref}) + val("name", name) + val("message", "") + val("constructor", ctor) + end + ) + + Heap.put_class_proto(ctor, {:obj, proto_ref}) + Heap.put_ctor_static(ctor, "prototype", {:obj, proto_ref}) + Heap.put_ctor_static(ctor, "__proto__", error_ctor) + {name, ctor} + end + + Map.put(derived, "Error", error_ctor) + end + + defp error_constructor(name, args) do + msg = List.first(args, "") + error = Heap.make_error(Runtime.stringify(msg), name) + Stacktrace.attach_stack(error) + end +end diff --git a/lib/quickbeam/vm/runtime/function.ex b/lib/quickbeam/vm/runtime/function.ex new file mode 100644 index 00000000..79a1cd2e --- /dev/null +++ b/lib/quickbeam/vm/runtime/function.ex @@ -0,0 +1,106 @@ +defmodule QuickBEAM.VM.Runtime.Function do + @moduledoc false + alias QuickBEAM.VM.{Builtin, Bytecode, Heap, Invocation} + + # ── Function prototype ── + + def proto_property(fun, "call") do + {:builtin, "call", fn args, this -> fn_call(fun, args, this) end} + end + + def proto_property(fun, "apply") do + {:builtin, "apply", fn args, this -> fn_apply(fun, args, this) end} + end + + def proto_property(fun, "bind") do + {:builtin, "bind", fn args, this -> fn_bind(fun, args, this) end} + end + + def proto_property(%Bytecode.Function{} = f, "name"), do: f.name || "" + def proto_property(%Bytecode.Function{} = f, "length"), do: f.defined_arg_count + def proto_property(%Bytecode.Function{} = f, "fileName"), do: f.filename || "" + def proto_property(%Bytecode.Function{} = f, "lineNumber"), do: f.line_num + def proto_property(%Bytecode.Function{} = f, "columnNumber"), do: f.col_num + + def proto_property({:closure, _, %Bytecode.Function{} = f}, "name"), + do: f.name || "" + + def proto_property({:closure, _, %Bytecode.Function{} = f}, "length"), + do: f.defined_arg_count + + def proto_property({:closure, _, %Bytecode.Function{} = f}, "fileName"), do: f.filename || "" + def proto_property({:closure, _, %Bytecode.Function{} = f}, "lineNumber"), do: f.line_num + def proto_property({:closure, _, %Bytecode.Function{} = f}, "columnNumber"), do: f.col_num + + def proto_property({:bound, _, inner, _, _}, key) when key not in ["length", "name"], + do: proto_property(inner, key) + + def proto_property({:bound, len, _, _, _}, "length"), do: len + def proto_property(_fun, "length"), do: 0 + def proto_property({:bound, _, {:builtin, name, _}, _, _}, "name"), do: name + def proto_property(_fun, "name"), do: "" + def proto_property(_fun, _), do: :undefined + + defp fn_call(fun, [this_arg | args], _this) do + invoke_fun(fun, args, this_arg) + end + + defp fn_apply(fun, [this_arg | rest], _this) do + args_array = List.first(rest) + + args = + case args_array do + {:obj, ref} -> + case Heap.get_obj(ref, []) do + {:qb_arr, arr} -> :array.to_list(arr) + list when is_list(list) -> list + _ -> [] + end + + {:qb_arr, arr} -> + :array.to_list(arr) + + list when is_list(list) -> + list + + _ -> + [] + end + + invoke_fun(fun, args, this_arg) + end + + defp fn_bind(fun, [this_arg | bound_args], _this) do + orig_len = + case fun do + %Bytecode.Function{defined_arg_count: n} -> n + {:closure, _, %Bytecode.Function{defined_arg_count: n}} -> n + _ -> 0 + end + + orig_name = + case fun do + %Bytecode.Function{name: n} when is_binary(n) -> n + {:closure, _, %Bytecode.Function{name: n}} when is_binary(n) -> n + {:builtin, n, _} -> n + _ -> "" + end + + bound_len = max(0, orig_len - length(bound_args)) + bound_fn = fn args, _this2 -> invoke_fun(fun, bound_args ++ args, this_arg) end + {:bound, bound_len, {:builtin, "bound " <> orig_name, bound_fn}, fun, bound_args} + end + + defp invoke_fun(fun, args, this_arg) do + case fun do + %Bytecode.Function{} -> + Invocation.invoke_with_receiver(fun, args, this_arg) + + {:closure, _, %Bytecode.Function{}} -> + Invocation.invoke_with_receiver(fun, args, this_arg) + + other -> + Builtin.call(other, args, this_arg) + end + end +end diff --git a/lib/quickbeam/vm/runtime/global_numeric.ex b/lib/quickbeam/vm/runtime/global_numeric.ex new file mode 100644 index 00000000..a7ef6e20 --- /dev/null +++ b/lib/quickbeam/vm/runtime/global_numeric.ex @@ -0,0 +1,88 @@ +defmodule QuickBEAM.VM.Runtime.GlobalNumeric do + @moduledoc false + + def parse_int([string, radix | _], _) when is_binary(string) and is_number(radix) do + base = trunc(radix) + string = String.trim_leading(string) + + if base == 0 or base == 10 do + parse_int([string], nil) + else + cond do + base == 16 -> + string = string |> String.replace_prefix("0x", "") |> String.replace_prefix("0X", "") + + case Integer.parse(string, 16) do + {n, _} -> n + :error -> :nan + end + + base in 2..36 -> + case Integer.parse(string, base) do + {n, _} -> n + :error -> :nan + end + + true -> + :nan + end + end + end + + def parse_int([string | _], _) when is_binary(string) do + string = String.trim_leading(string) + + if String.starts_with?(string, "0x") or String.starts_with?(string, "0X") do + case Integer.parse(binary_part(string, 2, byte_size(string) - 2), 16) do + {n, _} -> n + :error -> :nan + end + else + case Integer.parse(string) do + {n, _} -> n + :error -> :nan + end + end + end + + def parse_int([n | _], _) when is_number(n), do: trunc(n) + def parse_int(_, _), do: :nan + + def parse_float([string | _], _) when is_binary(string) do + string = String.trim(string) + + cond do + String.starts_with?(string, "Infinity") or String.starts_with?(string, "+Infinity") -> + :infinity + + String.starts_with?(string, "-Infinity") -> + :neg_infinity + + true -> + case Float.parse(string) do + {n, _} -> n + :error -> :nan + end + end + end + + def parse_float([n | _], _) when is_number(n), do: n * 1.0 + def parse_float(_, _), do: :nan + + def nan?([:nan | _], _), do: true + def nan?([n | _], _) when is_number(n), do: false + + def nan?([string | _], _) when is_binary(string) do + case Float.parse(String.trim_leading(string)) do + {_, _} -> false + :error -> true + end + end + + def nan?(_, _), do: true + + def finite?([n | _], _) when is_number(n), do: true + def finite?([:infinity | _], _), do: false + def finite?([:neg_infinity | _], _), do: false + def finite?(_, _), do: false +end diff --git a/lib/quickbeam/vm/runtime/globals.ex b/lib/quickbeam/vm/runtime/globals.ex new file mode 100644 index 00000000..ba529013 --- /dev/null +++ b/lib/quickbeam/vm/runtime/globals.ex @@ -0,0 +1,157 @@ +defmodule QuickBEAM.VM.Runtime.Globals do + @moduledoc "JS global scope: constructors, global functions, and the binding map." + + import QuickBEAM.VM.Builtin, only: [build_object: 1] + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Runtime + + alias QuickBEAM.VM.Runtime.{ + ArrayBuffer, + Boolean, + Console, + Errors, + GlobalNumeric, + JSON, + Math, + Object, + PromiseBuiltins, + Reflect, + Symbol, + TypedArray + } + + alias QuickBEAM.VM.Runtime.Date, as: JSDate + alias QuickBEAM.VM.Runtime.Globals.{Constructors, Functions} + alias QuickBEAM.VM.Runtime.Map, as: JSMap + alias QuickBEAM.VM.Runtime.Set, as: JSSet + + def build do + obj_proto = ensure_object_prototype() + obj_ctor = register("Object", &Constructors.object/2, prototype: obj_proto) + + bindings() + |> Map.put("Object", obj_ctor) + |> Map.merge(typed_arrays()) + |> Map.merge(Errors.bindings()) + |> tap(&Heap.put_global_cache/1) + end + + # ── Binding map ── + + defp bindings do + %{ + "Array" => register("Array", &Constructors.array/2), + "String" => register("String", &Constructors.string/2), + "Number" => register("Number", &Constructors.number/2), + "BigInt" => register("BigInt", &Constructors.bigint/2), + "Boolean" => register("Boolean", Boolean.constructor()), + "Function" => register("Function", &Constructors.function/2), + "RegExp" => register("RegExp", &Constructors.regexp/2), + "Date" => register("Date", &JSDate.constructor/2, module: JSDate), + "Promise" => + register("Promise", PromiseBuiltins.constructor(), + module: PromiseBuiltins, + prototype: PromiseBuiltins.prototype() + ), + "Symbol" => register("Symbol", Symbol.constructor(), module: Symbol), + "Map" => register("Map", JSMap.constructor()), + "Set" => register("Set", JSSet.constructor()), + "WeakMap" => register("WeakMap", JSMap.weak_constructor()), + "WeakSet" => register("WeakSet", JSSet.weak_constructor()), + "WeakRef" => register("WeakRef", fn _, _ -> Runtime.new_object() end), + "FinalizationRegistry" => + register("FinalizationRegistry", &Constructors.finalization_registry/2), + "DataView" => register("DataView", fn _, _ -> Runtime.new_object() end), + "ArrayBuffer" => + ( + ab_ctor = register("ArrayBuffer", &ArrayBuffer.constructor/2) + + Heap.put_ctor_static( + ab_ctor, + {:symbol, "Symbol.species"}, + {:accessor, {:builtin, "get [Symbol.species]", fn _, _ -> ab_ctor end}, nil} + ) + + ab_ctor + ), + "Proxy" => register("Proxy", &Constructors.proxy/2), + "Math" => Math.object(), + "JSON" => JSON.object(), + "Reflect" => Reflect.object(), + "console" => Console.object(), + "parseInt" => builtin("parseInt", &GlobalNumeric.parse_int/2), + "parseFloat" => builtin("parseFloat", &GlobalNumeric.parse_float/2), + "isNaN" => builtin("isNaN", &GlobalNumeric.nan?/2), + "isFinite" => builtin("isFinite", &GlobalNumeric.finite?/2), + "eval" => builtin("eval", &Functions.js_eval/2), + "require" => builtin("require", &Functions.js_require/2), + "structuredClone" => builtin("structuredClone", fn [val | _], _ -> val end), + "queueMicrotask" => builtin("queueMicrotask", &Functions.queue_microtask/2), + "gc" => builtin("gc", fn _, _ -> :undefined end), + "os" => Heap.wrap(%{"platform" => "elixir"}), + "qjs" => + build_object do + method "getStringKind" do + s = hd(args) + if is_binary(s) and byte_size(s) > 256, do: 1, else: 0 + end + end, + "globalThis" => Runtime.new_object(), + "NaN" => :nan, + "Infinity" => :infinity, + "undefined" => :undefined + } + end + + # ── Registration helpers ── + + defp builtin(name, fun), do: {:builtin, name, fun} + + defp register(name, constructor, opts \\ []) do + ctor = {:builtin, name, constructor} + + case Keyword.get(opts, :module) do + nil -> :ok + mod -> Heap.put_ctor_static(ctor, :__module__, mod) + end + + case Keyword.get(opts, :prototype) do + nil -> + :ok + + proto -> + Heap.put_class_proto(ctor, proto) + Heap.put_ctor_static(ctor, "prototype", proto) + end + + ctor + end + + defp ensure_object_prototype do + case Heap.get_object_prototype() do + nil -> Object.build_prototype() + existing -> existing + end + end + + defp typed_arrays do + ta_base = + {:builtin, "TypedArray", + fn _args, _this -> + throw( + {:js_throw, Heap.make_error("Abstract class TypedArray cannot be called", "TypeError")} + ) + end} + + ta_base_ref = make_ref() + Heap.put_obj(ta_base_ref, %{"__proto__" => nil}) + Heap.put_ctor_static(ta_base, "prototype", {:obj, ta_base_ref}) + + for {name, type} <- TypedArray.types(), into: %{} do + ctor = register(name, TypedArray.constructor(type)) + Heap.put_ctor_static(ctor, "__proto__", ta_base) + {name, ctor} + end + end +end diff --git a/lib/quickbeam/vm/runtime/globals/constructors.ex b/lib/quickbeam/vm/runtime/globals/constructors.ex new file mode 100644 index 00000000..b2052bcf --- /dev/null +++ b/lib/quickbeam/vm/runtime/globals/constructors.ex @@ -0,0 +1,157 @@ +defmodule QuickBEAM.VM.Runtime.Globals.Constructors do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + import QuickBEAM.VM.Builtin, only: [build_object: 1] + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.Runtime + + def object([arg | _], _) do + case arg do + {:symbol, _, _} = symbol -> + ref = make_ref() + Heap.put_obj(ref, %{"__wrapped_symbol__" => symbol}) + {:obj, ref} + + {:obj, _} = obj -> + obj + + value when is_binary(value) -> + ref = make_ref() + Heap.put_obj(ref, %{"__wrapped_string__" => value}) + {:obj, ref} + + value when is_number(value) -> + ref = make_ref() + Heap.put_obj(ref, %{"__wrapped_number__" => value}) + {:obj, ref} + + value when is_boolean(value) -> + ref = make_ref() + Heap.put_obj(ref, %{"__wrapped_boolean__" => value}) + {:obj, ref} + + _ -> + Runtime.new_object() + end + end + + def object(_, _), do: Runtime.new_object() + + def array(args, _) do + list = + case args do + [n] when is_integer(n) and n >= 0 -> List.duplicate(:undefined, n) + _ -> args + end + + Heap.wrap(list) + end + + def string(args, _), do: Runtime.stringify(List.first(args, "")) + def number(args, _), do: Runtime.to_number(List.first(args, 0)) + + def function(args, _) do + ctx = Heap.get_ctx() + + if ctx && ctx.runtime_pid do + {params, body} = + case Enum.reverse(args) do + [body | param_parts] -> + {Enum.join(Enum.reverse(param_parts), ","), body} + + [] -> + {"", ""} + end + + code = "(function(" <> params <> "){" <> body <> "})" + + case QuickBEAM.Runtime.compile(ctx.runtime_pid, code) do + {:ok, bytecode} -> + case Bytecode.decode(bytecode) do + {:ok, parsed} -> + case Interpreter.eval( + parsed.value, + [], + %{gas: Runtime.gas_budget(), runtime_pid: ctx.runtime_pid}, + parsed.atoms + ) do + {:ok, value} -> value + _ -> throw({:js_throw, Heap.make_error("Invalid function", "SyntaxError")}) + end + + _ -> + throw({:js_throw, Heap.make_error("Invalid function", "SyntaxError")}) + end + + _ -> + throw({:js_throw, Heap.make_error("Invalid function", "SyntaxError")}) + end + else + throw({:js_throw, Heap.make_error("Function constructor requires runtime", "Error")}) + end + end + + def bigint([n | _], _) when is_integer(n), do: {:bigint, n} + def bigint([{:bigint, n} | _], _), do: {:bigint, n} + + def bigint([string | _], _) when is_binary(string) do + case Integer.parse(string) do + {n, ""} -> {:bigint, n} + _ -> throw({:js_throw, Heap.make_error("Cannot convert to BigInt", "SyntaxError")}) + end + end + + def bigint(_, _) do + throw({:js_throw, Heap.make_error("Cannot convert to BigInt", "TypeError")}) + end + + def regexp([pattern | rest], _) do + flags = + case rest do + [flag | _] when is_binary(flag) -> flag + _ -> "" + end + + source = + case pattern do + {:regexp, value, _} -> value + value when is_binary(value) -> value + _ -> "" + end + + {:regexp, source, flags} + end + + def proxy([target, handler | _], _) do + Heap.wrap(%{proxy_target() => target, proxy_handler() => handler}) + end + + def proxy(_, _), do: Runtime.new_object() + + def finalization_registry([_callback | _], _) do + build_object do + method "register" do + :undefined + end + + method "unregister" do + :undefined + end + end + end + + def finalization_registry(_, _) do + build_object do + method "register" do + :undefined + end + + method "unregister" do + :undefined + end + end + end +end diff --git a/lib/quickbeam/vm/runtime/globals/functions.ex b/lib/quickbeam/vm/runtime/globals/functions.ex new file mode 100644 index 00000000..1b6d45d2 --- /dev/null +++ b/lib/quickbeam/vm/runtime/globals/functions.ex @@ -0,0 +1,44 @@ +defmodule QuickBEAM.VM.Runtime.Globals.Functions do + @moduledoc false + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.Runtime + + def js_eval([code | _], _) when is_binary(code) do + ctx = Heap.get_ctx() + + with %{runtime_pid: pid} when pid != nil <- ctx, + {:ok, bytecode} <- QuickBEAM.Runtime.compile(pid, code), + {:ok, parsed} <- Bytecode.decode(bytecode), + {:ok, value} <- + Interpreter.eval( + parsed.value, + [], + %{gas: Runtime.gas_budget(), runtime_pid: pid}, + parsed.atoms + ) do + value + else + %{runtime_pid: nil} -> :undefined + nil -> :undefined + {:error, %{message: msg}} -> throw({:js_throw, Heap.make_error(msg, "SyntaxError")}) + {:error, msg} when is_binary(msg) -> throw({:js_throw, Heap.make_error(msg, "SyntaxError")}) + _ -> :undefined + end + end + + def js_eval(_, _), do: :undefined + + def js_require([name | _], _) do + case Heap.get_module(name) do + nil -> throw({:js_throw, Heap.make_error("Cannot find module '#{name}'", "Error")}) + exports -> exports + end + end + + def queue_microtask([callback | _], _) do + Heap.enqueue_microtask({:resolve, nil, callback, :undefined}) + :undefined + end +end diff --git a/lib/quickbeam/vm/runtime/json.ex b/lib/quickbeam/vm/runtime/json.ex new file mode 100644 index 00000000..9b9a457f --- /dev/null +++ b/lib/quickbeam/vm/runtime/json.ex @@ -0,0 +1,211 @@ +defmodule QuickBEAM.VM.Runtime.JSON do + @moduledoc "JSON.parse and JSON.stringify." + + use QuickBEAM.VM.Builtin + + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.Bytecode + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.ObjectModel.Get + alias QuickBEAM.VM.Runtime + + js_object "JSON" do + method "parse" do + parse(args) + end + + method "stringify" do + stringify(args) + end + end + + defp parse([s | _]) when is_binary(s) do + decoded = + try do + :json.decode(s) + rescue + _ -> throw({:js_throw, Heap.make_error("Unexpected end of JSON input", "SyntaxError")}) + catch + _, _ -> throw({:js_throw, Heap.make_error("Unexpected end of JSON input", "SyntaxError")}) + end + + to_js_root(decoded, s) + end + + defp parse(_), + do: throw({:js_throw, Heap.make_error("Unexpected end of JSON input", "SyntaxError")}) + + defp to_js_root(val, json_str) when is_map(val) do + keys = + case Jason.decode(json_str, objects: :ordered_objects) do + {:ok, %Jason.OrderedObject{values: pairs}} -> + pairs |> Enum.map(&elem(&1, 0)) |> Enum.reverse() + + _ -> + Map.keys(val) |> Enum.reverse() + end + + to_js(val, keys) + end + + defp to_js_root(val, _) when is_list(val), do: Enum.map(val, &to_js/1) + defp to_js_root(val, _), do: to_js(val) + + defp to_js(nil), do: nil + defp to_js(:null), do: nil + defp to_js(val) when is_map(val), do: to_js(val, nil) + defp to_js(val) when is_list(val), do: Enum.map(val, &to_js/1) + defp to_js(val), do: val + + defp to_js(val, key_order) when is_map(val) do + ref = make_ref() + map = Map.new(val, fn {k, v} -> {k, to_js(v, nil)} end) + order = key_order || Map.keys(val) |> Enum.reverse() + Heap.put_obj(ref, Map.put(map, key_order(), order)) + {:obj, ref} + end + + defp to_js(val, _) when is_list(val), do: Enum.map(val, &to_js/1) + defp to_js(val, _), do: to_js(val) + + defp stringify([val | rest]) do + if val == :undefined do + :undefined + else + replacer = Enum.at(rest, 0) + space = Enum.at(rest, 1) + + try do + result = to_json(val) + if result == :undefined, do: :undefined, else: encode(result, replacer, space) + rescue + _ -> :undefined + end + end + end + + defp stringify([]), do: :undefined + + defp encode(result, replacer, space) do + result = apply_replacer(result, replacer) + elixir_val = to_elixir(result) + + opts = + case space do + n when is_integer(n) and n > 0 -> [pretty: [indent: String.duplicate(" ", min(n, 10))]] + s when is_binary(s) and s != "" -> [pretty: [indent: String.slice(s, 0, 10)]] + _ -> [] + end + + case Jason.encode(elixir_val, opts) do + {:ok, json} -> json + _ -> :undefined + end + end + + defp to_elixir({:ordered_map, pairs}) do + Jason.OrderedObject.new(Enum.map(pairs, fn {k, v} -> {k, to_elixir(v)} end)) + end + + defp to_elixir(list) when is_list(list), do: Enum.map(list, &to_elixir/1) + defp to_elixir(:null), do: nil + defp to_elixir(val), do: val + + defp apply_replacer({:ordered_map, pairs}, {:obj, ref}) do + allowed = Heap.to_list({:obj, ref}) + + if allowed != [] and Enum.all?(allowed, &is_binary/1) do + {:ordered_map, Enum.filter(pairs, fn {k, _} -> k in allowed end)} + else + {:ordered_map, pairs} + end + end + + defp apply_replacer({:ordered_map, pairs}, replacer) + when replacer != nil and replacer != :undefined do + filtered = + Enum.reduce(pairs, [], fn {k, v}, acc -> + result = Runtime.call_callback(replacer, [k, v]) + if result == :undefined, do: acc, else: [{k, result} | acc] + end) + + {:ordered_map, Enum.reverse(filtered)} + end + + defp apply_replacer(result, _), do: result + + defp to_json({:obj, ref} = obj) do + case Heap.get_obj(ref) do + nil -> + %{} + + {:qb_arr, arr} -> + :array.to_list(arr) |> Enum.map(&to_json/1) + + list when is_list(list) -> + Enum.map(list, &to_json/1) + + map when is_map(map) -> + case Map.get(map, "toJSON") do + fun when fun != nil and fun != :undefined -> + result = Runtime.call_callback(fun, []) + to_json(result) + + _ -> + order = + case Map.get(map, key_order()) do + {:qb_arr, arr} -> :array.to_list(arr) + list when is_list(list) -> Enum.reverse(list) + _ -> nil + end + + entries = + map + |> Map.drop([key_order()]) + |> Enum.reject(fn {k, v} -> v == :undefined or internal?(k) end) + + entries = + if order do + Enum.sort_by(entries, fn {k, _} -> + case Enum.find_index(order, &(&1 == k)) do + nil -> length(order) + idx -> idx + end + end) + else + entries + end + + pairs = + entries + |> Enum.map(fn {k, v} -> {to_string(k), to_json(resolve_value(v, obj))} end) + |> Enum.reject(fn {_, v} -> v == :undefined end) + + {:ordered_map, pairs} + end + end + end + + defp to_json(nil), do: :null + defp to_json(:undefined), do: :null + defp to_json({:closure, _, _}), do: :undefined + defp to_json(%Bytecode.Function{}), do: :undefined + defp to_json({:builtin, _, _}), do: :undefined + defp to_json({:bound, _, _, _, _}), do: :undefined + defp to_json(:nan), do: :null + defp to_json(:infinity), do: :null + defp to_json(list) when is_list(list), do: Enum.map(list, &to_json/1) + defp to_json({:accessor, _, _}), do: :undefined + defp to_json(val), do: val + + defp resolve_value({:accessor, getter, _}, obj) when getter != nil do + Get.call_getter(getter, obj) + rescue + _ -> :undefined + catch + _, _ -> :undefined + end + + defp resolve_value(val, _obj), do: val +end diff --git a/lib/quickbeam/vm/runtime/map.ex b/lib/quickbeam/vm/runtime/map.ex new file mode 100644 index 00000000..4065e3b2 --- /dev/null +++ b/lib/quickbeam/vm/runtime/map.ex @@ -0,0 +1,205 @@ +defmodule QuickBEAM.VM.Runtime.Map do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Runtime + + def constructor do + fn args, _this -> + ref = make_ref() + + entries = + case args do + [list] when is_list(list) -> + Map.new(list, fn [k, v] -> {k, v} end) + + [{:obj, r}] -> + stored = Heap.get_obj(r, []) + + if is_list(stored) or match?({:qb_arr, _}, stored) do + Map.new(stored, fn + [k, v] -> + {k, v} + + {:obj, eref} -> + case Heap.get_obj(eref, []) do + [k, v | _] -> {k, v} + _ -> {nil, nil} + end + + _ -> + {nil, nil} + end) + else + %{} + end + + _ -> + %{} + end + + Heap.put_obj(ref, %{ + map_data() => entries, + "size" => map_size(entries) + }) + + {:obj, ref} + end + end + + def weak_constructor do + fn args, _this -> + ref = make_ref() + + init = + case args do + [{:obj, _} = entries | _] -> + Heap.to_list(entries) + |> Enum.reduce(%{}, fn + {:obj, eref}, acc -> + case Heap.get_obj(eref, []) do + [k, v | _] -> + validate_weak_key!(k, "WeakMap") + Map.put(acc, k, v) + + _ -> + acc + end + + _, acc -> + acc + end) + + _ -> + %{} + end + + Heap.put_obj(ref, %{map_data() => init, "size" => map_size(init), :weak => true}) + {:obj, ref} + end + end + + def proto_property("get"), do: {:builtin, "get", &get/2} + def proto_property("set"), do: {:builtin, "set", &set/2} + def proto_property("has"), do: {:builtin, "has", &has/2} + def proto_property("delete"), do: {:builtin, "delete", &delete/2} + def proto_property("clear"), do: {:builtin, "clear", &clear/2} + def proto_property("keys"), do: {:builtin, "keys", &keys/2} + def proto_property("values"), do: {:builtin, "values", &values/2} + def proto_property("entries"), do: {:builtin, "entries", &entries/2} + def proto_property("forEach"), do: {:builtin, "forEach", &for_each/2} + + def proto_property("size") do + {:builtin, "size", + fn _, {:obj, ref} -> + Heap.get_obj(ref, %{}) + |> Map.get(map_data(), %{}) + |> map_size() + end} + end + + def proto_property(_), do: :undefined + + defp validate_weak_key!({:obj, _}, _), do: :ok + defp validate_weak_key!({:symbol, _, _}, _), do: :ok + + defp validate_weak_key!(_, kind) do + throw({:js_throw, Heap.make_error("invalid value used as #{kind} key", "TypeError")}) + end + + defp normalize_key(k) when is_float(k) and k == trunc(k), do: trunc(k) + defp normalize_key(k), do: k + + defp get([key | _], {:obj, ref}) do + data = Heap.get_obj(ref, %{}) |> Map.get(map_data(), %{}) + Map.get(data, normalize_key(key), :undefined) + end + + defp set([key, val | _], {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + if Map.get(obj, :weak), do: validate_weak_key!(key, "WeakMap") + key = normalize_key(key) + data = Map.get(obj, map_data(), %{}) + order = Map.get(obj, key_order(), []) + order = if Map.has_key?(data, key), do: order, else: [key | order] + new_data = Map.put(data, key, val) + + Heap.put_obj( + ref, + Map.merge(obj, %{ + map_data() => new_data, + "size" => map_size(new_data), + key_order() => order + }) + ) + + {:obj, ref} + end + + defp has([key | _], {:obj, ref}) do + data = Heap.get_obj(ref, %{}) |> Map.get(map_data(), %{}) + Map.has_key?(data, normalize_key(key)) + end + + defp delete([key | _], {:obj, ref}) do + key = normalize_key(key) + obj = Heap.get_obj(ref, %{}) + data = Map.get(obj, map_data(), %{}) + new_data = Map.delete(data, key) + order = Map.get(obj, key_order(), []) |> List.delete(key) + + Heap.put_obj( + ref, + Map.merge(obj, %{ + map_data() => new_data, + "size" => map_size(new_data), + key_order() => order + }) + ) + + true + end + + defp clear(_, {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + Heap.put_obj(ref, %{obj | map_data() => %{}, "size" => 0}) + :undefined + end + + defp keys(_, {:obj, ref}) do + order = Heap.get_obj(ref, %{}) |> Map.get(key_order(), []) |> Enum.reverse() + Heap.wrap(order) + end + + defp values(_, {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + data = Map.get(obj, map_data(), %{}) + order = Map.get(obj, key_order(), []) |> Enum.reverse() + Heap.wrap(Enum.map(order, &Map.get(data, &1))) + end + + defp entries(_, {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + data = Map.get(obj, map_data(), %{}) + order = Map.get(obj, key_order(), []) |> Enum.reverse() + items = Enum.map(order, fn key -> Heap.wrap([key, Map.get(data, key)]) end) + Heap.wrap(items) + end + + defp for_each([cb | _], {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + data = Map.get(obj, map_data(), %{}) + order = Map.get(obj, key_order(), []) |> Enum.reverse() + + Enum.each(order, fn key -> + case Map.fetch(data, key) do + {:ok, value} -> Runtime.call_callback(cb, [value, key, {:obj, ref}]) + :error -> :ok + end + end) + + :undefined + end +end diff --git a/lib/quickbeam/vm/runtime/math.ex b/lib/quickbeam/vm/runtime/math.ex new file mode 100644 index 00000000..2f5d5900 --- /dev/null +++ b/lib/quickbeam/vm/runtime/math.ex @@ -0,0 +1,286 @@ +defmodule QuickBEAM.VM.Runtime.Math do + @moduledoc false + + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter.Values + alias QuickBEAM.VM.Runtime + + js_object "Math" do + method "floor" do + floor(Runtime.to_float(hd(args))) + end + + method "ceil" do + ceil(Runtime.to_float(hd(args))) + end + + method "round" do + round(Runtime.to_float(hd(args))) + end + + method "abs" do + abs(hd(args)) + end + + method "max" do + case args do + [] -> :neg_infinity + _ -> Enum.max(args) + end + end + + method "min" do + case args do + [] -> :infinity + _ -> Enum.min(args) + end + end + + method "sqrt" do + :math.sqrt(Runtime.to_float(hd(args))) + end + + method "pow" do + [a, b | _] = args + :math.pow(Runtime.to_float(a), Runtime.to_float(b)) + end + + method "random" do + :rand.uniform() + end + + method "trunc" do + trunc(Runtime.to_float(hd(args))) + end + + method "sign" do + a = hd(args) + + cond do + is_number(a) and a > 0 -> 1 + is_number(a) and a < 0 -> -1 + is_number(a) -> a + true -> :nan + end + end + + method "log" do + :math.log(Runtime.to_float(hd(args))) + end + + method "log2" do + :math.log2(Runtime.to_float(hd(args))) + end + + method "log10" do + :math.log10(Runtime.to_float(hd(args))) + end + + method "sin" do + :math.sin(Runtime.to_float(hd(args))) + end + + method "cos" do + :math.cos(Runtime.to_float(hd(args))) + end + + method "tan" do + :math.tan(Runtime.to_float(hd(args))) + end + + method "clz32" do + n = Values.to_uint32(hd(args)) + if n == 0, do: 32, else: 31 - trunc(:math.log2(n)) + end + + method "fround" do + f = Runtime.to_float(hd(args)) + <> = <> + f32 * 1.0 + end + + method "imul" do + [a, b | _] = args + + Values.to_int32( + Values.to_int32(a) * + Values.to_int32(b) + ) + end + + method "atan2" do + [a, b | _] = args + :math.atan2(Runtime.to_float(a), Runtime.to_float(b)) + end + + method "asin" do + :math.asin(Runtime.to_float(hd(args))) + end + + method "acos" do + :math.acos(Runtime.to_float(hd(args))) + end + + method "atan" do + :math.atan(Runtime.to_float(hd(args))) + end + + method "exp" do + :math.exp(Runtime.to_float(hd(args))) + end + + method "cbrt" do + f = Runtime.to_float(hd(args)) + sign = if f < 0, do: -1, else: 1 + sign * :math.pow(abs(f), 1.0 / 3.0) + end + + method "log1p" do + :math.log(1 + Runtime.to_float(hd(args))) + end + + method "expm1" do + :math.exp(Runtime.to_float(hd(args))) - 1 + end + + method "cosh" do + :math.cosh(Runtime.to_float(hd(args))) + end + + method "sinh" do + :math.sinh(Runtime.to_float(hd(args))) + end + + method "tanh" do + :math.tanh(Runtime.to_float(hd(args))) + end + + method "acosh" do + :math.acosh(Runtime.to_float(hd(args))) + end + + method "asinh" do + :math.asinh(Runtime.to_float(hd(args))) + end + + method "atanh" do + :math.atanh(Runtime.to_float(hd(args))) + end + + method "sumPrecise" do + list = + case hd(args) do + {:obj, ref} -> + data = Heap.get_obj(ref, []) + + case data do + {:qb_arr, arr} -> :array.to_list(arr) + l when is_list(l) -> l + _ -> [] + end + + {:qb_arr, arr} -> + :array.to_list(arr) + + l when is_list(l) -> + l + + _ -> + [] + end + + shewchuk_sum(list) + end + + method "hypot" do + sum = Enum.reduce(args, 0.0, fn a, acc -> acc + :math.pow(Runtime.to_float(a), 2) end) + :math.sqrt(sum) + end + + val("PI", :math.pi()) + val("E", :math.exp(1)) + val("LN2", :math.log(2)) + val("LN10", :math.log(10)) + val("LOG2E", :math.log2(:math.exp(1))) + val("LOG10E", :math.log10(:math.exp(1))) + val("SQRT2", :math.sqrt(2)) + val("SQRT1_2", :math.sqrt(2) / 2) + val("MAX_SAFE_INTEGER", 9_007_199_254_740_991) + val("MIN_SAFE_INTEGER", -9_007_199_254_740_991) + end + + defp shewchuk_sum(list) do + partials = + Enum.reduce(list, [], fn v, partials -> + x = Runtime.to_float(v) + grow(partials, x, []) + end) + + case partials do + [] -> + 0.0 + + [x] -> + x + + _ -> + partials = Enum.reverse(partials) + finalize_partials(partials) + end + end + + defp grow([], x, new_partials), do: if(x != 0.0, do: new_partials ++ [x], else: new_partials) + + defp grow([p | rest], x, new_partials) do + {hi, lo} = two_sum(x, p) + new_partials = if lo != 0.0, do: new_partials ++ [lo], else: new_partials + grow(rest, hi, new_partials) + end + + # CPython fsum-style finalization: detect halfway cases where + # remaining partials should break the tie + defp finalize_partials([]), do: 0.0 + defp finalize_partials([x]), do: x + + defp finalize_partials(partials) do + [hi | rest] = partials + {hi, lo, remaining} = fold_top(hi, rest) + + cond do + lo == 0.0 -> + hi + + remaining == [] -> + hi + lo + + true -> + [next | _] = remaining + # lo is the rounding error. If remaining partials have the same sign + # as lo, the true value is farther from hi than lo suggests — round away + if (lo > 0 and next > 0) or (lo < 0 and next < 0) do + # Adjust lo to break tie in favor of rounding away from hi + nudged = lo + lo + result = hi + nudged + if result == hi + lo, do: hi + lo, else: result + else + hi + lo + end + end + end + + defp fold_top(hi, []), do: {hi, 0.0, []} + + defp fold_top(hi, [lo | rest]) do + {s, t} = two_sum(hi, lo) + if t == 0.0, do: fold_top(s, rest), else: {s, t, rest} + end + + defp two_sum(a, b) do + s = a + b + v = s - a + t = a - (s - v) + (b - v) + {s, t} + end +end diff --git a/lib/quickbeam/vm/runtime/number.ex b/lib/quickbeam/vm/runtime/number.ex new file mode 100644 index 00000000..21c8cdf2 --- /dev/null +++ b/lib/quickbeam/vm/runtime/number.ex @@ -0,0 +1,298 @@ +defmodule QuickBEAM.VM.Runtime.Number do + @moduledoc false + + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Runtime + alias QuickBEAM.VM.Runtime.GlobalNumeric + + # ── Number.prototype ── + + proto "toString" do + to_string_with_radix(this, args) + end + + proto "toFixed" do + to_fixed(this, args) + end + + proto "valueOf" do + this + end + + proto "toExponential" do + to_exponential(this, args) + end + + proto "toPrecision" do + to_precision(this, args) + end + + # ── Number static ── + + static "isNaN" do + hd(args) == :nan + end + + static "isFinite" do + is_number(hd(args)) + end + + static "isInteger" do + is_integer(hd(args)) or (is_float(hd(args)) and hd(args) == Float.floor(hd(args))) + end + + static "parseInt" do + GlobalNumeric.parse_int(args, nil) + end + + static "parseFloat" do + GlobalNumeric.parse_float(args, nil) + end + + static_val("NaN", :nan) + static_val("POSITIVE_INFINITY", :infinity) + static_val("NEGATIVE_INFINITY", :neg_infinity) + static_val("MAX_SAFE_INTEGER", 9_007_199_254_740_991) + static_val("MIN_SAFE_INTEGER", -9_007_199_254_740_991) + static_val("EPSILON", 2.220446049250313e-16) + static_val("MIN_VALUE", 5.0e-324) + + # ── toString(radix) ── + + defp to_string_with_radix(n, [radix | _]) when is_number(n) do + r = Runtime.to_int(radix) + + cond do + r == 10 -> + Runtime.stringify(n) + + r >= 2 and r <= 36 and n == trunc(n) -> + Integer.to_string(trunc(n), r) |> String.downcase() + + r >= 2 and r <= 36 -> + format_float_with_runtime(n * 1.0, r) || float_to_radix(n * 1.0, r) + + true -> + Runtime.stringify(n) + end + end + + defp to_string_with_radix(n, _), do: Runtime.stringify(n) + + defp format_float_with_runtime(n, radix) do + case QuickBEAM.VM.Heap.get_ctx() do + %{runtime_pid: runtime_pid} when runtime_pid != nil -> + literal = :erlang.float_to_binary(n, [:short]) + + case QuickBEAM.Runtime.eval(runtime_pid, "(#{literal}).toString(#{radix})") do + {:ok, value} when is_binary(value) -> value + _ -> nil + end + + _ -> + nil + end + end + + defp float_to_radix(n, radix) do + {sign, n} = if n < 0, do: {"-", -n}, else: {"", n} + int_part = trunc(n) + frac_part = n - int_part + + int_str = + if int_part == 0, do: "0", else: Integer.to_string(int_part, radix) |> String.downcase() + + if frac_part == 0.0 do + sign <> int_str + else + precision = ceil(53 * :math.log(2) / :math.log(radix)) + digits = frac_digits_list(frac_part, radix, precision + 3) + digits = round_and_trim(digits, precision, radix, frac_part) + chars = Enum.map(digits, &String.at("0123456789abcdefghijklmnopqrstuvwxyz", &1)) + sign <> int_str <> "." <> Enum.join(chars) + end + end + + defp frac_digits_list(_frac, _radix, 0), do: [] + + defp frac_digits_list(frac, radix, remaining) do + prod = frac * radix + digit = trunc(prod) + rest = prod - digit + + if rest == 0.0 do + [digit] + else + [digit | frac_digits_list(rest, radix, remaining - 1)] + end + end + + defp round_and_trim(digits, precision, radix, original_frac) do + truncated = Enum.take(digits, precision) |> trim_trailing_zeros() + rounded = round_radix_digits(digits, precision, radix) |> trim_trailing_zeros() + + if truncated == rounded do + truncated + else + trunc_rt = digits_to_float_precise(truncated, radix) + round_rt = digits_to_float_precise(rounded, radix) + + trunc_exact = trunc_rt == original_frac + round_exact = round_rt == original_frac + + cond do + trunc_exact and not round_exact -> + truncated + + round_exact and not trunc_exact -> + rounded + + true -> + trunc_err = abs(trunc_rt - original_frac) + round_err = abs(round_rt - original_frac) + if round_err < trunc_err, do: rounded, else: truncated + end + end + end + + defp digits_to_float_precise(digits, radix) do + {num, denom} = + Enum.reduce(Enum.with_index(digits), {0, 1}, fn {d, i}, {n, _} -> + power = round(:math.pow(radix, i + 1)) + {n * radix + d, power} + end) + + num / denom + end + + defp round_radix_digits(digits, precision, _radix) when length(digits) <= precision do + digits + end + + defp round_radix_digits(digits, precision, radix) do + {keep, tail} = Enum.split(digits, precision) + + should_round_up = + case tail do + [d | _] when d >= div(radix, 2) + 1 -> + true + + [d | rest] when d == div(radix, 2) -> + Enum.any?(rest, &(&1 > 0)) or rem(List.last(keep, 0), 2) == 1 + + _ -> + false + end + + if should_round_up do + propagate_carry(keep, radix) + else + keep + end + end + + defp propagate_carry(digits, radix) do + {result, carry} = + digits + |> Enum.reverse() + |> Enum.map_reduce(1, fn d, carry -> + sum = d + carry + {rem(sum, radix), div(sum, radix)} + end) + + if carry > 0, do: [carry | result], else: result + end + + defp trim_trailing_zeros(digits) do + digits + |> Enum.reverse() + |> Enum.drop_while(&(&1 == 0)) + |> Enum.reverse() + end + + # ── toFixed(digits) ── + + defp to_fixed(:nan, _), do: "NaN" + defp to_fixed(:infinity, _), do: "Infinity" + defp to_fixed(:neg_infinity, _), do: "-Infinity" + + defp to_fixed(n, [digits | _]) when is_number(n) do + :erlang.float_to_binary(n * 1.0, decimals: max(0, Runtime.to_int(digits))) + end + + defp to_fixed(n, _), do: Runtime.stringify(n) + + # ── toExponential(digits) ── + + defp to_exponential(n, [digits | _]) when is_number(n) do + d = Runtime.to_int(digits) + f = js_round_significant(abs(n * 1.0), d + 1) + sign = if n < 0, do: "-", else: "" + sign <> (:erlang.float_to_binary(f, [{:scientific, d}]) |> strip_exponent_zeros()) + end + + defp to_exponential(n, _), do: Runtime.stringify(n) + + defp strip_exponent_zeros(s) do + case String.split(s, "e") do + [mantissa, exp_str] -> mantissa <> "e" <> format_exponent(String.to_integer(exp_str)) + _ -> s + end + end + + # ── toPrecision(precision) ── + + defp to_precision(n, [prec | _]) when is_number(n) do + p = max(1, Runtime.to_int(prec)) + f = n * 1.0 + + if f == 0.0 do + zero_precision(n < 0, p) + else + format_precision(f, p) + end + end + + defp to_precision(n, _), do: Runtime.stringify(n) + + defp zero_precision(negative?, p) do + prefix = if negative?, do: "-", else: "" + prefix <> "0" <> if(p > 1, do: "." <> String.duplicate("0", p - 1), else: "") + end + + defp format_precision(f, p) do + exp = trunc(:math.floor(:math.log10(abs(f)))) + sign = if f < 0, do: "-", else: "" + f = js_round_significant(abs(f), p) + + if exp >= p or exp < -6 do + sci = :erlang.float_to_binary(f, [{:scientific, p - 1}]) + + case String.split(sci, "e") do + [mantissa, exp_str] -> + sign <> mantissa <> "e" <> format_exponent(String.to_integer(exp_str)) + + _ -> + Runtime.stringify(f) + end + else + sign <> :erlang.float_to_binary(f, decimals: p - exp - 1) + end + end + + defp js_round_significant(f, p) do + if f == 0.0, do: 0.0, else: do_js_round_sig(f, p) + end + + defp do_js_round_sig(f, p) do + exp = :math.floor(:math.log10(f)) + factor = :math.pow(10, p - 1 - exp) + scaled = f * factor + rounded = :erlang.trunc(scaled + 0.5) + rounded / factor + end + + defp format_exponent(exp) when exp >= 0, do: "+" <> Integer.to_string(exp) + defp format_exponent(exp), do: Integer.to_string(exp) +end diff --git a/lib/quickbeam/vm/runtime/object.ex b/lib/quickbeam/vm/runtime/object.ex new file mode 100644 index 00000000..9bf2abc9 --- /dev/null +++ b/lib/quickbeam/vm/runtime/object.ex @@ -0,0 +1,624 @@ +defmodule QuickBEAM.VM.Runtime.Object do + @moduledoc "Object static methods." + + use QuickBEAM.VM.Builtin + + import QuickBEAM.VM.Heap.Keys + alias QuickBEAM.VM.Bytecode + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter.Values + alias QuickBEAM.VM.Runtime + alias QuickBEAM.VM.Runtime.TypedArray + + def build_prototype do + ref = make_ref() + + Heap.put_obj( + ref, + build_methods do + method "toString" do + "[object Object]" + end + + method "valueOf" do + this + end + + method "hasOwnProperty" do + has_own_property(args, this) + end + + method "isPrototypeOf" do + false + end + + method "propertyIsEnumerable" do + property_enumerable?(args, this) + end + end + ) + + proto = {:obj, ref} + + for key <- [ + "toString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "constructor" + ] do + Heap.put_prop_desc(ref, key, %{enumerable: false, configurable: true, writable: true}) + end + + Heap.put_object_prototype(proto) + proto + end + + defp has_own_property([key | _], {:obj, r}) do + data = Heap.get_obj(r, %{}) + is_map(data) and Map.has_key?(data, key) + end + + defp has_own_property(_, _), do: false + + defp property_enumerable?([key | _], {:obj, r}) do + not match?(%{enumerable: false}, Heap.get_prop_desc(r, key)) + end + + defp property_enumerable?(_, _), do: false + + static "keys" do + keys(args) + end + + static "values" do + values(args) + end + + static "entries" do + entries(args) + end + + static "assign" do + assign(args) + end + + static "freeze" do + case hd(args) do + {:obj, ref} = obj -> + Heap.freeze(ref) + obj + + obj -> + obj + end + end + + static "is" do + [a, b | _] = args + + cond do + is_number(a) and is_number(b) and a == 0 and b == 0 -> + Values.neg_zero?(a) == Values.neg_zero?(b) + + is_number(a) and is_number(b) -> + a === b + + a == :nan and b == :nan -> + true + + true -> + a === b + end + end + + static "create" do + case args do + [nil | _] -> Heap.wrap(%{}) + [proto | _] -> Heap.wrap(%{proto() => proto}) + _ -> Runtime.new_object() + end + end + + static "getPrototypeOf" do + case args do + [{:obj, ref} | _] -> + Map.get(Heap.get_obj(ref, %{}), proto(), nil) + + [{:qb_arr, _} | _] -> + func_proto() + + [val | _] when is_list(val) -> + Heap.get_class_proto(Runtime.global_bindings()["Array"]) + + [{:builtin, _, _} = b | _] -> + case Map.get(Heap.get_ctor_statics(b), "__proto__") do + nil -> func_proto() + parent -> parent + end + + [{:closure, _, _} = c | _] -> + case Map.get(Heap.get_ctor_statics(c), "__proto__") do + nil -> func_proto() + parent -> parent + end + + [%Bytecode.Function{} | _] -> + func_proto() + + [val | _] when is_function(val) -> + func_proto() + + _ -> + nil + end + end + + defp func_proto do + case Process.get(:qb_func_proto) do + nil -> + call_fn = + {:builtin, "call", + fn [this | args], _ -> + Runtime.call_callback(this, args) + end} + + apply_fn = + {:builtin, "apply", + fn [this, arg_array], _ -> + args = + case arg_array do + {:obj, r} -> Heap.obj_to_list(r) + _ -> [] + end + + Runtime.call_callback(this, args) + end} + + bind_fn = + {:builtin, "bind", + fn [this | bound_args], func -> + {:bound, "bound", func, this, bound_args} + end} + + proto = + build_object do + val("call", call_fn) + val("apply", apply_fn) + val("bind", bind_fn) + val("constructor", :undefined) + end + + Process.put(:qb_func_proto, proto) + proto + + existing -> + existing + end + end + + static "defineProperty" do + define_property(args) + end + + static "getOwnPropertyNames" do + get_own_property_names(args) + end + + static "getOwnPropertyDescriptor" do + get_own_property_descriptor(args) + end + + static "fromEntries" do + from_entries(args) + end + + static "getOwnPropertySymbols" do + case args do + [{:obj, ref} | _] -> + data = Heap.get_obj(ref, %{}) + + syms = + if is_map(data), do: Enum.filter(Map.keys(data), &match?({:symbol, _, _}, &1)), else: [] + + Heap.wrap(syms) + + _ -> + Heap.wrap([]) + end + end + + static "hasOwn" do + case args do + [{:obj, ref}, key | _] -> + prop_name = if is_binary(key), do: key, else: to_string(key) + map = Heap.get_obj(ref, %{}) + is_map(map) and Map.has_key?(map, prop_name) + + _ -> + false + end + end + + static "setPrototypeOf" do + case args do + [{:obj, ref} = obj, proto | _] -> + map = Heap.get_obj(ref, %{}) + if is_map(map), do: Heap.put_obj(ref, Map.put(map, proto(), proto)) + obj + + [obj | _] -> + obj + + _ -> + :undefined + end + end + + defp from_entries([{:obj, ref} | _]) do + entries = + case Heap.obj_to_list(ref) do + list when is_list(list) -> list + _ -> [] + end + + result_ref = make_ref() + + map = + Enum.reduce(entries, %{}, fn + {:obj, eref}, acc -> + case Heap.obj_to_list(eref) do + [k, v | _] -> Map.put(acc, Runtime.stringify(k), v) + _ -> acc + end + + [k, v | _], acc -> + Map.put(acc, Runtime.stringify(k), v) + + _, acc -> + acc + end) + + Heap.put_obj(result_ref, map) + {:obj, result_ref} + end + + defp from_entries(_), do: Runtime.new_object() + + defp keys([{:obj, ref} | _]) do + data = Heap.get_obj(ref, %{}) + + if is_list(data) or match?({:qb_arr, _}, data) do + Heap.wrap(array_indices(data)) + else + keys_from_map(ref, data) + end + end + + defp keys(_) do + Heap.wrap([]) + end + + defp keys_from_map(_ref, {:qb_arr, arr}) do + for i <- 0..(:array.size(arr) - 1), do: Integer.to_string(i) + end + + defp keys_from_map(_ref, list) when is_list(list) do + Heap.wrap(array_indices(list)) + end + + defp keys_from_map(ref, map) when is_map(map) do + Heap.wrap(enumerable_keys(ref)) + end + + defp get_own_property_names([{:obj, ref} | _]) do + data = Heap.get_obj(ref, %{}) + + names = + case data do + {:qb_arr, arr} -> + for(i <- 0..(:array.size(arr) - 1), do: Integer.to_string(i)) ++ ["length"] + + list when is_list(list) -> + array_indices(list) ++ ["length"] + + map when is_map(map) -> + Map.keys(map) + |> Enum.filter(&is_binary/1) + |> Enum.reject(fn k -> String.starts_with?(k, "__") and String.ends_with?(k, "__") end) + + _ -> + [] + end + + Heap.wrap(names) + end + + defp get_own_property_names(_) do + Heap.wrap([]) + end + + defp enumerable_keys(ref) do + data = Heap.get_obj(ref, %{}) + + case data do + list when is_list(list) -> + array_indices(list) + + map when is_map(map) -> + raw = + case Map.get(map, key_order()) do + order when is_list(order) -> Enum.reverse(order) + _ -> Map.keys(map) + end + + Runtime.sort_numeric_keys(raw) + |> Enum.filter(fn k -> + not String.starts_with?(k, "__") and + Map.has_key?(map, k) and + not match?(%{enumerable: false}, Heap.get_prop_desc(ref, k)) + end) + + _ -> + [] + end + end + + defp values([{:obj, ref} | _]) do + map = Heap.get_obj(ref, %{}) + Heap.wrap(Enum.map(enumerable_keys(ref), fn k -> Map.get(map, k) end)) + end + + defp values([map | _]) when is_map(map), do: Map.values(map) + defp values(_), do: [] + + defp entries([{:obj, ref} | _]) do + map = Heap.get_obj(ref, %{}) + pairs = Enum.map(enumerable_keys(ref), fn k -> Heap.wrap([k, Map.get(map, k)]) end) + Heap.wrap(pairs) + end + + defp entries([map | _]) when is_map(map) do + Enum.map(Map.to_list(map), fn {k, v} -> [k, v] end) + end + + defp entries(_), do: [] + + defp assign([target | sources]) do + Enum.reduce(sources, target, fn + {:obj, ref}, {:obj, tref} -> + src_map = Heap.get_obj(ref, %{}) + tgt_map = Heap.get_obj(tref, %{}) + Heap.put_obj(tref, Map.merge(tgt_map, src_map)) + {:obj, tref} + + map, {:obj, tref} when is_map(map) -> + tgt_map = Heap.get_obj(tref, %{}) + Heap.put_obj(tref, Map.merge(tgt_map, map)) + {:obj, tref} + + _, acc -> + acc + end) + end + + defp define_property([{:obj, ref} = obj, key, {:obj, desc_ref} | _]) do + desc = Heap.get_obj(desc_ref, %{}) + prop_name = if is_binary(key), do: key, else: to_string(key) + existing = Heap.get_obj(ref, %{}) + + if is_list(existing) or match?({:qb_arr, _}, existing) do + case Integer.parse(prop_name) do + {idx, ""} when idx >= 0 -> + writable = Map.get(desc, "writable", true) + enumerable = Map.get(desc, "enumerable", true) + configurable = Map.get(desc, "configurable", true) + + Heap.put_prop_desc(ref, prop_name, %{ + writable: writable, + enumerable: enumerable, + configurable: configurable + }) + + if Map.has_key?(desc, "value") do + Heap.array_set(ref, idx, Map.get(desc, "value")) + end + + throw({:early_return, obj}) + + _ -> + :ok + end + end + + if is_map(existing) and Map.get(existing, typed_array()) do + case Integer.parse(prop_name) do + {idx, ""} when idx >= 0 -> + val = Map.get(desc, "value") + if val != nil, do: TypedArray.set_element(obj, idx, val) + throw({:early_return, obj}) + + _ -> + :ok + end + end + + getter = Map.get(desc, "get") + setter = Map.get(desc, "set") + + if getter != nil or setter != nil do + existing_desc = Map.get(existing, prop_name) + + {old_get, old_set} = + case existing_desc do + {:accessor, g, s} -> {g, s} + _ -> {nil, nil} + end + + new_get = if getter != nil, do: getter, else: old_get + new_set = if setter != nil, do: setter, else: old_set + Heap.put_obj(ref, Map.put(existing, prop_name, {:accessor, new_get, new_set})) + else + val = Map.get(desc, "value", Map.get(existing, prop_name, :undefined)) + Heap.put_obj(ref, Map.put(existing, prop_name, val)) + end + + writable = Map.get(desc, "writable", true) + enumerable = Map.get(desc, "enumerable", true) + configurable = Map.get(desc, "configurable", true) + + Heap.put_prop_desc(ref, prop_name, %{ + writable: writable, + enumerable: enumerable, + configurable: configurable + }) + + obj + catch + {:early_return, val} -> val + end + + defp define_property([{:builtin, _, _} = b, key, {:obj, desc_ref} | _]) do + desc = Heap.get_obj(desc_ref, %{}) + prop_key = if is_binary(key), do: key, else: key + + getter = Map.get(desc, "get") + setter = Map.get(desc, "set") + + if getter != nil or setter != nil do + Heap.put_ctor_static(b, prop_key, {:accessor, getter, setter}) + else + val = Map.get(desc, "value", :undefined) + Heap.put_ctor_static(b, prop_key, val) + end + + b + end + + defp define_property([obj | _]), do: obj + + defp get_own_property_descriptor([{:obj, ref}, key | _]) do + prop_name = if is_binary(key), do: key, else: to_string(key) + data = Heap.get_obj(ref, %{}) + + cond do + is_list(data) or match?({:qb_arr, _}, data) -> + case Integer.parse(prop_name) do + {idx, ""} when idx >= 0 -> + val = Heap.array_get(ref, idx) + + if val == :undefined and Heap.get_prop_desc(ref, prop_name) == nil do + :undefined + else + data_desc = + Heap.get_prop_desc(ref, prop_name) || + %{writable: true, enumerable: true, configurable: true} + + data_descriptor_obj(val, data_desc) + end + + _ -> + :undefined + end + + is_map(data) and Map.get(data, typed_array()) -> + case Integer.parse(prop_name) do + {idx, ""} when idx >= 0 -> + val = TypedArray.get_element({:obj, ref}, idx) + + if val == :undefined do + :undefined + else + immutable = TypedArray.immutable?({:obj, ref}) + desc_ref = make_ref() + + Heap.put_obj(desc_ref, %{ + "value" => val, + "writable" => not immutable, + "enumerable" => true, + "configurable" => not immutable + }) + + {:obj, desc_ref} + end + + _ -> + :undefined + end + + is_map(data) -> + case Map.get(data, prop_name) do + nil -> + :undefined + + {:accessor, getter, setter} -> + desc = Heap.get_prop_desc(ref, prop_name) || %{enumerable: true, configurable: true} + desc_ref = make_ref() + + Heap.put_obj(desc_ref, %{ + "get" => getter || :undefined, + "set" => setter || :undefined, + "enumerable" => desc.enumerable, + "configurable" => desc.configurable + }) + + {:obj, desc_ref} + + val -> + data_desc = + Heap.get_prop_desc(ref, prop_name) || + %{writable: true, enumerable: true, configurable: true} + + data_descriptor_obj(val, data_desc) + end + + true -> + :undefined + end + end + + defp get_own_property_descriptor([{:builtin, _, _} = b, key | _]) do + prop_key = if is_binary(key), do: key, else: key + statics = Heap.get_ctor_statics(b) + + case Map.get(statics, prop_key) do + {:accessor, getter, setter} -> + desc_ref = make_ref() + + Heap.put_obj(desc_ref, %{ + "get" => getter || :undefined, + "set" => setter || :undefined, + "enumerable" => false, + "configurable" => true + }) + + {:obj, desc_ref} + + nil -> + :undefined + + val -> + data_descriptor_obj(val, %{writable: true, enumerable: true, configurable: true}) + end + end + + defp get_own_property_descriptor(_), do: :undefined + + defp data_descriptor_obj(val, desc) do + desc_ref = make_ref() + + Heap.put_obj(desc_ref, %{ + "value" => val, + "writable" => desc.writable, + "enumerable" => desc.enumerable, + "configurable" => desc.configurable + }) + + {:obj, desc_ref} + end + + defp array_indices(list) do + list |> Enum.with_index() |> Enum.map(fn {_, i} -> Integer.to_string(i) end) + end +end diff --git a/lib/quickbeam/vm/runtime/promise_builtins.ex b/lib/quickbeam/vm/runtime/promise_builtins.ex new file mode 100644 index 00000000..db607c8b --- /dev/null +++ b/lib/quickbeam/vm/runtime/promise_builtins.ex @@ -0,0 +1,119 @@ +defmodule QuickBEAM.VM.Runtime.PromiseBuiltins do + @moduledoc false + + use QuickBEAM.VM.Builtin + + import QuickBEAM.VM.Heap.Keys + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.PromiseState + + def constructor do + fn _args, _this -> Heap.wrap(%{}) end + end + + def prototype do + build_object do + val("then", {:builtin, "then", &PromiseState.promise_then/2}) + val("catch", {:builtin, "catch", &PromiseState.promise_catch/2}) + val("finally", {:builtin, "finally", &PromiseState.promise_finally/2}) + end + end + + static "resolve" do + case args do + [val | _] -> PromiseState.resolved(val) + [] -> PromiseState.resolved(:undefined) + end + end + + static "reject" do + PromiseState.rejected(List.first(args, :undefined)) + end + + static "all" do + promise_all(hd(args)) + end + + static "allSettled" do + promise_all_settled(hd(args)) + end + + static "any" do + promise_any(hd(args)) + end + + static "race" do + promise_race(hd(args)) + end + + defp unwrap_value({:obj, r} = obj) do + case Heap.get_obj(r, %{}) do + %{promise_state() => :resolved, promise_value() => val} -> val + _ -> obj + end + end + + defp unwrap_value(val), do: val + + defp promise_all(arr) do + items = Heap.to_list(arr) + + results = Enum.map(items, &unwrap_value/1) + + PromiseState.resolved(Heap.wrap(results)) + end + + defp promise_all_settled(arr) do + items = Heap.to_list(arr) + + results = + Enum.map(items, fn item -> + {status, val} = + case item do + {:obj, r} -> + case Heap.get_obj(r, %{}) do + %{promise_state() => :resolved, promise_value() => v} -> {"fulfilled", v} + %{promise_state() => :rejected, promise_value() => v} -> {"rejected", v} + _ -> {"fulfilled", item} + end + + _ -> + {"fulfilled", item} + end + + if status == "fulfilled", + do: Heap.wrap(%{"status" => status, "value" => val}), + else: Heap.wrap(%{"status" => status, "reason" => val}) + end) + + PromiseState.resolved(Heap.wrap(results)) + end + + defp promise_any(arr) do + items = Heap.to_list(arr) + + result = + Enum.find_value(items, fn + {:obj, r} -> + case Heap.get_obj(r, %{}) do + %{promise_state() => :resolved, promise_value() => v} -> v + _ -> nil + end + + val -> + val + end) + + PromiseState.resolved(result || :undefined) + end + + defp promise_race(arr) do + items = Heap.to_list(arr) + + case items do + [first | _] -> PromiseState.resolved(unwrap_value(first)) + [] -> PromiseState.resolved(:undefined) + end + end +end diff --git a/lib/quickbeam/vm/runtime/reflect.ex b/lib/quickbeam/vm/runtime/reflect.ex new file mode 100644 index 00000000..e80d81c3 --- /dev/null +++ b/lib/quickbeam/vm/runtime/reflect.ex @@ -0,0 +1,61 @@ +defmodule QuickBEAM.VM.Runtime.Reflect do + @moduledoc false + + use QuickBEAM.VM.Builtin + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.ObjectModel.{Get, Put} + alias QuickBEAM.VM.Runtime + + js_object "Reflect" do + method "apply" do + [target, this_arg | rest] = args + args_array = List.first(rest) + + if args_array == :undefined or args_array == nil do + throw( + {:js_throw, + Heap.make_error("CreateListFromArrayLike called on non-object", "TypeError")} + ) + end + + call_args = Heap.to_list(args_array) + + Interpreter.invoke_with_receiver( + target, + call_args, + Runtime.gas_budget(), + this_arg + ) + end + + method "construct" do + [target, args_array | _] = args + call_args = Heap.to_list(args_array) + Runtime.call_callback(target, call_args) + end + + method "get" do + [obj, key | _] = args + Get.get(obj, key) + end + + method "set" do + [obj, key, val | _] = args + Put.put(obj, key, val) + true + end + + method "has" do + [obj, key | _] = args + Put.has_property(obj, key) + end + + method "ownKeys" do + case hd(args) do + {:obj, ref} -> Heap.wrap(Map.keys(Heap.get_obj(ref, %{}))) + _ -> Heap.wrap([]) + end + end + end +end diff --git a/lib/quickbeam/vm/runtime/regexp.ex b/lib/quickbeam/vm/runtime/regexp.ex new file mode 100644 index 00000000..afcc3943 --- /dev/null +++ b/lib/quickbeam/vm/runtime/regexp.ex @@ -0,0 +1,100 @@ +defmodule QuickBEAM.VM.Runtime.RegExp do + @moduledoc false + + use QuickBEAM.VM.Builtin + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.ObjectModel.Get + + proto "test" do + test(this, args) + end + + proto "exec" do + exec(this, args) + end + + proto "toString" do + regexp_to_string(this) + end + + def nif_exec(bytecode, str, last_index) when is_binary(bytecode) and is_binary(str) do + raw_bc = utf8_to_latin1(bytecode) + # Unicode regexes expect UTF-8 input; non-unicode expect Latin-1 + flags = + if byte_size(bytecode) >= 2, + do: :binary.at(bytecode, 0) + :binary.at(bytecode, 1) * 256, + else: 0 + + is_unicode = Bitwise.band(flags, 0x10) != 0 + raw_str = if is_unicode, do: str, else: utf8_to_latin1(str) + + case QuickBEAM.Native.regexp_exec(raw_bc, raw_str, last_index) do + nil -> + nil + + captures when is_list(captures) -> + Enum.map(captures, fn + {start, end_off} -> {start, end_off - start} + nil -> nil + end) + end + end + + def nif_exec(_, _, _), do: nil + + defp test({:regexp, bytecode, _source}, [s | _]) when is_binary(bytecode) and is_binary(s) do + nif_exec(bytecode, s, 0) != nil + end + + defp test(_, _), do: false + + defp exec({:regexp, bytecode, _source}, [s | _]) when is_binary(bytecode) and is_binary(s) do + case nif_exec(bytecode, s, 0) do + nil -> + nil + + captures -> + strings = + Enum.map(captures, fn + {start, len} -> String.slice(s, start, len) + nil -> :undefined + end) + + match_start = + case hd(captures) do + {start, _} -> start + _ -> 0 + end + + ref = make_ref() + Heap.put_obj(ref, strings) + + # Store extra properties accessible via get_own_property + Process.put({:qb_regexp_result, ref}, %{ + "index" => match_start, + "input" => s, + "groups" => :undefined + }) + + {:obj, ref} + end + end + + defp exec(_, _), do: nil + + defp regexp_to_string({:regexp, bytecode, source}) do + flags = Get.regexp_flags(bytecode) + "/#{source}/#{flags}" + end + + defp regexp_to_string(_), do: "/(?:)/" + + defp utf8_to_latin1(bin) do + bin + |> :unicode.characters_to_list(:utf8) + |> Enum.map(fn cp -> Bitwise.band(cp, 0xFF) end) + |> :erlang.list_to_binary() + rescue + _ -> bin + end +end diff --git a/lib/quickbeam/vm/runtime/set.ex b/lib/quickbeam/vm/runtime/set.ex new file mode 100644 index 00000000..3143e838 --- /dev/null +++ b/lib/quickbeam/vm/runtime/set.ex @@ -0,0 +1,451 @@ +defmodule QuickBEAM.VM.Runtime.Set do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Bytecode + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Interpreter + alias QuickBEAM.VM.ObjectModel.Get + alias QuickBEAM.VM.Runtime + + def constructor do + fn args, _this -> + ref = make_ref() + items = Heap.to_list(List.first(args)) |> Enum.uniq() + Heap.put_obj(ref, build_object(ref, items)) + {:obj, ref} + end + end + + def weak_constructor do + fn args, _this -> + ref = make_ref() + + items = + case args do + [source | _] -> + Heap.to_list(source) + |> Enum.each(&validate_weak_key!(&1, "WeakSet")) + + Heap.to_list(source) + + _ -> + [] + end + + Heap.put_obj(ref, %{set_data() => items, "size" => length(items), :weak => true}) + {:obj, ref} + end + end + + def proto_property("has"), do: {:builtin, "has", &has/2} + def proto_property("add"), do: {:builtin, "add", &add/2} + def proto_property("delete"), do: {:builtin, "delete", &delete/2} + def proto_property("clear"), do: {:builtin, "clear", &clear/2} + def proto_property("values"), do: {:builtin, "values", &values/2} + def proto_property("keys"), do: proto_property("values") + def proto_property("entries"), do: {:builtin, "entries", &entries/2} + def proto_property("forEach"), do: {:builtin, "forEach", &for_each/2} + def proto_property(_), do: :undefined + + defp validate_weak_key!({:obj, _}, _), do: :ok + defp validate_weak_key!({:symbol, _, _}, _), do: :ok + + defp validate_weak_key!(_, kind) do + throw({:js_throw, Heap.make_error("invalid value used as #{kind} key", "TypeError")}) + end + + defp build_object(set_ref, items) do + methods = + build_methods do + method "values" do + values_iterator(set_ref) + end + + method "keys" do + values_iterator(set_ref) + end + + method "entries" do + entries_iterator(set_ref) + end + + method "add" do + add_value(set_ref, hd(args)) + end + + method "delete" do + delete_value(set_ref, hd(args)) + end + + method "clear" do + update_data(set_ref, []) + :undefined + end + + method "has" do + hd(args) in data(set_ref) + end + + method "forEach" do + for_each_value(set_ref, hd(args)) + end + + method "difference" do + difference(set_ref, hd(args)) + end + + method "intersection" do + intersection(set_ref, hd(args)) + end + + method "union" do + union(set_ref, hd(args)) + end + + method "symmetricDifference" do + symmetric_difference(set_ref, hd(args)) + end + + method "isSubsetOf" do + subset?(set_ref, hd(args)) + end + + method "isSupersetOf" do + superset?(set_ref, hd(args)) + end + + method "isDisjointFrom" do + disjoint?(set_ref, hd(args)) + end + + val(set_data(), items) + val("size", length(items)) + end + + Map.put(methods, {:symbol, "Symbol.iterator"}, methods["values"]) + end + + defp data(set_ref), do: Heap.get_obj(set_ref, %{}) |> Map.get(set_data(), []) + + defp update_data(set_ref, new_data) do + map = Heap.get_obj(set_ref, %{}) + + Heap.put_obj(set_ref, %{ + map + | set_data() => new_data, + "size" => length(new_data) + }) + end + + defp values_iterator(set_ref) do + items = data(set_ref) + pos_ref = make_ref() + Heap.put_obj(pos_ref, %{pos: 0, list: items}) + + next_fn = + {:builtin, "next", + fn _, _ -> + state = Heap.get_obj(pos_ref, %{pos: 0, list: []}) + list = if is_list(state.list), do: state.list, else: [] + + if state.pos >= length(list) do + Heap.put_obj(pos_ref, %{state | pos: state.pos + 1}) + Heap.wrap(%{"value" => :undefined, "done" => true}) + else + value = Enum.at(list, state.pos) + Heap.put_obj(pos_ref, %{state | pos: state.pos + 1}) + Heap.wrap(%{"value" => value, "done" => false}) + end + end} + + build_object do + val("next", next_fn) + end + end + + defp entries_iterator(set_ref) do + set_ref + |> data() + |> Enum.map(fn value -> Heap.wrap([value, value]) end) + |> Heap.wrap() + end + + defp add_value(set_ref, value) do + items = data(set_ref) + unless value in items, do: update_data(set_ref, items ++ [value]) + {:obj, set_ref} + end + + defp delete_value(set_ref, value) do + items = data(set_ref) + update_data(set_ref, List.delete(items, value)) + value in items + end + + defp for_each_value(set_ref, callback) do + for value <- data(set_ref) do + Runtime.call_callback(callback, [value, value]) + end + + :undefined + end + + defp other_data(other) do + case other do + {:obj, ref} -> + map = Heap.get_obj(ref, %{}) + + case Map.get(map, set_data()) do + items when is_list(items) -> + items + + _ -> + other + |> Get.get("keys") + |> iterate_setlike(other) + end + + _ -> + [] + end + end + + defp other_size(other) do + case other do + {:obj, _} -> Get.get(other, "size") + _ -> 0 + end + end + + defp validate_set_like!(other) do + size = other_size(other) + + cond do + size == :nan or size == :NaN -> + throw({:js_throw, Heap.make_error("can't convert to number: .size is NaN", "TypeError")}) + + is_number(size) and size < 0 -> + throw({:js_throw, Heap.make_error("invalid .size: must be non-negative", "RangeError")}) + + size == :neg_infinity -> + throw({:js_throw, Heap.make_error("invalid .size: must be non-negative", "RangeError")}) + + true -> + :ok + end + end + + defp other_has(other, value) do + has_fn = Get.get(other, "has") + + case has_fn do + {:builtin, _, fun} when is_function(fun) -> fun.([value], other) == true + fun -> Runtime.call_callback(fun, [value]) == true + end + end + + defp iterate_setlike(keys_fn, _other) when keys_fn in [:undefined, nil], do: [] + + defp iterate_setlike(keys_fn, other) do + iterator = call_with_this(keys_fn, [], other) + collect_iterator(iterator, []) + end + + defp collect_iterator(iterator, acc) do + next_fn = Get.get(iterator, "next") + result = call_with_this(next_fn, [], iterator) + + if Get.get(result, "done") == true do + Enum.reverse(acc) + else + value = Get.get(result, "value") + collect_iterator(iterator, [value | acc]) + end + end + + defp call_with_this(fun, args, this) do + case fun do + {:builtin, _, callback} when is_function(callback) -> + callback.(args, this) + + %Bytecode.Function{} = function -> + Interpreter.invoke_with_receiver(function, args, Runtime.gas_budget(), this) + + {:closure, _, %Bytecode.Function{}} = closure -> + Interpreter.invoke_with_receiver(closure, args, Runtime.gas_budget(), this) + + _ -> + Runtime.call_callback(fun, args) + end + end + + defp difference(set_ref, other) do + validate_set_like!(other) + constructor().([data(set_ref) -- other_data(other)], nil) + end + + defp intersection(set_ref, other) do + validate_set_like!(other) + other_items = other_data(other) + constructor().([Enum.filter(data(set_ref), &(&1 in other_items))], nil) + end + + defp union(set_ref, other) do + validate_set_like!(other) + constructor().([Enum.uniq(data(set_ref) ++ other_data(other))], nil) + end + + defp symmetric_difference(set_ref, other) do + validate_set_like!(other) + items = data(set_ref) + other_items = other_data(other) + constructor().([(items -- other_items) ++ (other_items -- items)], nil) + end + + defp subset?(set_ref, other) do + other_items = other_data(other) + Enum.all?(data(set_ref), &(&1 in other_items)) + end + + defp superset?(set_ref, other) do + items = data(set_ref) + size = other_size(other) + + if is_number(size) and length(items) >= size do + iterator = other |> Get.get("keys") |> call_with_this([], other) + iterate_check_all(iterator, items) + else + false + end + end + + defp disjoint?(set_ref, other) do + items = data(set_ref) + size = other_size(other) + + if is_number(size) and length(items) > size do + iterator = other |> Get.get("keys") |> call_with_this([], other) + iterate_check_none(iterator, items) + else + not Enum.any?(items, fn value -> other_has(other, value) end) + end + end + + defp iterate_check_all(iterator, set_data) do + next_fn = Get.get(iterator, "next") + do_iterate_check(iterator, next_fn, set_data, :all) + end + + defp iterate_check_none(iterator, set_data) do + next_fn = Get.get(iterator, "next") + do_iterate_check(iterator, next_fn, set_data, :none) + end + + defp do_iterate_check(iterator, next_fn, set_data, mode) do + result = call_with_this(next_fn, [], iterator) + + if Get.get(result, "done") == true do + true + else + value = Get.get(result, "value") + in_set = value in set_data + + case mode do + :all -> + if in_set do + do_iterate_check(iterator, next_fn, set_data, mode) + else + call_iterator_return(iterator) + false + end + + :none -> + if in_set do + call_iterator_return(iterator) + false + else + do_iterate_check(iterator, next_fn, set_data, mode) + end + end + end + end + + defp call_iterator_return(iterator) do + return_fn = Get.get(iterator, "return") + + if return_fn != :undefined and return_fn != nil do + call_with_this(return_fn, [], iterator) + end + end + + defp has([value | _], {:obj, ref}) do + items = Heap.get_obj(ref, %{}) |> Map.get(set_data(), []) + value in items + end + + defp add([value | _], {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + if Map.get(obj, :weak), do: validate_weak_key!(value, "WeakSet") + items = Map.get(obj, set_data(), []) + + unless value in items do + new_items = items ++ [value] + + Heap.put_obj(ref, %{ + obj + | set_data() => new_items, + "size" => length(new_items) + }) + end + + {:obj, ref} + end + + defp delete([value | _], {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + items = Map.get(obj, set_data(), []) + new_items = List.delete(items, value) + + Heap.put_obj(ref, %{ + obj + | set_data() => new_items, + "size" => length(new_items) + }) + + true + end + + defp clear(_, {:obj, ref}) do + obj = Heap.get_obj(ref, %{}) + Heap.put_obj(ref, %{obj | set_data() => [], "size" => 0}) + :undefined + end + + defp values(_, {:obj, ref}) do + ref + |> Heap.get_obj(%{}) + |> Map.get(set_data(), []) + |> Heap.wrap() + end + + defp entries(_, {:obj, ref}) do + ref + |> Heap.get_obj(%{}) + |> Map.get(set_data(), []) + |> Enum.map(fn value -> Heap.wrap([value, value]) end) + |> Heap.wrap() + end + + defp for_each([callback | _], {:obj, ref}) do + items = Heap.get_obj(ref, %{}) |> Map.get(set_data(), []) + + Enum.each(items, fn value -> + Runtime.call_callback(callback, [value, value, {:obj, ref}]) + end) + + :undefined + end +end diff --git a/lib/quickbeam/vm/runtime/string.ex b/lib/quickbeam/vm/runtime/string.ex new file mode 100644 index 00000000..1e2f277b --- /dev/null +++ b/lib/quickbeam/vm/runtime/string.ex @@ -0,0 +1,619 @@ +defmodule QuickBEAM.VM.Runtime.String do + @moduledoc "String.prototype methods." + + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.ObjectModel.Get + alias QuickBEAM.VM.Runtime + alias QuickBEAM.VM.Runtime.RegExp + + # ── Dispatch ── + + proto "charAt" do + char_at(this, args) + end + + proto "charCodeAt" do + char_code_at(this, args) + end + + proto "codePointAt" do + code_point_at(this, args) + end + + proto "indexOf" do + index_of(this, args) + end + + proto "lastIndexOf" do + last_index_of(this, args) + end + + proto "includes" do + includes(this, args) + end + + proto "startsWith" do + starts_with(this, args) + end + + proto "endsWith" do + ends_with(this, args) + end + + proto "slice" do + slice(this, args) + end + + proto "substring" do + substring(this, args) + end + + proto "substr" do + substr(this, args) + end + + proto "split" do + split(this, args) + end + + proto "trim" do + String.trim(this) + end + + proto "trimStart" do + String.trim_leading(this) + end + + proto "trimEnd" do + String.trim_trailing(this) + end + + proto "toUpperCase" do + :string.uppercase(this) |> IO.iodata_to_binary() + end + + proto "toLowerCase" do + :string.lowercase(this) |> IO.iodata_to_binary() + end + + proto "repeat" do + String.duplicate(this, Runtime.to_int(hd(args))) + end + + proto "padStart" do + pad(this, args, :start) + end + + proto "padEnd" do + pad(this, args, :end) + end + + proto "replace" do + replace(this, args) + end + + proto "replaceAll" do + replace_all(this, args) + end + + proto "match" do + match(this, args) + end + + proto "matchAll" do + match_all(this, args) + end + + proto "localeCompare" do + other = List.first(args, "") + other_str = if is_binary(other), do: other, else: Runtime.stringify(other) + + cond do + this < other_str -> -1 + this > other_str -> 1 + true -> 0 + end + end + + proto "search" do + search(this, args) + end + + proto "normalize" do + this + end + + proto "concat" do + this <> Enum.map_join(args, &Runtime.stringify/1) + end + + proto "toString" do + this + end + + proto "valueOf" do + this + end + + proto "at" do + string_at(this, args) + end + + # ── Implementations ── + + defp string_at(s, [idx | _]) when is_binary(s) do + i = if is_number(idx), do: trunc(idx), else: 0 + len = String.length(s) + i = if i < 0, do: len + i, else: i + if i >= 0 and i < len, do: String.at(s, i) || :undefined, else: :undefined + end + + defp string_at(_, _), do: :undefined + + defp char_at(s, [idx | _]) when is_binary(s) do + i = Runtime.to_int(idx) + + if i < 0 or i >= String.length(s) do + "" + else + String.at(s, i) + end + end + + defp char_at(_, _), do: "" + + defp char_code_at(s, [idx | _]) when is_binary(s) do + i = Runtime.to_int(idx) + chars = codepoints(s) + + if i >= 0 and i < tuple_size(chars) do + case elem(chars, i) do + cp when cp >= 0xF0000 and cp <= 0xF07FF -> cp - 0xF0000 + 0xD800 + cp -> cp + end + else + :nan + end + end + + defp char_code_at(_, _), do: :nan + + defp code_point_at(s, [idx | _]) when is_binary(s) do + i = Runtime.to_int(idx) + chars = codepoints(s) + if i >= 0 and i < tuple_size(chars), do: elem(chars, i), else: :undefined + end + + defp code_point_at(_, _), do: :undefined + + defp index_of(s, [sub | rest]) when is_binary(s) and is_binary(sub) do + from = + case rest do + [:infinity | _] -> String.length(s) + [f | _] when is_number(f) -> max(0, Runtime.to_int(f)) + _ -> 0 + end + + if sub == "" do + min(from, String.length(s)) + else + if byte_size(s) == String.length(s) do + if from >= byte_size(s) do + -1 + else + case :binary.match(s, sub, scope: {from, byte_size(s) - from}) do + {pos, _len} -> pos + :nomatch -> -1 + end + end + else + search = String.slice(s, from..-1//1) + + case :binary.match(search, sub) do + {pos, _len} -> from + pos + :nomatch -> -1 + end + end + end + end + + defp index_of(_, _), do: -1 + + defp last_index_of(s, [sub | rest]) when is_binary(s) and is_binary(sub) do + from = + case rest do + [:neg_infinity | _] -> 0 + [f | _] when is_number(f) -> max(0, min(Runtime.to_int(f), String.length(s))) + _ -> String.length(s) + end + + cond do + sub == "" -> + from + + byte_size(s) == String.length(s) -> + scope_len = min(from + byte_size(sub), byte_size(s)) + + case :binary.matches(s, sub, scope: {0, scope_len}) do + [] -> -1 + matches -> elem(List.last(matches), 0) + end + + true -> + search = String.slice(s, 0, from + String.length(sub)) + parts = :binary.split(search, sub, [:global]) + + if length(parts) > 1 do + byte_size(search) - byte_size(List.last(parts)) - byte_size(sub) + else + -1 + end + end + end + + defp last_index_of(_, _), do: -1 + + defp includes(s, [sub | _]) when is_binary(s) and is_binary(sub), do: String.contains?(s, sub) + defp includes(_, _), do: false + + defp starts_with(s, [sub | rest]) when is_binary(s) and is_binary(sub) do + pos = + case rest do + [p | _] -> Runtime.to_int(p) + _ -> 0 + end + + String.starts_with?(String.slice(s, pos..-1//1), sub) + end + + defp starts_with(_, _), do: false + + defp ends_with(s, [sub | _]) when is_binary(s) and is_binary(sub), do: String.ends_with?(s, sub) + defp ends_with(_, _), do: false + + defp slice(s, args) when is_binary(s) do + len = String.length(s) + + {start_idx, end_idx} = + case args do + [st, en] -> {Runtime.normalize_index(st, len), Runtime.normalize_index(en, len)} + [st] -> {Runtime.normalize_index(st, len), len} + [] -> {0, len} + end + + if start_idx < end_idx, do: String.slice(s, start_idx, end_idx - start_idx), else: "" + end + + defp substring(s, [start, end_ | _]) when is_binary(s) do + {a, b} = {Runtime.to_int(start), Runtime.to_int(end_)} + {s2, e2} = if a > b, do: {b, a}, else: {a, b} + String.slice(s, max(s2, 0), max(e2 - s2, 0)) + end + + defp substring(s, [start | _]) when is_binary(s), + do: String.slice(s, max(Runtime.to_int(start), 0)..-1//1) + + defp substring(s, _), do: s + + defp substr(s, [start, len | _]) when is_binary(s), + do: String.slice(s, Runtime.to_int(start), Runtime.to_int(len)) + + defp substr(s, [start | _]) when is_binary(s), do: String.slice(s, Runtime.to_int(start)..-1//1) + defp substr(s, _), do: s + + defp split(s, [{:regexp, bytecode, _source} | rest]) + when is_binary(s) and is_binary(bytecode) do + limit = + case rest do + [n | _] when is_integer(n) -> n + _ -> :infinity + end + + cond do + limit == 0 -> + [] + + s == "" -> + if RegExp.nif_exec(bytecode, s, 0) != nil, do: [], else: [""] + + true -> + nif_regex_split(s, bytecode, 0, 0, limit, []) + end + end + + defp split(s, [sep | rest]) when is_binary(s) and is_binary(sep) do + limit = + case rest do + [n | _] when is_integer(n) -> n + _ -> :infinity + end + + if limit == 0 do + [] + else + parts = if sep == "", do: String.codepoints(s), else: :binary.split(s, sep, [:global]) + if limit == :infinity, do: parts, else: Enum.take(parts, limit) + end + end + + defp split(s, [nil | _]) when is_binary(s), do: [s] + defp split(s, []) when is_binary(s), do: [s] + defp split(_, _), do: [] + + defp nif_regex_split(s, bytecode, offset, last_end, limit, acc) do + slen = byte_size(s) + + case RegExp.nif_exec(bytecode, s, offset) do + nil -> + finalize_split(s, last_end, limit, acc) + + [{match_start, match_len} | captures] -> + match_end = match_start + match_len + + if match_end == last_end do + if offset + 1 >= slen do + finalize_split(s, last_end, limit, acc) + else + nif_regex_split(s, bytecode, offset + 1, last_end, limit, acc) + end + else + before = binary_part(s, last_end, match_start - last_end) + acc = [before | acc] + + cap_values = + Enum.map(captures, fn + {start, len} -> binary_part(s, start, len) + nil -> :undefined + end) + + acc = Enum.reverse(cap_values) ++ acc + + if limit != :infinity and length(acc) >= limit do + Enum.reverse(acc) |> Enum.take(limit) + else + next_offset = if match_len == 0, do: match_end + 1, else: match_end + + if next_offset >= slen do + finalize_split(s, match_end, limit, acc) + else + nif_regex_split(s, bytecode, next_offset, match_end, limit, acc) + end + end + end + end + end + + defp finalize_split(s, last_end, limit, acc) do + tail = + if last_end >= byte_size(s), do: "", else: binary_part(s, last_end, byte_size(s) - last_end) + + result = Enum.reverse([tail | acc]) + if limit != :infinity, do: Enum.take(result, limit), else: result + end + + defp pad(s, [len | rest], dir) when is_binary(s) do + fill = + case rest do + [f | _] when is_binary(f) -> String.slice(f, 0, 1) + _ -> " " + end + + target = Runtime.to_int(len) - String.length(s) + if target <= 0, do: s, else: pad_str(s, target, fill, dir) + end + + defp pad(s, _, _), do: s + + defp pad_str(s, n, fill, :start), do: String.duplicate(fill, n) <> s + defp pad_str(s, n, fill, :end), do: s <> String.duplicate(fill, n) + + defp replace(s, [pattern, replacement | _]) when is_binary(s) do + case pattern do + {:regexp, _bytecode, _source} = r -> + regex_replace(s, r, replacement) + + pat when is_binary(pat) -> + :binary.replace(s, pat, Runtime.stringify(replacement)) + + _ -> + s + end + end + + defp replace(s, _), do: s + + defp replace_all(s, [pattern, replacement | _]) when is_binary(s) do + case pattern do + {:regexp, _bytecode, _source} = r -> + regex_replace(s, r, replacement) + + pat when is_binary(pat) -> + :binary.replace(s, pat, Runtime.stringify(replacement), [:global]) + + _ -> + s + end + end + + defp replace_all(s, _), do: s + + defp match(s, [{:regexp, bytecode, _source} = re | _]) + when is_binary(s) and is_binary(bytecode) do + flags = Get.regexp_flags(bytecode) + + if String.contains?(flags, "g") do + match_all_strings(s, re, 0, []) + else + case RegExp.nif_exec(bytecode, s, 0) do + nil -> + nil + + captures -> + Enum.map(captures, fn + {start, len} -> binary_part(s, start, len) + nil -> :undefined + end) + end + end + end + + defp match(s, [pattern | _]) when is_binary(s) and is_binary(pattern) do + case QuickBEAM.Native.regexp_compile(Regex.escape(pattern), 0) do + bytecode when is_binary(bytecode) -> match(s, [{:regexp, bytecode, pattern}]) + _ -> nil + end + end + + defp match(_, _), do: nil + + defp match_all_strings(s, {:regexp, bytecode, _} = re, offset, acc) do + case RegExp.nif_exec(bytecode, s, offset) do + nil -> + if acc == [], do: nil, else: Enum.reverse(acc) + + [{start, len} | _] -> + matched = binary_part(s, start, len) + new_offset = start + max(len, 1) + + if new_offset > byte_size(s), + do: Enum.reverse([matched | acc]), + else: match_all_strings(s, re, new_offset, [matched | acc]) + end + end + + defp match_all_with_captures(s, {:regexp, bytecode, _} = re, offset, acc) do + case RegExp.nif_exec(bytecode, s, offset) do + nil -> + Enum.reverse(acc) + + [{start, len} | captures] -> + strings = + [binary_part(s, start, len)] ++ + Enum.map(captures, fn + {cs, cl} -> binary_part(s, cs, cl) + nil -> :undefined + end) + + new_offset = start + max(len, 1) + + if new_offset > byte_size(s), + do: Enum.reverse([strings | acc]), + else: match_all_with_captures(s, re, new_offset, [strings | acc]) + end + end + + defp regex_replace(s, {:regexp, bytecode, _source}, replacement) + when is_binary(s) and is_binary(bytecode) do + rep = Runtime.stringify(replacement) + + case RegExp.nif_exec(bytecode, s, 0) do + nil -> + s + + [{match_start, match_len} | _captures] -> + before = binary_part(s, 0, match_start) + + after_str = + binary_part(s, match_start + match_len, byte_size(s) - match_start - match_len) + + before <> rep <> after_str + end + end + + defp regex_replace(s, _, _), do: s + + defp search(s, [{:regexp, bytecode, _source} | _]) when is_binary(s) and is_binary(bytecode) do + case RegExp.nif_exec(bytecode, s, 0) do + nil -> -1 + [{start, _} | _] -> start + end + end + + defp search(s, [pattern | _]) when is_binary(s) and is_binary(pattern) do + case :binary.match(s, pattern) do + {pos, _len} -> pos + :nomatch -> -1 + end + end + + defp search(_, _), do: -1 + + defp match_all(s, [{:regexp, bytecode, _source} = re | _]) + when is_binary(s) and is_binary(bytecode) do + results = match_all_with_captures(s, re, 0, []) + ref = make_ref() + Heap.put_obj(ref, results) + {:obj, ref} + end + + defp match_all(_, _) do + ref = make_ref() + Heap.put_obj(ref, []) + {:obj, ref} + end + + # ── String static methods ── + + static "fromCodePoint" do + Enum.map_join(args, fn n -> + cp = Runtime.to_int(n) + if cp >= 0 and cp <= 0x10FFFF, do: <>, else: "" + end) + end + + static "fromCharCode" do + Enum.map_join(args, fn n -> + cp = Bitwise.band(Runtime.to_int(n), 0xFFFF) + + mapped = + if cp >= 0xD800 and cp <= 0xDFFF, + do: 0xF0000 + (cp - 0xD800), + else: cp + + if mapped >= 0 and mapped <= 0x10FFFF, do: <>, else: "" + end) + end + + static "raw" do + [strings | subs] = args + + map = + case strings do + {:obj, ref} -> Heap.get_obj(ref, %{}) + _ -> %{} + end + + raw_map = + case Map.get(map, "raw") do + {:obj, rref} -> Heap.get_obj(rref, %{}) + _ -> map + end + + len = Map.get(raw_map, "length", 0) + + Enum.reduce(0..(len - 1), "", fn i, acc -> + part = Map.get(raw_map, Integer.to_string(i), "") + + sub = + if i < length(subs), + do: Runtime.stringify(Enum.at(subs, i)), + else: "" + + acc <> Runtime.stringify(part) <> sub + end) + end + + defp codepoints(s) do + case Process.get({:qb_string_codepoints, s}) do + nil -> + chars = s |> String.to_charlist() |> List.to_tuple() + Process.put({:qb_string_codepoints, s}, chars) + chars + + chars -> + chars + end + end +end diff --git a/lib/quickbeam/vm/runtime/symbol.ex b/lib/quickbeam/vm/runtime/symbol.ex new file mode 100644 index 00000000..30beef54 --- /dev/null +++ b/lib/quickbeam/vm/runtime/symbol.ex @@ -0,0 +1,52 @@ +defmodule QuickBEAM.VM.Runtime.Symbol do + @moduledoc false + + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Heap + + def constructor do + fn args, _this -> + desc = + case args do + [s | _] when is_binary(s) -> s + _ -> "" + end + + {:symbol, desc, make_ref()} + end + end + + static_val("iterator", {:symbol, "Symbol.iterator"}) + static_val("toPrimitive", {:symbol, "Symbol.toPrimitive"}) + static_val("hasInstance", {:symbol, "Symbol.hasInstance"}) + static_val("toStringTag", {:symbol, "Symbol.toStringTag"}) + static_val("asyncIterator", {:symbol, "Symbol.asyncIterator"}) + static_val("isConcatSpreadable", {:symbol, "Symbol.isConcatSpreadable"}) + static_val("species", {:symbol, "Symbol.species"}) + static_val("match", {:symbol, "Symbol.match"}) + static_val("replace", {:symbol, "Symbol.replace"}) + static_val("search", {:symbol, "Symbol.search"}) + static_val("split", {:symbol, "Symbol.split"}) + + static "for" do + key = hd(args) + + case Heap.get_symbol(key) do + nil -> + sym = {:symbol, key} + Heap.put_symbol(key, sym) + sym + + existing -> + existing + end + end + + static "keyFor" do + case hd(args) do + {:symbol, key} -> key + _ -> :undefined + end + end +end diff --git a/lib/quickbeam/vm/runtime/typed_array.ex b/lib/quickbeam/vm/runtime/typed_array.ex new file mode 100644 index 00000000..fc9bb6ba --- /dev/null +++ b/lib/quickbeam/vm/runtime/typed_array.ex @@ -0,0 +1,599 @@ +defmodule QuickBEAM.VM.Runtime.TypedArray do + @moduledoc false + + import QuickBEAM.VM.Heap.Keys + + use QuickBEAM.VM.Builtin + + alias QuickBEAM.VM.Heap + alias QuickBEAM.VM.Runtime + + @types %{ + "Uint8Array" => :uint8, + "Int8Array" => :int8, + "Uint8ClampedArray" => :uint8_clamped, + "Uint16Array" => :uint16, + "Int16Array" => :int16, + "Uint32Array" => :uint32, + "Int32Array" => :int32, + "Float32Array" => :float32, + "Float64Array" => :float64, + "Float16Array" => :float16 + } + + def types, do: @types + + def constructor(type) do + fn args, _this -> + {buf, offset, len, orig_buf} = parse_args(args, type) + ref = make_ref() + + methods = + build_methods do + method("set", do: set(ref, args)) + method("subarray", do: subarray(ref, args)) + method("join", do: join(ref, args)) + method("forEach", do: for_each(ref, args, this)) + method("map", do: map(ref, args, this)) + method("filter", do: filter(ref, args, this)) + method("every", do: every(ref, args, this)) + method("some", do: some(ref, args, this)) + method("reduce", do: reduce(ref, args, this)) + method("indexOf", do: index_of(ref, args)) + method("find", do: find(ref, args, this)) + method("sort", do: sort(ref)) + method("reverse", do: reverse(ref)) + method("slice", do: slice(ref, args)) + method("fill", do: fill(ref, args)) + method("toString", do: join(ref, [","])) + end + + obj = + Map.merge(methods, %{ + typed_array() => true, + type_key() => type, + buffer() => buf, + offset() => offset, + "length" => len, + "byteLength" => len * elem_size(type), + "byteOffset" => offset, + "BYTES_PER_ELEMENT" => elem_size(type), + "buffer" => orig_buf || make_buffer_ref(buf) + }) + + Heap.put_obj(ref, obj) + {:obj, ref} + end + end + + # ── Element access (public, used by ObjectModel.Put) ── + + def immutable?({:obj, ref}) do + is_immutable_buffer?(Heap.get_obj(ref, %{})) + end + + def get_element({:obj, ref}, idx) do + b = buf(ref) + if b == nil, do: :undefined, else: read_element(b, idx, type(ref)) + end + + def set_element({:obj, ref}, idx, val) do + ta = Heap.get_obj(ref, %{}) + + if Map.get(ta, "__immutable__") || is_immutable_buffer?(ta) do + :ok + else + t = Map.get(ta, type_key(), :uint8) + new_buf = write_element(buf(ref) || <<>>, idx, val, t) + update_buffer(ref, new_buf) + end + end + + # credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames + defp is_immutable_buffer?(ta) do + case Map.get(ta, "buffer") do + {:obj, buf_ref} -> + case Heap.get_obj(buf_ref, %{}) do + m when is_map(m) -> Map.get(m, "__immutable__", false) + _ -> false + end + + _ -> + false + end + end + + # ── State readers ── + + defp state(ref), do: Heap.get_obj(ref, %{}) + + defp buf(ref) do + s = state(ref) + + case Map.get(s, "buffer") do + {:obj, buf_ref} -> + case Heap.get_obj(buf_ref, %{}) do + m when is_map(m) -> + if Map.get(m, "__detached__") do + nil + else + ab_buf = Map.get(m, buffer(), <<>>) + offset = Map.get(s, "byteOffset", 0) + byte_len = Map.get(s, "byteLength", byte_size(ab_buf) - offset) + + if offset == 0 and byte_len == byte_size(ab_buf) do + ab_buf + else + binary_part(ab_buf, offset, min(byte_len, byte_size(ab_buf) - offset)) + end + end + + _ -> + Map.get(s, buffer(), <<>>) + end + + _ -> + Map.get(s, buffer(), <<>>) + end + end + + defp len(ref), do: Map.get(state(ref), "length", 0) + defp type(ref), do: Map.get(state(ref), type_key(), :uint8) + + # ── Method implementations ── + + defp set(ref, args) do + {source, offset} = + case args do + [s, o | _] when is_number(o) -> {s, trunc(o)} + [s | _] -> {s, 0} + _ -> {nil, 0} + end + + src_list = Heap.to_list(source) + t = type(ref) + + new_buf = + src_list + |> Enum.with_index(offset) + |> Enum.reduce(buf(ref), fn {v, i}, acc -> write_element(acc, i, v, t) end) + + update_buffer(ref, new_buf) + :undefined + end + + defp subarray(ref, args) do + l = len(ref) + t = type(ref) + s = max(0, min(to_idx(Enum.at(args, 0, 0)), l)) + e = min(to_idx(Enum.at(args, 1, l)), l) + new_len = max(0, e - s) + es = elem_size(t) + + Heap.wrap(%{ + typed_array() => true, + type_key() => t, + buffer() => binary_part(buf(ref), s * es, new_len * es), + offset() => 0, + "length" => new_len, + "byteLength" => new_len * es, + "byteOffset" => 0, + "buffer" => Map.get(state(ref), "buffer") + }) + end + + defp join(ref, args) do + sep = + case args do + [s | _] when is_binary(s) -> s + _ -> "," + end + + {b, l, t} = {buf(ref), len(ref), type(ref)} + Enum.map_join(0..max(0, l - 1), sep, &Integer.to_string(trunc(read_element(b, &1, t)))) + end + + defp for_each(ref, [cb | _], this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + for i <- 0..(l - 1), do: call(cb, [read_element(b, i, t), i, this]) + :undefined + end + + defp map(ref, [cb | _], this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + + new_buf = + Enum.reduce(0..(l - 1), b, fn i, acc -> + write_element(acc, i, call(cb, [read_element(acc, i, t), i, this]), t) + end) + + elements = for i <- 0..(l - 1), do: read_element(new_buf, i, t) + constructor(t).([elements], nil) + end + + defp filter(ref, [cb | _], this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + + vals = + for i <- 0..(l - 1), + ( + v = read_element(b, i, t) + Runtime.truthy?(call(cb, [v, i, this])) + ), + do: v + + constructor(t).([vals], nil) + end + + defp every(ref, [cb | _], this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + Enum.all?(0..max(0, l - 1), &Runtime.truthy?(call(cb, [read_element(b, &1, t), &1, this]))) + end + + defp some(ref, [cb | _], this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + Enum.any?(0..max(0, l - 1), &Runtime.truthy?(call(cb, [read_element(b, &1, t), &1, this]))) + end + + defp reduce(ref, args, this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + cb = List.first(args) + init = Enum.at(args, 1) + {start, acc} = if init != nil, do: {0, init}, else: {1, read_element(b, 0, t)} + + Enum.reduce(start..max(start, l - 1), acc, fn i, a -> + call(cb, [a, read_element(b, i, t), i, this]) + end) + end + + defp index_of(ref, [target | _]) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + + Enum.find_value(0..max(0, l - 1), -1, fn i -> + if read_element(b, i, t) == target, do: i + end) + end + + defp find(ref, [cb | _], this) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + + Enum.find_value(0..max(0, l - 1), :undefined, fn i -> + v = read_element(b, i, t) + if Runtime.truthy?(call(cb, [v, i, this])), do: v + end) + end + + defp sort(ref) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + vals = Enum.map(0..max(0, l - 1), &read_element(b, &1, t)) |> Enum.sort() + new_buf = rebuild_buffer(vals, b, t) + update_buffer(ref, new_buf) + {:obj, ref} + end + + defp reverse(ref) do + {b, l, t} = {buf(ref), len(ref), type(ref)} + vals = Enum.map(0..max(0, l - 1), &read_element(b, &1, t)) |> Enum.reverse() + new_buf = rebuild_buffer(vals, b, t) + update_buffer(ref, new_buf) + {:obj, ref} + end + + defp slice(ref, args) do + l = len(ref) + t = type(ref) + s = max(0, to_idx(Enum.at(args, 0, 0))) + e = min(l, to_idx(Enum.at(args, 1, l))) + new_len = max(0, e - s) + es = elem_size(t) + new_buf = if new_len > 0, do: binary_part(buf(ref), s * es, new_len * es), else: <<>> + + species_ctor = get_species_ctor({:obj, ref}) + + if species_ctor do + result = Runtime.call_callback(species_ctor, [new_len]) + + case result do + {:obj, _result_ref} -> + for i <- 0..(new_len - 1) do + val = read_element(new_buf, i, t) + set_element(result, i, val) + end + + _ -> + :ok + end + + result + else + elements = for i <- 0..(new_len - 1), do: read_element(new_buf, i, t) + constructor(t).([elements], nil) + end + end + + defp get_species_ctor({:obj, ref}) do + map = Heap.get_obj(ref, %{}) + ctor = Map.get(map, "constructor") + + case ctor do + {:obj, ctor_ref} -> + ctor_map = Heap.get_obj(ctor_ref, %{}) + species = Map.get(ctor_map, {:symbol, "Symbol.species"}) + if species != nil, do: species, else: nil + + _ -> + nil + end + end + + defp fill(ref, [val | _]) do + {l, t} = {len(ref), type(ref)} + new_buf = Enum.reduce(0..(l - 1), buf(ref) || <<>>, &write_element(&2, &1, val, t)) + update_buffer(ref, new_buf) + {:obj, ref} + end + + defp update_buffer(ref, new_buf) do + s = state(ref) + Heap.put_obj(ref, Map.put(s, buffer(), new_buf)) + + case Map.get(s, "buffer") do + {:obj, buf_ref} -> + buf_map = Heap.get_obj(buf_ref, %{}) + + if is_map(buf_map) do + offset = Map.get(s, "byteOffset", 0) + ab_buf = Map.get(buf_map, buffer(), <<>>) + + before = + if offset > 0, do: binary_part(ab_buf, 0, min(offset, byte_size(ab_buf))), else: <<>> + + after_offset = offset + byte_size(new_buf) + + after_part = + if after_offset < byte_size(ab_buf), + do: binary_part(ab_buf, after_offset, byte_size(ab_buf) - after_offset), + else: <<>> + + merged = before <> new_buf <> after_part + Heap.put_obj(buf_ref, Map.put(buf_map, buffer(), merged)) + end + + _ -> + :ok + end + end + + # ── Helpers ── + + defp decode_float16(bits) do + sign = Bitwise.bsr(bits, 15) |> Bitwise.band(1) + exp = Bitwise.bsr(bits, 10) |> Bitwise.band(0x1F) + frac = Bitwise.band(bits, 0x3FF) + s = if sign == 1, do: -1.0, else: 1.0 + + cond do + exp == 0 and frac == 0 -> s * 0.0 + exp == 0 -> s * frac * :math.pow(2, -24) + exp == 31 and frac == 0 -> if(s == -1.0, do: :neg_infinity, else: :infinity) + exp == 31 -> :nan + true -> s * :math.pow(2, exp - 15) * (1 + frac / 1024) + end + end + + defp encode_float16(n) when n in [:nan, :NaN], do: 0x7E00 + defp encode_float16(:infinity), do: 0x7C00 + defp encode_float16(:neg_infinity), do: 0xFC00 + + defp encode_float16(n) when is_number(n) do + f = n * 1.0 + sign = if f < 0, do: 1, else: 0 + abs_f = abs(f) + + cond do + abs_f == 0.0 -> + Bitwise.bsl(sign, 15) + + abs_f >= 65_520.0 -> + Bitwise.bsl(sign, 15) |> Bitwise.bor(0x7C00) + + true -> + exp = trunc(:math.floor(:math.log2(abs_f))) + exp = max(-14, min(15, exp)) + frac = trunc((abs_f / :math.pow(2, exp) - 1) * 1024 + 0.5) |> Bitwise.band(0x3FF) + exp_biased = exp + 15 + + Bitwise.bsl(sign, 15) + |> Bitwise.bor(Bitwise.bsl(exp_biased, 10)) + |> Bitwise.bor(frac) + end + end + + defp encode_float16(_), do: 0 + + defp bankers_round(n) when is_float(n) do + floor = trunc(n) + frac = n - floor + + cond do + frac > 0.5 -> floor + 1 + frac < 0.5 -> floor + rem(floor, 2) == 0 -> floor + true -> floor + 1 + end + end + + defp bankers_round(n) when is_integer(n), do: n + defp bankers_round(_), do: 0 + + defp call(cb, args), do: Runtime.call_callback(cb, args) + defp to_idx(n) when is_integer(n), do: n + defp to_idx(n) when is_float(n), do: trunc(n) + defp to_idx(_), do: 0 + + defp rebuild_buffer(vals, buf, type) do + vals + |> Enum.with_index() + |> Enum.reduce(buf, fn {v, i}, acc -> write_element(acc, i, v, type) end) + end + + defp parse_args(args, type) do + case args do + [{:obj, buf_ref} = buf_obj | rest] -> + buf = Heap.get_obj(buf_ref, %{}) + + cond do + match?({:qb_arr, _}, buf) -> + list = :array.to_list(elem(buf, 1)) + {list_to_buffer(list, type), 0, length(list), nil} + + is_list(buf) -> + {list_to_buffer(buf, type), 0, length(buf), nil} + + is_map(buf) and Map.has_key?(buf, buffer()) -> + bin = Map.get(buf, buffer()) + off = Enum.at(rest, 0) || 0 + len = Enum.at(rest, 1) || div(byte_size(bin) - off, elem_size(type)) + {bin, off, len, buf_obj} + + true -> + {<<>>, 0, 0, nil} + end + + [n | _] when is_integer(n) -> + {:binary.copy(<<0>>, n * elem_size(type)), 0, n, nil} + + [{:qb_arr, arr} | _] -> + list = :array.to_list(arr) + {list_to_buffer(list, type), 0, length(list), nil} + + [list | _] when is_list(list) -> + {list_to_buffer(list, type), 0, length(list), nil} + + _ -> + {<<>>, 0, 0, nil} + end + end + + # ── Element read/write ── + + defp elem_size(:uint8), do: 1 + defp elem_size(:int8), do: 1 + defp elem_size(:uint8_clamped), do: 1 + defp elem_size(:uint16), do: 2 + defp elem_size(:int16), do: 2 + defp elem_size(:uint32), do: 4 + defp elem_size(:int32), do: 4 + defp elem_size(:float16), do: 2 + defp elem_size(:float32), do: 4 + defp elem_size(:float64), do: 8 + defp elem_size(:bigint64), do: 8 + defp elem_size(:biguint64), do: 8 + + defp read_element(buf, pos, :uint8) when pos < byte_size(buf), do: :binary.at(buf, pos) + defp read_element(buf, pos, :uint8_clamped) when pos < byte_size(buf), do: :binary.at(buf, pos) + + defp read_element(buf, pos, :int8) when pos < byte_size(buf) do + v = :binary.at(buf, pos) + if v >= 128, do: v - 256, else: v + end + + defp read_element(buf, pos, :uint16) when pos * 2 + 1 < byte_size(buf), + do: :binary.decode_unsigned(:binary.part(buf, pos * 2, 2), :little) + + defp read_element(buf, pos, :int16) when pos * 2 + 1 < byte_size(buf) do + v = :binary.decode_unsigned(:binary.part(buf, pos * 2, 2), :little) + if v >= 0x8000, do: v - 0x10000, else: v + end + + defp read_element(buf, pos, :uint32) when pos * 4 + 3 < byte_size(buf), + do: :binary.decode_unsigned(:binary.part(buf, pos * 4, 4), :little) + + defp read_element(buf, pos, :int32) when pos * 4 + 3 < byte_size(buf) do + v = :binary.decode_unsigned(:binary.part(buf, pos * 4, 4), :little) + if v >= 0x80000000, do: v - 0x100000000, else: v + end + + defp read_element(buf, pos, :float16) when pos * 2 + 1 < byte_size(buf) do + <<_::binary-size(pos * 2), half::16-little, _::binary>> = buf + decode_float16(half) + end + + defp read_element(buf, pos, :float32) when pos * 4 + 3 < byte_size(buf) do + <> = :binary.part(buf, pos * 4, 4) + f + end + + defp read_element(buf, pos, :float64) when pos * 8 + 7 < byte_size(buf) do + <> = :binary.part(buf, pos * 8, 8) + f + end + + defp read_element(_, _, _), do: :undefined + + defp write_element(buf, pos, val, :uint8_clamped) when pos < byte_size(buf) do + v = max(0, min(255, bankers_round(val || 0))) + <> = buf + <> + end + + defp write_element(buf, pos, val, :uint8) when pos < byte_size(buf) do + v = trunc(val || 0) |> Bitwise.band(0xFF) + <> = buf + <> + end + + defp write_element(buf, pos, val, :int8) when pos < byte_size(buf) do + <> = buf + <> + end + + defp write_element(buf, pos, val, :int32) when pos * 4 + 3 < byte_size(buf) do + bp = pos * 4 + <> = buf + <> + end + + defp write_element(buf, pos, val, :float64) when pos * 8 + 7 < byte_size(buf) do + bp = pos * 8 + <> = buf + <> + end + + defp write_element(buf, pos, val, :float16) when pos * 2 + 1 < byte_size(buf) do + half = encode_float16(val || 0) + <> = buf + <> + end + + defp write_element(buf, pos, val, :float32) when pos * 4 + 3 < byte_size(buf) do + bp = pos * 4 + <> = buf + <> + end + + defp write_element(buf, pos, val, type) do + es = elem_size(type) + bp = pos * es + + if bp + es <= byte_size(buf) do + <> = buf + <> + else + buf + end + end + + defp list_to_buffer(list, type) do + es = elem_size(type) + buf = :binary.copy(<<0>>, length(list) * es) + + list + |> Enum.with_index() + |> Enum.reduce(buf, fn {val, i}, acc -> write_element(acc, i, val, type) end) + end + + defp make_buffer_ref(buffer_data) do + Heap.wrap(%{buffer() => buffer_data, "byteLength" => byte_size(buffer_data)}) + end +end diff --git a/lib/quickbeam/vm/stacktrace.ex b/lib/quickbeam/vm/stacktrace.ex new file mode 100644 index 00000000..c9484b56 --- /dev/null +++ b/lib/quickbeam/vm/stacktrace.ex @@ -0,0 +1,127 @@ +defmodule QuickBEAM.VM.Stacktrace do + @moduledoc false + + import QuickBEAM.VM.Builtin, only: [build_object: 1] + + alias QuickBEAM.VM.{Bytecode, Heap} + alias QuickBEAM.VM.Runtime + + def attach_stack({:obj, ref} = error_obj, filter_fun \\ nil) do + stack = build_stack(error_obj, filter_fun) + Heap.update_obj(ref, %{}, &Map.put(&1, "stack", stack)) + error_obj + end + + def build_stack(error_obj, filter_fun \\ nil) do + frames = current_frames(filter_fun) + + case prepare_stack_trace() do + fun when fun != nil and fun != :undefined -> + Runtime.call_callback(fun, [error_obj, Heap.wrap(Enum.map(frames, &callsite_object/1))]) + + _ -> + format_stack(frames) + end + end + + def current_frames(filter_fun \\ nil) do + frames = Process.get(:qb_active_frames, []) + limit = stack_trace_limit() + + frames + |> maybe_drop_until(filter_fun) + |> Enum.take(limit) + |> Enum.map(&frame_info/1) + end + + defp maybe_drop_until(frames, nil), do: frames + + defp maybe_drop_until(frames, filter_fun) do + case Enum.split_while(frames, fn %{fun: fun} -> fun !== filter_fun end) do + {_, []} -> frames + {before, [_matched | rest]} when before == [] -> rest + {_before, [_matched | rest]} -> rest + end + end + + defp frame_info(%{fun: fun_term, pc: pc}) do + fun = bytecode_fun(fun_term) + {line, col} = Bytecode.source_position(fun, pc) + + %{ + function: fun_term, + function_name: function_name(fun), + file_name: fun.filename || "", + line_number: line, + column_number: col + } + end + + defp bytecode_fun({:closure, _, %Bytecode.Function{} = fun}), do: fun + defp bytecode_fun(%Bytecode.Function{} = fun), do: fun + + defp function_name(%Bytecode.Function{name: name}) when is_binary(name) and name != "", do: name + defp function_name(_), do: nil + + defp prepare_stack_trace, do: error_static("prepareStackTrace", :undefined) + + defp stack_trace_limit do + case error_static("stackTraceLimit", 10) do + n when is_integer(n) and n >= 0 -> n + n when is_float(n) and n >= 0 -> trunc(n) + _ -> 10 + end + end + + defp error_static(key, default) do + case Heap.get_ctx() do + %{globals: globals} -> + case Map.get(globals, "Error") do + {:builtin, _, _} = ctor -> Map.get(Heap.get_ctor_statics(ctor), key, default) + _ -> default + end + + _ -> + default + end + end + + defp format_stack(frames) do + Enum.map_join(frames, "\n", fn frame -> + suffix = "#{frame.file_name}:#{frame.line_number}:#{frame.column_number}" + + case frame.function_name do + nil -> " at #{suffix}" + name -> " at #{name} (#{suffix})" + end + end) + end + + defp callsite_object(frame) do + build_object do + method "getFileName" do + frame.file_name + end + + method "getFunction" do + frame.function + end + + method "getFunctionName" do + frame.function_name || :undefined + end + + method "getLineNumber" do + frame.line_number + end + + method "getColumnNumber" do + frame.column_number + end + + method "isNative" do + false + end + end + end +end diff --git a/lib/quickbeam/worker.zig b/lib/quickbeam/worker.zig index 030f3992..1e1d87cb 100644 --- a/lib/quickbeam/worker.zig +++ b/lib/quickbeam/worker.zig @@ -419,7 +419,7 @@ pub const WorkerState = struct { self.set_ok_term(val, result); } - pub fn do_compile(self: *WorkerState, code: []const u8, result: *Result) void { + pub fn do_compile(self: *WorkerState, code: []const u8, filename: []const u8, result: *Result) void { const code_z = gpa.dupeZ(u8, code) catch { result.ok = false; result.json = "Out of memory"; @@ -428,7 +428,15 @@ pub const WorkerState = struct { defer gpa.free(code_z); const flags: c_int = qjs.JS_EVAL_TYPE_GLOBAL | qjs.JS_EVAL_FLAG_COMPILE_ONLY; - const func = qjs.JS_Eval(self.ctx, code_z.ptr, code.len, "", flags); + const fname = if (filename.len > 0) filename else ""; + const fname_z = gpa.dupeZ(u8, fname) catch { + result.ok = false; + result.json = "Out of memory"; + return; + }; + defer gpa.free(fname_z); + + const func = qjs.JS_Eval(self.ctx, code_z.ptr, code.len, fname_z.ptr, flags); defer qjs.JS_FreeValue(self.ctx, func); if (js.js_is_exception(func)) { @@ -965,8 +973,9 @@ pub fn worker_main(rd: *types.RuntimeData, owner_pid: beam.pid) void { }, .compile => |p| { var result = Result{}; - state.do_compile(p.code, &result); + state.do_compile(p.code, p.filename, &result); gpa.free(p.code); + if (p.filename.len > 0) gpa.free(p.filename); types.send_reply(p.caller_pid, p.ref_env, p.ref_term, result.ok, result.env, result.term, result.json); }, .call_fn => |p| { diff --git a/mix.exs b/mix.exs index b943dce5..dbdcd8c2 100644 --- a/mix.exs +++ b/mix.exs @@ -27,7 +27,7 @@ defmodule QuickBEAM.MixProject do def application do [ - extra_applications: [:logger, :inets, :ssl, :public_key, :xmerl], + extra_applications: [:logger, :inets, :ssl, :public_key, :xmerl, :tools, :runtime_tools], mod: {QuickBEAM.Application, []} ] end @@ -68,6 +68,7 @@ defmodule QuickBEAM.MixProject do {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:ex_dna, "~> 1.1", only: [:dev, :test], runtime: false}, + {:jason, "~> 1.4"}, {:ex_slop, "~> 0.2", only: [:dev, :test], runtime: false}, {:oxc, ">= 0.7.0"}, {:npm, "~> 0.5.2"}, @@ -77,7 +78,9 @@ defmodule QuickBEAM.MixProject do {:websock_adapter, "~> 0.5", only: :test}, {:benchee, "~> 1.3", only: :bench, runtime: false}, {:quickjs_ex, "~> 0.3.1", only: :bench, runtime: false}, - {:ex_doc, "~> 0.35", only: :dev, runtime: false} + {:ex_doc, "~> 0.35", only: :dev, runtime: false}, + {:reach, "~> 1.6", only: :dev, runtime: false}, + {:ex_ast, "~> 0.3", only: [:dev, :test]} ] end diff --git a/mix.lock b/mix.lock index 4608f575..7bb44d95 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, + "ex_ast": {:hex, :ex_ast, "0.3.0", "e3786564075ca54706e4af82c9408a067857f43b6721d3ed9f7b9045ea657f63", [:mix], [{:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a278b850e00728649c6fc0d7bf06d4f267f1a08c4daaa99f91bd28f31170d857"}, "ex_dna": {:hex, :ex_dna, "1.1.0", "3ced06be2d1648074e79617a4f1592dbe929b08a0357d08ad79b3d0025966147", [:mix], [{:gen_lsp, "~> 0.11", [hex: :gen_lsp, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0274e36c69bee3bb990097c8138a878c7ac416a8a234730a44e042f90b5eefe0"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "ex_slop": {:hex, :ex_slop, "0.2.0", "28ee70d62975636242dabf47d24e247acfbfdffd9c7cdc5a0d1c396aa42b421d", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "da8ccad55b61eebf7972fee1ab723f5227e58ed052ea09be4f9fe315c5e89cc2"}, @@ -16,6 +17,7 @@ "hex_solver": {:hex, :hex_solver, "0.2.3", "0d2ee20fbceb251d573f03ef34852e325529c2874ab66d1b576384021318996c", [:mix], [], "hexpm", "9daeae2ea6b8ad3dc7f51a10484f6bc1d0f705c31063383830a7167ab93b887b"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, @@ -33,9 +35,11 @@ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "protoss": {:hex, :protoss, "1.1.0", "853533313989751c7da5b0e1ce71501a23f3dc41f128709478af40daccc6e959", [:mix], [], "hexpm", "c2f874383dd047fcfdf467b814dd33a208a4d24a467aef90d86185b41a0752ad"}, "quickjs_ex": {:hex, :quickjs_ex, "0.3.1", "4357d7636a2f811ed879ae4cd6a47b3b24125a794238c1838307ebb61343d1c2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "e49dc454b2e2e527742c576b57cc19a1da857e62fdbd377725aae44bad585e46"}, + "reach": {:hex, :reach, "1.6.0", "4ec1b24746dc9cac68dcfacffe3c347c3a8d7a0781518666266746bb490d8b57", [:mix], [{:boxart, "~> 0.3", [hex: :boxart, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: true]}], "hexpm", "2ceb69baf4d8c4b293e2727b79daf9e34744ebbb55365de884cac3a1cb96f170"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rustler": {:hex, :rustler, "0.37.3", "5f4e6634d43b26f0a69834dd1d3ed4e1710b022a053bf4a670220c9540c92602", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a6872c6f53dcf00486d1e7f9e046e20e01bf1654bdacc4193016c2e8002b32a2"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, + "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, diff --git a/npm.lock b/npm.lock index 55dfb931..28554b44 100644 --- a/npm.lock +++ b/npm.lock @@ -43,31 +43,31 @@ }, "@emnapi/core": { "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "version": "1.9.1" + "tarball": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "version": "1.10.0" }, "@emnapi/runtime": { "dependencies": { "tslib": "^2.4.0" }, - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "version": "1.9.1" + "tarball": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "version": "1.10.0" }, "@emnapi/wasi-threads": { "dependencies": { "tslib": "^2.4.0" }, - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "version": "1.2.0" + "tarball": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "version": "1.2.1" }, "@jscpd/badge-reporter": { "dependencies": { @@ -75,24 +75,24 @@ "colors": "^1.4.0", "fs-extra": "^11.2.0" }, - "integrity": "sha512-I9b4MmLXPM2vo0SxSUWnNGKcA4PjQlD3GzXvFK60z43cN/EIdLbOq3FVwCL+dg2obUqGXKIzAm7EsDFTg0D+mQ==", + "integrity": "sha512-SLVhP00R9lkQ//Ivaanfm7k0L9sewpBven670kk1uGec2SWUOa7MVQcuad/TV59KEZ73UIC1lXvi6O9hAnbpUw==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.0.4.tgz", - "version": "4.0.4" + "tarball": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.0.5.tgz", + "version": "4.0.5" }, "@jscpd/core": { "dependencies": { "eventemitter3": "^5.0.1" }, - "integrity": "sha512-QGMT3iXEX1fI6lgjPH+x8eyJwhwr2KkpSF5uBpjC0Z5Xloj0yFTFLtwJT+RhxP/Ob4WYrtx2jvpKB269oIwgMQ==", + "integrity": "sha512-Udvym21nWzxjYRVXwwpYNBqZ6b50QV2zHN3fFNzOPPg4cfQVYOZerILB7xNDUsXHC1PCr/N52Tq3q7AElvjWWA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@jscpd/core/-/core-4.0.4.tgz", - "version": "4.0.4" + "tarball": "https://registry.npmjs.org/@jscpd/core/-/core-4.0.5.tgz", + "version": "4.0.5" }, "@jscpd/finder": { "dependencies": { - "@jscpd/core": "4.0.4", - "@jscpd/tokenizer": "4.0.4", + "@jscpd/core": "4.0.5", + "@jscpd/tokenizer": "4.0.5", "blamer": "^1.0.6", "bytes": "^3.1.2", "cli-table3": "^0.6.5", @@ -102,10 +102,10 @@ "markdown-table": "^2.0.0", "pug": "^3.0.3" }, - "integrity": "sha512-qVUWY7Nzuvfd5OIk+n7/5CM98LmFroLqblRXAI2gDABwZrc7qS+WH2SNr0qoUq0f4OqwM+piiwKvwL/VDNn/Cg==", + "integrity": "sha512-/2VkRoVrrfya+51sitZo5I9MdwsRaPKB8X3L3khAYoHFXk4L/mUuG81RmGazDHjUIGg22ItlkQtwzorNZ2+aPw==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.0.4.tgz", - "version": "4.0.4" + "tarball": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.0.5.tgz", + "version": "4.0.5" }, "@jscpd/html-reporter": { "dependencies": { @@ -113,21 +113,21 @@ "fs-extra": "^11.2.0", "pug": "^3.0.3" }, - "integrity": "sha512-YiepyeYkeH74Kx59PJRdUdonznct0wHPFkf6FLQN+mCBoy6leAWCcOfHtcexnp+UsBFDlItG5nRdKrDSxSH+Kg==", + "integrity": "sha512-drK2J8KyPIW9wvaElSIobZFp4dBO9GA++JW4gx3oihvLdDSp8qSo/CNqH47Dw0XkjQTxND3j/+Wz5JWvYRBgFQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.0.4.tgz", - "version": "4.0.4" + "tarball": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.0.5.tgz", + "version": "4.0.5" }, "@jscpd/tokenizer": { "dependencies": { - "@jscpd/core": "4.0.4", + "@jscpd/core": "4.0.5", "reprism": "^0.0.11", "spark-md5": "^3.0.2" }, - "integrity": "sha512-xxYYY/qaLah/FlwogEbGIxx9CjDO+G9E6qawcy26WwrflzJb6wsnhjwdneN6Wb0RNCDsqvzY+bzG453jsin4UQ==", + "integrity": "sha512-WzRujQtN5WedxZVDKuoanxmKAFrxcLrHpcA6kaM4z8AhGtWXZ325yseqgL5TZ8OK7Auwu7kQLlqhfk05fGYG7A==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.0.4.tgz", - "version": "4.0.4" + "tarball": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.0.5.tgz", + "version": "4.0.5" }, "@napi-rs/wasm-runtime": { "dependencies": { @@ -710,136 +710,136 @@ }, "@oxlint/binding-android-arm-eabi": { "dependencies": {}, - "integrity": "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==", + "integrity": "sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-android-arm64": { "dependencies": {}, - "integrity": "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==", + "integrity": "sha512-CkwLR69MUnyv5wjzebvbbtTSUwqLxM35CXE79bHqDIK+NtKmPEUpStTcLQRZMCo4MP0qRT6TXIQVpK0ZVScnMA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-darwin-arm64": { "dependencies": {}, - "integrity": "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==", + "integrity": "sha512-8JbefTkbmvqkqWjmQrHke+MdpgT2UghhD/ktM4FOQSpGeCgbMToJEKdl9zwhr/YWTl92i4QI1KiTwVExpcUN8A==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-darwin-x64": { "dependencies": {}, - "integrity": "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==", + "integrity": "sha512-uWpoxDT47hTnDLcdEh5jVbso8rlTTu5o0zuqa9J8E0JAKmIWn7kGFEIB03Pycn2hd2vKxybPGLhjURy/9We5FQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-freebsd-x64": { "dependencies": {}, - "integrity": "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==", + "integrity": "sha512-K/o4hEyW7flfMel0iBVznmMBt7VIMHGdjADocHKpK1DUF9erpWnJ+BSSWd2W0c8K3mPtpph+CuHzRU6CI3l9jQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-arm-gnueabihf": { "dependencies": {}, - "integrity": "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==", + "integrity": "sha512-P6040ZkcyweJ0Po9yEFqJCdvZnf3VNCGs1SIHgXDf8AAQNC6ID/heXQs9iSgo2FH7gKaKq32VWc59XZwL34C5Q==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-arm-musleabihf": { "dependencies": {}, - "integrity": "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==", + "integrity": "sha512-bwxrGCzTZkuB+THv2TQ1aTkVEfv5oz8sl+0XZZCpoYzErJD8OhPQOTA0ENPd1zJz8QsVdSzSrS2umKtPq4/JXg==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-arm64-gnu": { "dependencies": {}, - "integrity": "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==", + "integrity": "sha512-vkhb9/wKguMkLlrm3FoJW/Xmdv31GgYAE+x8lxxQ+7HeOxXUySI0q36a3NTVIuQUdLzxCI1zzMGsk1o37FOe3w==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-arm64-musl": { "dependencies": {}, - "integrity": "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==", + "integrity": "sha512-bl1dQh8LnVqsj6oOQAcxwbuOmNJkwc4p6o//HTBZhNTzJy21TLDwAviMqUFNUxDHkPGpmdKTSN4tWTjLryP8xg==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-ppc64-gnu": { "dependencies": {}, - "integrity": "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==", + "integrity": "sha512-QoOX6KB2IiEpyOj/HKqaxi+NQHPnOgNgnr22n9N4ANJCzXkUlj1UmeAbFb4PpqdlHIzvGDM5xZ0OKtcLq9RhiQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-riscv64-gnu": { "dependencies": {}, - "integrity": "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==", + "integrity": "sha512-1TGcTerjY6p152wCof3oKElccq3xHljS/Mucp04gV/4ATpP6nO7YNnp7opEg6SHkv2a57/b4b8Ndm9znJ1/qAw==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-riscv64-musl": { "dependencies": {}, - "integrity": "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==", + "integrity": "sha512-65wXEmZIrX2ADwC8i/qFL4EWLSbeuBpAm3suuX1vu4IQkKd+wLT/HU/BOl84kp91u2SxPkPDyQgu4yrqp8vwVA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-s390x-gnu": { "dependencies": {}, - "integrity": "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==", + "integrity": "sha512-TVvhgMvor7Qa6COeXxCJ7ENOM+lcAOGsQ0iUdPSCv2hxb9qSHLQ4XF1h50S6RE1gBOJ0WV3rNukg4JJJP1LWRA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-x64-gnu": { "dependencies": {}, - "integrity": "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==", + "integrity": "sha512-SjpS5uYuFoDnDdZPwZE59ndF95AsY47R5MliuneTWR1pDm2CxGJaYXbKULI71t5TVfLQUWmrHEGRL9xvuq6dnA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-linux-x64-musl": { "dependencies": {}, - "integrity": "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==", + "integrity": "sha512-gGfAeGD4sNJGILZbc/yKcIimO9wQnPMoYp9swAaKeEtwsSQAbU+rsdQze5SBtIP6j0QDzeYd4XSSUCRCF+LIeQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-openharmony-arm64": { "dependencies": {}, - "integrity": "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==", + "integrity": "sha512-OlVT0LrG/ct33EVtWRyR+B/othwmDWeRxfi13wUdPeb3lAT5TgTcFDcfLfarZtzB4W1nWF/zICMgYdkggX2WmQ==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-win32-arm64-msvc": { "dependencies": {}, - "integrity": "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==", + "integrity": "sha512-vI//NZPJk6DToiovPtaiwD4iQ7kO1r5ReWQD0sOOyKRtP3E2f6jxin4uvwi3OvDzHA2EFfd7DcZl5dtkQh7g1w==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-win32-ia32-msvc": { "dependencies": {}, - "integrity": "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==", + "integrity": "sha512-0ySj4/4zd2XjePs3XAQq7IigIstN4LPQZgCyigX5/ERMLjdWAJfnxcTsrtxZxuij8guJW8foXuHmhGxW0H4dDA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.61.0.tgz", + "version": "1.61.0" }, "@oxlint/binding-win32-x64-msvc": { "dependencies": {}, - "integrity": "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==", + "integrity": "sha512-0xgSiyeqDLDZxXoe9CVJrOx3TUVsfyoOY7cNi03JbItNcC9WCZqrSNdrAbHONxhSPaVh/lzfnDcON1RqSUMhHw==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz", - "version": "1.56.0" + "tarball": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.61.0.tgz", + "version": "1.61.0" }, "@tybys/wasm-util": { "dependencies": { @@ -1193,13 +1193,6 @@ "tarball": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "version": "5.2.0" }, - "gitignore-to-glob": { - "dependencies": {}, - "integrity": "sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA==", - "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/gitignore-to-glob/-/gitignore-to-glob-0.3.0.tgz", - "version": "0.3.0" - }, "glob-parent": { "dependencies": { "is-glob": "^4.0.1" @@ -1243,10 +1236,10 @@ "dependencies": { "function-bind": "^1.1.2" }, - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "version": "2.0.2" + "tarball": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "version": "2.0.3" }, "human-signals": { "dependencies": {}, @@ -1346,21 +1339,20 @@ }, "jscpd": { "dependencies": { - "@jscpd/badge-reporter": "4.0.4", - "@jscpd/core": "4.0.4", - "@jscpd/finder": "4.0.4", - "@jscpd/html-reporter": "4.0.4", - "@jscpd/tokenizer": "4.0.4", + "@jscpd/badge-reporter": "4.0.5", + "@jscpd/core": "4.0.5", + "@jscpd/finder": "4.0.5", + "@jscpd/html-reporter": "4.0.5", + "@jscpd/tokenizer": "4.0.5", "colors": "^1.4.0", "commander": "^5.0.0", "fs-extra": "^11.2.0", - "gitignore-to-glob": "^0.3.0", - "jscpd-sarif-reporter": "4.0.6" + "jscpd-sarif-reporter": "4.0.7" }, - "integrity": "sha512-d2VNT/2Hv4dxT2/59He8Lyda4DYOxPRyRG9zBaOpTZAqJCVf2xLrBlZkT8Va6Lo9u3X2qz8Bpq4HrDi4JsrQhA==", + "integrity": "sha512-fp6Sh42W3mIPoQgZmgYmKDLQzEDnnX2vaGlTN4haILkB2vsi+ewcCHEtWR/2CR/QbsBvAvsNo8U5Sa+p9aHiGw==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/jscpd/-/jscpd-4.0.8.tgz", - "version": "4.0.8" + "tarball": "https://registry.npmjs.org/jscpd/-/jscpd-4.0.9.tgz", + "version": "4.0.9" }, "jscpd-sarif-reporter": { "dependencies": { @@ -1368,21 +1360,21 @@ "fs-extra": "^11.2.0", "node-sarif-builder": "^3.4.0" }, - "integrity": "sha512-b9Sm3IPZ3+m8Lwa4gZa+4/LhDhlc/ZLEsLXKSOy1DANQ6kx0ueqZT+fUHWEdQ6m0o3+RIVIa7DmvLSojQD05ng==", + "integrity": "sha512-Q/VlfTI/Nbjc8dZ/2pDVIf1aRi2bM2CTYujcAoeYr7brRnS4o5ZeW86W8q7MM7cQu40gezlNckl+E9wKFSMFiA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.6.tgz", - "version": "4.0.6" + "tarball": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.7.tgz", + "version": "4.0.7" }, "jsonfile": { "dependencies": { "universalify": "^2.0.0" }, - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "optional_dependencies": { "graceful-fs": "^4.1.6" }, - "tarball": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "version": "6.2.0" + "tarball": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "version": "6.2.1" }, "jstransformer": { "dependencies": { @@ -1516,30 +1508,30 @@ }, "oxlint": { "dependencies": {}, - "integrity": "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==", + "integrity": "sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==", "optional_dependencies": { - "@oxlint/binding-android-arm-eabi": "1.56.0", - "@oxlint/binding-android-arm64": "1.56.0", - "@oxlint/binding-darwin-arm64": "1.56.0", - "@oxlint/binding-darwin-x64": "1.56.0", - "@oxlint/binding-freebsd-x64": "1.56.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", - "@oxlint/binding-linux-arm-musleabihf": "1.56.0", - "@oxlint/binding-linux-arm64-gnu": "1.56.0", - "@oxlint/binding-linux-arm64-musl": "1.56.0", - "@oxlint/binding-linux-ppc64-gnu": "1.56.0", - "@oxlint/binding-linux-riscv64-gnu": "1.56.0", - "@oxlint/binding-linux-riscv64-musl": "1.56.0", - "@oxlint/binding-linux-s390x-gnu": "1.56.0", - "@oxlint/binding-linux-x64-gnu": "1.56.0", - "@oxlint/binding-linux-x64-musl": "1.56.0", - "@oxlint/binding-openharmony-arm64": "1.56.0", - "@oxlint/binding-win32-arm64-msvc": "1.56.0", - "@oxlint/binding-win32-ia32-msvc": "1.56.0", - "@oxlint/binding-win32-x64-msvc": "1.56.0" - }, - "tarball": "https://registry.npmjs.org/oxlint/-/oxlint-1.56.0.tgz", - "version": "1.56.0" + "@oxlint/binding-android-arm-eabi": "1.61.0", + "@oxlint/binding-android-arm64": "1.61.0", + "@oxlint/binding-darwin-arm64": "1.61.0", + "@oxlint/binding-darwin-x64": "1.61.0", + "@oxlint/binding-freebsd-x64": "1.61.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.61.0", + "@oxlint/binding-linux-arm-musleabihf": "1.61.0", + "@oxlint/binding-linux-arm64-gnu": "1.61.0", + "@oxlint/binding-linux-arm64-musl": "1.61.0", + "@oxlint/binding-linux-ppc64-gnu": "1.61.0", + "@oxlint/binding-linux-riscv64-gnu": "1.61.0", + "@oxlint/binding-linux-riscv64-musl": "1.61.0", + "@oxlint/binding-linux-s390x-gnu": "1.61.0", + "@oxlint/binding-linux-x64-gnu": "1.61.0", + "@oxlint/binding-linux-x64-musl": "1.61.0", + "@oxlint/binding-openharmony-arm64": "1.61.0", + "@oxlint/binding-win32-arm64-msvc": "1.61.0", + "@oxlint/binding-win32-ia32-msvc": "1.61.0", + "@oxlint/binding-win32-x64-msvc": "1.61.0" + }, + "tarball": "https://registry.npmjs.org/oxlint/-/oxlint-1.61.0.tgz", + "version": "1.61.0" }, "oxlint-tsgolint": { "dependencies": {}, @@ -1576,6 +1568,13 @@ "tarball": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "version": "2.3.2" }, + "preact": { + "dependencies": {}, + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "version": "10.29.1" + }, "promise": { "dependencies": { "asap": "~2.0.3" @@ -1745,14 +1744,15 @@ }, "resolve": { "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "version": "1.22.11" + "tarball": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "version": "1.22.12" }, "reusify": { "dependencies": {}, @@ -1801,13 +1801,11 @@ "version": "3.0.2" }, "sqlite-napi": { - "dependencies": { - "sqlite-napi": "^1.0.0" - }, - "integrity": "sha512-qgW6ebeXgg+4JebrFe00sn62lieyMHoK+6ExF7h+QwoP8Lq8H8TxuxGWqpc+b1n+67suPpdpLF3h6AGiJ25P4g==", + "dependencies": {}, + "integrity": "sha512-Bpuyc50G89uCN4+H6ups+vXYaGFJgBENJg+lLF4zFCxdQV3g9XYS/yAl5gFfNtyeKiBMdINTn7NUaWiWXWG6+Q==", "optional_dependencies": {}, - "tarball": "https://registry.npmjs.org/sqlite-napi/-/sqlite-napi-1.0.1.tgz", - "version": "1.0.1" + "tarball": "https://registry.npmjs.org/sqlite-napi/-/sqlite-napi-1.2.0.tgz", + "version": "1.2.0" }, "string-width": { "dependencies": { diff --git a/package.json b/package.json index a81a3c8d..4286b39d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "jscpd": "^4.0.8", "oxfmt": "^0.37.0", "oxlint": "^1.52.0", - "oxlint-tsgolint": "^0.16.0" + "oxlint-tsgolint": "^0.16.0", + "preact": "^10.29.1" }, "private": true, "scripts": { diff --git a/test/quickbeam_test.exs b/test/quickbeam_test.exs index 0a4e6f32..25e38f69 100644 --- a/test/quickbeam_test.exs +++ b/test/quickbeam_test.exs @@ -1,7 +1,9 @@ defmodule QuickBEAMTest do use ExUnit.Case, async: true - doctest QuickBEAM + unless System.get_env("QUICKBEAM_MODE") == "beam" do + doctest QuickBEAM + end setup do {:ok, rt} = QuickBEAM.start() @@ -60,6 +62,17 @@ defmodule QuickBEAMTest do assert {:ok, 42} = QuickBEAM.call(rt, "add", [10, 32]) end + test "beam eval keeps returned closure captures alive" do + {:ok, rt} = QuickBEAM.start(mode: :beam, apis: false) + + assert {:ok, {:closure, _, _} = closure} = + QuickBEAM.eval(rt, "(() => { const x = 1; return function f(){ return x } })()") + + assert 1 == QuickBEAM.VM.Interpreter.invoke(closure, [], 1_000_000) + QuickBEAM.stop(rt) + end + + @tag :nif_only test "arrow functions", %{rt: rt} do QuickBEAM.eval(rt, "globalThis.double = x => x * 2") assert {:ok, 84} = QuickBEAM.call(rt, "double", [42]) @@ -84,6 +97,7 @@ defmodule QuickBEAMTest do QuickBEAM.eval(rt, "if (") end + @tag :nif_only test "error has stack trace", %{rt: rt} do assert {:error, %QuickBEAM.JSError{stack: stack}} = QuickBEAM.eval(rt, ~s[throw new Error("test")]) @@ -108,11 +122,13 @@ defmodule QuickBEAMTest do assert {:ok, 42} = QuickBEAM.eval(rt, "Promise.resolve(42)") end + @tag :nif_only test "Promise.reject", %{rt: rt} do assert {:error, %QuickBEAM.JSError{message: "nope"}} = QuickBEAM.eval(rt, "Promise.reject(new Error('nope'))") end + @tag :nif_only test "async/await", %{rt: rt} do assert {:ok, 99} = QuickBEAM.eval(rt, "await Promise.resolve(99)") end @@ -124,6 +140,7 @@ defmodule QuickBEAMTest do end describe "timers" do + @tag :nif_only test "setTimeout", %{rt: rt} do QuickBEAM.eval( rt, @@ -134,6 +151,7 @@ defmodule QuickBEAMTest do assert {:ok, true} = QuickBEAM.eval(rt, "globalThis.fired") end + @tag :nif_only test "setTimeout with delay", %{rt: rt} do QuickBEAM.eval( rt, @@ -146,6 +164,7 @@ defmodule QuickBEAMTest do end describe "console" do + @tag :nif_only test "console.log outputs to stderr", %{rt: rt} do assert {:ok, nil} = QuickBEAM.eval(rt, ~s[console.log("test output")]) end @@ -159,6 +178,7 @@ defmodule QuickBEAMTest do end describe "reset" do + @tag :nif_only test "clears global state", %{rt: rt} do QuickBEAM.eval(rt, "globalThis.x = 42") assert {:ok, 42} = QuickBEAM.eval(rt, "globalThis.x") @@ -168,6 +188,7 @@ defmodule QuickBEAMTest do assert {:ok, "undefined"} = QuickBEAM.eval(rt, "typeof globalThis.x") end + @tag :nif_only test "functions still work after reset", %{rt: rt} do :ok = QuickBEAM.reset(rt) QuickBEAM.eval(rt, "function sq(x) { return x * x; }") @@ -176,6 +197,7 @@ defmodule QuickBEAMTest do end describe "Beam.call" do + @tag :nif_only test "simple handler" do {:ok, rt} = QuickBEAM.start( @@ -188,6 +210,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "string handler" do {:ok, rt} = QuickBEAM.start( @@ -200,6 +223,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "multiple args" do {:ok, rt} = QuickBEAM.start( @@ -212,6 +236,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "chained calls with await" do {:ok, rt} = QuickBEAM.start( @@ -233,6 +258,7 @@ defmodule QuickBEAMTest do end describe "isolation" do + @tag :nif_only test "multiple runtimes are isolated" do {:ok, rt1} = QuickBEAM.start() {:ok, rt2} = QuickBEAM.start() @@ -249,6 +275,7 @@ defmodule QuickBEAMTest do end describe "introspection" do + @tag :nif_only test "globals returns sorted list of all global names" do {:ok, rt} = QuickBEAM.start() {:ok, globals} = QuickBEAM.globals(rt) @@ -261,6 +288,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "globals with user_only: true excludes builtins" do {:ok, rt} = QuickBEAM.start() {:ok, empty} = QuickBEAM.globals(rt, user_only: true) @@ -275,6 +303,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "get_global returns primitive values" do {:ok, rt} = QuickBEAM.start() QuickBEAM.eval(rt, "globalThis.n = 42; globalThis.s = 'hello'; globalThis.b = true") @@ -285,12 +314,14 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "get_global returns nil for undefined" do {:ok, rt} = QuickBEAM.start() assert {:ok, nil} = QuickBEAM.get_global(rt, "nonexistent") QuickBEAM.stop(rt) end + @tag :nif_only test "get_global returns map for objects" do {:ok, rt} = QuickBEAM.start() QuickBEAM.eval(rt, "globalThis.obj = { x: 1, y: 2 }") @@ -298,6 +329,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "info returns handlers, memory, and global count" do {:ok, rt} = QuickBEAM.start(handlers: %{"greet" => fn [n] -> "Hi #{n}" end}) QuickBEAM.eval(rt, "globalThis.x = 1") @@ -313,6 +345,7 @@ defmodule QuickBEAMTest do end describe "bytecode" do + @tag :nif_only test "compile returns binary" do {:ok, rt} = QuickBEAM.start() {:ok, bytecode} = QuickBEAM.compile(rt, "1 + 2") @@ -321,6 +354,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "compile and load_bytecode round-trip" do {:ok, rt} = QuickBEAM.start() {:ok, bytecode} = QuickBEAM.compile(rt, "40 + 2") @@ -329,6 +363,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "bytecode transfers between runtimes" do {:ok, rt1} = QuickBEAM.start() {:ok, bytecode} = QuickBEAM.compile(rt1, "function mul(a, b) { return a * b }") @@ -341,12 +376,14 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt2) end + @tag :nif_only test "compile reports syntax errors" do {:ok, rt} = QuickBEAM.start() {:error, %QuickBEAM.JSError{}} = QuickBEAM.compile(rt, "function {") QuickBEAM.stop(rt) end + @tag :nif_only test "bytecode is compact binary" do {:ok, rt} = QuickBEAM.start() @@ -363,6 +400,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "compiled globals persist after load" do {:ok, rt} = QuickBEAM.start() {:ok, bytecode} = QuickBEAM.compile(rt, "globalThis.answer = 42") @@ -373,6 +411,7 @@ defmodule QuickBEAMTest do end describe "disasm" do + @tag :nif_only test "disasm/1 decodes bytecode without a runtime" do {:ok, rt} = QuickBEAM.start(apis: false) {:ok, bytecode} = QuickBEAM.compile(rt, "1 + 2") @@ -384,6 +423,7 @@ defmodule QuickBEAMTest do assert bc.opcodes != [] end + @tag :nif_only test "disasm/2 compiles and disassembles in one call" do {:ok, rt} = QuickBEAM.start(apis: false) @@ -399,6 +439,22 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only + test "disasm/2 returns raw beam_disasm output for beam runtimes" do + {:ok, rt} = QuickBEAM.start(apis: false, mode: :beam) + + {:ok, {:beam_file, _module, exports, _attributes, _compile_info, code}} = + QuickBEAM.disasm( + rt, + "function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2) }" + ) + + assert Enum.any?(exports, &match?({:run, arity, _} when arity in [0, 1], &1)) + assert Enum.any?(code, &match?({:function, :run, arity, _, _} when arity in [0, 1], &1)) + QuickBEAM.stop(rt) + end + + @tag :nif_only test "nested functions in constant pool" do {:ok, rt} = QuickBEAM.start(apis: false) @@ -412,6 +468,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "closure variables are reported" do {:ok, rt} = QuickBEAM.start(apis: false) @@ -424,11 +481,13 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "error on invalid bytecode" do assert {:error, _} = QuickBEAM.disasm("garbage") assert {:error, _} = QuickBEAM.disasm(<<>>) end + @tag :nif_only test "source text included when available" do {:ok, rt} = QuickBEAM.start(apis: false) @@ -439,6 +498,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt) end + @tag :nif_only test "opcodes include byte offsets" do {:ok, rt} = QuickBEAM.start(apis: false) {:ok, bc} = QuickBEAM.disasm(rt, "1 + 2") @@ -453,6 +513,7 @@ defmodule QuickBEAMTest do end describe "resource limits" do + @tag :nif_only test "max_stack_size allows deeper recursion" do code = "function deep(n) { return n <= 0 ? 0 : deep(n - 1) }; deep(50)" @@ -465,6 +526,7 @@ defmodule QuickBEAMTest do QuickBEAM.stop(rt_large) end + @tag :nif_only test "memory_limit caps allocation" do {:ok, rt} = QuickBEAM.start(memory_limit: 1024 * 1024) diff --git a/test/test_helper.exs b/test/test_helper.exs index 94bb91a0..9e4e7057 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -21,7 +21,15 @@ unless File.exists?(test_addon_out) and {_, 0} = System.cmd("cc", args, stderr_to_stdout: true) end -ExUnit.start() +# Load shared test modules + +beam_mode? = System.get_env("QUICKBEAM_MODE") == "beam" + +exclude = + [:pending_beam, :pending_class, :js_engine] ++ + if(beam_mode?, do: [:nif_only], else: []) + +ExUnit.start(exclude: exclude) # Force garbage collection before BEAM exits to prevent NIF finalizer crashes. # On OTP 27.0.x, the BEAM shutdown races with QuickJS worker thread cleanup. diff --git a/test/vm/assert.js b/test/vm/assert.js new file mode 100644 index 00000000..c8240c88 --- /dev/null +++ b/test/vm/assert.js @@ -0,0 +1,49 @@ +export function assert(actual, expected, message) { + if (arguments.length === 1) + expected = true; + + if (typeof actual === typeof expected) { + if (actual === expected) { + if (actual !== 0 || (1 / actual) === (1 / expected)) + return; + } + if (typeof actual === 'number') { + if (isNaN(actual) && isNaN(expected)) + return; + } + if (typeof actual === 'object') { + if (actual !== null && expected !== null + && actual.constructor === expected.constructor + && actual.toString() === expected.toString()) + return; + } + } + throw Error("assertion failed: got |" + actual + "|" + + ", expected |" + expected + "|" + + (message ? " (" + message + ")" : "")); +} + +export function assertThrows(err, func) +{ + var ex; + ex = false; + try { + func(); + } catch(e) { + ex = true; + assert(e instanceof err); + } + assert(ex, true, "exception expected"); +} + +export function assertArrayEquals(a, b) +{ + if (!Array.isArray(a) || !Array.isArray(b)) + return assert(false); + + assert(a.length, b.length); + + a.forEach((value, idx) => { + assert(b[idx], value); + }); +} diff --git a/test/vm/beam_compat_test.exs b/test/vm/beam_compat_test.exs new file mode 100644 index 00000000..0e936837 --- /dev/null +++ b/test/vm/beam_compat_test.exs @@ -0,0 +1,1944 @@ +defmodule QuickBEAM.VM.BeamCompatTest do + @moduledoc """ + Mirrors existing QuickBEAM tests through beam mode. + + Only tests self-contained JS expressions (no cross-eval state, handlers, + promises, timers, or vars — those need NIF integration). + """ + use ExUnit.Case, async: true + + setup_all do + {:ok, rt} = QuickBEAM.start() + %{rt: rt} + end + + defp ev(rt, code), do: QuickBEAM.eval(rt, code, mode: :beam) + + defp ok(rt, code, expected) do + assert {:ok, result} = ev(rt, code) + + assert result == expected, + "#{code}\n expected: #{inspect(expected)}\n got: #{inspect(result)}" + end + + # ── Basic types (mirrors quickbeam_test.exs "basic types") ── + + describe "basic types" do + test "numbers", %{rt: rt} do + ok(rt, "1 + 2", 3) + ok(rt, "42", 42) + ok(rt, "3.14", 3.14) + ok(rt, "0", 0) + ok(rt, "-1", -1) + ok(rt, "1e3", 1000.0) + end + + test "booleans", %{rt: rt} do + ok(rt, "true", true) + ok(rt, "false", false) + end + + test "null and undefined", %{rt: rt} do + ok(rt, "null", nil) + ok(rt, "undefined", nil) + end + + test "strings", %{rt: rt} do + ok(rt, ~s["hello"], "hello") + ok(rt, ~s[""], "") + ok(rt, ~s["hello world"], "hello world") + ok(rt, ~s["he" + "llo"], "hello") + end + + test "arrays", %{rt: rt} do + ok(rt, "[1, 2, 3]", [1, 2, 3]) + ok(rt, "[]", []) + ok(rt, ~s|["a", 1, true]|, ["a", 1, true]) + end + + test "objects", %{rt: rt} do + ok(rt, "({a: 1})", %{"a" => 1}) + ok(rt, ~s[({name: "QuickBEAM", version: 1})], %{"name" => "QuickBEAM", "version" => 1}) + end + end + + # ── Arithmetic (mirrors quickbeam_test.exs) ── + + describe "arithmetic" do + test "basic operations", %{rt: rt} do + ok(rt, "2 + 3", 5) + ok(rt, "10 - 3", 7) + ok(rt, "4 * 5", 20) + ok(rt, "10 / 2", 5.0) + ok(rt, "10 % 3", 1) + end + + test "precedence", %{rt: rt} do + ok(rt, "2 + 3 * 4", 14) + ok(rt, "(2 + 3) * 4", 20) + end + + test "unary", %{rt: rt} do + ok(rt, "-5", -5) + ok(rt, "+5", 5) + ok(rt, "-(3 + 2)", -5) + end + + test "increment/decrement", %{rt: rt} do + ok(rt, "(function(){ var x = 5; return x++ })()", 5) + ok(rt, "(function(){ var x = 5; return ++x })()", 6) + ok(rt, "(function(){ var x = 5; return x-- })()", 5) + ok(rt, "(function(){ var x = 5; return --x })()", 4) + end + + test "compound assignment", %{rt: rt} do + ok(rt, "(function(){ var x = 10; x += 5; return x })()", 15) + ok(rt, "(function(){ var x = 10; x -= 3; return x })()", 7) + ok(rt, "(function(){ var x = 10; x *= 2; return x })()", 20) + ok(rt, "(function(){ var x = 10; x /= 2; return x })()", 5.0) + ok(rt, "(function(){ var x = 10; x %= 3; return x })()", 1) + end + end + + # ── Comparison (mirrors quickbeam_test.exs) ── + + describe "comparison" do + test "strict equality", %{rt: rt} do + ok(rt, "1 === 1", true) + ok(rt, "1 === 2", false) + ok(rt, ~s["a" === "a"], true) + ok(rt, ~s["a" === "b"], false) + ok(rt, "null === null", true) + ok(rt, "undefined === undefined", true) + ok(rt, "null === undefined", false) + end + + test "strict inequality", %{rt: rt} do + ok(rt, "1 !== 2", true) + ok(rt, "1 !== 1", false) + end + + test "abstract equality", %{rt: rt} do + ok(rt, "1 == 1", true) + ok(rt, "1 == '1'", true) + ok(rt, "null == undefined", true) + ok(rt, "0 == false", true) + end + + test "relational", %{rt: rt} do + ok(rt, "1 < 2", true) + ok(rt, "2 < 1", false) + ok(rt, "1 <= 1", true) + ok(rt, "1 > 0", true) + ok(rt, "1 >= 1", true) + end + end + + # ── Logical operators ── + + describe "logical operators" do + test "and/or", %{rt: rt} do + ok(rt, "true && true", true) + ok(rt, "true && false", false) + ok(rt, "false || true", true) + ok(rt, "false || false", false) + end + + test "short-circuit", %{rt: rt} do + ok(rt, "1 && 2", 2) + ok(rt, "0 && 2", 0) + ok(rt, "1 || 2", 1) + ok(rt, "0 || 2", 2) + ok(rt, "null || 'default'", "default") + end + + test "not", %{rt: rt} do + ok(rt, "!true", false) + ok(rt, "!false", true) + ok(rt, "!0", true) + ok(rt, "!null", true) + ok(rt, "!1", false) + ok(rt, "!!1", true) + end + end + + # ── Strings (mirrors eval_vars_test patterns) ── + + describe "string operations" do + test "concatenation", %{rt: rt} do + ok(rt, ~s|"hello" + " " + "world"|, "hello world") + end + + test "template literals", %{rt: rt} do + ok(rt, ~s|`${1 + 2}`|, "3") + ok(rt, ~s|(function(){ var name = "World"; return `Hello ${name}` })()|, "Hello World") + end + + test "length", %{rt: rt} do + ok(rt, ~s|"hello".length|, 5) + ok(rt, ~s|"".length|, 0) + end + + test "charCodeAt", %{rt: rt} do + ok(rt, ~s|"A".charCodeAt(0)|, 65) + end + + test "indexOf", %{rt: rt} do + ok(rt, ~s|"hello".indexOf("ll")|, 2) + ok(rt, ~s|"hello".indexOf("xx")|, -1) + end + + test "slice", %{rt: rt} do + ok(rt, ~s|"hello".slice(1, 3)|, "el") + ok(rt, ~s|"hello".slice(2)|, "llo") + end + + test "toUpperCase/toLowerCase", %{rt: rt} do + ok(rt, ~s|"hello".toUpperCase()|, "HELLO") + ok(rt, ~s|"HELLO".toLowerCase()|, "hello") + end + + test "trim", %{rt: rt} do + ok(rt, ~s|" hi ".trim()|, "hi") + end + + test "split", %{rt: rt} do + ok(rt, ~s|"a,b,c".split(",")|, ["a", "b", "c"]) + ok(rt, ~s|"abc".split("")|, ["a", "b", "c"]) + end + + test "replace", %{rt: rt} do + ok(rt, ~s|"hello".replace("l", "r")|, "herlo") + end + + test "repeat", %{rt: rt} do + ok(rt, ~s|"ab".repeat(3)|, "ababab") + end + + test "includes", %{rt: rt} do + ok(rt, ~s|"hello".includes("ell")|, true) + ok(rt, ~s|"hello".includes("xyz")|, false) + end + + test "startsWith/endsWith", %{rt: rt} do + ok(rt, ~s|"hello".startsWith("hel")|, true) + ok(rt, ~s|"hello".endsWith("llo")|, true) + ok(rt, ~s|"hello".startsWith("xyz")|, false) + end + + test "padStart/padEnd", %{rt: rt} do + ok(rt, ~s|"5".padStart(3, "0")|, "005") + ok(rt, ~s|"5".padEnd(3, "0")|, "500") + end + + test "substring", %{rt: rt} do + ok(rt, ~s|"hello".substring(1, 3)|, "el") + end + end + + # ── Arrays (mirrors quickbeam_test.exs array patterns) ── + + describe "arrays" do + test "literal", %{rt: rt} do + ok(rt, "[1, 2, 3]", [1, 2, 3]) + ok(rt, "[]", []) + end + + test "indexing", %{rt: rt} do + ok(rt, "[10, 20, 30][1]", 20) + ok(rt, "[10, 20, 30][0]", 10) + end + + test "length", %{rt: rt} do + ok(rt, "[1, 2, 3].length", 3) + ok(rt, "[].length", 0) + end + + test "push/pop", %{rt: rt} do + ok(rt, "(function(){ var a = [1]; a.push(2); return a.length })()", 2) + ok(rt, "(function(){ var a = [1,2]; return a.pop() })()", 2) + ok(rt, "(function(){ var a = [1,2]; a.pop(); return a.length })()", 1) + end + + test "shift/unshift", %{rt: rt} do + ok(rt, "(function(){ var a = [1,2,3]; a.shift(); return a })()", [2, 3]) + ok(rt, "(function(){ var a = [1]; a.unshift(0); return a })()", [0, 1]) + end + + test "map", %{rt: rt} do + ok(rt, "[1,2,3].map(function(x){ return x*2 })", [2, 4, 6]) + ok(rt, "[1,2,3].map(function(x){ return x*2 })[1]", 4) + end + + test "filter", %{rt: rt} do + ok(rt, "[1,2,3,4].filter(function(x){ return x > 2 })", [3, 4]) + ok(rt, "[1,2,3,4].filter(function(x){ return x > 2 }).length", 2) + end + + test "reduce", %{rt: rt} do + ok(rt, "[1,2,3].reduce(function(a,b){ return a+b }, 0)", 6) + ok(rt, "[1,2,3].reduce(function(a,b){ return a*b }, 1)", 6) + end + + test "indexOf", %{rt: rt} do + ok(rt, "[10,20,30].indexOf(20)", 1) + ok(rt, "[10,20,30].indexOf(99)", -1) + end + + test "includes", %{rt: rt} do + ok(rt, "[10,20,30].includes(20)", true) + ok(rt, "[10,20,30].includes(99)", false) + end + + test "slice", %{rt: rt} do + ok(rt, "[1,2,3,4].slice(1,3)", [2, 3]) + ok(rt, "[1,2,3,4].slice(1,3).length", 2) + end + + test "splice", %{rt: rt} do + ok(rt, "(function(){ var a = [1,2,3,4]; a.splice(1,2); return a })()", [1, 4]) + end + + test "join", %{rt: rt} do + ok(rt, ~s|[1,2,3].join("-")|, "1-2-3") + ok(rt, ~s|[1,2,3].join()|, "1,2,3") + end + + test "concat", %{rt: rt} do + ok(rt, "[1,2].concat([3,4])", [1, 2, 3, 4]) + ok(rt, "[1,2].concat([3,4]).length", 4) + end + + test "reverse", %{rt: rt} do + ok(rt, "(function(){ var a = [1,2,3]; a.reverse(); return a })()", [3, 2, 1]) + end + + test "sort", %{rt: rt} do + ok(rt, "(function(){ var a = [3,1,2]; a.sort(); return a })()", [1, 2, 3]) + end + + test "find/findIndex", %{rt: rt} do + ok(rt, "[1,2,3,4].find(function(x){ return x > 2 })", 3) + ok(rt, "[1,2,3,4].findIndex(function(x){ return x > 2 })", 2) + end + + test "every/some", %{rt: rt} do + ok(rt, "[1,2,3].every(function(x){ return x > 0 })", true) + ok(rt, "[1,2,3].every(function(x){ return x > 1 })", false) + ok(rt, "[1,2,3].some(function(x){ return x > 2 })", true) + ok(rt, "[1,2,3].some(function(x){ return x > 5 })", false) + end + + test "flat", %{rt: rt} do + ok(rt, "[1,[2,3],[4,[5]]].flat()", [1, 2, 3, 4, [5]]) + end + + test "forEach", %{rt: rt} do + ok(rt, "(function(){ var s=0; [1,2,3].forEach(function(x){ s+=x }); return s })()", 6) + end + + test "forEach with closure mutation", %{rt: rt} do + ok(rt, "(function(){ var s=0; [1,2,3].forEach(function(x){ s += x }); return s })()", 6) + end + + test "Array.isArray", %{rt: rt} do + ok(rt, "Array.isArray([1,2])", true) + ok(rt, "Array.isArray(1)", false) + ok(rt, ~s|Array.isArray("hi")|, false) + end + end + + # ── Objects (mirrors quickbeam_test.exs object patterns) ── + + describe "objects" do + test "property access", %{rt: rt} do + ok(rt, "({a: 1}).a", 1) + ok(rt, ~s|({name: "test"}).name|, "test") + end + + test "nested", %{rt: rt} do + ok(rt, "({a: {b: 2}}).a.b", 2) + end + + test "string keys", %{rt: rt} do + ok(rt, ~s|({"name": "test"}).name|, "test") + end + + test "computed keys", %{rt: rt} do + ok(rt, ~s|(function(){ var k = "x"; var o = {}; o[k] = 1; return o.x })()|, 1) + end + + test "Object.keys", %{rt: rt} do + ok(rt, ~s|Object.keys({a: 1, b: 2})|, ["a", "b"]) + end + + test "Object.values", %{rt: rt} do + ok(rt, "Object.values({a: 1, b: 2})", [1, 2]) + end + + test "Object.entries", %{rt: rt} do + ok(rt, ~s|Object.entries({a: 1})|, [["a", 1]]) + end + + test "Object.assign", %{rt: rt} do + ok(rt, ~s|Object.assign({a: 1}, {b: 2})|, %{"a" => 1, "b" => 2}) + end + + test "in operator", %{rt: rt} do + ok(rt, ~s|"a" in {a: 1}|, true) + ok(rt, ~s|"b" in {a: 1}|, false) + end + + test "delete", %{rt: rt} do + ok(rt, "(function(){ var o = {a: 1, b: 2}; delete o.a; return Object.keys(o) })()", ["b"]) + end + end + + # ── Functions (mirrors quickbeam_test.exs function patterns) ── + + describe "functions" do + test "anonymous IIFE", %{rt: rt} do + ok(rt, "(function(x) { return x * 2; })(21)", 42) + end + + test "closure captures variable", %{rt: rt} do + ok(rt, "(function() { let x = 10; return (function() { return x })() })()", 10) + end + + test "closure with argument", %{rt: rt} do + ok(rt, "(function(x) { return (function() { return x })() })(42)", 42) + end + + test "arrow function", %{rt: rt} do + ok(rt, "(function(){ var double = x => x * 2; return double(21) })()", 42) + end + + test "recursive function", %{rt: rt} do + ok(rt, "(function f(n){ return n <= 1 ? n : f(n-1) + f(n-2) })(10)", 55) + end + + test "higher-order function", %{rt: rt} do + ok( + rt, + "(function(){ function apply(f, x) { return f(x) }; return apply(function(x){ return x+1 }, 5) })()", + 6 + ) + end + + test "default parameter", %{rt: rt} do + ok(rt, "(function(x, y = 10){ return x + y })(5)", 15) + ok(rt, "(function(x, y = 10){ return x + y })(5, 20)", 25) + end + + test "rest parameter", %{rt: rt} do + ok(rt, "(function(...args){ return args.length })(1,2,3)", 3) + ok(rt, "(function(...args){ return args })(1,2,3)", [1, 2, 3]) + end + end + + # ── Control flow (mirrors quickbeam_test.exs) ── + + describe "control flow" do + test "if/else", %{rt: rt} do + ok(rt, "(function(){ if(true) return 1; return 0 })()", 1) + ok(rt, "(function(){ if(false) return 1; return 0 })()", 0) + ok(rt, "(function(){ var x; if(true) x = 1; else x = 2; return x })()", 1) + end + + test "ternary", %{rt: rt} do + ok(rt, "true ? 'yes' : 'no'", "yes") + ok(rt, "false ? 'yes' : 'no'", "no") + ok(rt, "1 > 0 ? 'pos' : 'non-pos'", "pos") + end + + test "while loop", %{rt: rt} do + ok(rt, "(function(){ var s=0,i=0; while(i<5){s+=i;i++} return s })()", 10) + end + + test "for loop", %{rt: rt} do + ok(rt, "(function(){ var s=0; for(var i=0;i<5;i++){s+=i} return s })()", 10) + end + + test "for-in loop", %{rt: rt} do + ok( + rt, + ~s|(function(){ var o = {a:1,b:2}; var keys = []; for(var k in o) keys.push(k); return keys })()|, + ["a", "b"] + ) + end + + test "do-while", %{rt: rt} do + ok(rt, "(function(){ var s=0,i=0; do { s+=i; i++ } while(i<5); return s })()", 10) + end + + test "break", %{rt: rt} do + ok( + rt, + "(function(){ var s=0; for(var i=0;i<10;i++){ if(i>2) break; s+=i } return s })()", + 3 + ) + end + + test "continue", %{rt: rt} do + ok( + rt, + "(function(){ var s=0; for(var i=0;i<5;i++){ if(i===2) continue; s+=i } return s })()", + 8 + ) + end + + test "switch", %{rt: rt} do + ok( + rt, + "(function(n){ switch(n){ case 1: return 'one'; case 2: return 'two'; default: return 'other' } })(1)", + "one" + ) + + ok( + rt, + "(function(n){ switch(n){ case 1: return 'one'; case 2: return 'two'; default: return 'other' } })(3)", + "other" + ) + end + end + + # ── typeof ── + + describe "typeof" do + test "primitives", %{rt: rt} do + ok(rt, "typeof 42", "number") + ok(rt, "typeof 'hi'", "string") + ok(rt, "typeof true", "boolean") + ok(rt, "typeof undefined", "undefined") + ok(rt, "typeof function(){}", "function") + ok(rt, "typeof null", "object") + end + + test "objects", %{rt: rt} do + ok(rt, "typeof {}", "object") + ok(rt, "typeof []", "object") + end + end + + # ── Destructuring ── + + describe "destructuring" do + test "array destructuring", %{rt: rt} do + ok(rt, "(function(){ var [a,b] = [1,2]; return a + b })()", 3) + end + + test "object destructuring", %{rt: rt} do + ok(rt, "(function(){ var {a,b} = {a:1,b:2}; return a + b })()", 3) + end + + test "nested destructuring", %{rt: rt} do + ok(rt, "(function(){ var {a: {b}} = {a: {b: 42}}; return b })()", 42) + end + end + + # ── Spread/rest ── + + describe "spread" do + test "spread array", %{rt: rt} do + ok(rt, "(function(){ var a = [1,2]; var b = [...a, 3]; return b })()", [1, 2, 3]) + end + + test "spread object", %{rt: rt} do + ok(rt, "(function(){ var a = {x: 1}; var b = {...a, y: 2}; return b })()", %{ + "x" => 1, + "y" => 2 + }) + end + + test "spread in function call", %{rt: rt} do + ok( + rt, + "(function(){ function add(a,b,c){ return a+b+c } var args = [1,2,3]; return add(...args) })()", + 6 + ) + end + end + + # ── Math (mirrors quickbeam_test.exs built-ins) ── + + describe "Math" do + test "floor/ceil/round", %{rt: rt} do + ok(rt, "Math.floor(3.7)", 3) + ok(rt, "Math.ceil(3.1)", 4) + ok(rt, "Math.round(3.5)", 4) + end + + test "abs", %{rt: rt} do + ok(rt, "Math.abs(-5)", 5) + ok(rt, "Math.abs(5)", 5) + end + + test "max/min", %{rt: rt} do + ok(rt, "Math.max(1, 2, 3)", 3) + ok(rt, "Math.min(1, 2, 3)", 1) + end + + test "sqrt/pow", %{rt: rt} do + ok(rt, "Math.sqrt(9)", 3.0) + ok(rt, "Math.pow(2, 3)", 8.0) + end + + test "constants", %{rt: rt} do + assert {:ok, val} = ev(rt, "Math.PI") + assert val > 3.14 and val < 3.15 + assert {:ok, val} = ev(rt, "Math.E") + assert val > 2.71 and val < 2.72 + end + + test "trunc/sign", %{rt: rt} do + ok(rt, "Math.trunc(3.7)", 3) + ok(rt, "Math.trunc(-3.7)", -3) + ok(rt, "Math.sign(5)", 1) + ok(rt, "Math.sign(-5)", -1) + ok(rt, "Math.sign(0)", 0) + end + + test "random", %{rt: rt} do + assert {:ok, val} = ev(rt, "Math.random()") + assert is_float(val) and val >= 0.0 and val < 1.0 + end + end + + # ── JSON ── + + describe "JSON" do + test "parse", %{rt: rt} do + ok(rt, ~s|JSON.parse('{"a":1}').a|, 1) + end + + test "stringify", %{rt: rt} do + ok(rt, ~s|JSON.stringify({a: 1})|, ~s|{"a":1}|) + end + + test "round-trip", %{rt: rt} do + ok(rt, ~s|JSON.parse(JSON.stringify({x: 1, y: "hi"})).y|, "hi") + end + end + + # ── parseInt/parseFloat ── + + describe "global functions" do + test "parseInt", %{rt: rt} do + ok(rt, ~s|parseInt("42")|, 42) + ok(rt, ~s|parseInt("0xff", 16)|, 255) + ok(rt, ~s|parseInt("3.14")|, 3) + end + + test "parseFloat", %{rt: rt} do + ok(rt, ~s|parseFloat("3.14")|, 3.14) + ok(rt, ~s|parseFloat("42")|, 42.0) + end + + test "isNaN", %{rt: rt} do + ok(rt, "isNaN(NaN)", true) + ok(rt, "isNaN(42)", false) + end + + test "isFinite", %{rt: rt} do + ok(rt, "isFinite(42)", true) + ok(rt, "isFinite(Infinity)", false) + ok(rt, "isFinite(NaN)", false) + end + end + + # ── Try/catch (mirrors quickbeam_test.exs error patterns) ── + + describe "try/catch" do + test "catch Error", %{rt: rt} do + ok( + rt, + ~s|(function(){ try { throw new Error("boom") } catch(e) { return e.message } })()|, + "boom" + ) + end + + test "catch thrown value", %{rt: rt} do + ok( + rt, + ~s|(function(){ try { throw "just a string" } catch(e) { return e } })()|, + "just a string" + ) + end + + test "finally", %{rt: rt} do + ok(rt, "(function(){ var x = 0; try { x = 1 } finally { x = 2 } return x })()", 2) + end + + test "try/catch/finally", %{rt: rt} do + ok( + rt, + ~s|(function(){ var x=0; try { throw "err" } catch(e) { x=1 } finally { x+=1 } return x })()|, + 2 + ) + end + end + + # ── console (mirrors quickbeam_test.exs) ── + + describe "console" do + test "console.log returns undefined", %{rt: rt} do + ok(rt, ~s|console.log("test")|, nil) + end + end + + # ── Closures (mirrors quickbeam_test.exs) ── + + describe "closures" do + test "mutable closure", %{rt: rt} do + ok( + rt, + "(function(){ var count = 0; function inc() { count++ } inc(); inc(); return count })()", + 2 + ) + end + + test "multiple closures share state", %{rt: rt} do + ok( + rt, + "(function(){ var n = 0; function inc() { n++ } function get() { return n } inc(); inc(); return get() })()", + 2 + ) + end + + test "closure over loop variable", %{rt: rt} do + ok( + rt, + "(function(){ var fns = []; for(var i = 0; i < 3; i++) { fns.push(function(){ return i }) } return fns[1]() })()", + 3 + ) + end + + test "closure over let loop variable", %{rt: rt} do + ok( + rt, + "(function(){ var fns = []; for(let i = 0; i < 3; i++) { fns.push(function(){ return i }) } return fns[1]() })()", + 1 + ) + end + + test "counter factory", %{rt: rt} do + ok( + rt, + "(function(){ function counter() { var n = 0; return function() { return ++n } } var c = counter(); c(); return c() })()", + 2 + ) + end + end + + # ── Errors (mirrors quickbeam_test.exs error patterns) ── + + describe "errors" do + test "throw new Error", %{rt: rt} do + assert {:error, %QuickBEAM.JSError{message: "boom"}} = ev(rt, ~s|throw new Error("boom")|) + end + + test "throw string", %{rt: rt} do + assert {:error, %QuickBEAM.JSError{message: "just a string"}} = + ev(rt, ~s|throw "just a string"|) + end + + test "reference error", %{rt: rt} do + assert {:error, %QuickBEAM.JSError{name: "ReferenceError"}} = ev(rt, "undeclaredVar") + end + end + + # ── Bitwise operators ── + + describe "bitwise operators" do + test "and/or/xor", %{rt: rt} do + ok(rt, "5 & 3", 1) + ok(rt, "5 | 3", 7) + ok(rt, "5 ^ 3", 6) + end + + test "shift", %{rt: rt} do + ok(rt, "1 << 3", 8) + ok(rt, "8 >> 2", 2) + ok(rt, "-1 >>> 1", 2_147_483_647) + end + + test "not", %{rt: rt} do + ok(rt, "~0", -1) + ok(rt, "~1", -2) + end + end + + # ── Equality edge cases ── + + describe "equality edge cases" do + test "NaN", %{rt: rt} do + ok(rt, "NaN === NaN", false) + ok(rt, "Number.isNaN(NaN)", true) + end + + test "null coalescing", %{rt: rt} do + ok(rt, "null ?? 'default'", "default") + ok(rt, "1 ?? 'default'", 1) + ok(rt, "undefined ?? 'default'", "default") + end + + test "optional chaining", %{rt: rt} do + ok(rt, "null?.foo", nil) + ok(rt, "undefined?.foo", nil) + ok(rt, "({a: 1})?.a", 1) + end + end + + # ── Class syntax ── + + describe "classes" do + test "basic class", %{rt: rt} do + ok( + rt, + "(function(){ class Point { constructor(x,y) { this.x = x; this.y = y } } var p = new Point(1,2); return p.x + p.y })()", + 3 + ) + end + + test "class method", %{rt: rt} do + ok( + rt, + "(function(){ class Rect { constructor(w,h) { this.w = w; this.h = h } area() { return this.w * this.h } } return new Rect(3,4).area() })()", + 12 + ) + end + + test "class prototype methods are non-enumerable", %{rt: rt} do + ok( + rt, + "(function(){ class A { m(){ return 1 } } return [Object.keys(A.prototype).length, A.prototype.propertyIsEnumerable(\"constructor\"), A.prototype.propertyIsEnumerable(\"m\")] })()", + [0, false, false] + ) + end + + test "class prototype accessors are non-enumerable", %{rt: rt} do + ok( + rt, + "(function(){ class A { get x(){ return 1 } set x(v){} } return [Object.keys(A.prototype).length, A.prototype.propertyIsEnumerable(\"x\")] })()", + [0, false] + ) + end + + test "class inheritance", %{rt: rt} do + ok( + rt, + "(function(){ class Animal { constructor(name) { this.name = name } speak() { return this.name + ' speaks' } } class Dog extends Animal { speak() { return this.name + ' barks' } } return new Dog('Rex').speak() })()", + "Rex barks" + ) + end + + test "class explicit super()", %{rt: rt} do + ok( + rt, + "(function(){ class A { constructor(x) { this.x = x } } class B extends A { constructor(x) { super(x) } } return new B(42).x })()", + 42 + ) + end + + test "class multi-level inheritance", %{rt: rt} do + ok( + rt, + "(function(){ class A { constructor(x) { this.x = x } } class B extends A {} class C extends B {} return new C(99).x })()", + 99 + ) + end + + test "class super with method", %{rt: rt} do + ok( + rt, + "(function(){ class A { constructor(x) { this.val = x } get() { return this.val } } class B extends A { constructor(x) { super(x * 2) } } return new B(21).get() })()", + 42 + ) + end + + test "class static methods", %{rt: rt} do + ok(rt, "(function(){ class A { static foo() { return 42 } } return A.foo() })()", 42) + end + + test "class fields", %{rt: rt} do + ok(rt, "(function(){ class A { x = 42 } return new A().x })()", 42) + end + + test "class static and instance methods", %{rt: rt} do + ok( + rt, + "(function(){ class A { static s() { return 1 } i() { return 2 } } return A.s() + new A().i() })()", + 3 + ) + end + end + + describe "error handling" do + test "ReferenceError is catchable", %{rt: rt} do + ok( + rt, + "(function(){ try { undeclaredVar } catch(e) { return e.name } })()", + "ReferenceError" + ) + end + + test "TypeError on null property access", %{rt: rt} do + ok(rt, "(function(){ try { null.foo } catch(e) { return e.name } })()", "TypeError") + end + + test "TypeError on calling non-function", %{rt: rt} do + ok(rt, "(function(){ try { var x = 1; x() } catch(e) { return e.name } })()", "TypeError") + end + + test "error.message accessible", %{rt: rt} do + ok( + rt, + "(function(){ try { undeclaredVar } catch(e) { return e.message } })()", + "undeclaredVar is not defined" + ) + end + + test "typeof caught error is object", %{rt: rt} do + ok(rt, "(function(){ try { null.foo } catch(e) { return typeof e } })()", "object") + end + + test "throw from called function is catchable", %{rt: rt} do + ok( + rt, + "(function(){ function f() { throw new Error('boom') } try { f() } catch(e) { return e.message } })()", + "boom" + ) + end + + test "uncaught TypeError propagates through call stack", %{rt: rt} do + ok( + rt, + "(function(){ function f() { null.x } try { f() } catch(e) { return e.name } })()", + "TypeError" + ) + end + end + + describe "instanceof" do + test "instanceof class", %{rt: rt} do + ok(rt, "(function(){ class A {} return new A() instanceof A })()", true) + end + + test "instanceof with inheritance", %{rt: rt} do + ok( + rt, + "(function(){ class A {} class B extends A {} return new B() instanceof A })()", + true + ) + end + end + + describe "getters and setters" do + test "object literal getter", %{rt: rt} do + ok(rt, "(function(){ var o = { get x() { return 42 } }; return o.x })()", 42) + end + + test "getter and setter", %{rt: rt} do + ok( + rt, + "(function(){ var o = { _v: 0, set v(x) { this._v = x }, get v() { return this._v } }; o.v = 7; return o.v })()", + 7 + ) + end + + test "Object.defineProperty getter", %{rt: rt} do + ok( + rt, + "(function(){ var o = {}; Object.defineProperty(o, 'x', { get: function() { return 42 } }); return o.x })()", + 42 + ) + end + end + + describe "coercion" do + test "valueOf for arithmetic", %{rt: rt} do + ok(rt, "(function(){ var o = { valueOf: function() { return 42 } }; return o + 1 })()", 43) + end + + test "toString for concatenation", %{rt: rt} do + ok( + rt, + "(function(){ var o = { toString: function() { return 'hi' } }; return o + '!' })()", + "hi!" + ) + end + end + + describe "array methods" do + test "flatMap", %{rt: rt} do + ok( + rt, + "(function(){ return [1,2,3].flatMap(function(x){return [x, x*2]}).join(',') })()", + "1,2,2,4,3,6" + ) + end + + test "fill", %{rt: rt} do + ok(rt, "(function(){ return [1,2,3].fill(0).join(',') })()", "0,0,0") + end + + test "Array.from with map callback", %{rt: rt} do + ok( + rt, + "(function(){ return Array.from([1,2,3], function(x){return x*2}).join(',') })()", + "2,4,6" + ) + end + end + + describe "iteration" do + test "for-of string", %{rt: rt} do + ok(rt, ~s[(function(){ var r = ""; for (var c of "abc") r += c; return r })()], "abc") + end + + test "tagged template literal", %{rt: rt} do + code = + "(function(){ function tag(s, ...v) { return s[0] + v[0] + s[1]; } return tag" <> + <<96>> <> "a${42}b" <> <<96>> <> "; })()" + + ok(rt, code, "a42b") + end + + test "WeakMap get/set", %{rt: rt} do + ok( + rt, + "(function(){ var w = new WeakMap(); var k = {}; w.set(k, 42); return w.get(k) })()", + 42 + ) + end + + test "Array.copyWithin", %{rt: rt} do + ok(rt, "(function(){ return [1,2,3,4,5].copyWithin(0,3).join(',') })()", "4,5,3,4,5") + end + + test "regexp match", %{rt: rt} do + ok(rt, "(function(){ return \"hello world\".match(/\\w+/)[0] })()", "hello") + end + end + + # ── Generator functions ── + + describe "generators" do + test "generator next", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { yield 1; yield 2; yield 3 } var i = g(); return i.next().value })()", + 1 + ) + end + + test "generator sequence", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { yield 1; yield 2 } var i = g(); i.next(); return i.next().value })()", + 2 + ) + end + + test "generator done", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { yield 1 } var i = g(); i.next(); return i.next().done })()", + true + ) + end + + test "generator return value", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { yield 1; return 42 } var i = g(); i.next(); return i.next().value })()", + 42 + ) + end + + test "generator for-of", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { yield 1; yield 2; yield 3 } var sum = 0; for (var x of g()) sum += x; return sum })()", + 6 + ) + end + + test "generator with args", %{rt: rt} do + ok( + rt, + "(function(){ function* range(s, e) { for (var i = s; i < e; i++) yield i } var r = []; for (var x of range(3, 6)) r.push(x); return r.join(',') })()", + "3,4,5" + ) + end + + test "generator fibonacci", %{rt: rt} do + ok( + rt, + "(function(){ function* fib() { var a = 0, b = 1; while(true) { yield a; var t = a; a = b; b = t + b } } var i = fib(); var r = []; for(var j = 0; j < 8; j++) r.push(i.next().value); return r.join(',') })()", + "0,1,1,2,3,5,8,13" + ) + end + + test "yield expression receives next() arg", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { var x = yield 1; yield x + 10 } var i = g(); i.next(); return i.next(5).value })()", + 15 + ) + end + + test "generator return() stops iteration", %{rt: rt} do + ok( + rt, + "(function(){ function* g() { yield 1; yield 2; yield 3 } var i = g(); i.next(); i.return(); return i.next().done })()", + true + ) + end + end + + describe "async/await" do + test "async function returns resolved value", %{rt: rt} do + ok(rt, "(async function(){ return 42 })()", 42) + end + + test "await plain value", %{rt: rt} do + ok(rt, "(async function(){ var x = await 42; return x })()", 42) + end + + test "await Promise.resolve", %{rt: rt} do + ok(rt, "(async function(){ return await Promise.resolve(42) })()", 42) + end + + test "await multiple values", %{rt: rt} do + ok(rt, "(async function(){ var a = await 10; var b = await 20; return a + b })()", 30) + end + + test "async arrow function", %{rt: rt} do + ok(rt, "(async () => { return await 7 })()", 7) + end + + test "async try/catch", %{rt: rt} do + ok( + rt, + "(async function(){ try { throw new Error('boom') } catch(e) { return e.message } })()", + "boom" + ) + end + + test "chained await", %{rt: rt} do + ok( + rt, + "(async function(){ return await Promise.resolve(await Promise.resolve(42)) })()", + 42 + ) + end + + test "Promise.resolve().then()", %{rt: rt} do + ok( + rt, + "(async function(){ return await Promise.resolve(1).then(function(v) { return v + 1 }) })()", + 2 + ) + end + end + + # ── Map/Set ── + + describe "Map/Set" do + test "Map basic", %{rt: rt} do + result = ev(rt, "(function(){ var m = new Map(); m.set('a', 1); return m.get('a') })()") + + case result do + {:ok, 1} -> :ok + # Map not yet supported + {:error, _} -> :ok + end + end + + test "Set basic", %{rt: rt} do + result = + ev(rt, "(function(){ var s = new Set(); s.add(1); s.add(2); s.add(1); return s.size })()") + + case result do + {:ok, 2} -> :ok + # Set not yet supported + {:error, _} -> :ok + end + end + end + + # ── Nested/complex expressions (mirrors eval_vars_test patterns) ── + + describe "complex expressions" do + test "nested object access", %{rt: rt} do + ok( + rt, + ~s|(function(){ var data = {order: {items: [{sku: "A"}, {sku: "B"}]}}; return data.order.items.map(function(i){ return i.sku }).join(",") })()|, + "A,B" + ) + end + + test "fibonacci", %{rt: rt} do + ok(rt, "(function fib(n){ return n <= 1 ? n : fib(n-1) + fib(n-2) })(20)", 6765) + end + + test "nested closures", %{rt: rt} do + ok( + rt, + "(function(){ function makeAdder(x) { return function(y) { return x + y } } var add5 = makeAdder(5); return add5(3) })()", + 8 + ) + end + + test "sort with comparator", %{rt: rt} do + ok( + rt, + "(function(){ var a = [{v:3},{v:1},{v:2}]; a.sort(function(a,b){ return a.v - b.v }); return a[0].v })()", + 1 + ) + end + + test "flatten array manually", %{rt: rt} do + ok( + rt, + "(function(){ var nested = [[1,2],[3,4],[5]]; var flat = []; nested.forEach(function(arr){ arr.forEach(function(x){ flat.push(x) }) }); return flat })()", + [1, 2, 3, 4, 5] + ) + end + + test "string manipulation pipeline", %{rt: rt} do + ok( + rt, + ~s|(function(){ var s = " Hello World "; return s.trim().toLowerCase().split(" ").join("-") })()|, + "hello-world" + ) + end + + test "memoize pattern", %{rt: rt} do + ok( + rt, + "(function(){ var cache = {}; function memo(n) { if(n in cache) return cache[n]; var r = n * n; cache[n] = r; return r } memo(5); return memo(5) })()", + 25 + ) + end + end + + # ── null vs undefined distinction ── + + describe "null vs undefined" do + test "typeof null is object", %{rt: rt} do + ok(rt, "typeof null", "object") + end + + test "typeof undefined is undefined", %{rt: rt} do + ok(rt, "typeof undefined", "undefined") + end + + test "null == undefined", %{rt: rt} do + ok(rt, "null == undefined", true) + end + + test "null === undefined", %{rt: rt} do + ok(rt, "null === undefined", false) + end + end + + # ── Template literals ── + + describe "template literals" do + test "basic interpolation", %{rt: rt} do + ok(rt, ~s|`${1 + 2}`|, "3") + end + + test "variable interpolation", %{rt: rt} do + ok(rt, ~s|(function(){ var name = "World"; return `Hello ${name}` })()|, "Hello World") + end + + test "expression interpolation", %{rt: rt} do + ok(rt, ~s|(function(){ var a = 1, b = 2; return `${a} + ${b} = ${a+b}` })()|, "1 + 2 = 3") + end + + test "nested template", %{rt: rt} do + ok(rt, ~s|(function(){ var cond = true; return `${cond ? "yes" : "no"}` })()|, "yes") + end + end + + # ── P1 features ── + + describe "TypedArrays" do + test "ArrayBuffer", %{rt: rt} do + ok(rt, "(function(){ var buf = new ArrayBuffer(8); return buf.byteLength })()", 8) + end + + test "Uint8Array set/get", %{rt: rt} do + ok(rt, "(function(){ var a = new Uint8Array(4); a[0] = 42; return a[0] })()", 42) + end + + test "Uint8Array from array", %{rt: rt} do + ok(rt, "(function(){ var a = new Uint8Array([1,2,3]); return a.length })()", 3) + end + + test "Int32Array signed", %{rt: rt} do + ok(rt, "(function(){ var a = new Int32Array(2); a[0] = -1; return a[0] })()", -1) + end + + test "Float64Array", %{rt: rt} do + ok(rt, "(function(){ var a = new Float64Array([1.5, 2.5]); return a[0] + a[1] })()", 4.0) + end + end + + describe "BigInt" do + test "typeof", %{rt: rt} do + ok(rt, "(function(){ return typeof 42n })()", "bigint") + end + + test "addition", %{rt: rt} do + ok(rt, "(function(){ return Number(10n + 20n) })()", 30) + end + + test "multiplication", %{rt: rt} do + ok(rt, "(function(){ return Number(3n * 4n) })()", 12) + end + + test "comparison", %{rt: rt} do + ok(rt, "(function(){ return 10n > 5n })()", true) + end + + test "exponentiation", %{rt: rt} do + ok(rt, "(function(){ return Number(2n ** 10n) })()", 1024) + end + end + + # ── P0 features ── + + describe "private fields" do + test "private field read", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 42; get() { return this.#x } } return new A().get() })()", + 42 + ) + end + + test "private field write", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 0; set(v) { this.#x = v } get() { return this.#x } } var a = new A(); a.set(99); return a.get() })()", + 99 + ) + end + + test "private field in constructor", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x; constructor(v) { this.#x = v } get() { return this.#x } } return new A(42).get() })()", + 42 + ) + end + + test "private in operator", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 1; has() { return #x in this } } return new A().has() })()", + true + ) + end + + test "private static field read", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #x = 42; static get() { return A.#x } } return A.get() })()", + 42 + ) + end + + test "private static field write", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #x = 1; static set(v){ A.#x = v } static get(){ return A.#x } } A.set(9); return A.get() })()", + 9 + ) + end + + test "private static method", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #m(){ return 5 } static get(){ return A.#m() } } return A.get() })()", + 5 + ) + end + + test "private static accessor", %{rt: rt} do + ok( + rt, + "(function(){ class A { static get #x(){ return 7 } static read(){ return A.#x } } return A.read() })()", + 7 + ) + end + + test "private static in operator", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #x = 1; static has(){ return #x in A } } return A.has() })()", + true + ) + end + + test "private field wrong receiver throws", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 1; get(){ return this.#x } } const g = (new A()).get; try { return g.call({}) } catch (e) { return e instanceof TypeError } })()", + true + ) + end + + test "private method wrong receiver throws", %{rt: rt} do + ok( + rt, + "(function(){ class A { #m(){ return 1 } get(){ return this.#m() } } const g = (new A()).get; try { return g.call({}) } catch (e) { return e instanceof TypeError } })()", + true + ) + end + + test "private field cross class throws", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 1; get(o){ try { return o.#x } catch (e) { return e instanceof TypeError } } } class B {} return new A().get(new B()) })()", + true + ) + end + + test "private static field cross class throws", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #x = 1; static get(o){ try { return o.#x } catch (e) { return e instanceof TypeError } } } class B {} return A.get(B) })()", + true + ) + end + + test "private setter wrong receiver throws", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 1; set(v){ this.#x = v } } const s = (new A()).set; try { s.call({}, 2); return false } catch (e) { return e instanceof TypeError } })()", + true + ) + end + + test "private fields work on subclass instances", %{rt: rt} do + ok( + rt, + "(function(){ class A { #x = 1; get(){ return this.#x } } class B extends A {} return new B().get() })()", + 1 + ) + end + + test "private methods work on subclass instances", %{rt: rt} do + ok( + rt, + "(function(){ class A { #m(){ return 1 } call(){ return this.#m() } } class B extends A {} return new B().call() })()", + 1 + ) + end + + test "private static fields are not inherited", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #x = 1; static get(){ return this.#x } } class B extends A {} try { return B.get() } catch (e) { return e instanceof TypeError } })()", + true + ) + end + + test "static methods named call are inherited", %{rt: rt} do + ok( + rt, + "(function(){ class A { static call(){ return 1 } } class B extends A {} return B.call() })()", + 1 + ) + end + + test "private static methods are not inherited", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #m(){ return 1 } static call(){ return this.#m() } } class B extends A {} try { return B.call() } catch (e) { return e instanceof TypeError } })()", + true + ) + end + + test "private static blocks update private fields", %{rt: rt} do + ok( + rt, + "(function(){ class A { static #x = 1; static { this.#x += 2 } static get(){ return this.#x } } return A.get() })()", + 3 + ) + end + + test "private methods work through super calls", %{rt: rt} do + ok( + rt, + "(function(){ class A { #m(){ return 1 } call(){ return this.#m() } } class B extends A { call2(){ return super.call() } } return new B().call2() })()", + 1 + ) + end + + test "static super setters target the derived constructor", %{rt: rt} do + ok( + rt, + "(function(){ class A { static set x(v){ this.y = v + 1 } } class B extends A { static g(){ super.x = 2; return this.y } } return B.g() })()", + 3 + ) + end + + test "derived constructors can return objects", %{rt: rt} do + ok( + rt, + "(function(){ class A { constructor(){ this.a = 1 } } class B extends A { constructor(){ super(); return {b:2} } } return new B().b })()", + 2 + ) + end + + test "class expressions keep their inner name", %{rt: rt} do + ok( + rt, + "(function(){ const C = class D { static n(){ return D.name } }; return C.n() })()", + "D" + ) + end + + test "computed static fields are assigned", %{rt: rt} do + ok(rt, "(function(){ const k = \"x\"; class A { static [k] = 4 } return A.x })()", 4) + end + + test "computed static methods are assigned", %{rt: rt} do + ok(rt, "(function(){ class A { static [\"m\"](){ return 1 } } return A.m() })()", 1) + end + + test "derived super calls preserve new.target", %{rt: rt} do + ok( + rt, + "(function(){ class A { constructor(){ this.v = new.target.name } } class B extends A { constructor(...args){ super(...args) } } return new B().v })()", + "B" + ) + end + end + + describe "super property access" do + test "super.method()", %{rt: rt} do + ok( + rt, + "(function(){ class A { greet() { return 'hello' } } class B extends A { test() { return super.greet() } } return new B().test() })()", + "hello" + ) + end + + test "super with override", %{rt: rt} do + ok( + rt, + "(function(){ class A { val() { return 10 } } class B extends A { val() { return super.val() + 5 } } return new B().val() })()", + 15 + ) + end + + test "inherited method without override", %{rt: rt} do + ok( + rt, + "(function(){ class A { greet() { return 'hello' } } class B extends A {} return new B().greet() })()", + "hello" + ) + end + + test "static super getter uses the derived constructor as receiver", %{rt: rt} do + ok( + rt, + "(function(){ class A { static get x(){ return this.y } } class B extends A { static y = 7; static g(){ return super.x } } return B.g() })()", + 7 + ) + end + end + + describe "function hoisting" do + test "hoisted function", %{rt: rt} do + ok(rt, "(function(){ return f(); function f() { return 42 } })()", 42) + end + end + + describe "Function.prototype" do + test "call", %{rt: rt} do + ok( + rt, + "(function(){ function f(x) { return this.v + x } return f.call({v: 10}, 5) })()", + 15 + ) + end + + test "apply", %{rt: rt} do + ok(rt, "(function(){ function f(a,b) { return a + b } return f.apply(null, [3, 4]) })()", 7) + end + + test "bind", %{rt: rt} do + ok( + rt, + "(function(){ function f(x) { return this.v + x } var g = f.bind({v: 100}); return g(5) })()", + 105 + ) + end + end + + # ── with statement ── + + describe "with statement" do + test "with get", %{rt: rt} do + ok(rt, "(function(){ var o = {x: 42, y: 10}; with(o) { return x + y } })()", 52) + end + + test "with set", %{rt: rt} do + ok(rt, "(function(){ var o = {x: 1}; with(o) { x = 42 } return o.x })()", 42) + end + + test "with fallback to outer scope", %{rt: rt} do + ok(rt, "(function(){ var z = 99; var o = {x: 1}; with(o) { return z } })()", 99) + end + + test "with nested", %{rt: rt} do + ok( + rt, + "(function(){ var o = {x: 10}; var p = {y: 20}; with(o) { with(p) { return x + y } } })()", + 30 + ) + end + end + + # ── Symbol ── + + describe "Symbol" do + test "typeof Symbol()", %{rt: rt} do + ok(rt, "(function(){ return typeof Symbol() })()", "symbol") + end + + test "Symbol.toString()", %{rt: rt} do + ok(rt, "(function(){ return Symbol('foo').toString() })()", "Symbol(foo)") + end + + test "Symbol uniqueness", %{rt: rt} do + ok(rt, "(function(){ return Symbol('a') === Symbol('a') })()", false) + end + + test "Symbol same reference equality", %{rt: rt} do + ok(rt, "(function(){ var s = Symbol(); return s === s })()", true) + end + + test "Symbol as object key", %{rt: rt} do + ok(rt, "(function(){ var s = Symbol('k'); var o = {}; o[s] = 42; return o[s] })()", 42) + end + + test "Symbol.iterator type", %{rt: rt} do + ok(rt, "(function(){ return typeof Symbol.iterator })()", "symbol") + end + + test "Symbol.for global registry", %{rt: rt} do + ok(rt, "(function(){ return Symbol.for('x') === Symbol.for('x') })()", true) + end + + test "custom iterable with Symbol.iterator", %{rt: rt} do + ok( + rt, + "(function(){ var o = {}; o[Symbol.iterator] = function() { var i = 0; return { next: function() { return { value: i++, done: i > 3 } } } }; var r = []; for (var x of o) r.push(x); return r.join(',') })()", + "0,1,2" + ) + end + end + + # ── Proxy ── + + describe "Proxy" do + test "get trap", %{rt: rt} do + ok( + rt, + "(function(){ var p = new Proxy({x: 1}, { get: function(t,k) { return t[k] * 2 } }); return p.x })()", + 2 + ) + end + + test "set trap", %{rt: rt} do + ok( + rt, + "(function(){ var o = {x: 1}; var p = new Proxy(o, { set: function(t,k,v) { t[k] = v * 10; return true } }); p.x = 5; return o.x })()", + 50 + ) + end + + test "no trap passthrough", %{rt: rt} do + ok(rt, "(function(){ var p = new Proxy({x: 42}, {}); return p.x })()", 42) + end + end + + describe "for-in" do + test "enumerate object keys", %{rt: rt} do + ok( + rt, + "(function(){ var o = {a:1,b:2}; var r = []; for (var k in o) r.push(k); return r.join(',') })()", + "a,b" + ) + end + end + + describe "switch" do + test "matching case", %{rt: rt} do + ok( + rt, + "(function(){ switch(2) { case 1: return 'a'; case 2: return 'b'; default: return 'c' } })()", + "b" + ) + end + + test "default case", %{rt: rt} do + ok(rt, "(function(){ switch(99) { case 1: return 'a'; default: return 'z' } })()", "z") + end + end + + describe "optional chaining and nullish" do + test "optional chain on null", %{rt: rt} do + ok(rt, "(function(){ var o = null; return o?.x })()", nil) + end + + test "nullish coalescing", %{rt: rt} do + ok(rt, "(function(){ return null ?? 42 })()", 42) + end + end + + describe "rest and spread" do + test "rest params", %{rt: rt} do + ok(rt, "(function(){ function f(...args) { return args.length } return f(1,2,3) })()", 3) + end + + test "spread call", %{rt: rt} do + ok(rt, "(function(){ function f(a,b,c) { return a+b+c } return f(...[1,2,3]) })()", 6) + end + + test "default params", %{rt: rt} do + ok(rt, "(function(){ function f(a, b=10) { return a + b } return f(5) })()", 15) + end + end + + describe "Date" do + test "Date.now returns number", %{rt: rt} do + ok(rt, "(function(){ return typeof Date.now() })()", "number") + end + + test "new Date().getTime()", %{rt: rt} do + ok(rt, "(function(){ return typeof new Date().getTime() })()", "number") + end + end + + describe "WeakMap" do + test "set and get", %{rt: rt} do + ok( + rt, + "(function(){ var w = new WeakMap(); var k = {}; w.set(k, 42); return w.get(k) })()", + 42 + ) + end + end + + describe "Object methods" do + test "Object.create", %{rt: rt} do + ok(rt, "(function(){ var p = {x:42}; var o = Object.create(p); return o.x })()", 42) + end + + test "Object.freeze", %{rt: rt} do + ok(rt, "(function(){ var o = {x:1}; Object.freeze(o); o.x = 2; return o.x })()", 1) + end + + test "Object.keys on class instance", %{rt: rt} do + ok( + rt, + "(function(){ class A { constructor() { this.x = 1; this.y = 2 } } return Object.keys(new A()).length })()", + 2 + ) + end + end + + describe "Error types" do + test "new Error message", %{rt: rt} do + ok(rt, "(function(){ return new Error('boom').message })()", "boom") + end + + test "Error instanceof", %{rt: rt} do + ok(rt, "(function(){ return new Error() instanceof Error })()", true) + end + + test "TypeError instanceof", %{rt: rt} do + ok(rt, "(function(){ return new TypeError() instanceof TypeError })()", true) + end + end + + describe "regexp" do + test "regexp test", %{rt: rt} do + ok(rt, "(function(){ return /abc/.test('xabcy') })()", true) + end + + test "regexp exec group", %{rt: rt} do + ok(rt, "(function(){ return /a(b)c/.exec('xabcy')[1] })()", "b") + end + end + + describe "Promise" do + test "Promise.prototype exposes then", %{rt: rt} do + ok(rt, "typeof Promise.prototype.then", "function") + ok(rt, "typeof Promise.resolve(1).then", "function") + end + + test "Promise.resolve then", %{rt: rt} do + ok(rt, "(async function(){ return await Promise.resolve(42) })()", 42) + end + + test "Promise.all", %{rt: rt} do + ok( + rt, + "(async function(){ var r = await Promise.all([Promise.resolve(1), Promise.resolve(2)]); return r.length })()", + 2 + ) + end + end + + describe "async generators" do + test "async generator next", %{rt: rt} do + ok( + rt, + "(async function(){ async function* ag() { yield 1 } var g = ag(); var r = await g.next(); return r.value })()", + 1 + ) + end + end + + describe "yield* delegation" do + test "yield* forwards values", %{rt: rt} do + ok( + rt, + "(function(){ function* a() { yield 1; yield 2 } function* b() { yield* a(); yield 3 } var r = []; for (var x of b()) r.push(x); return r.join(',') })()", + "1,2,3" + ) + end + end + + describe "Array new methods" do + test "at", %{rt: rt} do + ok(rt, "(function(){ return [1,2,3].at(-1) })()", 3) + end + + test "findLast", %{rt: rt} do + ok(rt, "(function(){ return [1,2,3,4].findLast(function(x){return x<3}) })()", 2) + end + + test "toReversed", %{rt: rt} do + ok(rt, "(function(){ return [1,2,3].toReversed().join(',') })()", "3,2,1") + end + end + + describe "String.at" do + test "positive index", %{rt: rt} do + ok(rt, "(function(){ return 'hello'.at(1) })()", "e") + end + + test "negative index", %{rt: rt} do + ok(rt, "(function(){ return 'hello'.at(-1) })()", "o") + end + end + + describe "Object new methods" do + test "fromEntries", %{rt: rt} do + ok(rt, "(function(){ return Object.fromEntries([['a',1],['b',2]]).a })()", 1) + end + + test "hasOwn", %{rt: rt} do + ok(rt, "(function(){ return Object.hasOwn({x:1}, 'x') })()", true) + end + end + + describe "Function properties" do + test "name", %{rt: rt} do + ok(rt, "(function(){ function foo() {} return foo.name })()", "foo") + end + + test "length", %{rt: rt} do + ok(rt, "(function(){ function foo(a,b,c) {} return foo.length })()", 3) + end + end + + describe "microtask queue" do + test "then chaining", %{rt: rt} do + ok( + rt, + "(async function(){ return await Promise.resolve(1).then(function(v){ return v + 1 }).then(function(v){ return v * 10 }) })()", + 20 + ) + end + + test "microtask ordering", %{rt: rt} do + ok( + rt, + "(async function(){ var log = []; log.push(1); Promise.resolve().then(function(){ log.push(3) }); log.push(2); await Promise.resolve(); return log.join(',') })()", + "1,2,3" + ) + end + + test "catch rejected promise", %{rt: rt} do + ok( + rt, + "(async function(){ return await Promise.reject('err').catch(function(e){ return e + '!' }) })()", + "err!" + ) + end + + test "queueMicrotask", %{rt: rt} do + ok( + rt, + "(async function(){ var x = 0; queueMicrotask(function(){ x = 42 }); await Promise.resolve(); return x })()", + 42 + ) + end + end + + # ── Edge cases ── + + describe "edge cases" do + test "empty function returns undefined", %{rt: rt} do + ok(rt, "(function(){})()", nil) + end + + test "void 0", %{rt: rt} do + ok(rt, "void 0", nil) + end + + test "comma operator", %{rt: rt} do + ok(rt, "(1, 2, 3)", 3) + end + + test "property access on primitives", %{rt: rt} do + ok(rt, ~s|"hello"[0]|, "h") + ok(rt, ~s|"hello"["length"]|, 5) + end + + test "toFixed", %{rt: rt} do + ok(rt, "(3.14159).toFixed(2)", "3.14") + end + + test "String()", %{rt: rt} do + ok(rt, "String(42)", "42") + ok(rt, "String(true)", "true") + ok(rt, "String(null)", "null") + end + + test "Number()", %{rt: rt} do + ok(rt, ~s|Number("42")|, 42) + ok(rt, ~s|Number("3.14")|, 3.14) + end + + test "Boolean()", %{rt: rt} do + ok(rt, "Boolean(0)", false) + ok(rt, "Boolean(1)", true) + ok(rt, ~s|Boolean("")|, false) + ok(rt, ~s|Boolean("hi")|, true) + end + end +end diff --git a/test/vm/beam_mode_test.exs b/test/vm/beam_mode_test.exs new file mode 100644 index 00000000..92257f9e --- /dev/null +++ b/test/vm/beam_mode_test.exs @@ -0,0 +1,107 @@ +defmodule QuickBEAM.BeamModeTest do + use ExUnit.Case, async: true + + setup_all do + {:ok, rt} = QuickBEAM.start() + %{rt: rt} + end + + defp eval_beam(rt, code) do + QuickBEAM.eval(rt, code, mode: :beam) + end + + describe "basic types (beam mode)" do + test "numbers", %{rt: rt} do + assert {:ok, 3} = eval_beam(rt, "1 + 2") + assert {:ok, 42} = eval_beam(rt, "42") + assert {:ok, 3.14} = eval_beam(rt, "3.14") + end + + test "booleans", %{rt: rt} do + assert {:ok, true} = eval_beam(rt, "true") + assert {:ok, false} = eval_beam(rt, "false") + end + + test "null and undefined", %{rt: rt} do + assert {:ok, nil} = eval_beam(rt, "null") + assert {:ok, nil} = eval_beam(rt, "undefined") + end + + test "strings", %{rt: rt} do + assert {:ok, "hello"} = eval_beam(rt, ~s["hello"]) + assert {:ok, ""} = eval_beam(rt, ~s[""]) + assert {:ok, "hello world"} = eval_beam(rt, ~s["hello world"]) + end + end + + describe "arithmetic" do + test "operations", %{rt: rt} do + assert {:ok, 6} = eval_beam(rt, "2 * 3") + assert {:ok, 7} = eval_beam(rt, "10 - 3") + assert {:ok, 5.0} = eval_beam(rt, "10 / 2") + assert {:ok, 1} = eval_beam(rt, "10 % 3") + end + + test "precedence", %{rt: rt} do + assert {:ok, 14} = eval_beam(rt, "2 + 3 * 4") + assert {:ok, 20} = eval_beam(rt, "(2 + 3) * 4") + end + end + + describe "functions" do + test "anonymous", %{rt: rt} do + assert {:ok, 42} = eval_beam(rt, "(function(x) { return x * 2; })(21)") + end + + test "closure", %{rt: rt} do + assert {:ok, 7} = eval_beam(rt, "(function() { var x = 3; var y = 4; return x + y; })()") + end + end + + describe "control flow" do + test "ternary", %{rt: rt} do + assert {:ok, "yes"} = eval_beam(rt, "true ? 'yes' : 'no'") + assert {:ok, "no"} = eval_beam(rt, "false ? 'yes' : 'no'") + end + + test "comparison", %{rt: rt} do + assert {:ok, true} = eval_beam(rt, "1 === 1") + assert {:ok, false} = eval_beam(rt, "1 === 2") + assert {:ok, true} = eval_beam(rt, "1 !== 2") + end + end + + describe "objects" do + test "property access", %{rt: rt} do + assert {:ok, "test"} = eval_beam(rt, ~s|({name: "test"}).name|) + end + end + + describe "arrays" do + test "literal length", %{rt: rt} do + assert {:ok, 3} = eval_beam(rt, "[1, 2, 3].length") + end + + test "indexing", %{rt: rt} do + assert {:ok, 20} = eval_beam(rt, "[10, 20, 30][1]") + end + end + + describe "built-ins" do + test "Math.floor", %{rt: rt} do + assert {:ok, 3} = eval_beam(rt, "Math.floor(3.7)") + end + end + + describe "loops" do + test "while loop", %{rt: rt} do + code = "(function() { var s = 0; var i = 0; while (i < 10) { s += i; i++; } return s; })()" + assert {:ok, 45} = eval_beam(rt, code) + end + + test "for loop", %{rt: rt} do + code = "(function() { var s = 0; for (var i = 0; i < 10; i++) { s += i; } return s; })()" + assert {:ok, 45} = eval_beam(rt, code) + end + end +end diff --git a/test/vm/bytecode_test.exs b/test/vm/bytecode_test.exs new file mode 100644 index 00000000..a0f98b0e --- /dev/null +++ b/test/vm/bytecode_test.exs @@ -0,0 +1,234 @@ +defmodule QuickBEAM.VM.BytecodeTest do + use ExUnit.Case, async: true + + alias QuickBEAM.VM.Bytecode + + setup do + {:ok, rt} = QuickBEAM.start() + + on_exit(fn -> + try do + QuickBEAM.stop(rt) + catch + :exit, _ -> :ok + end + end) + + %{rt: rt} + end + + # Helper: compile JS code and decode the bytecode. + # The top-level is always an eval wrapper; extract the first Function from cpool. + defp compile_and_decode(rt, code) do + {:ok, bc} = QuickBEAM.compile(rt, code) + {:ok, parsed} = Bytecode.decode(bc) + parsed + end + + # Get the first user function from the constant pool (skipping the eval wrapper) + defp user_function(parsed) do + fun = parsed.value + # For simple expressions, the top-level function IS the eval wrapper. + # The actual code is in the top-level function itself. + # For function expressions, the user function is in the cpool. + inner = for %Bytecode.Function{} = f <- fun.constants, do: f + + case inner do + [first | _] -> first + [] -> fun + end + end + + describe "decode/1 structure" do + test "parses version and atom table", %{rt: rt} do + parsed = compile_and_decode(rt, "42") + assert parsed.version == 25 + assert is_tuple(parsed.atoms) + end + + test "top-level is always a Function", %{rt: rt} do + parsed = compile_and_decode(rt, "42") + assert is_struct(parsed.value, Bytecode.Function) + end + end + + describe "simple expressions" do + test "integer literal", %{rt: rt} do + parsed = compile_and_decode(rt, "42") + fun = parsed.value + assert is_struct(fun, Bytecode.Function) + assert fun.arg_count == 0 + assert byte_size(fun.byte_code) > 0 + end + + test "string literal", %{rt: rt} do + parsed = compile_and_decode(rt, ~s|"hello"|) + fun = parsed.value + assert is_struct(fun, Bytecode.Function) + # String literals are pushed by bytecode ops, not stored in cpool for simple cases + assert fun.stack_size > 0 + assert byte_size(fun.byte_code) > 0 + end + + test "boolean, null, undefined", %{rt: rt} do + for code <- ["true", "null", "undefined"] do + parsed = compile_and_decode(rt, code) + assert is_struct(parsed.value, Bytecode.Function) + end + end + end + + describe "functions" do + test "simple add function", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(a,b){return a+b})") + fun = user_function(parsed) + + assert fun.arg_count == 2 + assert fun.var_count == 0 + assert fun.stack_size > 0 + assert byte_size(fun.byte_code) > 0 + end + + test "function with locals", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(n){let s=0;for(let i=0;i hd() |> Map.get(:name) == "x" + end + + test "recursive function", %{rt: rt} do + parsed = compile_and_decode(rt, "(function f(n){return n<=1?n:f(n-1)+f(n-2)})") + fun = user_function(parsed) + # Named function — name should be "f" + assert fun.name == "f" + end + end + + describe "objects and arrays" do + test "object literal", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(){return {a:1,b:2}})") + fun = user_function(parsed) + assert is_list(fun.constants) + end + + test "array literal", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(){return [1,2,3]})") + fun = user_function(parsed) + assert is_struct(fun, Bytecode.Function) + end + end + + describe "control flow" do + test "if/else", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(x){if(x>0)return 1;else return -1})") + fun = user_function(parsed) + assert fun.arg_count == 1 + end + + test "try/catch", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(){try{throw 1}catch(e){return e}})") + fun = user_function(parsed) + assert is_struct(fun, Bytecode.Function) + end + + test "for/in", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(o){let s=0;for(let k in o)s+=o[k];return s})") + fun = user_function(parsed) + assert fun.arg_count == 1 + end + end + + describe "advanced features" do + test "arrow functions in map", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(){return [1,2,3].map(x=>x*2)})") + fun = user_function(parsed) + inner_funs = for %Bytecode.Function{} = f <- fun.constants, do: f + assert inner_funs != [] + end + + test "class", %{rt: rt} do + parsed = + compile_and_decode(rt, "(function(){class A{constructor(x){this.x=x}} return new A(1)})") + + fun = user_function(parsed) + assert is_struct(fun, Bytecode.Function) + end + + test "destructuring", %{rt: rt} do + parsed = compile_and_decode(rt, "(function({a,b}){return a+b})") + fun = user_function(parsed) + assert fun.arg_count == 1 + end + + test "template literal", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(name){return `hello ${name}`})") + fun = user_function(parsed) + assert fun.arg_count == 1 + end + + test "async function", %{rt: rt} do + parsed = compile_and_decode(rt, "(async function(){return await 42})") + fun = user_function(parsed) + assert fun.func_kind in [2, 3] + end + end + + describe "error cases" do + test "bad version", %{rt: rt} do + {:ok, bc} = QuickBEAM.compile(rt, "42") + bad_bc = <<0, binary_part(bc, 1, byte_size(bc) - 1)::binary>> + assert {:error, {:bad_version, 0}} = Bytecode.decode(bad_bc) + end + + test "truncated data" do + assert {:error, _} = Bytecode.decode(<<24, 0, 0, 0, 0>>) + end + + test "empty binary" do + assert {:error, _} = Bytecode.decode(<<>>) + end + end + + describe "atoms" do + test "atom table is populated", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(a,b){return a+b})") + atom_list = Tuple.to_list(parsed.atoms) + assert "a" in atom_list + assert "b" in atom_list + end + end + + describe "locals" do + test "var defs have correct names", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(x){let y=1;let z=2;return x+y+z})") + fun = user_function(parsed) + names = Enum.map(fun.locals, & &1.name) + assert "x" in names + assert "y" in names + assert "z" in names + end + + test "let vs var vs const", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(){let a=1;var b=2;const c=3;return a+b+c})") + fun = user_function(parsed) + locals_by_name = Map.new(fun.locals, &{&1.name, &1}) + assert locals_by_name["a"].is_lexical + assert not locals_by_name["b"].is_lexical + assert locals_by_name["c"].is_const + end + end +end diff --git a/test/vm/compiler/analysis_test.exs b/test/vm/compiler/analysis_test.exs new file mode 100644 index 00000000..d71988e8 --- /dev/null +++ b/test/vm/compiler/analysis_test.exs @@ -0,0 +1,83 @@ +defmodule QuickBEAM.VM.Compiler.AnalysisTest do + use ExUnit.Case, async: true + + alias QuickBEAM.VM.{Bytecode, Decoder, Heap} + alias QuickBEAM.VM.Compiler.Analysis.{CFG, Stack, Types} + + setup do + Heap.reset() + {:ok, rt} = QuickBEAM.start() + + on_exit(fn -> + try do + QuickBEAM.stop(rt) + catch + :exit, _ -> :ok + end + end) + + %{rt: rt} + end + + defp compile_parsed(rt, code) do + {:ok, bc} = QuickBEAM.compile(rt, code) + {:ok, parsed} = Bytecode.decode(bc) + parsed + end + + defp compile_function(rt, code) do + parsed = compile_parsed(rt, code) + + case for %Bytecode.Function{} = fun <- parsed.value.constants, do: fun do + [fun | _] -> fun + [] -> parsed.value + end + end + + defp infer_types(fun) do + {:ok, instructions} = Decoder.decode(fun.byte_code, fun.arg_count) + entries = CFG.block_entries(instructions) + {:ok, stack_depths} = Stack.infer_block_stack_depths(instructions, entries) + + {:ok, {entry_types, return_type}} = + Types.infer_block_entry_types(fun, instructions, entries, stack_depths) + + {entry_types, return_type} + end + + test "infers recursive self-call return type from literal base cases", %{rt: rt} do + fun = compile_function(rt, "(function f(n){ return n ? f(n - 1) : 0 })") + + {_entry_types, return_type} = infer_types(fun) + + assert return_type == :integer + end + + test "propagates numeric local types across loop backedges", %{rt: rt} do + fun = + compile_function(rt, "(function(n){let s=0; let i=0; while(i + try do + QuickBEAM.stop(rt) + catch + :exit, _ -> :ok + end + end) + + %{rt: rt} + end + + defp compile_and_decode(rt, code) do + {:ok, bc} = QuickBEAM.compile(rt, code) + {:ok, parsed} = Bytecode.decode(bc) + cache_function_atoms(parsed.value, parsed.atoms) + parsed + end + + defp cache_function_atoms(%Bytecode.Function{} = fun, atoms) do + Process.put({:qb_fn_atoms, fun.byte_code}, atoms) + + Enum.each(fun.constants, fn + %Bytecode.Function{} = inner -> cache_function_atoms(inner, atoms) + _ -> :ok + end) + end + + defp user_function(parsed) do + case for %Bytecode.Function{} = fun <- parsed.value.constants, do: fun do + [fun | _] -> fun + [] -> parsed.value + end + end + + defp beam_extfuncs({:beam_file, _module, _exports, _attributes, _compile_info, code}) do + for {:function, _name, _arity, _label, instructions} <- code, + {op, _argc, {:extfunc, mod, fun, arity}} <- instructions, + op in [:call_ext, :call_ext_last, :call_ext_only] do + {mod, fun, arity} + end + end + + defp beam_function_instructions( + {:beam_file, _module, _exports, _attributes, _compile_info, code}, + name + ) do + Enum.find_value(code, fn + {:function, ^name, _arity, _label, instructions} -> instructions + _ -> nil + end) + end + + describe "compile/1" do + test "compiles a straight-line arithmetic function", %{rt: rt} do + fun = compile_and_decode(rt, "(function(a,b){return a+b})") |> user_function() + + assert {:ok, {_mod, :run_ctx}} = Compiler.compile(fun) + assert {:ok, 7} = Compiler.invoke(fun, [3, 4]) + end + + test "compiles locals and reassignment in straight-line code", %{rt: rt} do + fun = compile_and_decode(rt, "(function(a){let x=1; x=x+a; return x})") |> user_function() + + assert {:ok, 6} = Compiler.invoke(fun, [5]) + end + + test "compiles top-level var declarations and writes", %{rt: rt} do + root = compile_and_decode(rt, "var x = 1; x = x + 2; x").value + + assert {:ok, {_mod, :run_ctx}} = Compiler.compile(root) + assert {:ok, 3} = Compiler.invoke(root, []) + end + + test "compiles top-level function declarations", %{rt: rt} do + root = compile_and_decode(rt, "function inc(x){ return x + 1 } inc(2)").value + + assert {:ok, {_mod, :run_ctx}} = Compiler.compile(root) + assert {:ok, 3} = Compiler.invoke(root, []) + end + + test "compiled disasm skips TDZ helper after initialized unknown locals", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(f){ const value = f(); return value + value })") + |> user_function() + + assert {:ok, beam_file} = Compiler.disasm(fun) + refute {RuntimeHelpers, :ensure_initialized_local!, 1} in beam_extfuncs(beam_file) + + callback = {:builtin, "one", fn [], _ -> 1 end} + assert {:ok, 2} = Compiler.invoke(fun, [callback]) + end + + test "compiles conditional branches", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(x){if(x>0)return 1;else return 2})") |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, [3]) + assert {:ok, 2} = Compiler.invoke(fun, [-1]) + end + + test "compiles simple while loops", %{rt: rt} do + code = "(function(n){let s=0; let i=0; while(i user_function() + + assert {:ok, beam_file} = Compiler.disasm(fun) + refute {QuickBEAM.VM.Interpreter.Values, :truthy?, 1} in beam_extfuncs(beam_file) + + block = beam_function_instructions(beam_file, :block_6) + + assert Enum.any?(block, fn + {:test, :is_number, _, _} -> true + _ -> false + end) + + assert Enum.any?(block, fn + {:bif, :<, _, _, _} -> true + _ -> false + end) + + assert {:ok, 10} = Compiler.invoke(fun, [5]) + end + + test "compiles loops over array length and array indexing", %{rt: rt} do + code = + "(function(arr){let s=0; let i=0; while(i user_function() + + assert {:ok, 10} = Compiler.invoke(fun, [Heap.wrap([1, 2, 3, 4])]) + end + + test "compiles object destructuring", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(obj){ const {x} = obj; return x })") |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [Heap.wrap(%{"x" => 7})]) + end + + test "compiles regexp literals", %{rt: rt} do + fun = compile_and_decode(rt, "(function(){ return /a+/.test('aa') })") |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "compiles object field access", %{rt: rt} do + fun = compile_and_decode(rt, "(function(obj){return obj.x})") |> user_function() + + assert {:ok, beam_file} = Compiler.disasm(fun) + block = beam_function_instructions(beam_file, :block_0) + + assert Enum.any?(block, fn + {:call, 2, {_, :op_get_field, 2}} -> true + {:call_only, 2, {_, :op_get_field, 2}} -> true + {:call_last, 2, {_, :op_get_field, 2}, _} -> true + _ -> false + end) + + assert {:ok, 7} = Compiler.invoke(fun, [Heap.wrap(%{"x" => 7})]) + end + + test "compiles object creation plus field writes", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(v){ let o={}; o.x=v; return o.x })") |> user_function() + + assert {:ok, 9} = Compiler.invoke(fun, [9]) + end + + test "compiles object literals", %{rt: rt} do + fun = compile_and_decode(rt, "(function(v){ return {x:v} })") |> user_function() + + assert {:ok, beam_file} = Compiler.disasm(fun) + block = beam_function_instructions(beam_file, :block_0) + + assert Enum.any?(block, fn + {:call_ext, 1, {:extfunc, QuickBEAM.VM.Heap, :wrap, 1}} -> true + {:call_ext_last, 1, {:extfunc, QuickBEAM.VM.Heap, :wrap, 1}, _} -> true + {:call_ext_only, 1, {:extfunc, QuickBEAM.VM.Heap, :wrap, 1}} -> true + {:call_ext, 2, {:extfunc, QuickBEAM.VM.Heap, :wrap_keyed, 2}} -> true + {:call_ext_last, 2, {:extfunc, QuickBEAM.VM.Heap, :wrap_keyed, 2}, _} -> true + {:call_ext_only, 2, {:extfunc, QuickBEAM.VM.Heap, :wrap_keyed, 2}} -> true + _ -> false + end) + + assert {:ok, {:obj, ref}} = Compiler.invoke(fun, [5]) + assert %{"x" => 5} = Heap.get_obj(ref) + end + + test "compiles function calls through arguments", %{rt: rt} do + fun = compile_and_decode(rt, "(function(f,x){return f(x)})") |> user_function() + callback = {:builtin, "double", fn [x], _ -> x * 2 end} + + assert {:ok, 8} = Compiler.invoke(fun, [callback, 4]) + end + + test "fuses captured var-ref calls into one runtime helper", %{rt: rt} do + outer = + compile_and_decode(rt, "(function(f){ return function(x){ return f(x) } })") + |> user_function() + + inner = Enum.find(outer.constants, &match?(%Bytecode.Function{closure_vars: [_ | _]}, &1)) + assert %Bytecode.Function{} = inner + + assert {:ok, beam_file} = Compiler.disasm(inner) + block = beam_function_instructions(beam_file, :block_0) + + assert Enum.any?(block, fn + {:call_ext, 2, {:extfunc, QuickBEAM.VM.Compiler.RuntimeHelpers, :get_capture, 2}} -> true + {:call_ext_last, 2, {:extfunc, QuickBEAM.VM.Compiler.RuntimeHelpers, :get_capture, 2}, _} -> true + _ -> false + end) + + callback = {:builtin, "double", fn [x], _ -> x * 2 end} + assert {:ok, {:closure, _, _} = closure} = Compiler.invoke(outer, [callback]) + assert {:ok, 8} = Compiler.invoke(closure, [4]) + end + + test "compiles captured var-ref calls with more than three arguments", %{rt: rt} do + outer = + compile_and_decode(rt, "(function(f){ return function(a,b,c,d){ return f(a,b,c,d) } })") + |> user_function() + + callback = {:builtin, "sum4", fn [a, b, c, d], _ -> a + b + c + d end} + assert {:ok, {:closure, _, _} = closure} = Compiler.invoke(outer, [callback]) + assert {:ok, 10} = Compiler.invoke(closure, [1, 2, 3, 4]) + end + + test "compiles transitive captured closures", %{rt: rt} do + outer = + compile_and_decode( + rt, + "(function(f){ return function(){ return function(x){ return f(x) } } })" + ) + |> user_function() + + callback = {:builtin, "double", fn [x], _ -> x * 2 end} + assert {:ok, {:closure, _, _} = mid} = Compiler.invoke(outer, [callback]) + assert {:ok, {:closure, _, _} = inner} = Compiler.invoke(mid, []) + assert {:ok, 8} = Compiler.invoke(inner, [4]) + end + + test "compiles method calls with receiver", %{rt: rt} do + fun = compile_and_decode(rt, "(function(o,x){return o.inc(x)})") |> user_function() + + assert {:ok, beam_file} = Compiler.disasm(fun) + refute {RuntimeHelpers, :invoke_method_runtime, 4} in beam_extfuncs(beam_file) + + obj = + Heap.wrap(%{ + "base" => 10, + "inc" => {:builtin, "inc", fn [x], this -> Get.get(this, "base") + x end} + }) + + assert {:ok, 13} = Compiler.invoke(fun, [obj, 3]) + end + + test "compiles global lookup plus method call", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){return Math.abs(x)})") |> user_function() + + assert {:ok, 12} = Compiler.invoke(fun, [-12]) + end + + test "compiles array writes with indexed reads", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(v){ let a=[]; a[0]=v; return a[0] })") + |> user_function() + + assert {:ok, 11} = Compiler.invoke(fun, [11]) + end + + test "compiles compound array updates", %{rt: rt} do + fun = compile_and_decode(rt, "(function(a,v){ a[0] += v; return a[0] })") |> user_function() + + assert {:ok, 8} = Compiler.invoke(fun, [Heap.wrap([3]), 5]) + end + + test "compiles loose-null checks before indexed writes", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(i,v){ if (i == null) i = 0; let a=[]; a[i]=v; return a[i] })" + ) + |> user_function() + + assert {:ok, 12} = Compiler.invoke(fun, [nil, 12]) + assert {:ok, 13} = Compiler.invoke(fun, [1, 13]) + end + + test "compiles local increments", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ x++; return x })") |> user_function() + + assert {:ok, 6} = Compiler.invoke(fun, [5]) + end + + test "compiles post-increment expression results", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ return x++ })") |> user_function() + + assert {:ok, 5} = Compiler.invoke(fun, [5]) + end + + test "compiles exponentiation", %{rt: rt} do + fun = compile_and_decode(rt, "(function(a,b){ return a ** b })") |> user_function() + + assert {:ok, 8.0} = Compiler.invoke(fun, [2, 3]) + end + + test "compiles bitwise operators", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(a,b){ return ((a & b) ^ 1) << 2 })") |> user_function() + + assert {:ok, 0} = Compiler.invoke(fun, [3, 1]) + end + + test "compiles modulo", %{rt: rt} do + fun = compile_and_decode(rt, "(function(a,b){ return a % b })") |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, [10, 3]) + end + + test "compiles logical not", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ return !x })") |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, [0]) + assert {:ok, false} = Compiler.invoke(fun, [1]) + end + + test "compiles bitwise not", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ return ~x })") |> user_function() + + assert {:ok, -6} = Compiler.invoke(fun, [5]) + end + + test "compiles typeof", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ return typeof x })") |> user_function() + + assert {:ok, "number"} = Compiler.invoke(fun, [5]) + assert {:ok, "undefined"} = Compiler.invoke(fun, [:undefined]) + end + + test "compiles specialized typeof comparisons", %{rt: rt} do + function_fun = + compile_and_decode(rt, "(function(x){ return typeof x === 'function' })") + |> user_function() + + undefined_fun = + compile_and_decode(rt, "(function(x){ return typeof x === 'undefined' })") + |> user_function() + + assert {:ok, true} = + Compiler.invoke(function_fun, [{:builtin, "noop", fn _, _ -> :undefined end}]) + + assert {:ok, false} = Compiler.invoke(function_fun, [5]) + assert {:ok, true} = Compiler.invoke(undefined_fun, [:undefined]) + assert {:ok, true} = Compiler.invoke(undefined_fun, [nil]) + assert {:ok, false} = Compiler.invoke(undefined_fun, [0]) + end + + test "compiles null checks", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ return x === null })") |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, [nil]) + assert {:ok, false} = Compiler.invoke(fun, [:undefined]) + end + + test "compiles in operator", %{rt: rt} do + fun = compile_and_decode(rt, "(function(k,o){ return k in o })") |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, ["x", Heap.wrap(%{"x" => 1})]) + assert {:ok, false} = Compiler.invoke(fun, ["y", Heap.wrap(%{"x" => 1})]) + end + + test "compiles delete with atom property names", %{rt: rt} do + fun = compile_and_decode(rt, "(function(o){ delete o.x; return o.x })") |> user_function() + + assert {:ok, :undefined} = Compiler.invoke(fun, [Heap.wrap(%{"x" => 7})]) + end + + test "compiles instanceof", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(obj, ctor){ return obj instanceof ctor })") + |> user_function() + + parent_proto = Heap.wrap(%{}) + child = Heap.wrap(%{proto() => parent_proto}) + ctor = Heap.wrap(%{"prototype" => parent_proto}) + + assert {:ok, true} = Compiler.invoke(fun, [child, ctor]) + assert {:ok, false} = Compiler.invoke(fun, [5, ctor]) + end + + test "compiles instanceof through prototype chains", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(obj, ctor){ return obj instanceof ctor })") + |> user_function() + + parent_proto = Heap.wrap(%{}) + mid_proto = Heap.wrap(%{proto() => parent_proto}) + child = Heap.wrap(%{proto() => mid_proto}) + ctor = Heap.wrap(%{"prototype" => parent_proto}) + + assert {:ok, true} = Compiler.invoke(fun, [child, ctor]) + end + + test "compiles constructor calls", %{rt: rt} do + ctor = compile_and_decode(rt, "(function A(x){ this.x = x })") |> user_function() + fun = compile_and_decode(rt, "(function(C,x){ return new C(x).x })") |> user_function() + + assert {:ok, 9} = Compiler.invoke(fun, [ctor, 9]) + end + + test "compiles constructor calls without arguments", %{rt: rt} do + ctor = compile_and_decode(rt, "(function A(){ this.x = 1 })") |> user_function() + fun = compile_and_decode(rt, "(function(C){ return new C().x })") |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, [ctor]) + end + + test "compiles constructor calls used in later control flow", %{rt: rt} do + ctor = compile_and_decode(rt, "(function A(x){ this.x = x })") |> user_function() + + fun = + compile_and_decode( + rt, + "(function(C,x){ const o = {}; o.value = new C(x); let i = 0; if (i < x) return o.value.x; return 0 })" + ) + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [ctor, 7]) + end + + test "compiles wrapped non-capturing closures", %{rt: rt} do + fun = compile_and_decode(rt, "(function(x){ return x + 1 })") |> user_function() + + assert {:ok, 6} = Compiler.invoke({:closure, %{}, fun}, [5]) + assert match?({:compiled, _, _}, Heap.get_compiled({fun.byte_code, fun.arg_count})) + end + + test "compiles class constructor closures without var ref reads", %{rt: rt} do + outer = + compile_and_decode( + rt, + "(function(){ class A { constructor(x){ this.x = x } } return A })" + ) + |> user_function() + + ctor = + Enum.find(outer.constants, fn + %Bytecode.Function{source: source} when is_binary(source) -> + String.contains?(source, "constructor") + + _ -> + false + end) + + assert %Bytecode.Function{var_ref_count: 0} = ctor + + closure = {:closure, %{}, ctor} + + assert {:obj, ref} = RuntimeHelpers.construct_runtime(closure, closure, [9]) + assert 9 == Heap.get_obj(ref)["x"] + assert match?({:compiled, _, _}, Heap.get_compiled({ctor.byte_code, ctor.arg_count})) + end + + test "compiles array spread", %{rt: rt} do + fun = compile_and_decode(rt, "(function(a){ return [...a].length })") |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, [Heap.wrap([1, 2, 3])]) + end + + test "compiles object spread", %{rt: rt} do + fun = compile_and_decode(rt, "(function(o){ return {...o}.x })") |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [Heap.wrap(%{"x" => 7})]) + end + + test "compiles object spread followed by field definition", %{rt: rt} do + fun = compile_and_decode(rt, "(function(o){ return {...o, y:1}.y })") |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, [Heap.wrap(%{"x" => 7})]) + end + + test "compiles for-of loops over arrays", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(a){ let s=0; for (const x of a) s += x; return s })") + |> user_function() + + assert {:ok, 10} = Compiler.invoke(fun, [Heap.wrap([1, 2, 3, 4])]) + end + + test "compiles for-of loops over strings", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(s){ let out=''; for (const ch of s) out += ch; return out })" + ) + |> user_function() + + assert {:ok, "abc"} = Compiler.invoke(fun, ["abc"]) + end + + test "compiles try catch around explicit throws", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(e){ try { throw e } catch(err) { return err } })") + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [7]) + end + + test "compiles try catch around throwing calls", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(f){ try { return f() } catch(err) { return err } })") + |> user_function() + + throwing_fun = {:builtin, "boom", fn [], _ -> throw({:js_throw, 11}) end} + + assert {:ok, 11} = Compiler.invoke(fun, [throwing_fun]) + end + + test "compiles nested try catch rethrows", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(f){ try { try { return f() } catch(err) { throw err } } catch(err) { return err } })" + ) + |> user_function() + + throwing_fun = {:builtin, "boom", fn [], _ -> throw({:js_throw, 13}) end} + + assert {:ok, 13} = Compiler.invoke(fun, [throwing_fun]) + end + + test "compiles for-in loops over object keys", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(o){ let s=''; for (const k in o) s += k; return s })") + |> user_function() + + assert {:ok, "ab"} = Compiler.invoke(fun, [Heap.wrap(%{"a" => 1, "b" => 2})]) + end + + test "compiles for-in loops over array indexes", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(a){ let s=''; for (const k in a) s += k; return s })") + |> user_function() + + assert {:ok, "012"} = Compiler.invoke(fun, [Heap.wrap([10, 20, 30])]) + end + + test "compiles empty for-in fallthrough", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(o){ for (const k in o) return k; return 'none' })") + |> user_function() + + assert {:ok, "none"} = Compiler.invoke(fun, [Heap.wrap(%{})]) + end + + test "compiles try finally with side effects", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(){ var x=0; try { x=1 } finally { x=2 } return x })") + |> user_function() + + assert {:ok, 2} = Compiler.invoke(fun, []) + end + + test "compiles try catch finally", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ var x=0; try { throw 'err' } catch(e) { x=1 } finally { x+=1 } return x })" + ) + |> user_function() + + assert {:ok, 2} = Compiler.invoke(fun, []) + end + + test "compiles try finally around returns", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(f){ try { return f() } finally { 1 } })") + |> user_function() + + assert {:ok, 5} = Compiler.invoke(fun, [{:builtin, "five", fn [], _ -> 5 end}]) + end + + test "compiles nested plain functions", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(){ function f(a,b){ return a+b } return f(1,2) })") + |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, []) + end + + test "compiles nested rest-parameter functions", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ function f(...args){ return args.length } return f(1,2,3) })" + ) + |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, []) + end + + test "compiles nested default-parameter functions", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(){ function f(a,b=10){ return a+b } return f(5) })") + |> user_function() + + assert {:ok, 15} = Compiler.invoke(fun, []) + end + + test "compiles nested captured-argument functions", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(x){ function f(y){ return x+y } return f(2) })") + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [5]) + end + + test "compiles nested captured-local updates", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(x){ let y=x; function f(z){ return y+z } y=5; return f(2) })" + ) + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [1]) + end + + test "compiles nested closures that mutate captured locals", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ let x=1; function f(){ x+=1; return x } return f()+f() })" + ) + |> user_function() + + assert {:ok, 5} = Compiler.invoke(fun, []) + end + + test "compiles arrow closures with inferred names", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(x){ const f = (y) => x + y; return f(2) })") + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [5]) + end + + test "compiles object literal methods", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(){ return { m(){ return 1 } }.m() })") + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "compiles object literal methods with captures", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(x){ return { m(y){ return x+y } }.m(2) })") + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, [5]) + end + + test "compiles computed object literal methods", %{rt: rt} do + fun = + compile_and_decode(rt, ~s|(function(){ return ({ ["m"](){ return 1 } })["m"]() })|) + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "compiles computed-name function expressions", %{rt: rt} do + fun = + compile_and_decode( + rt, + ~s|(function(){ const n = "x"; return ({ [n]: function(){ return 1 } })[n]() })| + ) + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "compiles simple classes", %{rt: rt} do + fun = + compile_and_decode(rt, "(function(){ class A { m(){ return 1 } } return new A().m() })") + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "keeps class prototype methods non-enumerable", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { m(){ return 1 } } return [Object.keys(A.prototype).length, A.prototype.propertyIsEnumerable(\"constructor\"), A.prototype.propertyIsEnumerable(\"m\")] })" + ) + |> user_function() + + assert {:ok, {:obj, ref}} = Compiler.invoke(fun, []) + assert [0, false, false] = Heap.to_list({:obj, ref}) + end + + test "keeps class prototype accessors non-enumerable", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { get x(){ return 1 } set x(v){} } return [Object.keys(A.prototype).length, A.prototype.propertyIsEnumerable(\"x\")] })" + ) + |> user_function() + + assert {:ok, {:obj, ref}} = Compiler.invoke(fun, []) + assert [0, false] = Heap.to_list({:obj, ref}) + end + + test "compiles classes with constructors", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { constructor(x){ this.x=x } } return new A(3).x })" + ) + |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, []) + end + + test "compiles class inheritance with super methods", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { m(){ return 1 } } class B extends A { m(){ return super.m()+1 } } return new B().m() })" + ) + |> user_function() + + assert {:ok, 2} = Compiler.invoke(fun, []) + end + + test "compiles private field classes", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #x = 42; get() { return this.#x } } return new A().get() })" + ) + |> user_function() + + assert {:ok, 42} = Compiler.invoke(fun, []) + end + + test "compiles private field setters", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #x = 0; set(v) { this.#x = v } get() { return this.#x } } var a = new A(); a.set(99); return a.get() })" + ) + |> user_function() + + assert {:ok, 99} = Compiler.invoke(fun, []) + end + + test "compiles private methods", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #m() { return 3 } get() { return this.#m() } } return new A().get() })" + ) + |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, []) + end + + test "compiles private accessors", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { get #x() { return 7 } read() { return this.#x } } return new A().read() })" + ) + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, []) + end + + test "compiles private static fields", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #x = 42; static get() { return A.#x } } return A.get() })" + ) + |> user_function() + + assert {:ok, 42} = Compiler.invoke(fun, []) + end + + test "compiles private static writes", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #x = 1; static set(v){ A.#x = v } static get(){ return A.#x } } A.set(9); return A.get() })" + ) + |> user_function() + + assert {:ok, 9} = Compiler.invoke(fun, []) + end + + test "compiles private static methods", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #m(){ return 5 } static get(){ return A.#m() } } return A.get() })" + ) + |> user_function() + + assert {:ok, 5} = Compiler.invoke(fun, []) + end + + test "compiles private static accessors", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static get #x(){ return 7 } static read(){ return A.#x } } return A.read() })" + ) + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, []) + end + + test "compiles private static in checks", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #x = 1; static has(){ return #x in A } } return A.has() })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "rejects invalid private field receivers", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #x = 1; get(){ return this.#x } } const g = (new A()).get; try { return g.call({}) } catch (e) { return e instanceof TypeError } })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "rejects invalid private method receivers", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #m(){ return 1 } get(){ return this.#m() } } const g = (new A()).get; try { return g.call({}) } catch (e) { return e instanceof TypeError } })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "rejects invalid private receivers across classes", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #x = 1; get(o){ try { return o.#x } catch (e) { return e instanceof TypeError } } } class B {} return new A().get(new B()) })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "rejects invalid private static receivers across classes", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #x = 1; static get(o){ try { return o.#x } catch (e) { return e instanceof TypeError } } } class B {} return A.get(B) })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "rejects invalid private setters", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #x = 1; set(v){ this.#x = v } } const s = (new A()).set; try { s.call({}, 2); return false } catch (e) { return e instanceof TypeError } })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "supports private members on subclass instances", %{rt: rt} do + field_fun = + compile_and_decode( + rt, + "(function(){ class A { #x = 1; get(){ return this.#x } } class B extends A {} return new B().get() })" + ) + |> user_function() + + method_fun = + compile_and_decode( + rt, + "(function(){ class A { #m(){ return 1 } call(){ return this.#m() } } class B extends A {} return new B().call() })" + ) + |> user_function() + + assert {:ok, 1} = Compiler.invoke(field_fun, []) + assert {:ok, 1} = Compiler.invoke(method_fun, []) + end + + test "rejects inherited private static access", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #x = 1; static get(){ return this.#x } } class B extends A {} try { return B.get() } catch (e) { return e instanceof TypeError } })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "inherits static methods named call", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static call(){ return 1 } } class B extends A {} return B.call() })" + ) + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "rejects inherited private static methods", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #m(){ return 1 } static call(){ return this.#m() } } class B extends A {} try { return B.call() } catch (e) { return e instanceof TypeError } })" + ) + |> user_function() + + assert {:ok, true} = Compiler.invoke(fun, []) + end + + test "compiles private static blocks", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static #x = 1; static { this.#x += 2 } static get(){ return this.#x } } return A.get() })" + ) + |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, []) + end + + test "compiles private super calls", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { #m(){ return 1 } call(){ return this.#m() } } class B extends A { call2(){ return super.call() } } return new B().call2() })" + ) + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "compiles static super setters", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static set x(v){ this.y = v + 1 } } class B extends A { static g(){ super.x = 2; return this.y } } return B.g() })" + ) + |> user_function() + + assert {:ok, 3} = Compiler.invoke(fun, []) + end + + test "compiles static super getters", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static get x(){ return this.y } } class B extends A { static y = 7; static g(){ return super.x } } return B.g() })" + ) + |> user_function() + + assert {:ok, 7} = Compiler.invoke(fun, []) + end + + test "compiles computed static methods", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { static [\"m\"](){ return 1 } } return A.m() })" + ) + |> user_function() + + assert {:ok, 1} = Compiler.invoke(fun, []) + end + + test "propagates new.target through derived super calls", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { constructor(){ this.v = new.target.name } } class B extends A { constructor(...args){ super(...args) } } return new B().v })" + ) + |> user_function() + + assert {:ok, "B"} = Compiler.invoke(fun, []) + end + + test "compiles derived constructors returning objects", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ class A { constructor(){ this.a = 1 } } class B extends A { constructor(){ super(); return {b:2} } } return new B().b })" + ) + |> user_function() + + assert {:ok, 2} = Compiler.invoke(fun, []) + end + + test "preserves inner class expression names", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ const C = class D { static n(){ return D.name } }; return C.n() })" + ) + |> user_function() + + assert {:ok, "D"} = Compiler.invoke(fun, []) + end + + test "compiles computed static fields", %{rt: rt} do + fun = + compile_and_decode( + rt, + "(function(){ const k = \"x\"; class A { static [k] = 4 } return A.x })" + ) + |> user_function() + + assert {:ok, 4} = Compiler.invoke(fun, []) + end + + test "preserves side-effectful dropped method calls", %{rt: rt} do + fun = compile_and_decode(rt, "(function(o){ o.bump(); return o.n })") |> user_function() + + obj = + Heap.wrap(%{ + "n" => 0, + "bump" => + {:builtin, "bump", + fn [], {:obj, ref} -> + Heap.put_obj(ref, Map.put(Heap.get_obj(ref, %{}), "n", 1)) + :undefined + end} + }) + + assert {:ok, 1} = Compiler.invoke(fun, [obj]) + end + end + + describe "Interpreter integration" do + test "eligible functions use the compiled cache", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(a,b){return a+b})") + fun = user_function(parsed) + + assert 9 == Interpreter.invoke(fun, [4, 5], 1_000) + + assert {:compiled, {_mod, :run_ctx}, _atoms} = + Heap.get_compiled({fun.byte_code, fun.arg_count}) + end + + test "branchy functions also use the compiled cache", %{rt: rt} do + parsed = compile_and_decode(rt, "(function(x){if(x>0)return 1;else return 2})") + fun = user_function(parsed) + + assert 1 == Interpreter.invoke(fun, [5], 1_000) + + assert {:compiled, {_mod, :run_ctx}, _atoms} = + Heap.get_compiled({fun.byte_code, fun.arg_count}) + end + end +end diff --git a/test/vm/dual_mode_test.exs b/test/vm/dual_mode_test.exs new file mode 100644 index 00000000..7533f3f7 --- /dev/null +++ b/test/vm/dual_mode_test.exs @@ -0,0 +1,575 @@ +defmodule QuickBEAM.VM.DualModeTest do + @moduledoc """ + Runs JS expressions through both NIF and beam mode, asserting identical results. + Catches semantic divergences between the QuickJS C engine and BEAM interpreter. + """ + use ExUnit.Case, async: true + + setup_all do + {:ok, rt} = QuickBEAM.start() + %{rt: rt} + end + + defp both(rt, code) do + nif = QuickBEAM.eval(rt, code) + beam = QuickBEAM.eval(rt, code, mode: :beam) + {nif, beam} + end + + defp assert_same(rt, code) do + {nif, beam} = both(rt, code) + nif_val = normalize(nif) + beam_val = normalize(beam) + + assert nif_val == beam_val, + "NIF vs BEAM mismatch for: #{code}\n NIF: #{inspect(nif)}\n BEAM: #{inspect(beam)}" + end + + defp normalize({:ok, :Infinity}), do: {:ok, :infinity} + defp normalize({:ok, :"-Infinity"}), do: {:ok, :neg_infinity} + defp normalize({:ok, :NaN}), do: {:ok, :nan} + + defp normalize({:ok, val}) when is_float(val) do + if val == Float.round(val, 0) and val == trunc(val) do + {:ok, trunc(val)} + else + {:ok, Float.round(val, 10)} + end + end + + defp normalize({:ok, val}), do: {:ok, val} + defp normalize({:error, _}), do: :error + defp normalize(other), do: other + + # ══════════════════════════════════════════════════════════════════════ + # Primitives + # ══════════════════════════════════════════════════════════════════════ + + @primitives [ + "42", + "0", + "-1", + "3.14", + "-0.5", + "true", + "false", + "null", + "undefined", + ~s|"hello"|, + ~s|""|, + ~s|"hello world"|, + "1 + 2", + "10 - 3", + "4 * 5", + "10 / 2", + "10 % 3", + "2 + 3 * 4", + "(2 + 3) * 4", + "-5", + "+5", + "-(3 + 2)", + "1 === 1", + "1 === 2", + "1 !== 2", + ~s|"a" === "a"|, + "null === null", + "null === undefined", + "1 == '1'", + "null == undefined", + "0 == false", + "1 < 2", + "2 < 1", + "1 <= 1", + "1 > 0", + "1 >= 1", + "true && true", + "true && false", + "false || true", + "1 && 2", + "0 && 2", + "1 || 2", + "0 || 2", + "!true", + "!false", + "!0", + "!null", + "!1", + "!!1", + "typeof 42", + ~s|typeof "hi"|, + "typeof true", + "typeof undefined", + "typeof null", + "typeof function(){}", + "typeof {}", + "typeof []", + "5 & 3", + "5 | 3", + "5 ^ 3", + "1 << 3", + "8 >> 2", + "~0", + "~1", + "true ? 'yes' : 'no'", + "false ? 'yes' : 'no'", + "null ?? 'default'", + "undefined ?? 'default'", + "0 ?? 'default'", + "null?.foo", + "undefined?.bar", + "({a: 1})?.a", + "void 0", + "(1, 2, 3)", + "NaN === NaN", + "NaN !== NaN" + ] + + describe "primitives" do + for code <- @primitives do + @tag_code code + test "#{code}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # String built-ins + # ══════════════════════════════════════════════════════════════════════ + + @string_tests [ + ~s|"hello" + " " + "world"|, + ~s|"hello".length|, + ~s|"".length|, + ~s|"hello".charAt(1)|, + ~s|"hi".charAt(99)|, + ~s|"ABC".charCodeAt(0)|, + ~s|"hello world".indexOf("world")|, + ~s|"hello".indexOf("xyz")|, + ~s|"hello".indexOf("")|, + ~s|"abcabc".lastIndexOf("abc")|, + ~s|"hello world".includes("world")|, + ~s|"hello".includes("xyz")|, + ~s|"hello".startsWith("hel")|, + ~s|"hello".endsWith("llo")|, + ~s|"hello".slice(1, 3)|, + ~s|"hello".slice(2)|, + ~s|"hello".slice(-3)|, + ~s|"hello".substring(1, 3)|, + ~s|"hello".substring(3, 1)|, + ~s|"a,b,c".split(",")|, + ~s|"abc".split("")|, + ~s|"hello".split("x")|, + ~s|" hello ".trim()|, + ~s|" hello ".trimStart()|, + ~s|" hello ".trimEnd()|, + ~s|"Hello World".toUpperCase()|, + ~s|"Hello World".toLowerCase()|, + ~s|"ab".repeat(3)|, + ~s|"abc".repeat(0)|, + ~s|"5".padStart(3, "0")|, + ~s|"5".padEnd(3, "0")|, + ~s|"aabaa".replace("a", "x")|, + ~s|"aabaa".replaceAll("a", "x")|, + ~s|"hello".concat(" ", "world")| + ] + + describe "String" do + for code <- @string_tests do + @tag_code code + test "#{code}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Array built-ins (self-contained expressions) + # ══════════════════════════════════════════════════════════════════════ + + @array_tests [ + "[1, 2, 3]", + "[]", + "[1, 2, 3][0]", + "[1, 2, 3][1]", + "[1, 2, 3].length", + "[].length", + "[1,2,3].map(function(x){ return x*2 })", + "[1,2,3].map(function(x){ return x*2 })[1]", + "[1,2,3,4].filter(function(x){ return x > 2 })", + "[1,2,3].reduce(function(a,b){ return a+b }, 0)", + "[1,2,3].reduce(function(a,b){ return a+b })", + "[10,20,30].indexOf(20)", + "[1,2,3].indexOf(99)", + "[10,20,30].includes(20)", + "[10,20,30].includes(99)", + "[1,2,3,4,5].slice(1,3)", + "[1,2,3,4].slice(2)", + "[1,2,3,4,5].slice(-2)", + ~s|[1,2,3].join("-")|, + "[1,2,3].join()", + ~s|[1,2,3].join("")|, + "[1,2].concat([3,4])", + "[1,2,3,4].find(function(x){ return x > 2 })", + "[1,2].find(function(x){ return x > 10 })", + "[10,20,30].findIndex(function(x){ return x === 20 })", + "[2,4,6].every(function(x){ return x % 2 === 0 })", + "[2,3,6].every(function(x){ return x % 2 === 0 })", + "[1,3,4].some(function(x){ return x % 2 === 0 })", + "[1,3,5].some(function(x){ return x % 2 === 0 })", + "[].every(function(x){ return false })", + "[].some(function(x){ return true })", + "[1,[2,3],[4]].flat()", + "Array.isArray([1,2])", + "Array.isArray(123)", + # mutating (need IIFE) + "(function(){ var a=[1]; a.push(2); return a.length })()", + "(function(){ var a=[1,2,3]; return a.pop() })()", + "(function(){ var a=[1,2,3]; a.pop(); return a.length })()", + "(function(){ var a=[1,2,3]; a.shift(); return a })()", + "(function(){ var a=[2,3]; a.unshift(1); return a[0] })()", + "(function(){ var a=[1,2,3,4,5]; a.splice(1,2); return a })()", + "(function(){ var a=[1,2,3]; a.reverse(); return a })()", + "(function(){ var a=[3,1,2]; a.sort(); return a })()", + "(function(){ var s=0; [1,2,3].forEach(function(x){ s+=x }); return s })()" + ] + + describe "Array" do + for code <- @array_tests do + @tag_code code + test "#{String.slice(code, 0, 72)}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Object built-ins + # ══════════════════════════════════════════════════════════════════════ + + @object_tests [ + "({a: 1})", + "({a: 1}).a", + "({a: {b: 2}}).a.b", + ~s|({name: "test"}).name|, + "Object.keys({a:1, b:2})", + "Object.keys({})", + "Object.values({a:1, b:2})", + "Object.entries({a:1})", + "Object.assign({a:1}, {b:2})", + "Object.assign({a:1}, {a:2})", + ~s|"a" in {a:1}|, + ~s|"b" in {a:1}|, + ~s|(function(){ var k="x"; var o={}; o[k]=1; return o.x })()|, + "(function(){ var o={a:1,b:2}; delete o.a; return Object.keys(o) })()" + ] + + describe "Object" do + for code <- @object_tests do + @tag_code code + test "#{String.slice(code, 0, 72)}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Math + # ══════════════════════════════════════════════════════════════════════ + + @math_tests [ + "Math.floor(3.7)", + "Math.floor(-4.1)", + "Math.ceil(4.1)", + "Math.ceil(-4.9)", + "Math.round(4.5)", + "Math.round(4.4)", + "Math.abs(-42)", + "Math.abs(0)", + "Math.max(1, 5, 3)", + "Math.min(1, 5, 3)", + "Math.sqrt(9)", + "Math.pow(2, 10)", + "Math.trunc(4.9)", + "Math.trunc(-4.9)", + "Math.sign(42)", + "Math.sign(-42)", + "Math.sign(0)" + ] + + describe "Math" do + for code <- @math_tests do + @tag_code code + test "#{code}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # JSON + # ══════════════════════════════════════════════════════════════════════ + + @json_tests [ + ~s|JSON.parse('{"a":1}')|, + ~s|JSON.parse('[1,2,3]')|, + ~s|JSON.parse('"hello"')|, + ~s|JSON.parse('42')|, + ~s|JSON.parse('true')|, + ~s|JSON.parse('null')|, + "JSON.stringify({a: 1})", + "JSON.stringify([1,2,3])", + "JSON.stringify(null)", + "JSON.stringify(true)" + ] + + describe "JSON" do + for code <- @json_tests do + @tag_code code + test "#{String.slice(code, 0, 72)}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Global functions + # ══════════════════════════════════════════════════════════════════════ + + @global_tests [ + ~s|parseInt("42")|, + ~s|parseInt("ff", 16)|, + ~s|parseInt("3.14")|, + ~s|parseFloat("3.14")|, + "isNaN(NaN)", + "isNaN(42)", + "isFinite(42)", + "isFinite(Infinity)", + "isFinite(NaN)", + "String(42)", + "String(true)", + "String(null)", + "Boolean(0)", + "Boolean(1)", + ~s|Boolean("")|, + ~s|Boolean("x")|, + "Number.isNaN(NaN)", + "Number.isNaN(42)", + "Number.isFinite(42)", + "Number.isFinite(Infinity)", + "Number.isInteger(42)", + "Number.isInteger(42.5)", + "Number.MAX_SAFE_INTEGER" + ] + + describe "global functions" do + for code <- @global_tests do + @tag_code code + test "#{code}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Control flow & functions + # ══════════════════════════════════════════════════════════════════════ + + @flow_tests [ + "(function(x){ return x * 2 })(21)", + "(function(){ var x=3, y=4; return x+y })()", + "(function(){ if(true) return 1; return 0 })()", + "(function(){ var x=5; return x++ })()", + "(function(){ var x=5; return ++x })()", + "(function(){ var x=10; x+=5; return x })()", + "(function(){ var s=0,i=0; while(i<5){s+=i;i++} return s })()", + "(function(){ var s=0; for(var i=0;i<5;i++){s+=i} return s })()", + "(function(){ var s=0; for(var i=0;i<10;i++){if(i>2)break;s+=i} return s })()", + "(function(){ var s=0; for(var i=0;i<5;i++){if(i===2)continue;s+=i} return s })()", + "(function(){ var s=0,i=0; do{s+=i;i++}while(i<5); return s })()", + "(function f(n){ return n<=1?n:f(n-1)+f(n-2) })(10)", + # closures + "(function(){ let x=10; return (function(){ return x })() })()", + "(function(x){ return (function(){ return x })() })(42)", + "(function(){ var count=0; function inc(){count++} inc();inc(); return count })()", + # try/catch + ~s|(function(){ try{throw "err"}catch(e){return e} })()|, + "(function(){ var x=0; try{x=1}finally{x=2} return x })()", + # switch + "(function(n){ switch(n){case 1:return 'one';default:return 'other'} })(1)", + "(function(n){ switch(n){case 1:return 'one';default:return 'other'} })(3)", + # template literals + ~s|`${1 + 2}`|, + # destructuring + "(function(){ var [a,b]=[1,2]; return a+b })()", + "(function(){ var {a,b}={a:1,b:2}; return a+b })()", + # spread + "(function(){ var a=[1,2]; var b=[...a, 3]; return b })()", + "(function(){ var a={x:1}; var b={...a, y:2}; return b })()", + # for-in + ~s|(function(){ var o={a:1,b:2}; var k=[]; for(var x in o)k.push(x); return k })()|, + # default params + "(function(x, y){ if(y===undefined) y=10; return x+y })(5)" + ] + + describe "control flow & functions" do + for code <- @flow_tests do + @tag_code code + test "#{String.slice(code, 0, 72)}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Type coercion + # ══════════════════════════════════════════════════════════════════════ + + @coercion_tests [ + ~s|"num:" + 42|, + ~s|42 + "!"|, + "true + 1", + "false + 1", + "null + 1", + "String(undefined)", + "Boolean(null)", + "Boolean(undefined)", + "(3.14159).toFixed(2)" + ] + + describe "type coercion" do + for code <- @coercion_tests do + @tag_code code + test "#{code}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Serialization edge cases (from core/serialization_test.exs) + # ══════════════════════════════════════════════════════════════════════ + + @serialization_tests [ + "1.0", + "'héllo'", + "'日本語'", + "'Ünïcödé'", + ~s|"emoji: 🎉"|, + ~s|"🎉".length|, + ~s|"日本語".length|, + "1000000", + "[1, [2, 3], 4]", + "[1, 'two', true, null]", + "({})", + "({a: {b: 1}})", + "({items: [1, 2, 3]})", + "({a: {b: {c: 42}}})", + "({a: {b: {c: {d: 42}}}})" + ] + + describe "serialization" do + for code <- @serialization_tests do + @tag_code code + test "#{String.slice(code, 0, 72)}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end + + # ══════════════════════════════════════════════════════════════════════ + # Recursive & complex (from quickbeam_test.exs patterns) + # ══════════════════════════════════════════════════════════════════════ + + @complex_tests [ + "(function f(n){ return n<=1?n:f(n-1)+f(n-2) })(15)", + "(function f(n){ return n<=1?1:n*f(n-1) })(10)", + "[1,2,3,4,5].filter(function(x){return x%2===0}).map(function(x){return x*x}).reduce(function(a,b){return a+b},0)", + "(function(){var n=0; function inc(){n++} inc();inc();inc(); return n})()", + "(function(){var s=0; [1,2,3,4,5].forEach(function(x){s+=x}); return s})()", + ~s|(function(){var o={a:{b:{c:42}}}; return o.a.b.c})()|, + ~s|(function(){ return " Hello World ".trim().toLowerCase().split(" ").join("-") })()|, + "(function(){var a=[5,3,8,1,2]; a.sort(function(a,b){return a-b}); return a})()", + ~s|(function(){var a=[1,2,3]; a.reverse(); return a.join(",")})()|, + ~s|JSON.parse(JSON.stringify({x:[1,2],y:"z"})).y|, + ~s|JSON.parse(JSON.stringify([1,"two",true,null]))|, + "[[1,2],[3,4],[5,6]][1][1]", + ~s|"hello world".split(" ").map(function(w){return w.charAt(0).toUpperCase()+w.slice(1)}).join(" ")|, + ~s|(function(){var o={}; for(var i=0;i<3;i++) o["k"+i]=i; return o.k0+o.k1+o.k2})()|, + "(function(x){return x>10?'big':x>5?'medium':'small'})(7)", + "(function(){var x=null; x=x||42; return x})()", + "(function(){var x=5; x=x||42; return x})()", + # this-binding + "(function(){ var o={x:10,f:function(){return this.x}}; return o.f() })()", + # get_loc0_loc1 ordering + "(function(){ var a=[2,5,8]; var m=Math.floor(1); return a[m] })()", + # deep recursion (memoized fib) + "(function(){ var m={}; function f(n){if(n in m)return m[n];if(n<=1)return n;m[n]=f(n-1)+f(n-2);return m[n]} return f(30) })()", + # rest params + "(function(...a){return a.length})(1,2,3)", + "(function(a,...b){return a+b.length})(10,20,30)", + # new Array + "new Array(3).length", + "new Array(1,2,3).length", + # string indexing + ~s|"hello"[1]|, + ~s|"hello"[0]|, + # obj method this + "(function(){var o={x:10,f:function(){return this.x}};return o.f()})()", + "(function(){var o={n:'world',greet:function(){return 'hello '+this.n}};return o.greet()})()", + # computed property key + ~s|(function(){var k="a";return {[k]:1}})()|, + # rest params edge + "(function(a,...b){return b})(1,2,3)", + # lastIndexOf + "[1,2,3,2,1].lastIndexOf(2)", + # charAt edge + ~s|"abc".charAt(-1)|, + ~s|"abc".charAt(99)|, + # array toString + "[1,2,3].toString()", + # exponent + "2**10", + # String.fromCharCode + "String.fromCharCode(72,101,108,108,111)", + # JSON.stringify undefined + "JSON.stringify(undefined)", + # negative zero + "1/(-0)===-Infinity", + "-Infinity", + "Infinity + 1 === Infinity", + # special arithmetic + "Infinity - Infinity", + "Infinity * 0", + # method shorthand + "(function(){ var o={f(){return 42}}; return o.f() })()", + # switch fallthrough + "(function(){var r='';switch(1){case 1:r+='a';case 2:r+='b';break;case 3:r+='c'}return r})()", + # while true break + "(function(){var i=0;while(true){if(i>=5)break;i++}return i})()", + # do while false + "(function(){var x=0;do{x++}while(false);return x})()", + # multi catch + "(function(){try{try{throw 1}catch(e){throw e+10}}catch(e){return e}})()", + # finally return + "(function(){try{return 1}finally{return 2}})()", + # comma operator in for + "(function(){for(var i=0,j=10;i<3;i++,j--);return i+j})()", + # neg zero + # special values + "Infinity+1===Infinity", + # concat coercion + ~s|""+0|, + ~s|""+null|, + ~s|+"42"| + ] + + describe "complex expressions" do + for code <- @complex_tests do + @tag_code code + test "#{String.slice(code, 0, 72)}", %{rt: rt} do + assert_same(rt, @tag_code) + end + end + end +end diff --git a/test/vm/interpreter_test.exs b/test/vm/interpreter_test.exs new file mode 100644 index 00000000..2f22f2b3 --- /dev/null +++ b/test/vm/interpreter_test.exs @@ -0,0 +1,378 @@ +defmodule QuickBEAM.VM.InterpreterTest do + use ExUnit.Case, async: true + + alias QuickBEAM.VM.{Bytecode, Interpreter} + + setup do + {:ok, rt} = QuickBEAM.start() + + on_exit(fn -> + try do + QuickBEAM.stop(rt) + catch + :exit, _ -> :ok + end + end) + + %{rt: rt} + end + + # Compile JS → decode → eval on BEAM + defp eval_js(rt, code) do + {:ok, bc} = QuickBEAM.compile(rt, code) + {:ok, parsed} = Bytecode.decode(bc) + Interpreter.eval(parsed.value, [], %{}, parsed.atoms) + end + + # Same but return the raw result (unwrap {:ok, _}) + defp eval_js!(rt, code) do + {:ok, result} = eval_js(rt, code) + result + end + + describe "arithmetic" do + test "integer addition", %{rt: rt} do + assert eval_js!(rt, "1 + 2") == 3 + end + + test "integer multiplication", %{rt: rt} do + assert eval_js!(rt, "6 * 7") == 42 + end + + test "integer subtraction", %{rt: rt} do + assert eval_js!(rt, "10 - 3") == 7 + end + + test "integer division", %{rt: rt} do + assert eval_js!(rt, "10 / 3") == 10 / 3 + end + + test "complex arithmetic", %{rt: rt} do + assert eval_js!(rt, "2 + 3 * 4") == 14 + end + + test "parenthesized expression", %{rt: rt} do + assert eval_js!(rt, "(2 + 3) * 4") == 20 + end + + test "unary negation", %{rt: rt} do + assert eval_js!(rt, "-42") == -42 + end + end + + describe "comparisons" do + test "less than", %{rt: rt} do + assert eval_js!(rt, "1 < 2") == true + assert eval_js!(rt, "2 < 1") == false + end + + test "greater than", %{rt: rt} do + assert eval_js!(rt, "2 > 1") == true + assert eval_js!(rt, "1 > 2") == false + end + + test "equality", %{rt: rt} do + assert eval_js!(rt, "1 === 1") == true + assert eval_js!(rt, "1 === 2") == false + end + + test "inequality", %{rt: rt} do + assert eval_js!(rt, "1 !== 2") == true + assert eval_js!(rt, "1 !== 1") == false + end + end + + describe "variables and locals" do + test "let binding", %{rt: rt} do + assert eval_js!(rt, "{ let x = 42; x }") == 42 + end + + test "multiple bindings", %{rt: rt} do + assert eval_js!(rt, "{ let a = 1; let b = 2; a + b }") == 3 + end + + test "reassignment", %{rt: rt} do + assert eval_js!(rt, "{ let x = 1; x = 2; x }") == 2 + end + end + + describe "control flow" do + test "if true", %{rt: rt} do + assert eval_js!(rt, "true ? 1 : 2") == 1 + end + + test "if false", %{rt: rt} do + assert eval_js!(rt, "false ? 1 : 2") == 2 + end + + test "if with comparison", %{rt: rt} do + assert eval_js!(rt, "{ let x = 5; if (x > 3) x; else 0 }") == 5 + end + + test "while loop", %{rt: rt} do + code = "{ let s = 0; let i = 0; while (i < 10) { s = s + i; i = i + 1; } s }" + assert eval_js!(rt, code) == 45 + end + + test "for loop", %{rt: rt} do + code = "{ let s = 0; for (let i = 0; i < 5; i = i + 1) s = s + i; s }" + assert eval_js!(rt, code) == 10 + end + end + + describe "functions" do + test "IIFE", %{rt: rt} do + assert eval_js!(rt, "(function(){return 42})()") == 42 + end + + test "IIFE with args", %{rt: rt} do + assert eval_js!(rt, "(function(a,b){return a+b})(3,4)") == 7 + end + + test "nested function", %{rt: rt} do + code = "(function(){return (function(x){return x*2})(21)})()" + assert eval_js!(rt, code) == 42 + end + end + + describe "values" do + test "null", %{rt: rt} do + assert eval_js!(rt, "null") == nil + end + + test "undefined", %{rt: rt} do + assert eval_js!(rt, "undefined") == :undefined + end + + test "true", %{rt: rt} do + assert eval_js!(rt, "true") == true + end + + test "false", %{rt: rt} do + assert eval_js!(rt, "false") == false + end + + test "string", %{rt: rt} do + assert eval_js!(rt, ~s|"hello"|) == "hello" + end + end + + describe "bitwise" do + test "AND", %{rt: rt} do + assert eval_js!(rt, "0xFF & 0x0F") == 0x0F + end + + test "OR", %{rt: rt} do + assert eval_js!(rt, "0xF0 | 0x0F") == 0xFF + end + + test "XOR", %{rt: rt} do + assert eval_js!(rt, "0xFF ^ 0x0F") == 0xF0 + end + + test "left shift", %{rt: rt} do + assert eval_js!(rt, "1 << 4") == 16 + end + + test "right shift", %{rt: rt} do + assert eval_js!(rt, "16 >> 2") == 4 + end + end + + describe "logical" do + test "logical NOT", %{rt: rt} do + assert eval_js!(rt, "!true") == false + assert eval_js!(rt, "!false") == true + end + + test "typeof", %{rt: rt} do + assert eval_js!(rt, "typeof 42") == "number" + assert eval_js!(rt, ~s|typeof "hello"|) == "string" + assert eval_js!(rt, "typeof true") == "boolean" + assert eval_js!(rt, "typeof undefined") == "undefined" + end + end + + describe "objects" do + test "object literal property access", %{rt: rt} do + assert eval_js!(rt, "({x: 1, y: 2}).x") == 1 + end + + test "object literal multiple properties", %{rt: rt} do + assert eval_js!(rt, "({x: 1, y: 2}).y") == 2 + end + + test "object property set and get", %{rt: rt} do + assert eval_js!(rt, "{ let o = {x: 1}; o.y = 2; o.x + o.y }") == 3 + end + + test "nested object", %{rt: rt} do + assert eval_js!(rt, "({a: {b: 42}}).a.b") == 42 + end + + test "object with string value", %{rt: rt} do + assert eval_js!(rt, ~s|({name: "test"}).name|) == "test" + end + end + + describe "arrays" do + test "array literal index access", %{rt: rt} do + assert eval_js!(rt, "[10, 20, 30][0]") == 10 + end + + test "array index access middle", %{rt: rt} do + assert eval_js!(rt, "[10, 20, 30][2]") == 30 + end + + test "array length", %{rt: rt} do + assert eval_js!(rt, "[1, 2, 3].length") == 3 + end + + test "empty array length", %{rt: rt} do + assert eval_js!(rt, "[].length") == 0 + end + + test "array out of bounds", %{rt: rt} do + assert eval_js!(rt, "[1,2,3][10]") == :undefined + end + end + + describe "closures" do + test "simple closure captures variable", %{rt: rt} do + code = "(function() { let x = 10; return (function() { return x })() })()" + assert eval_js!(rt, code) == 10 + end + + test "closure with argument", %{rt: rt} do + code = "(function(x) { return (function() { return x })() })(42)" + assert eval_js!(rt, code) == 42 + end + + test "closure captures local vars after arguments", %{rt: rt} do + code = "(function(a, b) { var c = 99; return (function() { return c })() })(1, 2)" + assert eval_js!(rt, code) == 99 + end + + test "default parameter scope arrow sees arguments object", %{rt: rt} do + code = + "(function() { var f = function(a, b = () => arguments) { return b; }; return f(12)()[0]; })()" + + assert eval_js!(rt, code) == 12 + end + + test "captured argument reassignment is visible inside nested callbacks", %{rt: rt} do + code = + "(function(){ function flatten(n, l){ return l = l || [], Array.isArray(n) ? n.some(function(x){ flatten(x, l) }) : l.push(n), l } return flatten([1,2,3]).length })()" + + assert eval_js!(rt, code) == 3 + end + end + + describe "string operations" do + test "string length", %{rt: rt} do + assert eval_js!(rt, ~s|"hello".length|) == 5 + end + + test "empty string length", %{rt: rt} do + assert eval_js!(rt, ~s|"".length|) == 0 + end + + test "string concatenation", %{rt: rt} do + assert eval_js!(rt, ~s|"hello" + " " + "world"|) == "hello world" + end + + test "string + number coercion", %{rt: rt} do + assert eval_js!(rt, ~s|"num: " + 42|) == "num: 42" + end + end + + describe "modulo and power" do + test "modulo", %{rt: rt} do + assert eval_js!(rt, "10 % 3") == 1 + end + + test "power", %{rt: rt} do + assert eval_js!(rt, "2 ** 10") == 1024.0 + end + end + + describe "null and undefined operators" do + test "null coalescing", %{rt: rt} do + assert eval_js!(rt, "null ?? 42") == 42 + end + + test "null coalescing non-null", %{rt: rt} do + assert eval_js!(rt, "1 ?? 42") == 1 + end + + test "optional chaining on null", %{rt: rt} do + assert eval_js!(rt, "null?.x") == :undefined + end + + test "is null check", %{rt: rt} do + assert eval_js!(rt, "null === null") == true + end + + test "undefined === undefined", %{rt: rt} do + assert eval_js!(rt, "undefined === undefined") == true + end + + test "null !== undefined (strict)", %{rt: rt} do + assert eval_js!(rt, "null !== undefined") == true + end + end + + describe "short-circuit evaluation" do + test "logical AND truthy", %{rt: rt} do + assert eval_js!(rt, "1 && 2") == 2 + end + + test "logical AND falsy", %{rt: rt} do + assert eval_js!(rt, "0 && 2") == 0 + end + + test "logical OR truthy", %{rt: rt} do + assert eval_js!(rt, "1 || 2") == 1 + end + + test "logical OR falsy", %{rt: rt} do + assert eval_js!(rt, "0 || 42") == 42 + end + end + + describe "ternary operator" do + test "ternary true branch", %{rt: rt} do + assert eval_js!(rt, "true ? 'yes' : 'no'") == "yes" + end + + test "ternary false branch", %{rt: rt} do + assert eval_js!(rt, "false ? 'yes' : 'no'") == "no" + end + + test "ternary with expression", %{rt: rt} do + assert eval_js!(rt, "(1 > 2) ? 10 : 20") == 20 + end + end + + describe "complex expressions" do + test "nested function calls", %{rt: rt} do + code = "(function(a,b){return a+b})((function(){return 3})(), 4)" + assert eval_js!(rt, code) == 7 + end + + test "fibonacci", %{rt: rt} do + code = "(function fib(n) { if (n <= 1) return n; return fib(n-1) + fib(n-2) })(10)" + assert eval_js!(rt, code) == 55 + end + + test "sum loop IIFE", %{rt: rt} do + code = "(function(n){let s=0;for(let i=0;i + {:builtin, "getStringKind", + fn + [s | _], _ when is_binary(s) -> if byte_size(s) > 256, do: 1, else: 0 + _, _ -> 0 + end} + }) + + os = Heap.wrap(%{"platform" => "elixir"}) + + Heap.put_persistent_globals( + Map.merge(Heap.get_persistent_globals(), %{ + "gc" => {:builtin, "gc", fn _, _ -> :undefined end}, + "os" => os, + "qjs" => qjs + }) + ) + + %{rt: rt} + end + + @js_dir Path.expand(".", __DIR__) + + for file <- ["test_builtin.js", "test_language.js"] do + source = File.read!(Path.join(@js_dir, file)) + skip_list = if file == "test_builtin.js", do: @skip_builtin, else: @skip_language + + {:ok, ast} = OXC.parse(source, file) + + fns = Enum.filter(ast.body, &(&1.type == :function_declaration)) + + test_fns = + fns + |> Enum.filter(&(String.starts_with?(&1.id.name, "test_") and &1.params == [])) + |> Enum.reject(&(&1.id.name in skip_list)) + + helper_fns = Enum.reject(fns, &(&1.id.name == "test")) + + for %{id: %{name: func_name}} = func <- test_fns do + func_body = binary_part(source, func.start, func[:end] - func.start) + func_line = source |> binary_part(0, func.start) |> String.split("\n") |> length() + + current_helpers = + helper_fns + |> Enum.reject(&(&1.id.name == func_name)) + |> Enum.map_join("\n", &binary_part(source, &1.start, &1[:end] - &1.start)) + + @tag :js_engine + test "#{file}: #{func_name}", %{rt: rt} do + QuickBEAM.eval(rt, unquote(current_helpers), mode: :beam) + + padding = String.duplicate("\n", unquote(func_line) - 1) + code = padding <> unquote(func_body) <> "\n" <> unquote(func_name) <> "();" + + case QuickBEAM.eval(rt, code, mode: :beam, filename: unquote(file)) do + {:ok, _} -> :ok + {:error, %QuickBEAM.JSError{message: msg}} -> flunk("JS: #{msg}") + {:error, err} -> flunk("JS error: #{inspect(err)}") + end + end + end + end + + defp strip_exports(source) do + {:ok, ast} = OXC.parse(source, "module.js") + + ast.body + + Enum.map_join(ast.body, "\n", fn + %{type: :export_named_declaration, declaration: decl} -> + binary_part(source, decl.start, decl[:end] - decl.start) + + node -> + binary_part(source, node.start, node[:end] - node.start) + end) + end +end diff --git a/test/vm/test_builtin.js b/test/vm/test_builtin.js new file mode 100644 index 00000000..02acc17c --- /dev/null +++ b/test/vm/test_builtin.js @@ -0,0 +1,1314 @@ +import * as os from "qjs:os"; +import { assert, assertThrows } from "./assert.js"; + +// Keep this at the top; it tests source positions. +function test_exception_source_pos() +{ + var e; + + try { + throw new Error(""); // line 10, column 19 + } catch(_e) { + e = _e; + } + + assert(e.stack.includes("test_builtin.js:10:19")); +} + +// Keep this at the top; it tests source positions. +function test_function_source_pos() // line 19, column 1 +{ + function inner() {} // line 21, column 5 + var f = eval("function f() {} f"); + assert(`${test_function_source_pos.lineNumber}:${test_function_source_pos.columnNumber}`, "19:1"); + assert(`${inner.lineNumber}:${inner.columnNumber}`, "21:5"); + assert(`${f.lineNumber}:${f.columnNumber}`, "1:1"); +} + +// Keep this at the top; it tests source positions. +function test_exception_prepare_stack() +{ + var e; + + Error.prepareStackTrace = (_, frames) => { + // Just return the array to check. + return frames; + }; + + try { + throw new Error(""); // line 39, column 19 + } catch(_e) { + e = _e; + } + + Error.prepareStackTrace = undefined; + + assert(e.stack.length, 2); + const f = e.stack[0]; + assert(f.getFunctionName(), 'test_exception_prepare_stack'); + assert(f.getFileName().endsWith('test_builtin.js')); + assert(f.getLineNumber(), 39); + assert(f.getColumnNumber(), 19); + assert(!f.isNative()); +} + +// Keep this at the top; it tests source positions. +function test_exception_stack_size_limit() +{ + var e; + + Error.stackTraceLimit = 1; + Error.prepareStackTrace = (_, frames) => { + // Just return the array to check. + return frames; + }; + + try { + throw new Error(""); // line 67, column 19 + } catch(_e) { + e = _e; + } + + Error.stackTraceLimit = 10; + Error.prepareStackTrace = undefined; + + assert(e.stack.length, 1); + const f = e.stack[0]; + assert(f.getFunctionName(), 'test_exception_stack_size_limit'); + assert(f.getFileName().endsWith('test_builtin.js')); + assert(f.getLineNumber(), 67); + assert(f.getColumnNumber(), 19); + assert(!f.isNative()); +} + +function test_exception_capture_stack_trace() +{ + var o = {}; + + assertThrows(TypeError, (function() { + Error.captureStackTrace(); + })); + + Error.captureStackTrace(o); + + assert(typeof o.stack === 'string'); + assert(o.stack.includes('test_exception_capture_stack_trace')); +} + +function test_exception_capture_stack_trace_filter() +{ + var o = {}; + const fun1 = () => { fun2(); }; + const fun2 = () => { fun3(); }; + const fun3 = () => { log_stack(); }; + function log_stack() { + Error.captureStackTrace(o, fun3); + } + fun1(); + + Error.captureStackTrace(o); + + assert(!o.stack.includes('fun3')); + assert(!o.stack.includes('log_stack')); +} + +function my_func(a, b) +{ + return a + b; +} + +function test_function() +{ + function f(a, b) { + var i, tab = []; + tab.push(this); + for(i = 0; i < arguments.length; i++) + tab.push(arguments[i]); + return tab; + } + function constructor1(a) { + this.x = a; + } + + var r, g; + + r = my_func.call(null, 1, 2); + assert(r, 3, "call"); + + r = my_func.apply(null, [1, 2]); + assert(r, 3, "apply"); + + r = (function () { return 1; }).apply(null, undefined); + assert(r, 1); + + assertThrows(TypeError, (function() { + Reflect.apply((function () { return 1; }), null, undefined); + })); + + r = new Function("a", "b", "return a + b;"); + assert(r(2,3), 5, "function"); + + g = f.bind(1, 2); + assert(g.length, 1); + assert(g.name, "bound f"); + assert(g(3), [1,2,3]); + + g = constructor1.bind(null, 1); + r = new g(); + assert(r.x, 1); +} + +function test() +{ + var r, a, b, c, err; + + r = Error("hello"); + assert(r.message, "hello", "Error"); + + a = new Object(); + a.x = 1; + assert(a.x, 1, "Object"); + + assert(Object.getPrototypeOf(a), Object.prototype, "getPrototypeOf"); + Object.defineProperty(a, "y", { value: 3, writable: true, configurable: true, enumerable: true }); + assert(a.y, 3, "defineProperty"); + + Object.defineProperty(a, "z", { get: function () { return 4; }, set: function(val) { this.z_val = val; }, configurable: true, enumerable: true }); + assert(a.z, 4, "get"); + a.z = 5; + assert(a.z_val, 5, "set"); + + a = { get z() { return 4; }, set z(val) { this.z_val = val; } }; + assert(a.z, 4, "get"); + a.z = 5; + assert(a.z_val, 5, "set"); + + b = Object.create(a); + assert(Object.getPrototypeOf(b), a, "create"); + c = {u:2}; + /* XXX: refcount bug in 'b' instead of 'a' */ + Object.setPrototypeOf(a, c); + assert(Object.getPrototypeOf(a), c, "setPrototypeOf"); + + a = {}; + assert(a.toString(), "[object Object]", "toString"); + + a = {x:1}; + assert(Object.isExtensible(a), true, "extensible"); + Object.preventExtensions(a); + + err = false; + try { + a.y = 2; + } catch(e) { + err = true; + } + assert(Object.isExtensible(a), false, "extensible"); + assert(typeof a.y, "undefined", "extensible"); + assert(err, true, "extensible"); + + assertThrows(TypeError, () => Object.setPrototypeOf(Object.prototype, {})); +} + +function test_enum() +{ + var a, tab; + a = {x:1, + "18014398509481984": 1, + "9007199254740992": 1, + "9007199254740991": 1, + "4294967296": 1, + "4294967295": 1, + y:1, + "4294967294": 1, + "1": 2}; + tab = Object.keys(a); +// console.log("tab=" + tab.toString()); + assert(tab, ["1","4294967294","x","18014398509481984","9007199254740992","9007199254740991","4294967296","4294967295","y"], "keys"); +} + +function test_array() +{ + var a, err; + + a = [1, 2, 3]; + assert(a.length, 3, "array"); + assert(a[2], 3, "array1"); + + a = new Array(10); + assert(a.length, 10, "array2"); + + a = new Array(1, 2); + assert(a.length === 2 && a[0] === 1 && a[1] === 2, true, "array3"); + + a = [1, 2, 3]; + a.length = 2; + assert(a.length === 2 && a[0] === 1 && a[1] === 2, true, "array4"); + + a = []; + a[1] = 10; + a[4] = 3; + assert(a.length, 5); + + a = [1,2]; + a.length = 5; + a[4] = 1; + a.length = 4; + assert(a[4] !== 1, true, "array5"); + + a = [1,2]; + a.push(3,4); + assert(a.join(), "1,2,3,4", "join"); + + a = [1,2,3,4,5]; + Object.defineProperty(a, "3", { configurable: false }); + err = false; + try { + a.length = 2; + } catch(e) { + err = true; + } + assert(err && a.toString() === "1,2,3,4"); +} + +function test_string() +{ + var a; + a = String("abc"); + assert(a.length, 3, "string"); + assert(a[1], "b", "string"); + assert(a.charCodeAt(1), 0x62, "string"); + assert(String.fromCharCode(65), "A", "string"); + assert(String.fromCharCode.apply(null, [65, 66, 67]), "ABC", "string"); + assert(a.charAt(1), "b"); + assert(a.charAt(-1), ""); + assert(a.charAt(3), ""); + + a = "abcd"; + assert(a.substring(1, 3), "bc", "substring"); + a = String.fromCharCode(0x20ac); + assert(a.charCodeAt(0), 0x20ac, "unicode"); + assert(a, "€", "unicode"); + assert(a, "\u20ac", "unicode"); + assert(a, "\u{20ac}", "unicode"); + assert("a", "\x61", "unicode"); + + a = "\u{10ffff}"; + assert(a.length, 2, "unicode"); + assert(a, "\u{dbff}\u{dfff}", "unicode"); + assert(a.codePointAt(0), 0x10ffff); + assert(String.fromCodePoint(0x10ffff), a); + + assert("a".concat("b", "c"), "abc"); + + assert("abcabc".indexOf("cab"), 2); + assert("abcabc".indexOf("cab2"), -1); + assert("abc".indexOf("c"), 2); + + assert("aaa".indexOf("a"), 0); + assert("aaa".indexOf("a", NaN), 0); + assert("aaa".indexOf("a", -Infinity), 0); + assert("aaa".indexOf("a", -1), 0); + assert("aaa".indexOf("a", -0), 0); + assert("aaa".indexOf("a", 0), 0); + assert("aaa".indexOf("a", 1), 1); + assert("aaa".indexOf("a", 2), 2); + assert("aaa".indexOf("a", 3), -1); + assert("aaa".indexOf("a", 4), -1); + assert("aaa".indexOf("a", Infinity), -1); + + assert("aaa".indexOf(""), 0); + assert("aaa".indexOf("", NaN), 0); + assert("aaa".indexOf("", -Infinity), 0); + assert("aaa".indexOf("", -1), 0); + assert("aaa".indexOf("", -0), 0); + assert("aaa".indexOf("", 0), 0); + assert("aaa".indexOf("", 1), 1); + assert("aaa".indexOf("", 2), 2); + assert("aaa".indexOf("", 3), 3); + assert("aaa".indexOf("", 4), 3); + assert("aaa".indexOf("", Infinity), 3); + + assert("aaa".lastIndexOf("a"), 2); + assert("aaa".lastIndexOf("a", NaN), 2); + assert("aaa".lastIndexOf("a", -Infinity), 0); + assert("aaa".lastIndexOf("a", -1), 0); + assert("aaa".lastIndexOf("a", -0), 0); + assert("aaa".lastIndexOf("a", 0), 0); + assert("aaa".lastIndexOf("a", 1), 1); + assert("aaa".lastIndexOf("a", 2), 2); + assert("aaa".lastIndexOf("a", 3), 2); + assert("aaa".lastIndexOf("a", 4), 2); + assert("aaa".lastIndexOf("a", Infinity), 2); + + assert("aaa".lastIndexOf(""), 3); + assert("aaa".lastIndexOf("", NaN), 3); + assert("aaa".lastIndexOf("", -Infinity), 0); + assert("aaa".lastIndexOf("", -1), 0); + assert("aaa".lastIndexOf("", -0), 0); + assert("aaa".lastIndexOf("", 0), 0); + assert("aaa".lastIndexOf("", 1), 1); + assert("aaa".lastIndexOf("", 2), 2); + assert("aaa".lastIndexOf("", 3), 3); + assert("aaa".lastIndexOf("", 4), 3); + assert("aaa".lastIndexOf("", Infinity), 3); + + assert("a,b,c".split(","), ["a","b","c"]); + assert(",b,c".split(","), ["","b","c"]); + assert("a,b,".split(","), ["a","b",""]); + + assert("aaaa".split(), [ "aaaa" ]); + assert("aaaa".split(undefined, 0), [ ]); + assert("aaaa".split(""), [ "a", "a", "a", "a" ]); + assert("aaaa".split("", 0), [ ]); + assert("aaaa".split("", 1), [ "a" ]); + assert("aaaa".split("", 2), [ "a", "a" ]); + assert("aaaa".split("a"), [ "", "", "", "", "" ]); + assert("aaaa".split("a", 2), [ "", "" ]); + assert("aaaa".split("aa"), [ "", "", "" ]); + assert("aaaa".split("aa", 0), [ ]); + assert("aaaa".split("aa", 1), [ "" ]); + assert("aaaa".split("aa", 2), [ "", "" ]); + assert("aaaa".split("aaa"), [ "", "a" ]); + assert("aaaa".split("aaaa"), [ "", "" ]); + assert("aaaa".split("aaaaa"), [ "aaaa" ]); + assert("aaaa".split("aaaaa", 0), [ ]); + assert("aaaa".split("aaaaa", 1), [ "aaaa" ]); + + assert(eval('"\0"'), "\0"); + + assert("abc".padStart(Infinity, ""), "abc"); + + assert(qjs.getStringKind("xyzzy".slice(1)), + /*JS_STRING_KIND_NORMAL*/0); + assert(qjs.getStringKind("xyzzy".repeat(512).slice(1)), + /*JS_STRING_KIND_SLICE*/1); +} + +function rope_concat(n, dir) +{ + var i, s; + s = ""; + if (dir > 0) { + for(i = 0; i < n; i++) + s += String.fromCharCode(i & 0xffff); + } else { + for(i = n - 1; i >= 0; i--) + s = String.fromCharCode(i & 0xffff) + s; + } + + for(i = 0; i < n; i++) { + /* test before the assert to go faster */ + if (s.charCodeAt(i) != (i & 0xffff)) { + assert(s.charCodeAt(i), i & 0xffff); + } + } +} + +function test_rope() +{ + var i, s, s2; + + /* test forward and backward concatenation */ + rope_concat(100000, 1); + rope_concat(100000, -1); + + /* test rope comparison */ + s = ""; + s2 = ""; + for (i = 0; i < 10000; i++) { + s += "abc"; + s2 += "abc"; + } + assert(s === s2, true); + assert(s < s2, false); + assert(s > s2, false); + + /* test rope indexing */ + s = ""; + for (i = 0; i < 10000; i++) + s += "x"; + assert(s.length, 10000); + assert(s[0], "x"); + assert(s[5000], "x"); + assert(s[9999], "x"); + + /* test rope with string methods */ + s = ""; + for (i = 0; i < 1000; i++) + s += "test"; + assert(s.indexOf("test"), 0); + assert(s.lastIndexOf("test"), 3996); + assert(s.includes("test"), true); + assert(s.slice(0, 8), "testtest"); + assert(s.substring(0, 8), "testtest"); +} + +function test_math() +{ + var a; + a = 1.4; + assert(Math.floor(a), 1); + assert(Math.ceil(a), 2); + assert(Math.imul(0x12345678, 123), -1088058456); + assert(Math.imul(0xB505, 0xB504), 2147441940); + assert(Math.imul(0xB505, 0xB505), -2147479015); + assert(Math.imul((-2)**31, (-2)**31), 0); + assert(Math.imul(2**31-1, 2**31-1), 1); + assert(Math.fround(0.1), 0.10000000149011612); + assert(Math.hypot(), 0); + assert(Math.hypot(-2), 2); + assert(Math.hypot(3, 4), 5); + assert(Math.abs(Math.hypot(3, 4, 5) - 7.0710678118654755) <= 1e-15); + assert(Math.sumPrecise([1,Number.EPSILON/2,Number.MIN_VALUE]), 1.0000000000000002); +} + +function test_number() +{ + assert(parseInt("123"), 123); + assert(parseInt(" 123r"), 123); + assert(parseInt("0x123"), 0x123); + assert(parseInt("0o123"), 0); + assert(+" 123 ", 123); + assert(+"0b111", 7); + assert(+"0o123", 83); + assert(parseFloat("2147483647"), 2147483647); + assert(parseFloat("2147483648"), 2147483648); + assert(parseFloat("-2147483647"), -2147483647); + assert(parseFloat("-2147483648"), -2147483648); + assert(parseFloat("0x1234"), 0); + assert(parseFloat("Infinity"), Infinity); + assert(parseFloat("-Infinity"), -Infinity); + assert(parseFloat("123.2"), 123.2); + assert(parseFloat("123.2e3"), 123200); + assert(Number.isNaN(Number("+"))); + assert(Number.isNaN(Number("-"))); + assert(Number.isNaN(Number("\x00a"))); + + assert((1-2**-53).toString(12), "0.bbbbbbbbbbbbbba"); + assert((1000000000000000128).toString(), "1000000000000000100"); + assert((1000000000000000128).toFixed(0), "1000000000000000128"); + assert((25).toExponential(0), "3e+1"); + assert((-25).toExponential(0), "-3e+1"); + assert((2.5).toPrecision(1), "3"); + assert((-2.5).toPrecision(1), "-3"); + assert((25).toPrecision(1) === "3e+1"); + assert((1.125).toFixed(2), "1.13"); + assert((-1.125).toFixed(2), "-1.13"); + assert((0.5).toFixed(0), "1"); + assert((-0.5).toFixed(0), "-1"); + assert((-1e-10).toFixed(0), "-0"); + + assert((1.3).toString(7), "1.2046204620462046205"); + assert((1.3).toString(35), "1.ahhhhhhhhhm"); + + assert((123.456).toExponential(100), + "1.2345600000000000306954461848363280296325683593750000000000000000000000000000000000000000000000000000e+2"); + assert((1.23e-99).toExponential(100), + "1.2299999999999999636794326616259654935901564299639709630577493044757187515388707554223010856511630028e-99"); + assert((-0.0007).toExponential(100), + "-6.9999999999999999288763374849509091291110962629318237304687500000000000000000000000000000000000000000e-4"); + assert((0).toExponential(100), + "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e+0"); +} + +function test_eval2() +{ + var g_call_count = 0; + /* force non strict mode for f1 and f2 */ + var f1 = new Function("eval", "eval(1, 2)"); + var f2 = new Function("eval", "eval(...[1, 2])"); + function g(a, b) { + assert(a, 1); + assert(b, 2); + g_call_count++; + } + f1(g); + f2(g); + assert(g_call_count, 2); + var e; + try { + new class extends Object { + constructor() { + (() => { + for (const _ in this); + eval(""); + })(); + } + }; + } catch (_e) { + e = _e; + } + assert(e?.message, "this is not initialized"); +} + +function test_eval() +{ + function f(b) { + var x = 1; + return eval(b); + } + var r, a; + + r = eval("1+1;"); + assert(r, 2, "eval"); + + r = eval("var my_var=2; my_var;"); + assert(r, 2, "eval"); + assert(typeof my_var, "undefined"); + + assert(eval("if (1) 2; else 3;"), 2); + assert(eval("if (0) 2; else 3;"), 3); + + assert(f.call(1, "this"), 1); + + a = 2; + assert(eval("a"), 2); + + eval("a = 3"); + assert(a, 3); + + assert(f("arguments.length", 1), 2); + assert(f("arguments[1]", 1), 1); + + a = 4; + assert(f("a"), 4); + f("a=3"); + assert(a, 3); + + test_eval2(); +} + +function test_typed_array() +{ + var buffer, a, i, str, b; + + a = new Uint8Array(4); + assert(a.length, 4); + for(i = 0; i < a.length; i++) + a[i] = i; + assert(a.join(","), "0,1,2,3"); + a[0] = -1; + assert(a[0], 255); + + a = new Int8Array(3); + a[0] = 255; + assert(a[0], -1); + + a = new Int32Array(3); + a[0] = Math.pow(2, 32) - 1; + assert(a[0], -1); + assert(a.BYTES_PER_ELEMENT, 4); + + a = new Uint8ClampedArray(4); + a[0] = -100; + a[1] = 1.5; + a[2] = 0.5; + a[3] = 1233.5; + assert(a.toString(), "0,2,0,255"); + + buffer = new ArrayBuffer(16); + assert(buffer.byteLength, 16); + a = new Uint32Array(buffer, 12, 1); + assert(a.length, 1); + a[0] = -1; + + a = new Uint16Array(buffer, 2); + a[0] = -1; + + a = new Float16Array(buffer, 8, 1); + a[0] = 1; + + a = new Float32Array(buffer, 8, 1); + a[0] = 1; + + a = new Uint8Array(buffer); + + str = a.toString(); + /* test little and big endian cases */ + if (str !== "0,0,255,255,0,0,0,0,0,0,128,63,255,255,255,255" && + str !== "0,0,255,255,0,0,0,0,63,128,0,0,255,255,255,255") { + assert(false); + } + + assert(a.buffer, buffer); + + a = new Uint8Array([1, 2, 3, 4]); + assert(a.toString(), "1,2,3,4"); + a.set([10, 11], 2); + assert(a.toString(), "1,2,10,11"); + + a = new Uint8Array(buffer, 0, 4); + a.constructor = { + [Symbol.species]: function (len) { + return new Uint8Array(buffer, 1, len); + }, + }; + b = a.slice(); + assert(a.buffer, b.buffer); + assert(a.toString(), "0,0,0,255"); + assert(b.toString(), "0,0,255,255"); + + const TypedArray = class extends Object.getPrototypeOf(Uint8Array) {}; + let caught = false; + try { + new TypedArray(); // extensible but not instantiable + } catch (e) { + assert(e instanceof TypeError); + assert(/cannot be called/.test(e.message)); + caught = true; + } + assert(caught); + + // https://github.com/quickjs-ng/quickjs/issues/1208 + buffer = new ArrayBuffer(16); + a = new Uint8Array(buffer); + a.fill(42); + assert(a[0], 42); + buffer.transfer(); + assert(a[0], undefined); + + // https://github.com/quickjs-ng/quickjs/issues/1210 + var buffer = new ArrayBuffer(16, {maxByteLength: 16}); + var desc = Object.getOwnPropertyDescriptor(ArrayBuffer, Symbol.species); + assert(typeof desc.get, "function"); + var get = function() { + buffer.resize(1); + return ArrayBuffer; + }; + Object.defineProperty(ArrayBuffer, Symbol.species, {...desc, get}); + let ex; + try { + buffer.slice(); + } catch (ex_) { + ex = ex_; + } + Object.defineProperty(ArrayBuffer, Symbol.species, desc); // restore + assert(ex instanceof TypeError); + assert("ArrayBuffer is detached", ex.message); + + var buffer = new ArrayBuffer(2); + var ta = new Uint16Array(buffer); + var desc = Object.getOwnPropertyDescriptor(ta, "0"); + ta[0] = 42; + assert(ta[0], 42); + Object.defineProperty(ta, "0", {value: 1337}); + assert(ta[0], 1337); + assert(desc.writable, true); + assert(desc.enumerable, true); + assert(desc.configurable, true); + + var buffer = new ArrayBuffer(2).sliceToImmutable(); + var ta = new Uint16Array(buffer); + var desc = Object.getOwnPropertyDescriptor(ta, "0"); + ta[0] = 42; + assert(ta[0], 0); + Object.defineProperty(ta, "0", {value: 1337}); + assert(ta[0], 0); + assert(desc.writable, false); + assert(desc.enumerable, true); + assert(desc.configurable, false); +} + +function test_json() +{ + var a, s; + s = '{"x":1,"y":true,"z":null,"a":[1,2,3],"s":"str"}'; + a = JSON.parse(s); + assert(a.x, 1); + assert(a.y, true); + assert(a.z, null); + assert(JSON.stringify(a), s); + + /* indentation test */ + assert(JSON.stringify([[{x:1,y:{},z:[]},2,3]],undefined,1), +`[ + [ + { + "x": 1, + "y": {}, + "z": [] + }, + 2, + 3 + ] +]`); +} + +function test_date() +{ + // Date Time String format is YYYY-MM-DDTHH:mm:ss.sssZ + // accepted date formats are: YYYY, YYYY-MM and YYYY-MM-DD + // accepted time formats are: THH:mm, THH:mm:ss, THH:mm:ss.sss + // expanded years are represented with 6 digits prefixed by + or - + // -000000 is invalid. + // A string containing out-of-bounds or nonconforming elements + // is not a valid instance of this format. + // Hence the fractional part after . should have 3 digits and how + // a different number of digits is handled is implementation defined. + assert(Date.parse(""), NaN); + assert(Date.parse("13"), NaN); + assert(Date.parse("31"), NaN); + assert(Date.parse("1000"), -30610224000000); + assert(Date.parse("1969"), -31536000000); + assert(Date.parse("1970"), 0); + assert(Date.parse("2000"), 946684800000); + assert(Date.parse("9999"), 253370764800000); + assert(Date.parse("275761"), NaN); + assert(Date.parse("999999"), NaN); + assert(Date.parse("1000000000"), NaN); + assert(Date.parse("-271821"), NaN); + assert(Date.parse("-271820"), -8639977881600000); + assert(Date.parse("-100000"), -3217862419200000); + assert(Date.parse("+100000"), 3093527980800000); + assert(Date.parse("+275760"), 8639977881600000); + assert(Date.parse("+275761"), NaN); + assert(Date.parse("2000-01"), 946684800000); + assert(Date.parse("2000-01-01"), 946684800000); + assert(Date.parse("2000-01-01T"), NaN); + assert(Date.parse("2000-01-01T00Z"), NaN); + assert(Date.parse("2000-01-01T00:00Z"), 946684800000); + assert(Date.parse("2000-01-01T00:00:00Z"), 946684800000); + assert(Date.parse("2000-01-01T00:00:00.1Z"), 946684800100); + assert(Date.parse("2000-01-01T00:00:00.10Z"), 946684800100); + assert(Date.parse("2000-01-01T00:00:00.100Z"), 946684800100); + assert(Date.parse("2000-01-01T00:00:00.1000Z"), 946684800100); + assert(Date.parse("2000-01-01T00:00:00+00:00"), 946684800000); + //assert(Date.parse("2000-01-01T00:00:00+00:30"), 946686600000); + var d = new Date("2000T00:00"); // Jan 1st 2000, 0:00:00 local time + assert(typeof d === 'object' && d.toString() != 'Invalid Date'); + assert((new Date('Jan 1 2000')).toISOString(), + d.toISOString()); + assert((new Date('Jan 1 2000 00:00')).toISOString(), + d.toISOString()); + assert((new Date('Jan 1 2000 00:00:00')).toISOString(), + d.toISOString()); + assert((new Date('Jan 1 2000 00:00:00 GMT+0100')).toISOString(), + '1999-12-31T23:00:00.000Z'); + assert((new Date('Jan 1 2000 00:00:00 GMT+0200')).toISOString(), + '1999-12-31T22:00:00.000Z'); + assert((new Date('Sat Jan 1 2000')).toISOString(), + d.toISOString()); + assert((new Date('Sat Jan 1 2000 00:00')).toISOString(), + d.toISOString()); + assert((new Date('Sat Jan 1 2000 00:00:00')).toISOString(), + d.toISOString()); + assert((new Date('Sat Jan 1 2000 00:00:00 GMT+0100')).toISOString(), + '1999-12-31T23:00:00.000Z'); + assert((new Date('Sat Jan 1 2000 00:00:00 GMT+0200')).toISOString(), + '1999-12-31T22:00:00.000Z'); + + var d = new Date(1506098258091); + assert(d.toISOString(), "2017-09-22T16:37:38.091Z"); + d.setUTCHours(18, 10, 11); + assert(d.toISOString(), "2017-09-22T18:10:11.091Z"); + var a = Date.parse(d.toISOString()); + assert((new Date(a)).toISOString(), d.toISOString()); + + assert((new Date("2020-01-01T01:01:01.123Z")).toISOString(), + "2020-01-01T01:01:01.123Z"); + /* implementation defined behavior */ + assert((new Date("2020-01-01T01:01:01.1Z")).toISOString(), + "2020-01-01T01:01:01.100Z"); + assert((new Date("2020-01-01T01:01:01.12Z")).toISOString(), + "2020-01-01T01:01:01.120Z"); + assert((new Date("2020-01-01T01:01:01.1234Z")).toISOString(), + "2020-01-01T01:01:01.123Z"); + assert((new Date("2020-01-01T01:01:01.12345Z")).toISOString(), + "2020-01-01T01:01:01.123Z"); + assert((new Date("2020-01-01T01:01:01.1235Z")).toISOString(), + "2020-01-01T01:01:01.123Z"); + assert((new Date("2020-01-01T01:01:01.9999Z")).toISOString(), + "2020-01-01T01:01:01.999Z"); + + assert(Date.UTC(2017), 1483228800000); + assert(Date.UTC(2017, 9), 1506816000000); + assert(Date.UTC(2017, 9, 22), 1508630400000); + assert(Date.UTC(2017, 9, 22, 18), 1508695200000); + assert(Date.UTC(2017, 9, 22, 18, 10), 1508695800000); + assert(Date.UTC(2017, 9, 22, 18, 10, 11), 1508695811000); + assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91), 1508695811091); + + assert(Date.UTC(NaN), NaN); + assert(Date.UTC(2017, NaN), NaN); + assert(Date.UTC(2017, 9, NaN), NaN); + assert(Date.UTC(2017, 9, 22, NaN), NaN); + assert(Date.UTC(2017, 9, 22, 18, NaN), NaN); + assert(Date.UTC(2017, 9, 22, 18, 10, NaN), NaN); + assert(Date.UTC(2017, 9, 22, 18, 10, 11, NaN), NaN); + assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91, NaN), 1508695811091); + + // TODO: Fix rounding errors on Windows/Cygwin. + if (!['win32', 'cygwin'].includes(os.platform)) { + // from test262/test/built-ins/Date/UTC/fp-evaluation-order.js + assert(Date.UTC(1970, 0, 1, 80063993375, 29, 1, -288230376151711740), 29312, + 'order of operations / precision in MakeTime'); + assert(Date.UTC(1970, 0, 213503982336, 0, 0, 0, -18446744073709552000), 34447360, + 'precision in MakeDate'); + } + //assert(Date.UTC(2017 - 1e9, 9 + 12e9), 1506816000000); // node fails this + assert(Date.UTC(2017, 9, 22 - 1e10, 18 + 24e10), 1508695200000); + assert(Date.UTC(2017, 9, 22, 18 - 1e10, 10 + 60e10), 1508695800000); + assert(Date.UTC(2017, 9, 22, 18, 10 - 1e10, 11 + 60e10), 1508695811000); + assert(Date.UTC(2017, 9, 22, 18, 10, 11 - 1e12, 91 + 1000e12), 1508695811091); + assert(new Date("2024 Apr 7 1:00 AM").toLocaleString(), "04/07/2024, 01:00:00 AM"); + assert(new Date("2024 Apr 7 2:00 AM").toLocaleString(), "04/07/2024, 02:00:00 AM"); + assert(new Date("2024 Apr 7 11:00 AM").toLocaleString(), "04/07/2024, 11:00:00 AM"); + assert(new Date("2024 Apr 7 12:00 AM").toLocaleString(), "04/07/2024, 12:00:00 AM"); + assert(new Date("2024 Apr 7 1:00 PM").toLocaleString(), "04/07/2024, 01:00:00 PM"); + assert(new Date("2024 Apr 7 2:00 PM").toLocaleString(), "04/07/2024, 02:00:00 PM"); + assert(new Date("2024 Apr 7 11:00 PM").toLocaleString(), "04/07/2024, 11:00:00 PM"); + assert(new Date("2024 Apr 7 12:00 PM").toLocaleString(), "04/07/2024, 12:00:00 PM"); +} + +function test_regexp() +{ + var a, str; + str = "abbbbbc"; + a = /(b+)c/.exec(str); + assert(a[0], "bbbbbc"); + assert(a[1], "bbbbb"); + assert(a.index, 1); + assert(a.input, str); + a = /(b+)c/.test(str); + assert(a, true); + assert(/\x61/.exec("a")[0], "a"); + assert(/\u0061/.exec("a")[0], "a"); + assert(/\ca/.exec("\x01")[0], "\x01"); + assert(/\\a/.exec("\\a")[0], "\\a"); + assert(/\c0/.exec("\\c0")[0], "\\c0"); + + a = /(\.(?=com|org)|\/)/.exec("ah.com"); + assert(a.index === 2 && a[0] === "."); + + a = /(\.(?!com|org)|\/)/.exec("ah.com"); + assert(a, null); + + a = /(?=(a+))/.exec("baaabac"); + assert(a.index === 1 && a[0] === "" && a[1] === "aaa"); + + a = /(z)((a+)?(b+)?(c))*/.exec("zaacbbbcac"); + assert(a, ["zaacbbbcac","z","ac","a",,"c"]); + + a = eval("/\0a/"); + assert(a.toString(), "/\0a/"); + assert(a.exec("\0a")[0], "\0a"); + + assert(/{1a}/.toString(), "/{1a}/"); + a = /a{1+/.exec("a{11"); + assert(a, ["a{11"] ); + + eval("/[a-]/"); // accepted with no flag + eval("/[a-]/u"); // accepted with 'u' flag + + let ex; + try { + eval("/[a-]/v"); // rejected with 'v' flag + } catch (_ex) { + ex = _ex; + } + assert(ex?.message, "invalid class range"); + + eval("/[\\-]/"); + eval("/[\\-]/u"); + + /* test zero length matches */ + a = /()*?a/.exec(","); + assert(a, null); + a = /(?:(?=(abc)))a/.exec("abc"); + assert(a, ["a", "abc"]); + a = /(?:(?=(abc)))?a/.exec("abc"); + assert(a, ["a", undefined]); + a = /(?:(?=(abc))){0,2}a/.exec("abc"); + assert(a, ["a", undefined]); + a = /(?:|[\w])+([0-9])/.exec("123a23"); + assert(a, ["123a23", "3"]); + a = "ab".split(/(c)*/); + assert(a, ["a", undefined, "b"]); +} + +function test_symbol() +{ + var a, b, obj, c; + a = Symbol("abc"); + obj = {}; + obj[a] = 2; + assert(obj[a], 2); + assert(typeof obj["abc"], "undefined"); + assert(String(a), "Symbol(abc)"); + b = Symbol("abc"); + assert(a == a); + assert(a === a); + assert(a != b); + assert(a !== b); + + b = Symbol.for("abc"); + c = Symbol.for("abc"); + assert(b === c); + assert(b !== a); + + assert(Symbol.keyFor(b), "abc"); + assert(Symbol.keyFor(a), undefined); + + a = Symbol("aaa"); + assert(a.valueOf(), a); + assert(a.toString(), "Symbol(aaa)"); + + b = Object(a); + assert(b.valueOf(), a); + assert(b.toString(), "Symbol(aaa)"); +} + +function test_map() +{ + var a, i, n, tab, o, v; + n = 1000; + + a = new Map(); + for (var i = 0; i < n; i++) { + a.set(i, i); + } + a.set(-2147483648, 1); + assert(a.get(-2147483648), 1); + assert(a.get(-2147483647 - 1), 1); + assert(a.get(-2147483647.5 - 0.5), 1); + + a = new Map(); + tab = []; + for(i = 0; i < n; i++) { + v = { }; + o = { id: i }; + tab[i] = [o, v]; + a.set(o, v); + } + + assert(a.size, n); + for(i = 0; i < n; i++) { + assert(a.get(tab[i][0]), tab[i][1]); + } + + i = 0; + a.forEach(function (v, o) { + assert(o, tab[i++][0]); + assert(a.has(o)); + assert(a.delete(o)); + assert(!a.has(o)); + }); + + assert(a.size, 0); +} + +function test_weak_map() +{ + var a, e, i, n, tab, o, v, n2; + a = new WeakMap(); + n = 10; + tab = []; + for (const k of [null, 42, "no", Symbol.for("forbidden")]) { + e = undefined; + try { + a.set(k, 42); + } catch (_e) { + e = _e; + } + assert(!!e); + assert(e.message, "invalid value used as WeakMap key"); + } + for(i = 0; i < n; i++) { + v = { }; + o = { id: i }; + tab[i] = [o, v]; + a.set(o, v); + } + o = null; + + n2 = n >> 1; + for(i = 0; i < n2; i++) { + a.delete(tab[i][0]); + } + for(i = n2; i < n; i++) { + tab[i][0] = null; /* should remove the object from the WeakMap too */ + } + /* the WeakMap should be empty here */ +} + +function test_set() +{ + const iter = { + a: [4, 5, 6], + nextCalls: 0, + returnCalls: 0, + next() { + const done = this.nextCalls >= this.a.length + const value = this.a[this.nextCalls] + this.nextCalls++ + return {done, value} + }, + return() { + this.returnCalls++ + return this + }, + } + const setlike = { + size: iter.a.length, + has(v) { return iter.a.includes(v) }, + keys() { return iter }, + } + // set must be bigger than iter.a to hit iter.next and iter.return + assert(new Set([4,5,6,7]).isSupersetOf(setlike), true) + assert(iter.nextCalls, 4) + assert(iter.returnCalls, 0) + iter.nextCalls = iter.returnCalls = 0 + assert(new Set([0,1,2,3]).isSupersetOf(setlike), false) + assert(iter.nextCalls, 1) + assert(iter.returnCalls, 1) + iter.nextCalls = iter.returnCalls = 0 + // set must be bigger than iter.a to hit iter.next and iter.return + assert(new Set([4,5,6,7]).isDisjointFrom(setlike), false) + assert(iter.nextCalls, 1) + assert(iter.returnCalls, 1) + iter.nextCalls = iter.returnCalls = 0 + assert(new Set([0,1,2,3]).isDisjointFrom(setlike), true) + assert(iter.nextCalls, 4) + assert(iter.returnCalls, 0) + iter.nextCalls = iter.returnCalls = 0 + function expectException(klass, sizes) { + for (const size of sizes) { + let ex + try { + new Set([]).union({size}) + } catch (e) { + ex = e + } + assert(ex instanceof klass) + assert(typeof ex.message, "string") + assert(ex.message.includes(".size")) + } + } + expectException(RangeError, [-1, -(Number.MAX_SAFE_INTEGER+1), -Infinity]) + expectException(TypeError, [NaN]) + const legal = [ + 0, -0, 1, 2, + Number.MAX_SAFE_INTEGER + 1, + Number.MAX_SAFE_INTEGER + 2, + Number.MAX_SAFE_INTEGER + 3, + Infinity + ] + for (const size of legal) { + new Set([]).union({ + size, + has() { return false }, + keys() { return [].values() }, + }) + } +} + +function test_weak_set() +{ + var a, e; + a = new WeakSet(); + for (const k of [null, 42, "no", Symbol.for("forbidden")]) { + e = undefined; + try { + a.add(k); + } catch (_e) { + e = _e; + } + assert(!!e); + assert(e.message, "invalid value used as WeakSet key"); + } +} + +function test_generator() +{ + function *f() { + var ret; + yield 1; + ret = yield 2; + assert(ret, "next_arg"); + return 3; + } + function *f2() { + yield 1; + yield 2; + return "ret_val"; + } + function *f1() { + var ret = yield *f2(); + assert(ret, "ret_val"); + return 3; + } + function *f3() { + var ret; + /* test stack consistency with nip_n to handle yield return + + * finally clause */ + try { + ret = 2 + (yield 1); + } catch(e) { + } finally { + ret++; + } + return ret; + } + var g, v; + g = f(); + v = g.next(); + assert(v.value === 1 && v.done === false); + v = g.next(); + assert(v.value === 2 && v.done === false); + v = g.next("next_arg"); + assert(v.value === 3 && v.done === true); + v = g.next(); + assert(v.value === undefined && v.done === true); + + g = f1(); + v = g.next(); + assert(v.value === 1 && v.done === false); + v = g.next(); + assert(v.value === 2 && v.done === false); + v = g.next(); + assert(v.value === 3 && v.done === true); + v = g.next(); + assert(v.value === undefined && v.done === true); + + g = f3(); + v = g.next(); + assert(v.value === 1 && v.done === false); + v = g.next(3); + assert(v.value === 6 && v.done === true); +} + +function test_proxy_iter() +{ + const p = new Proxy({}, { + getOwnPropertyDescriptor() { + return {configurable: true, enumerable: true, value: 42}; + }, + ownKeys() { + return ["x", "y"]; + }, + }); + const a = []; + for (const x in p) a.push(x); + assert(a[0], "x"); + assert(a[1], "y"); +} + +/* CVE-2023-31922 */ +function test_proxy_is_array() +{ + for (var r = new Proxy([], {}), y = 0; y < 331072; y++) + r = new Proxy(r, {}); + + try { + /* Without ASAN */ + assert(Array.isArray(r)); + } catch(e) { + /* With ASAN expect RangeError "Maximum call stack size exceeded" to be raised */ + if (e instanceof RangeError) { + assert(e.message, "Maximum call stack size exceeded", "Stack overflow error was not raised") + } else { + throw e; + } + } +} + +function test_finalization_registry() +{ + { + let expected = {}; + let actual; + let finrec = new FinalizationRegistry(v => { actual = v }); + finrec.register({}, expected); + queueMicrotask(() => { + assert(actual, expected); + }); + } + { + let expected = 42; + let actual; + let finrec = new FinalizationRegistry(v => { actual = v }); + finrec.register({}, expected); + queueMicrotask(() => { + assert(actual, expected); + }); + } +} + +function test_cur_pc() +{ + var a = []; + Object.defineProperty(a, '1', { + get: function() { throw Error("a[1]_get"); }, + set: function(x) { throw Error("a[1]_set"); } + }); + assertThrows(Error, function() { return a[1]; }); + assertThrows(Error, function() { a[1] = 1; }); + assertThrows(Error, function() { return [...a]; }); + assertThrows(Error, function() { return ({...b} = a); }); + + var o = {}; + Object.defineProperty(o, 'x', { + get: function() { throw Error("o.x_get"); }, + set: function(x) { throw Error("o.x_set"); } + }); + o.valueOf = function() { throw Error("o.valueOf"); }; + assertThrows(Error, function() { return +o; }); + assertThrows(Error, function() { return -o; }); + assertThrows(Error, function() { return o+1; }); + assertThrows(Error, function() { return o-1; }); + assertThrows(Error, function() { return o*1; }); + assertThrows(Error, function() { return o/1; }); + assertThrows(Error, function() { return o%1; }); + assertThrows(Error, function() { return o**1; }); + assertThrows(Error, function() { return o<<1; }); + assertThrows(Error, function() { return o>>1; }); + assertThrows(Error, function() { return o>>>1; }); + assertThrows(Error, function() { return o&1; }); + assertThrows(Error, function() { return o|1; }); + assertThrows(Error, function() { return o^1; }); + assertThrows(Error, function() { return o<1; }); + assertThrows(Error, function() { return o==1; }); + assertThrows(Error, function() { return o++; }); + assertThrows(Error, function() { return o--; }); + assertThrows(Error, function() { return ++o; }); + assertThrows(Error, function() { return --o; }); + assertThrows(Error, function() { return ~o; }); + + Object.defineProperty(globalThis, 'xxx', { + get: function() { throw Error("xxx_get"); }, + set: function(x) { throw Error("xxx_set"); } + }); + assertThrows(Error, function() { return xxx; }); + assertThrows(Error, function() { xxx = 1; }); +} + +test(); +test_function(); +test_enum(); +test_array(); +test_string(); +test_rope(); +test_math(); +test_number(); +test_eval(); +test_typed_array(); +test_json(); +test_date(); +test_regexp(); +test_symbol(); +test_map(); +test_weak_map(); +test_set(); +test_weak_set(); +test_generator(); +test_proxy_iter(); +test_proxy_is_array(); +test_finalization_registry(); +test_exception_source_pos(); +test_function_source_pos(); +test_exception_prepare_stack(); +test_exception_stack_size_limit(); +test_exception_capture_stack_trace(); +test_exception_capture_stack_trace_filter(); +test_cur_pc(); diff --git a/test/vm/test_language.js b/test/vm/test_language.js new file mode 100644 index 00000000..1050c58c --- /dev/null +++ b/test/vm/test_language.js @@ -0,0 +1,762 @@ +// This test cannot use imports because it needs to run in non-strict mode. + +function assert(actual, expected, message) { + if (arguments.length == 1) + expected = true; + + if (actual === expected) + return; + + if (typeof actual == 'number' && isNaN(actual) + && typeof expected == 'number' && isNaN(expected)) + return; + + if (actual !== null && expected !== null + && typeof actual == 'object' && typeof expected == 'object' + && actual.toString() === expected.toString()) + return; + + var msg = message ? " (" + message + ")" : ""; + throw Error("assertion failed: got |" + actual + "|" + + ", expected |" + expected + "|" + msg); +} + +function assert_throws(expected_error, func, message) +{ + var err = false; + var msg = message ? " (" + message + ")" : ""; + try { + switch (typeof func) { + case 'string': + eval(func); + break; + case 'function': + func(); + break; + } + } catch(e) { + err = true; + if (!(e instanceof expected_error)) { + throw Error(`expected ${expected_error.name}, got ${e.name}${msg}`); + } + } + if (!err) { + throw Error(`expected ${expected_error.name}${msg}`); + } +} + +/*----------------*/ + +function test_op1() +{ + var r, a; + r = 1 + 2; + assert(r, 3, "1 + 2 === 3"); + + r = 1 - 2; + assert(r, -1, "1 - 2 === -1"); + + r = -1; + assert(r, -1, "-1 === -1"); + + r = +2; + assert(r, 2, "+2 === 2"); + + r = 2 * 3; + assert(r, 6, "2 * 3 === 6"); + + r = 4 / 2; + assert(r, 2, "4 / 2 === 2"); + + r = 4 % 3; + assert(r, 1, "4 % 3 === 3"); + + r = 4 << 2; + assert(r, 16, "4 << 2 === 16"); + + r = 1 << 0; + assert(r, 1, "1 << 0 === 1"); + + r = 1 << 31; + assert(r, -2147483648, "1 << 31 === -2147483648"); + + r = 1 << 32; + assert(r, 1, "1 << 32 === 1"); + + r = (1 << 31) < 0; + assert(r, true, "(1 << 31) < 0 === true"); + + r = -4 >> 1; + assert(r, -2, "-4 >> 1 === -2"); + + r = -4 >>> 1; + assert(r, 0x7ffffffe, "-4 >>> 1 === 0x7ffffffe"); + + r = 1 & 1; + assert(r, 1, "1 & 1 === 1"); + + r = 0 | 1; + assert(r, 1, "0 | 1 === 1"); + + r = 1 ^ 1; + assert(r, 0, "1 ^ 1 === 0"); + + r = ~1; + assert(r, -2, "~1 === -2"); + + r = !1; + assert(r, false, "!1 === false"); + + assert((1 < 2), true, "(1 < 2) === true"); + + assert((2 > 1), true, "(2 > 1) === true"); + + assert(('b' > 'a'), true, "('b' > 'a') === true"); + + assert(2 ** 8, 256, "2 ** 8 === 256"); +} + +function test_cvt() +{ + assert((NaN | 0), 0); + assert((Infinity | 0), 0); + assert(((-Infinity) | 0), 0); + assert(("12345" | 0), 12345); + assert(("0x12345" | 0), 0x12345); + assert(((4294967296 * 3 - 4) | 0), -4); + + assert(("12345" >>> 0), 12345); + assert(("0x12345" >>> 0), 0x12345); + assert((NaN >>> 0), 0); + assert((Infinity >>> 0), 0); + assert(((-Infinity) >>> 0), 0); + assert(((4294967296 * 3 - 4) >>> 0), (4294967296 - 4)); + assert((19686109595169230000).toString(), "19686109595169230000"); +} + +function test_eq() +{ + assert(null == undefined); + assert(undefined == null); + assert(true == 1); + assert(0 == false); + assert("" == 0); + assert("123" == 123); + assert("122" != 123); + assert((new Number(1)) == 1); + assert(2 == (new Number(2))); + assert((new String("abc")) == "abc"); + assert({} != "abc"); +} + +function test_inc_dec() +{ + var a, r; + + a = 1; + r = a++; + assert(r === 1 && a === 2, true, "++"); + + a = 1; + r = ++a; + assert(r === 2 && a === 2, true, "++"); + + a = 1; + r = a--; + assert(r === 1 && a === 0, true, "--"); + + a = 1; + r = --a; + assert(r === 0 && a === 0, true, "--"); + + a = {x:true}; + a.x++; + assert(a.x, 2, "++"); + + a = {x:true}; + a.x--; + assert(a.x, 0, "--"); + + a = [true]; + a[0]++; + assert(a[0], 2, "++"); + + a = {x:true}; + r = a.x++; + assert(r === 1 && a.x === 2, true, "++"); + + a = {x:true}; + r = a.x--; + assert(r === 1 && a.x === 0, true, "--"); + + a = [true]; + r = a[0]++; + assert(r === 1 && a[0] === 2, true, "++"); + + a = [true]; + r = a[0]--; + assert(r === 1 && a[0] === 0, true, "--"); +} + +function F(x) +{ + this.x = x; +} + +function test_op2() +{ + var a, b; + a = new Object; + a.x = 1; + assert(a.x, 1, "new"); + b = new F(2); + assert(b.x, 2, "new"); + + a = {x : 2}; + assert(("x" in a), true, "in"); + assert(("y" in a), false, "in"); + + a = {}; + assert((a instanceof Object), true, "instanceof"); + assert((a instanceof String), false, "instanceof"); + + assert((typeof 1), "number", "typeof"); + assert((typeof Object), "function", "typeof"); + assert((typeof null), "object", "typeof"); + assert((typeof unknown_var), "undefined", "typeof"); + + a = {x: 1, if: 2, async: 3}; + assert(a.if === 2); + assert(a.async === 3); +} + +function test_delete() +{ + var a, err; + + a = {x: 1, y: 1}; + assert((delete a.x), true, "delete"); + assert(("x" in a), false, "delete"); + + /* the following are not tested by test262 */ + assert(delete "abc"[100], true); + + err = false; + try { + delete null.a; + } catch(e) { + err = (e instanceof TypeError); + } + assert(err, true, "delete"); + + err = false; + try { + a = { f() { delete super.a; } }; + a.f(); + } catch(e) { + err = (e instanceof ReferenceError); + } + assert(err, true, "delete"); +} + +function test_constructor() +{ + function *G() {} + let ex + try { new G() } catch (ex_) { ex = ex_ } + assert(ex instanceof TypeError) + assert(ex.message, "G is not a constructor") +} + +function test_prototype() +{ + var f = function f() { }; + assert(f.prototype.constructor, f, "prototype"); + + var g = function g() { }; + /* QuickJS bug */ + Object.defineProperty(g, "prototype", { writable: false }); + assert(g.prototype.constructor, g, "prototype"); +} + +function test_arguments() +{ + function f2() { + assert(arguments.length, 2, "arguments"); + assert(arguments[0], 1, "arguments"); + assert(arguments[1], 3, "arguments"); + } + f2(1, 3); + + /* mapped arguments with GC must not crash (non-detached var_refs) */ + function f3(a) { + arguments; + gc(); + } + f3(0); +} + +function test_class() +{ + var o; + class C { + constructor() { + this.x = 10; + } + f() { + return 1; + } + static F() { + return -1; + } + get y() { + return 12; + } + }; + class D extends C { + constructor() { + super(); + this.z = 20; + } + g() { + return 2; + } + static G() { + return -2; + } + h() { + return super.f(); + } + static H() { + return super["F"](); + } + } + + assert(C.F(), -1); + assert(Object.getOwnPropertyDescriptor(C.prototype, "y").get.name === "get y"); + + o = new C(); + assert(o.f(), 1); + assert(o.x, 10); + + assert(D.F(), -1); + assert(D.G(), -2); + assert(D.H(), -1); + + o = new D(); + assert(o.f(), 1); + assert(o.g(), 2); + assert(o.x, 10); + assert(o.z, 20); + assert(o.h(), 1); + + /* test class name scope */ + var E1 = class E { static F() { return E; } }; + assert(E1, E1.F()); + + class S { + static x = 42; + static y = S.x; + static z = this.x; + } + assert(S.x, 42); + assert(S.y, 42); + assert(S.z, 42); + + class P { + get; + set; + async; + get = () => "123"; + set = () => "456"; + async = () => "789"; + static() { return 42; } + } + assert(new P().get(), "123"); + assert(new P().set(), "456"); + assert(new P().async(), "789"); + assert(new P().static(), 42); + + /* test that division after private field in parens is not parsed as regex */ + class Q { + #x = 10; + f() { return (this.#x / 2); } + } + assert(new Q().f(), 5); +}; + +function test_template() +{ + var a, b; + b = 123; + a = `abc${b}d`; + assert(a, "abc123d"); + + a = String.raw `abc${b}d`; + assert(a, "abc123d"); + + a = "aaa"; + b = "bbb"; + assert(`aaa${a, b}ccc`, "aaabbbccc"); +} + +function test_template_skip() +{ + var a = "Bar"; + var { b = `${a + `a${a}` }baz` } = {}; + assert(b, "BaraBarbaz"); +} + +function test_object_literal() +{ + var x = 0, get = 1, set = 2; async = 3; + a = { get: 2, set: 3, async: 4, get a(){ return this.get} }; + assert(JSON.stringify(a), '{"get":2,"set":3,"async":4,"a":2}'); + assert(a.a, 2); + + a = { x, get, set, async }; + assert(JSON.stringify(a), '{"x":0,"get":1,"set":2,"async":3}'); +} + +function test_regexp_skip() +{ + var a, b; + [a, b = /abc\(/] = [1]; + assert(a, 1); + + [a, b =/abc\(/] = [2]; + assert(a, 2); +} + +function test_labels() +{ + do x: { break x; } while(0); + if (1) + x: { break x; } + else + x: { break x; } + with ({}) x: { break x; }; + while (0) x: { break x; }; +} + +function test_destructuring() +{ + function * g () { return 0; }; + var [x] = g(); + assert(x, void 0); +} + +function test_spread() +{ + var x; + x = [1, 2, ...[3, 4]]; + assert(x.toString(), "1,2,3,4"); + + x = [ ...[ , ] ]; + assert(Object.getOwnPropertyNames(x).toString(), "0,length"); +} + +function test_function_length() +{ + assert( ((a, b = 1, c) => {}).length, 1); + assert( (([a,b]) => {}).length, 1); + assert( (({a,b}) => {}).length, 1); + assert( ((c, [a,b] = 1, d) => {}).length, 1); +} + +function test_argument_scope() +{ + var f; + var c = "global"; + + f = function(a = eval("var arguments")) {}; + // for some reason v8 does not throw an exception here + if (typeof require === 'undefined') + assert_throws(SyntaxError, f); + + f = function(a = eval("1"), b = arguments[0]) { return b; }; + assert(f(12), 12); + + f = function(a, b = arguments[0]) { return b; }; + assert(f(12), 12); + + f = function(a, b = () => arguments) { return b; }; + assert(f(12)()[0], 12); + + f = function(a = eval("1"), b = () => arguments) { return b; }; + assert(f(12)()[0], 12); + + (function() { + "use strict"; + f = function(a = this) { return a; }; + assert(f.call(123), 123); + + f = function f(a = f) { return a; }; + assert(f(), f); + + f = function f(a = eval("f")) { return a; }; + assert(f(), f); + })(); + + f = (a = eval("var c = 1"), probe = () => c) => { + var c = 2; + assert(c, 2); + assert(probe(), 1); + } + f(); + + f = (a = eval("var arguments = 1"), probe = () => arguments) => { + var arguments = 2; + assert(arguments, 2); + assert(probe(), 1); + } + f(); + + f = function f(a = eval("var c = 1"), b = c, probe = () => c) { + assert(b, 1); + assert(c, 1); + assert(probe(), 1) + } + f(); + + assert(c, "global"); + f = function f(a, b = c, probe = () => c) { + eval("var c = 1"); + assert(c, 1); + assert(b, "global"); + assert(probe(), "global") + } + f(); + assert(c, "global"); + + f = function f(a = eval("var c = 1"), probe = (d = eval("c")) => d) { + assert(probe(), 1) + } + f(); +} + +function test_function_expr_name() +{ + var f; + + /* non strict mode test : assignment to the function name silently + fails */ + + f = function myfunc() { + myfunc = 1; + return myfunc; + }; + assert(f(), f); + + f = function myfunc() { + myfunc = 1; + (() => { + myfunc = 1; + })(); + return myfunc; + }; + assert(f(), f); + + f = function myfunc() { + eval("myfunc = 1"); + return myfunc; + }; + assert(f(), f); + + /* strict mode test : assignment to the function name raises a + TypeError exception */ + + f = function myfunc() { + "use strict"; + myfunc = 1; + }; + assert_throws(TypeError, f); + + f = function myfunc() { + "use strict"; + (() => { + myfunc = 1; + })(); + }; + assert_throws(TypeError, f); + + f = function myfunc() { + "use strict"; + eval("myfunc = 1"); + }; + assert_throws(TypeError, f); +} + +function test_expr(expr, err) { + if (err) + assert_throws(err, expr, `for ${expr}`); + else + assert(1, eval(expr), `for ${expr}`); +} + +function test_name(name, err) +{ + test_expr(`(function() { return typeof ${name} ? 1 : 1; })()`); + test_expr(`(function() { var ${name}; ${name} = 1; return ${name}; })()`); + test_expr(`(function() { let ${name}; ${name} = 1; return ${name}; })()`, name == 'let' ? SyntaxError : undefined); + test_expr(`(function() { const ${name} = 1; return ${name}; })()`, name == 'let' ? SyntaxError : undefined); + test_expr(`(function(${name}) { ${name} = 1; return ${name}; })()`); + test_expr(`(function({${name}}) { ${name} = 1; return ${name}; })({})`); + test_expr(`(function ${name}() { return ${name} ? 1 : 0; })()`); + test_expr(`"use strict"; (function() { return typeof ${name} ? 1 : 1; })()`, err); + test_expr(`"use strict"; (function() { if (0) ${name} = 1; return 1; })()`, err); + test_expr(`"use strict"; (function() { var x; if (0) x = ${name}; return 1; })()`, err); + test_expr(`"use strict"; (function() { var ${name}; return 1; })()`, err); + test_expr(`"use strict"; (function() { let ${name}; return 1; })()`, err); + test_expr(`"use strict"; (function() { const ${name} = 1; return 1; })()`, err); + test_expr(`"use strict"; (function() { var ${name}; ${name} = 1; return 1; })()`, err); + test_expr(`"use strict"; (function() { var ${name}; ${name} = 1; return ${name}; })()`, err); + test_expr(`"use strict"; (function(${name}) { return 1; })()`, err); + test_expr(`"use strict"; (function({${name}}) { return 1; })({})`, err); + test_expr(`"use strict"; (function ${name}() { return 1; })()`, err); + test_expr(`(function() { "use strict"; return typeof ${name} ? 1 : 1; })()`, err); + test_expr(`(function() { "use strict"; if (0) ${name} = 1; return 1; })()`, err); + test_expr(`(function() { "use strict"; var x; if (0) x = ${name}; return 1; })()`, err); + test_expr(`(function() { "use strict"; var ${name}; return 1; })()`, err); + test_expr(`(function() { "use strict"; let ${name}; return 1; })()`, err); + test_expr(`(function() { "use strict"; const ${name} = 1; return 1; })()`, err); + test_expr(`(function() { "use strict"; var ${name}; ${name} = 1; return 1; })()`, err); + test_expr(`(function() { "use strict"; var ${name}; ${name} = 1; return ${name}; })()`, err); + test_expr(`(function(${name}) { "use strict"; return 1; })()`, err); + test_expr(`(function({${name}}) { "use strict"; return 1; })({})`, SyntaxError); + test_expr(`(function ${name}() { "use strict"; return 1; })()`, err); +} + +function test_reserved_names() +{ + test_name('await'); + test_name('yield', SyntaxError); + test_name('implements', SyntaxError); + test_name('interface', SyntaxError); + test_name('let', SyntaxError); + test_name('package', SyntaxError); + test_name('private', SyntaxError); + test_name('protected', SyntaxError); + test_name('public', SyntaxError); + test_name('static', SyntaxError); +} + +function test_number_literals() +{ + assert(0.1.a, undefined); + assert(0x1.a, undefined); + assert(0b1.a, undefined); + assert(01.a, undefined); + assert(0o1.a, undefined); + test_expr('0.a', SyntaxError); + assert(parseInt("0_1"), 0); + assert(parseInt("1_0"), 1); + assert(parseInt("0_1", 8), 0); + assert(parseInt("1_0", 8), 1); + assert(parseFloat("0_1"), 0); + assert(parseFloat("1_0"), 1); + assert(1_0, 10); + assert(parseInt("Infinity"), NaN); + assert(parseFloat("Infinity"), Infinity); + assert(parseFloat("Infinity1"), Infinity); + assert(parseFloat("Infinity_"), Infinity); + assert(parseFloat("Infinity."), Infinity); + test_expr('0_0', SyntaxError); + test_expr('00_0', SyntaxError); + test_expr('01_0', SyntaxError); + test_expr('08_0', SyntaxError); + test_expr('09_0', SyntaxError); +} + +function test_syntax() +{ + assert_throws(SyntaxError, "do"); + assert_throws(SyntaxError, "do;"); + assert_throws(SyntaxError, "do{}"); + assert_throws(SyntaxError, "if"); + assert_throws(SyntaxError, "if\n"); + assert_throws(SyntaxError, "if 1"); + assert_throws(SyntaxError, "if \0"); + assert_throws(SyntaxError, "if ;"); + assert_throws(SyntaxError, "if 'abc'"); + assert_throws(SyntaxError, "if `abc`"); + assert_throws(SyntaxError, "if /abc/"); + assert_throws(SyntaxError, "if abc"); + assert_throws(SyntaxError, "if abc\u0064"); + assert_throws(SyntaxError, "if abc\\u0064"); + assert_throws(SyntaxError, "if \u0123"); + assert_throws(SyntaxError, "if \\u0123"); +} + +/* optional chaining tests not present in test262 */ +function test_optional_chaining() +{ + var a, z; + z = null; + a = { b: { c: 2 } }; + assert(delete z?.b.c, true); + assert(delete a?.b.c, true); + assert(JSON.stringify(a), '{"b":{}}', "optional chaining delete"); + + a = { b: { c: 2 } }; + assert(delete z?.b["c"], true); + assert(delete a?.b["c"], true); + assert(JSON.stringify(a), '{"b":{}}'); + + a = { + b() { return this._b; }, + _b: { c: 42 } + }; + + assert((a?.b)().c, 42); + + assert((a?.["b"])().c, 42); +} + +function test_parse_semicolon() +{ + /* 'yield' or 'await' may not be considered as a token if the + previous ';' is missing */ + function *f() + { + function func() { + } + yield 1; + var h = x => x + 1 + yield 2; + } + async function g() + { + function func() { + } + await 1; + var h = x => x + 1 + await 2; + } +} + +test_op1(); +test_cvt(); +test_eq(); +test_inc_dec(); +test_op2(); +test_delete(); +test_constructor(); +test_prototype(); +test_arguments(); +test_class(); +test_template(); +test_template_skip(); +test_object_literal(); +test_regexp_skip(); +test_labels(); +test_destructuring(); +test_spread(); +test_function_length(); +test_argument_scope(); +test_function_expr_name(); +test_reserved_names(); +test_number_literals(); +test_syntax(); +test_optional_chaining(); +test_parse_semicolon();