Open
Conversation
0eb3475 to
7c1c574
Compare
a3ae334 to
7ee5139
Compare
- strip_exports/1 parses with OXC, extracts inner declarations
- Import removal: OXC AST filters out :import_declaration nodes
- No more String.replace("export ", "") or ~r/^import .*/
- Removed @assert_js_path — assert.js loaded via strip_exports in setup
Skip list: 21 → 7 (4 source-position, 3 QuickJS C engine bugs) - Eval helpers and stubs into runtime separately so test function body keeps its original line numbers (fixes test isolation) - Stubs (gc, os, qjs) condensed to single line to minimize position shift - All 5 language tests now pass (test_reserved_names, test_syntax, etc.) - 9 builtin tests now pass (test_weak_map, test_proxy_iter, test_rope, etc.) Remaining skips: - 4 source-position tests: require original file layout, line numbers shift when functions are extracted and evaluated individually - test_cur_pc/test_eval/test_array: QuickJS NIF engine limitations (defineProperty on arrays, eval var scoping)
- gc, os, qjs defined as proper builtins in Runtime.Globals (beam mode) - Test setup registers them via QuickBEAM.eval for NIF mode compatibility - Removed @stubs_js inline JS string
Fixed tests:
- parseInt('0xff', 16): strip 0x prefix when radix is 16
- ArrayBuffer byteLength: fix constructor arity (/1 → /2)
- 'hello'['length']: add string-key clause to Objects.get_element
- Array.flat nested objects: convert_beam_value now recurses into plain lists
2 remaining pending_beam:
- let-scoped closure in for loop (per-iteration binding not implemented)
- Nested forEach closure mutation (deep closure capture issue)
close_loc: implement per-iteration cell snapshotting for let-scoped loop variables. Creates a fresh cell with the current value, so closures from the previous iteration keep the frozen value. Also fix put_loc_check to write through to closure cells (was only updating locals tuple). build_closure: handle closure_type 2 (grandparent scope). When an inner function captures a variable from its grandparent via the parent's var_refs, read directly from var_refs[var_idx] instead of locals[var_idx]. This fixes nested forEach where the innermost callback captures a variable from two scopes up. All 6 pending_beam tests now pass. Zero remaining.
Thread :filename option through QuickBEAM.eval → Runtime.eval → NIF. The QuickJS NIF already accepted a filename parameter but it was always passed as empty string. Now tests can pass the original JS filename. Pad test function bodies with newlines to preserve original line numbers when evaluating extracted functions individually. Skip list: 21 → 3 (test_cur_pc, test_eval, test_array — QuickJS C bugs) @skip_language is empty. 51 JS engine tests pass.
Reflect.apply, Error.captureStackTrace, Object.getOwnPropertySymbols, FinalizationRegistry stub, WeakMap/Set key validation Beam mode JS engine tests: 17 → 21 passing (test_weak_map, test_weak_set, test_exception_capture_stack_trace_filter, test_generator now pass)
- Scope.resolve_atom: safely stringify unknown atom tuples instead of crashing with Protocol.UndefinedError - FinalizationRegistry: return object with register/unregister methods (no-op stubs, proper GC finalization requires BEAM-level integration)
eval syntax error throwing, eval_code SyntaxError propagation
- Add String.fromCodePoint static method
- parseFloat('Infinity') now returns :infinity (was :nan)
- for_in_start checks Proxy handler's ownKeys trap
- eval of invalid syntax now throws SyntaxError instead of returning undefined
- eval opcode wraps eval_code in catch_js_throw for proper try/catch
Beam mode JS engine: 22→26 passing (4 more tests)
…numerability - String.match with global regex now returns all matches (was only first) - for-in now walks prototype chain to include inherited enumerable properties - Object.prototype methods marked non-enumerable via property descriptors - constructor property filtered from for-in prototype enumeration These fixes don't flip full JS engine tests because each test has multiple assertions, but they fix the underlying issues that many tests depend on.
…() from helpers
- new Function('a','b','return a+b') now works via eval compilation
- Reflect.apply throws TypeError when args is undefined (per spec)
- call_constructor allows function return values (not just objects)
- Exclude test() runner from JS engine test helpers (was causing garbled output)
fix predefined atom fallback serialization - Error.captureStackTrace() with no args now throws TypeError (was crash) - Added Error.prepareStackTrace static property - Implement apply_eval opcode (used by new Function with eval) - Fix PredefinedAtoms fallback: return string instead of tuple (was crash)
Iterative proxy unwrapping instead of single-level check. Handles the test_proxy_is_array test case which creates 331,072 nested proxies and checks Array.isArray. Throws RangeError at 500K depth.
…h opcode
- fn.bind() now preserves original function name ('bound f' not 'bound ')
- Property.get_own handles {:bound, _, _} → delegates to Function.proto_property
- get_length opcode handles {:bound, len, _}
- parseFloat handles Infinity prefix (Infinity1, etc.)
…properties
- JSON.stringify: support replacer array (filter keys) and space parameter
(indentation with proper colon spacing)
- Symbol + number/string: throw TypeError with properly wrapped error object
- Catch Symbol TypeError in :add opcode when catch_stack is active
- Fix bound function name to include original function name ('bound f')
Replace hand-rolled string manipulation (indent_json, add_colon_space, indent_lines) with Jason.encode/2 which handles pretty-printing, indentation, and key ordering via Jason.OrderedObject. Parsing still uses OTP :json.decode (fastest available). Encoding uses Jason for its mature pretty-printing support.
…r fn + toJSON - throw_error: decode atom_idx + error_type args, create proper error objects - delete on null/undefined now throws TypeError (was returning true) - JSON.stringify: support replacer functions (filter keys by callback) - JSON.stringify: support toJSON method on objects
QuickJS throw_error opcodes encode reason codes, not error type indices: 0=TypeError (read-only), 1=SyntaxError (redecl), 2=ReferenceError (TDZ), 3=ReferenceError (delete super), 4=TypeError (iterator throw)
Addition (Values.add):
- new String('1') + 1 now correctly returns '11' instead of 2
- Objects are coerced via ToPrimitive before the string-or-number check,
matching the ES spec for the addition operator
- Only calls to_primitive for {:obj, _} values to avoid overhead on
primitives (no perf regression for the common int/string paths)
make_loc_ref/make_var_ref/make_arg_ref/make_var_ref_ref:
- Fixed pattern match to accept 2-arg form [idx | _] from atom_u16
decoder (was [idx] which silently failed to match)
Same atom_u16 2-arg fix as the compiler, plus implement the missing make_var_ref_ref opcode (122) in the interpreter. This opcode creates a cell reference to a closure variable, needed by with-statement scope chains.
Bitwise operations:
- bnot(BigInt) returns {:bigint, -(n+1)} instead of truncating to int32
- shr (>>>) throws TypeError for BigInt (per spec)
- shl with negative BigInt shift does right-shift
- All bitwise ops (band/bor/bxor/shl/sar/shr) throw TypeError for
mixed BigInt/Number operands
- All bitwise ops call to_numeric on object operands (for Object(1n))
Arithmetic:
- sub/mul/div/mod throw TypeError for mixed BigInt/Number
- All arithmetic ops call to_numeric on object operands
- add recursively re-dispatches after to_primitive (handles
Object(1n) + Object(2n) = 3n)
- Renamed div to js_div to avoid Kernel.div conflict
Comparisons:
- lt/lte/gt/gte handle BigInt vs Number cross-comparison
- BigInt vs String comparison via Integer.parse
- NaN comparisons with BigInt return false
Increment/Decrement:
- inc/dec/post_inc/post_dec/inc_loc/dec_loc handle BigInt directly
({:bigint, n+1}) without going through add/sub
Interpreter:
- make_var_ref_ref opcode (122) implemented
- make_loc_ref/make_arg_ref fixed for atom_u16 2-arg decoder format
test262: 628 → 517 failures (111 tests fixed)
- Use QuickBEAM.start() (with APIs) instead of apis: false, giving tests access to the proper global object where this.x = val works - Pass test source directly to eval instead of wrapping in IIFE, so `this` at top level refers to the global object - Regenerate skip list with matching eval semantics test262: 517 → 508 failures
to_number({:obj, _}) now calls to_primitive first (which tries valueOf
then toString on the prototype chain), then converts the result to a
number. This fixes comparison operators (>, <, >=, <=) for objects
with custom toString but no valueOf.
Remaining test262 gap: 508 failures. Major root causes:
- Global scope mutation not propagating from toPrimitive callbacks (43)
The interpreter's functional architecture doesn't propagate mutable
state from type coercion callbacks back to the caller's scope.
- with statement scope chain + global this (42)
- Error constructor identity in compiled closures (58)
- new expression edge cases (27)
- instanceof Symbol.hasInstance (22)
…andler make_loc_ref, make_arg_ref, make_var_ref_ref use atom_u16 format which decodes to [atom_idx, var_idx]. The SECOND arg is the variable slot index, not the first (which is the variable name atom). Fixed both interpreter and compiler to use the correct argument. make_var_ref (opcode 123) uses :atom format which decodes to [atom_idx] only — it looks up the variable by NAME in global scope. Added handler in both interpreter (GlobalEnv.get by name) and compiler (RuntimeHelpers.make_var_ref). Also fixed if/else ambiguity warnings in BigInt comparison clauses. This fixes 45 :badarg crashes from accessing locals with invalid indices, and 21 unimplemented_opcode errors for make_var_ref. test262: 508 → 506 failures (with many more now passing that were previously crashing with :badarg)
make_var_ref in the interpreter was calling the private GlobalEnv.get/4. Changed to use Map.get on ctx.globals directly. Also added RuntimeHelpers.make_var_ref for the compiler path.
Crashes:
- put_element, define_property (3 locations), names.ex: replaced
Kernel.to_string with Values.stringify for JS value keys that may
be tuples ({:symbol, _}, {:obj, _}, etc.)
- delete error message: stringify key before interpolation
Infinite loop:
- add({:obj, _}, _) could loop infinitely when to_primitive returns
the object. Now falls through to stringify for unconvertible objects.
- shr({:obj, _}) used to_primitive instead of to_numeric.
test262: 506 → 496 failures (10 more passing)
- numeric_compare: handle :infinity/:neg_infinity atoms properly in all comparison operators (lt, lte, gt, gte) - safe_arith: wrap float arithmetic to catch ArithmeticError from BEAM float overflow (BEAM doesn't support IEEE infinity floats) - Add Number.MAX_VALUE (1.7976931348623157e+308) constant - Fix add/sub/mul to use safe_arith wrapper test262: 506 → 464 failures (42 more passing)
new operator: - Throw TypeError for non-constructable values (true, 1, 'str', null, undefined). Previously returned empty object. - Check for generators/async generators (not constructable) instanceof operator: - Throw TypeError when right-hand side is not callable - Throw TypeError when constructor.prototype is not an object (e.g. F.prototype = 42) - Fixes S11.2.2_A3/A4 and S15.3.5.3_A2 test families test262: 464 → 448 failures (16 more passing)
Allow {:obj, _} as RHS of instanceof (for proxy-like objects).
Throw different errors for non-object RHS vs non-object prototype.
Fix match? usage in case clause (not allowed in guard position).
- get_own for {:builtin, name, _} now returns the builtin's name
for the 'name' property. Fixes ReferenceError.name, TypeError.name
etc. returning empty string.
- instanceof: allow {:obj, _} RHS, differentiate error messages
for non-callable RHS vs non-object prototype.
test262 error messages now show actual constructor names instead of
empty strings in assert.throws failures.
…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)
- 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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a second QuickJS execution backend on the BEAM.
What’s in here
:beambackend viaQuickBEAM.disasm/2mode: :beamsupport in the public APIrequire(), module loading, dynamic import, globals, handlers, and interop for the VM pathError.captureStackTraceRuntime coverage
Object,Array,Function,String,Number,BooleanMath,JSON,Date,RegExpMap,Set,WeakMap,WeakSet,SymbolPromise,async/await, generators, async generatorsProxy,ReflectTypedArray,ArrayBuffer,BigIntsuper, private fields, private methods, private accessors, static private members, brand checksValidation
QUICKBEAM_BUILD=1 MIX_ENV=test mix testMIX_ENV=test QUICKBEAM_BUILD=1 mix test test/vm/js_engine_test.exs --include js_engine --seed 0mix compile --warnings-as-errorsmix format --check-formattedmix credo --strictmix dialyzermix ex_dnazlint lib/quickbeam/*.zig lib/quickbeam/napi/*.zigbunx oxlint -c oxlint.json --type-aware --type-check priv/ts/bunx jscpd lib/quickbeam/*.zig priv/ts/*.ts --min-tokens 50 --threshold 0Current local result:
2363 tests, 0 failures, 1 skipped, 54 excluded