Open
Conversation
0eb3475 to
7c1c574
Compare
Port QuickJS-ng's own test suite (test_builtin.js, test_language.js) as an Elixir test harness. Each test_*() function from the JS files becomes an ExUnit test case. Results (NIF mode): 20/37 pass, 17 fail Results (BEAM mode): 4/37 pass, 33 fail Failures are mostly Object.isExtensible/freeze/seal (not implemented), gc() (QuickJS-specific), and language edge cases in BEAM mode. Tagged :js_engine — excluded by default, opt in with: mix test test/js_engine/ --include js_engine QUICKBEAM_MODE=beam mix test test/js_engine/ --include js_engine The 16 tests that pass in NIF but fail in BEAM show exactly where the BEAM interpreter needs work (type coercion, delete, prototype chain, class edge cases, template literals, spread, etc).
Bitwise operations:
- to_int32 now properly wraps to 32-bit signed range (JS ToInt32)
- shl wraps result to int32 (1 << 31 === -2147483648)
- bnot wraps result to int32 (~0 === -1)
- to_int32/to_uint32 handle string→number conversion ("12345" | 0)
- to_number supports hex (0x), octal (0o), binary (0b) string literals
Test runner:
- Extract individual functions instead of evaling entire file
- Eliminates cross-contamination between test functions
- Uses brace-matching to find function boundaries
Results:
NIF: 24/37 pass (was 20)
BEAM: 6/37 pass (was 4)
test_language.js defines its own assert and assert_throws functions. The test runner now extracts the preamble (everything before the first test_ function) and prepends it to each test, providing the helper functions tests depend on. Results: NIF: 28/37 pass (was 24) BEAM: 6/37 pass (was 6) Remaining NIF failures: QuickJS-specific APIs (gc, os, qjs, F, my_func, test - helper functions defined elsewhere in test files). Remaining BEAM failures: abstract equality with wrappers, post-increment on properties, TDZ/uninitialized locals, prototype chain, template tag functions, float-to-string precision, Object.keys for arrays, Symbol.toString, charCodeAt unicode.
…rmatting
- charCodeAt: use String.to_charlist for proper Unicode codepoints
instead of raw byte access (€ returns 8364, not 226)
- String(Symbol('abc')): js_to_string handles symbol tuples
- Object.keys([1,2,3]): returns ['0','1','2'] instead of crashing
on list data. Object.values/entries also fixed to handle new
keys return format
- Float-to-string: use :erlang.float_to_binary(:short) with JS
Number.toString spec formatting. Handles exponential notation
cutoffs (1e-6..1e21), strips mantissa .0
735 beam_vm tests pass, 0 failures.
JS engine: NIF 28/37, BEAM 7/37.
- Include non-test helper functions (my_func, test, F, rope_concat, test_expr, test_name) alongside each test function - Only treat test_*() with no parameters as standalone test functions - test_expr(expr, err) and test_name(name, err) become helpers included for tests that reference them NIF: 29/35 pass. BEAM: 7/35. 733 beam_vm tests, 0 failures.
…totype - Function.prototype: auto-created with constructor back-reference when first accessed. Both get_property and call_constructor use it. Makes instanceof work for plain function constructors. - typeof_is_undefined: properly checks if value is undefined/nil instead of always returning false. Fixed arg pattern ([] not [_]). - typeof_is_function: checks for builtin/closure/function values instead of always returning false. Fixed arg pattern. NIF: 29/35. BEAM: 8/35. 733 beam_vm tests, 0 failures.
….set - String.prototype.codePointAt: proper Unicode codepoint access - Math.clz32: count leading zeros in 32-bit integer - Math.fround: round to 32-bit float - Math.imul: 32-bit integer multiplication - Math trig: asin, acos, atan, atan2, exp, cbrt, hypot - TypedArray.prototype.set: copy array data into typed array NIF: 29/35. BEAM: 8/35. 733 beam_vm tests, 0 failures.
Interpreter fixes:
- perm3/4/5: fix stack rotation direction (rotate down, not up).
Fixes post-increment on properties (a.x++ returns old value)
- Abstract equality (==): ToPrimitive coercion for objects vs
primitives. new Number(1) == 1 is now true. Guard against
infinite loops when ToPrimitive returns an object
- Template objects: bytecode decoder preserves raw array,
push_const materializes {:template_object, elems, raw} as
heap object with .raw property
- new Number/String/Boolean: store primitive value and add
valueOf/toString methods on wrapper objects
- typeof_is_undefined/typeof_is_function: fix arg pattern [],
implement actual type checks
- invoke/3: handle builtins, nil, undefined with proper errors
- Uninitialized locals throw ReferenceError instead of crash
- gc() added as no-op global
Runtime fixes:
- Object.keys: handle list (array) data, guard keys_from_map
- to_js_string: handle list-stored objects (arrays)
- Symbol toString in js_to_string
- Helper function extraction: fix set subtraction precedence
733 beam_vm tests pass, 0 failures.
…o_primitive Stack rotation ops (verified against QuickJS C source): - perm3: [a,b,c] → [a,c,b] (swap sp[-2] and sp[-3]) - perm4: [a,b,c,d] → [a,c,d,b] (rotate sp[-2]..sp[-4]) - perm5: [a,b,c,d,e] → [a,c,d,e,b] (rotate sp[-2]..sp[-5]) - rot3l: [a,b,c] → [c,a,b] (bottom to top) - rot3r: [a,b,c] → [b,c,a] (top to bottom) - rot4l: [a,b,c,d] → [d,a,b,c] - rot5l: [a,b,c,d,e] → [e,a,b,c,d] put_field: pops 2 values, pushes 0 (was incorrectly pushing obj back). define_field: correctly keeps obj on stack (stack_in=2, stack_out=1). post_inc/post_dec: coerce value via to_number before returning old value. true++ now returns 1 (not true). convert_beam_value: filter internal keys by __prefix__ AND __suffix__ pattern, not just __proto__. Prevents __buffer__, __promise_state__, __typed_array__ etc from leaking into Elixir results. to_primitive: try valueOf then toString on both own properties and prototype. Return nil (not obj) when method returns an object, preventing infinite recursion in abstract_eq. format_js_exponential: handle negative mantissa properly by extracting sign prefix before digit manipulation. 733 beam_vm tests pass, 0 failures, 0 warnings. NIF: 29/35 JS engine tests. BEAM: 11/35.
…ames
Template objects:
- materialize_constant creates map-based objects with indexed string
access and .raw property (was list-based, broke property access)
- Handle {:array, list}, :undefined, and plain list formats for raw
New builtins:
- String.raw: tagged template literal support
- TypedArray.prototype.subarray: extract sub-range
- TypedArray.prototype.fill: fill with value
Fixes:
- getOwnPropertyNames: handle list-backed arrays, return obj ref,
filter internal __ keys
- post_inc/post_dec: coerce via to_number (true++ returns 1)
733 beam_vm tests, 0 failures. NIF: 29/35. BEAM: 12/35.
Math.min() returns Infinity, Math.max() returns -Infinity per spec. Previously crashed with empty error on Enum.min/max([]). 733 beam_vm tests, 0 failures. NIF: 29/35. BEAM: 12/35.
… more Interpreter: - set_proto: actually sets __proto__ on object (was no-op) - get_or_create_prototype: check Heap.get_class_proto first so class constructors and .prototype access return the same object - append opcode: iterate via Symbol.iterator for non-array iterables (Set, Map, custom iterables). Added collect_iterator helper. Builtins: - Number.toString(16): lowercase hex output - Number.toExponential: proper mantissa/exponent calculation - Number.toPrecision: significant digit rounding - Math.min/max: handle NaN, type coerce args via to_float - Date.UTC: static method for UTC timestamp construction - Set: Symbol.iterator/values/keys for for-of and spread support - Set iterator: guard against non-list data Removed reach dependency (accidentally added). 733 beam_vm tests, 0 failures. NIF: 29/35. BEAM: 12/35.
… toExponential fix Math: log1p, expm1, cosh, sinh, tanh, acosh, asinh, atanh Date: getTimezoneOffset, getDay, getUTCFullYear, setTime, toLocaleDateString, toLocaleTimeString, toLocaleString, Date.UTC Set: add, entries, Symbol.iterator for spread support Number: toExponential proper mantissa/exponent calculation Spread: collect_iterator for non-array iterables (Set, Map, custom) 733 beam_vm tests, 0 failures.
…, JSON undefined Date: setFullYear/Month/Date/Hours/Minutes/Seconds/Milliseconds, parse (ISO 8601), getTimezoneOffset, getDay, getUTCFullYear, toLocaleDateString/TimeString/String TypedArray: join, forEach, map, filter, every, some, reduce, indexOf, find, sort, reverse, slice (13 methods) Math: sumPrecise, log1p, expm1, cosh/sinh/tanh, acosh/asinh/atanh JSON: stringify omits undefined values and internal __ keys Set: add, entries (additional methods) Objects: normalize float property keys to integer strings 733 beam_vm tests, 0 failures. Diag: 13/35 PASS.
TDZ (Temporal Dead Zone):
- Use :__tdz__ sentinel instead of :undefined for uninitialized let/const
- get_loc_check/put_loc_check/get_var_ref_check only throw for :__tdz__
- let x; return x now correctly returns undefined instead of throwing
Object.prototype:
- Created shared Object.prototype with toString, valueOf,
hasOwnProperty, isPrototypeOf, propertyIsEnumerable, constructor
- Objects created via :object opcode get __proto__ pointing to it
- {}.constructor === Object is now true
- {}.toString() returns '[object Object]'
- Array constructor property accessible via prototype chain
delete operator:
- Returns false for non-configurable properties
- Checks Heap.get_prop_desc before deleting
JSON.stringify:
- Resolves accessor (getter) values when serializing
- Filters undefined values from output
NIF: 29/35. BEAM diag: 14/35. 733 beam_vm tests, 0 failures.
get_property now resolves accessors on the prototype chain using the
original receiver object as 'this', not the prototype. Uses a raw
prototype walk (get_prototype_raw) that returns accessor tuples without
invoking them, then invokes at the top level with the correct receiver.
Fixes: class C { get y(){return this.x} } — new C(3).y now returns 6.
Also: {set x(v){...}, get x(){...}} correctly invokes getters/setters
through the prototype chain.
733 beam_vm tests, 0 failures. NIF: 29/35. BEAM diag: 14/35.
- Generator functions throw TypeError on new (func_kind check) - make_error_obj sets __proto__ to Error constructor's prototype, enabling e instanceof TypeError to work correctly - JSON.stringify invokes getters with correct this via invoke_getter - Prototype chain getter resolution uses get_prototype_raw to return accessor tuples, invokes with original receiver 733 beam_vm tests, 0 failures. NIF: 29/35. BEAM: 13/35.
Number.toString(radix): proper float-to-radix conversion for bases 2-36.
Handles fractional parts with iterative digit extraction.
Function.bind: returns {:bound, length, inner} with correct length
calculation. {:bound, ...} handled in call_function, tail_call,
call_method, invoke, typeof, call_builtin_callback.
Set methods (ES2025): difference, intersection, union,
symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom,
delete, clear.
Date methods: toDateString, toTimeString, toUTCString.
Math.sumPrecise: Kahan summation for floating-point precision.
JSON.stringify: invoke getters with correct this via invoke_getter.
Generator new: throws TypeError (generators are not constructors).
Error objects: __proto__ set to constructor's prototype for instanceof.
733 beam_vm tests, 0 failures. NIF: 29/35. BEAM: 13/35.
…e dead code Review items: - perm3/4/5, rot3l/3r/4l/5l: VERIFIED CORRECT against QuickJS C source. The reviewer confused TOS-first (our notation) with bottom-first (QuickJS comment notation). All opcodes produce correct results. Tested: a.x++ returns [old_val, new_val] correctly. Fixes: - get_prototype_raw: add depth limit (max 20) to prevent infinite loops on circular prototype chains - global_bindings: cache entire result in :qb_global_bindings_cache PD key. Object.prototype creation was already cached, now the full bindings map is cached too. Eliminates re-building the large globals map on every eval call. - Remove dead write_back_eval_locals no-op function and its call 733 beam_vm tests, 0 failures, 0 warnings.
Heap module:
- wrap(data): creates heap object, returns {:obj, ref} — replaces
the 69 instances of make_ref/put_obj/return pattern
- to_list(val): coerces JS value to Elixir list — replaces 8+
copy-paste blocks across builtins/typed_array/array
- iter_result(val, done): creates iterator result object
- make_error(message, name): creates error object with stack
- get_or_create_prototype(ctor): canonical implementation, removed
duplicate copies from interpreter.ex and runtime.ex
- PD accessors: get/put_object_prototype, get/put_global_cache,
get/put_atoms, get/put_persistent_globals, get/put_handler_globals,
get/put_runtime_mode — replaces direct Process.get/put calls
- sweep_keys(marked): extracted from gc/0, eliminates duplicated
sweep logic between fast-path and slow-path
Other:
- @moduledoc false added to Ctx, Frame, TypedArray, Date
- All direct Process.get/put for PD keys routed through Heap
733 tests pass in both modes, 0 warnings.
Mechanical replacement of the 3-line heap object creation pattern:
ref = make_ref()
Heap.put_obj(ref, data)
{:obj, ref}
with:
Heap.wrap(data)
Across interpreter.ex, runtime.ex, builtins.ex, object.ex,
typed_array.ex, date.ex, json.ex, array.ex.
Remaining make_ref calls (54) are cases where the ref is captured
in closures or used after creation and can't be simplified.
733 tests pass in both modes, 0 warnings.
…to_list
register_builtin(name, constructor, opts):
- Single function replaces register_error_builtin, register_date_statics,
register_promise_statics, register_symbol_statics
- Accepts statics: [...] and prototype: %{...} options
- Error types, Symbol, Promise, Date all use the same pattern now
Opcodes:
- Replace 40 lines of @bc_tag_X N / def bc_tag_X boilerplate with
a @bc_tags map and for comprehension that generates all functions
Date setters:
- Replace 7 copy-paste setFullYear/Month/Date/Hours/Minutes/Seconds
with single set_date_field(this, field, value) helper
Heap.to_list:
- Used in set_constructor to replace inline coerce-to-list block
date_statics():
- Extracted Date.UTC/parse/now registration from inline anonymous
functions in register_date_statics into a clean data function
733 tests pass in both modes, 0 warnings.
Runtime.js_to_string: now a single-clause delegation to
Values.to_js_string. Removed 25 lines of duplicated conversion
logic that differed from Values in subtle ways.
StringProto.index_of: replaced :binary.match (byte offsets) with
String.slice + String.split (char offsets). Fixes incorrect results
for multibyte UTF-8 strings like '€abc'.indexOf('a').
733 tests pass in both modes, 0 warnings.
…eys, bytecode with fix PD access: - All Process.get/put in interpreter.ex routed through Heap (persistent_globals, promise_waiters, atoms) - Added Heap.get/put/delete_promise_waiters - 0 direct Process calls remain in runtime.ex, 0 in interpreter.ex Conversion consolidation: - Runtime.to_number delegates to Values.to_number with BigInt guard (Values throws TypeError for BigInt, Runtime returns the number) - Runtime.js_to_string was already consolidated in previous commit InternalKeys module: - Central registry for all __dunder__ string constants - internal?/1 predicate for filtering Bytecode: - Replaced with anti-pattern (true <- expr || error) with validate_version/1 helper Frame: - Documented unused stack_size slot in tuple layout 733 tests pass in both modes, 0 warnings.
New Dispatch module (interpreter/dispatch.ex):
- call_builtin(fun, args, this): single dispatch for all builtin
callback patterns ({:builtin, _, cb}, {:bound, _, _}, plain fns)
- callable?(val): single predicate for function type checks
Replaced dispatch in:
- call_function, call_method, tail_call, tail_call_method (interpreter)
- invoke (interpreter)
- typeof_is_function (interpreter)
- call_builtin_callback (runtime)
Callback dispatch copies: 19 → 12 (remaining are in call_constructor
and invoke_callback with special semantics).
733 tests pass in both modes, 0 warnings.
interpreter/promise.ex (232 lines): make_resolved_promise, make_rejected_promise, make_then_fn, make_catch_fn, drain_microtask_queue, resolve_promise, generator_next, generator_return, yield_result, done_result interpreter/generator.ex (131 lines): invoke_generator, invoke_async_generator, invoke_async interpreter.ex: 2802 → 2460 lines (-342). Thin delegation functions in interpreter.ex forward to the new modules. Public run_frame/4 and invoke_callback/2 added for cross-module access. 733 tests pass in both modes, 0 warnings.
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