Skip to content

BEAM-native JS engine and compiler#5

Open
dannote wants to merge 665 commits intomasterfrom
beam-vm-interpreter
Open

BEAM-native JS engine and compiler#5
dannote wants to merge 665 commits intomasterfrom
beam-vm-interpreter

Conversation

@dannote
Copy link
Copy Markdown
Member

@dannote dannote commented Apr 15, 2026

Adds a second QuickJS execution backend on the BEAM.

What’s in here

  • QuickJS bytecode decoder in Elixir
  • interpreter for QuickJS bytecode on the BEAM
  • hybrid compiler from QuickJS bytecode to BEAM modules
  • raw BEAM disassembly for the :beam backend via QuickBEAM.disasm/2
  • mode: :beam support in the public API
  • require(), module loading, dynamic import, globals, handlers, and interop for the VM path
  • stack traces, source positions, and Error.captureStackTrace

Runtime coverage

  • Object, Array, Function, String, Number, Boolean
  • Math, JSON, Date, RegExp
  • Map, Set, WeakMap, WeakSet, Symbol
  • Promise, async/await, generators, async generators
  • Proxy, Reflect
  • TypedArray, ArrayBuffer, BigInt
  • classes, inheritance, super, private fields, private methods, private accessors, static private members, brand checks

Validation

  • QUICKBEAM_BUILD=1 MIX_ENV=test mix test
  • MIX_ENV=test QUICKBEAM_BUILD=1 mix test test/vm/js_engine_test.exs --include js_engine --seed 0
  • mix compile --warnings-as-errors
  • mix format --check-formatted
  • mix credo --strict
  • mix dialyzer
  • mix ex_dna
  • zlint lib/quickbeam/*.zig lib/quickbeam/napi/*.zig
  • bunx oxlint -c oxlint.json --type-aware --type-check priv/ts/
  • bunx jscpd lib/quickbeam/*.zig priv/ts/*.ts --min-tokens 50 --threshold 0

Current local result:

  • 2363 tests, 0 failures, 1 skipped, 54 excluded

@dannote dannote force-pushed the beam-vm-interpreter branch from 0eb3475 to 7c1c574 Compare April 15, 2026 14:06
@dannote dannote changed the title BEAM-native JS interpreter (Phase 0-1) BEAM-native JS interpreter Apr 16, 2026
@dannote dannote marked this pull request as ready for review April 16, 2026 08:41
@dannote dannote force-pushed the beam-vm-interpreter branch 2 times, most recently from 75fdba5 to 527d5b9 Compare April 20, 2026 08:45
@dannote dannote changed the title BEAM-native JS interpreter BEAM-native JS engine and compiler Apr 21, 2026
dannote added 9 commits April 23, 2026 14:44
…tring

Iterator:
- for-of/destructuring on null throws TypeError with proper message
- for-of/destructuring on undefined throws TypeError
- for-of on non-iterable values throws TypeError instead of silently
  using empty iterator

Function.prototype.toString:
- Returns source code for closures/bytecode functions
- Returns 'function name() { [native code] }' for builtins

test262: 451 → 444 failures (7 more passing)
Route toPrimitive valueOf/toString calls through Invocation instead
of Interpreter directly. Invocation properly manages context save/
restore and globals refresh after callbacks.
BigInt values now support .toString() returning the decimal string
representation and .valueOf() returning the BigInt value. Fixes
template literals and string coercion for BigInt values (e.g.
\`${1n}n\` in test262 harness assert._formatIdentityFreeValue).
Symbol.toPrimitive:
- to_primitive now checks @@toPrimitive on the object first (per spec
  7.1.1 ToPrimitive), before falling back to valueOf/toString

BigInt methods:
- (1n).toString() returns decimal string
- (1n).valueOf() returns the BigInt value

test262: 444 → 445 (1 regression in symbol coercion error identity)
get_or_create_prototype now sets __proto__ to Object.prototype on
auto-created function prototypes. This ensures objects created via
`new F()` inherit toString/valueOf from Object.prototype through
the prototype chain: obj -> F.prototype -> Object.prototype.

Also: Symbol.toPrimitive support in to_primitive, BigInt.toString/
valueOf methods, Function.prototype.toString.

test262: 445 → 443 (2 more passing: this/instanceof tests)
- Infinity == Infinity now returns true (was false because :infinity
  atom didn't match is_number guard)
- NaN == anything returns false (explicit clause)
- 0n == '' returns true (empty string treated as 0 for BigInt comparison)
- BigInt string comparison trims whitespace

test262: 443 → 439 (4 more passing)
mod now calls to_number on non-numeric operands before computing.
true % true = 0, null % 1 = 0, '1' % '1' = 0 now work correctly.

Also added numeric_mod helper for infinity/NaN/zero cases:
- x % ±Infinity = x (not NaN)
- ±Infinity % x = NaN
- x % 0 = NaN

test262: 439 → 430 (9 more passing)
- safe_mul: determines overflow sign from operand signs
  (-1.1 * MAX_VALUE → neg_infinity, not infinity)
- safe_add: determines overflow sign from operand signs
- add/sub for numbers now use safe_add to handle overflow
- div_inf: Infinity / 0 → Infinity (was NaN because 0 didn't match
  the n > 0 guard)

test262: 430 → 427 (3 more passing)
BigInt comparisons:
- 1n < true, 0n < true, 1n > false etc. now work by coercing
  booleans to numbers before comparing with BigInt
- Added boolean clauses for lt/lte/gt/gte with BigInt

Equality:
- Infinity == Infinity is true (atoms match)
- NaN == anything is false (explicit clauses)
- 0n == '' is true (empty string → 0)

Float overflow:
- safe_mul determines overflow sign from operand signs
- safe_add determines overflow sign from operand signs
- Infinity / 0 returns Infinity (not NaN)

test262: 430 → 425 (5 more passing from this batch)
dannote added 14 commits April 23, 2026 20:53
- has_property now checks prototype chain via Get.get fallback
  (fixes 'toString' in {}, 'valueOf' in {}, 'MAX_VALUE' in Number)
- has_property added for {:builtin, _, _} values
- 'in' operator throws TypeError for non-object RHS
  (fixes 'x' in true, 'x' in 42, etc.)

test262: 425 → 420 (5 more passing)
typeof:
- {:builtin, _, map} when is_map(map) returns 'object' instead of
  'function'. Fixes typeof Math === 'object', typeof JSON === 'object'.
  Callable builtins (functions) still return 'function'.

new:
- {:builtin, _, map} namespace objects (Math, JSON) throw TypeError
  when used with 'new'. Only callable builtins can be constructors.

test262: 420 → 418 (2 more passing)
When valueOf/toString throws during type coercion (e.g.
{valueOf: function(){throw 'x'}} & 1), the JS throw must be
caught by the interpreter's try/catch handling, not propagated
through the Elixir call stack.

Previously only op_add had a catch_js_throw wrapper. Now ALL
operators that can trigger toPrimitive coercion are wrapped:
- Arithmetic: add, sub, mul, div, mod, pow
- Bitwise: band, bor, bxor, shl, sar, shr, bnot
- Comparison: lt, lte, gt, gte, eq, neq
- Unary: neg, plus (to_number)

This fixes 14 test262 tests where valueOf/toString throws inside
try/catch blocks.

test262: 418 → 404 (14 more passing)
…t32 for objects

- lt/lte/gt/gte: handle BigInt vs :infinity/:neg_infinity/:nan
- abstract_eq: BigInt vs boolean coercion (0n == false → true)
- to_int32/to_uint32: call to_number for {:obj, _} and handle infinity/NaN

test262: 404 → 400
- typeof :neg_infinity returns 'number' (was falling to 'object')
- isNaN: convert non-number args via to_number before checking
  (isNaN(true) now correctly returns false)
- isFinite: same to_number coercion for non-number args
- BigInt vs infinity/NaN: explicit comparison clauses for all operators
- BigInt vs boolean: abstract_eq handles 0n == false → true
- to_int32/to_uint32: call to_number for {:obj, _} values

test262: 404 → 398 (6 more passing from batched fixes)
- truthy?(-0.0) returns false (Elixir distinguishes +0.0 from -0.0
  in pattern matching)
- op_truthy inline helper also handles -0.0
- isNaN: converts non-number args via to_number before checking
- isFinite: same to_number coercion

test262: 400 → 397
- Register Function constructor with auto_proto: true, creating
  Function.prototype with __proto__ → Object.prototype
- Fixes 'MyFunct instanceof Function' and similar checks
- Only Function gets auto_proto (Boolean/Number/String cause
  regressions due to __proto__ interference with property resolution)

Also includes: typeof :neg_infinity, -0.0 falsy, isNaN/isFinite
coercion, BigInt vs infinity/NaN comparisons.

test262: 404 → 396 (8 more passing from all batched fixes)
Set `this` to globalThis in eval context (was :undefined). When
get_var can't find a variable in ctx.globals, check globalThis
object properties as fallback. This bridges 'this.p1 = 1' with
bare 'p1' variable access.

Also: Object.defineProperties implementation.

test262: 396 → 345 (51 tests fixed — with-statement scope tests)
- get_var_undef: checks globalThis properties when variable not
  found in ctx.globals (for typeof x where x is on globalThis)
- put_var: syncs variable assignments to globalThis object so
  this.x and x stay consistent across scope boundaries

test262: 345 → 344
- op_delete_var returns false (var declarations are non-configurable
  per spec, delete x should return false)
- get_var_undef checks globalThis fallback for typeof
- put_var syncs assignments to globalThis for this.x/x consistency

test262: 345 → 341
Closures, bytecode functions, builtins, and bound functions now
return their toString() representation from to_primitive() instead
of the raw tuple (which caused NaN in arithmetic).

f1 + 1 → 'function f1(){return 0}1' (was NaN)

test262: 341 (no change — affected tests have other issues)
Closures, bytecode functions, builtins, and bound functions now
return proper string representations from stringify() instead of
'[object]'. Matches Function.prototype.toString behavior.

test262: 341 (no change)
dannote added 30 commits April 25, 2026 22:09
Result: {"status":"keep","failing_tests":172,"passing_tests":330}
Result: {"status":"keep","failing_tests":172,"passing_tests":330}
…r.locks, console extensions, Worker, EventSource, WritableStream fixes
…nceof, Promise.race, EventSource drain, Worker improvements
…ll NIF web API parity achieved.

Result: {"status":"keep","failing_tests":0,"passing_tests":502}
- 0 compiler warnings (--warnings-as-errors clean)
- 0 credo issues (--strict clean)
- 0 dialyzer errors (was 15)
- Fix FormData encode_multipart to handle string values (not just Blob)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant