diff --git a/examples/spec/assert.js b/examples/spec/assert.js new file mode 100644 index 0000000..ef4c524 --- /dev/null +++ b/examples/spec/assert.js @@ -0,0 +1,227 @@ +import { test, testThrows, testDeep, summary } from './helpers.js'; +import assert from 'node:assert'; + +console.log('assert.ok\n'); + +let threw = false; +try { assert(false); } catch (e) { threw = true; } +test('assert(false) throws', threw, true); + +threw = false; +try { assert(0); } catch (e) { threw = true; } +test('assert(0) throws', threw, true); + +threw = false; +try { assert(''); } catch (e) { threw = true; } +test('assert("") throws', threw, true); + +threw = false; +try { assert(null); } catch (e) { threw = true; } +test('assert(null) throws', threw, true); + +threw = false; +try { assert(1); } catch (e) { threw = true; } +test('assert(1) does not throw', threw, false); + +threw = false; +try { assert('hello'); } catch (e) { threw = true; } +test('assert("hello") does not throw', threw, false); + +threw = false; +try { assert.ok(false, 'custom message'); } catch (e) { threw = true; } +test('assert.ok with custom message throws', threw, true); + +console.log('\nassert.equal / assert.notEqual\n'); + +threw = false; +try { assert.equal(1, 1); } catch (e) { threw = true; } +test('equal(1, 1) passes', threw, false); + +threw = false; +try { assert.equal(1, '1'); } catch (e) { threw = true; } +test('equal(1, "1") passes (loose)', threw, false); + +threw = false; +try { assert.equal(1, 2); } catch (e) { threw = true; } +test('equal(1, 2) throws', threw, true); + +threw = false; +try { assert.equal(null, undefined); } catch (e) { threw = true; } +test('equal(null, undefined) passes (loose)', threw, false); + +threw = false; +try { assert.notEqual(1, 2); } catch (e) { threw = true; } +test('notEqual(1, 2) passes', threw, false); + +threw = false; +try { assert.notEqual(1, 1); } catch (e) { threw = true; } +test('notEqual(1, 1) throws', threw, true); + +console.log('\nassert.strictEqual / assert.notStrictEqual\n'); + +threw = false; +try { assert.strictEqual(1, 1); } catch (e) { threw = true; } +test('strictEqual(1, 1) passes', threw, false); + +threw = false; +try { assert.strictEqual('a', 'a'); } catch (e) { threw = true; } +test('strictEqual("a", "a") passes', threw, false); + +threw = false; +try { assert.strictEqual(1, '1'); } catch (e) { threw = true; } +test('strictEqual(1, "1") throws', threw, true); + +threw = false; +try { assert.strictEqual(null, undefined); } catch (e) { threw = true; } +test('strictEqual(null, undefined) throws', threw, true); + +threw = false; +try { assert.strictEqual(NaN, NaN); } catch (e) { threw = true; } +test('strictEqual(NaN, NaN) passes', threw, false); + +threw = false; +try { assert.notStrictEqual(1, 2); } catch (e) { threw = true; } +test('notStrictEqual(1, 2) passes', threw, false); + +threw = false; +try { assert.notStrictEqual(1, 1); } catch (e) { threw = true; } +test('notStrictEqual(1, 1) throws', threw, true); + +console.log('\nassert.deepEqual / assert.deepStrictEqual\n'); + +threw = false; +try { assert.deepEqual({ a: 1 }, { a: 1 }); } catch (e) { threw = true; } +test('deepEqual({a:1}, {a:1}) passes', threw, false); + +threw = false; +try { assert.deepEqual([1, 2, 3], [1, 2, 3]); } catch (e) { threw = true; } +test('deepEqual arrays passes', threw, false); + +threw = false; +try { assert.deepEqual({ a: 1 }, { a: 2 }); } catch (e) { threw = true; } +test('deepEqual({a:1}, {a:2}) throws', threw, true); + +threw = false; +try { assert.deepEqual([1, 2], [1, 2, 3]); } catch (e) { threw = true; } +test('deepEqual different length arrays throws', threw, true); + +threw = false; +try { assert.deepStrictEqual({ a: 1 }, { a: 1 }); } catch (e) { threw = true; } +test('deepStrictEqual({a:1}, {a:1}) passes', threw, false); + +threw = false; +try { assert.deepStrictEqual({ a: 1 }, { a: '1' }); } catch (e) { threw = true; } +test('deepStrictEqual({a:1}, {a:"1"}) throws', threw, true); + +threw = false; +try { assert.deepStrictEqual({ a: { b: { c: 42 } } }, { a: { b: { c: 42 } } }); } catch (e) { threw = true; } +test('deepStrictEqual nested objects passes', threw, false); + +threw = false; +try { assert.notDeepEqual({ a: 1 }, { a: 2 }); } catch (e) { threw = true; } +test('notDeepEqual({a:1}, {a:2}) passes', threw, false); + +threw = false; +try { assert.notDeepEqual({ a: 1 }, { a: 1 }); } catch (e) { threw = true; } +test('notDeepEqual({a:1}, {a:1}) throws', threw, true); + +threw = false; +try { assert.notDeepStrictEqual({ a: 1 }, { a: '1' }); } catch (e) { threw = true; } +test('notDeepStrictEqual({a:1}, {a:"1"}) passes', threw, false); + +console.log('\nassert.fail\n'); + +threw = false; +try { assert.fail('boom'); } catch (e) { threw = true; } +test('fail("boom") throws', threw, true); + +threw = false; +try { assert.fail(); } catch (e) { threw = true; } +test('fail() throws', threw, true); + +let failMsg = ''; +try { assert.fail('specific message'); } catch (e) { failMsg = e.message; } +test('fail message is preserved', failMsg, 'specific message'); + +console.log('\nassert.ifError\n'); + +threw = false; +try { assert.ifError(null); } catch (e) { threw = true; } +test('ifError(null) does not throw', threw, false); + +threw = false; +try { assert.ifError(undefined); } catch (e) { threw = true; } +test('ifError(undefined) does not throw', threw, false); + +threw = false; +try { assert.ifError(new Error('oops')); } catch (e) { threw = true; } +test('ifError(Error) throws', threw, true); + +threw = false; +try { assert.ifError('some error'); } catch (e) { threw = true; } +test('ifError(string) throws', threw, true); + +console.log('\nassert.throws / assert.doesNotThrow\n'); + +threw = false; +try { assert.throws(() => { throw new Error('boom'); }); } catch (e) { threw = true; } +test('throws with throwing fn passes', threw, false); + +threw = false; +try { assert.throws(() => {}); } catch (e) { threw = true; } +test('throws with non-throwing fn throws', threw, true); + +threw = false; +try { assert.doesNotThrow(() => {}); } catch (e) { threw = true; } +test('doesNotThrow with non-throwing fn passes', threw, false); + +threw = false; +try { assert.doesNotThrow(() => { throw new Error('oops'); }); } catch (e) { threw = true; } +test('doesNotThrow with throwing fn throws', threw, true); + +console.log('\nassert.match / assert.doesNotMatch\n'); + +threw = false; +try { assert.match('hello world', /hello/); } catch (e) { threw = true; } +test('match passes when pattern matches', threw, false); + +threw = false; +try { assert.match('hello world', /xyz/); } catch (e) { threw = true; } +test('match throws when pattern does not match', threw, true); + +threw = false; +try { assert.doesNotMatch('hello world', /xyz/); } catch (e) { threw = true; } +test('doesNotMatch passes when pattern does not match', threw, false); + +threw = false; +try { assert.doesNotMatch('hello world', /hello/); } catch (e) { threw = true; } +test('doesNotMatch throws when pattern matches', threw, true); + +console.log('\nassert.rejects / assert.doesNotReject\n'); + +let resolved = false; +await assert.rejects(async () => { throw new Error('boom'); }); +resolved = true; +test('rejects resolves when fn rejects', resolved, true); + +resolved = false; +try { await assert.rejects(async () => {}); } catch (e) { resolved = false; } +test('rejects rejects when fn does not reject', resolved, false); + +resolved = false; +await assert.doesNotReject(async () => {}); +resolved = true; +test('doesNotReject resolves when fn does not reject', resolved, true); + +console.log('\nAssertionError\n'); + +test('AssertionError is a constructor', typeof assert.AssertionError, 'function'); + +const ae = new assert.AssertionError({ message: 'test', actual: 1, expected: 2, operator: 'strictEqual' }); +test('AssertionError has name', ae.name, 'AssertionError'); +test('AssertionError has message', ae.message, 'test'); +test('AssertionError has actual', ae.actual, 1); +test('AssertionError has expected', ae.expected, 2); +test('AssertionError has operator', ae.operator, 'strictEqual'); + +summary(); diff --git a/examples/spec/v8.js b/examples/spec/v8.js new file mode 100644 index 0000000..9f43b92 --- /dev/null +++ b/examples/spec/v8.js @@ -0,0 +1,134 @@ +import { test, testDeep, summary } from './helpers.js'; +import v8 from 'node:v8'; + +console.log('startupSnapshot\n'); + +test('isBuildingSnapshot is a function', typeof v8.startupSnapshot.isBuildingSnapshot, 'function'); +test('isBuildingSnapshot returns false', v8.startupSnapshot.isBuildingSnapshot(), false); +test('addSerializeCallback is a function', typeof v8.startupSnapshot.addSerializeCallback, 'function'); +test('addDeserializeCallback is a function', typeof v8.startupSnapshot.addDeserializeCallback, 'function'); +test('addSerializeCallback returns undefined', v8.startupSnapshot.addSerializeCallback(() => {}), undefined); +test('addDeserializeCallback returns undefined', v8.startupSnapshot.addDeserializeCallback(() => {}), undefined); + +console.log('\ncachedDataVersionTag\n'); + +test('cachedDataVersionTag is a function', typeof v8.cachedDataVersionTag, 'function'); +const tag = v8.cachedDataVersionTag(); +test('cachedDataVersionTag returns a number', typeof tag, 'number'); +test('cachedDataVersionTag is non-zero', tag !== 0, true); +test('cachedDataVersionTag is stable', v8.cachedDataVersionTag() === v8.cachedDataVersionTag(), true); + +console.log('\ngetHeapStatistics\n'); + +const stats = v8.getHeapStatistics(); +test('getHeapStatistics returns object', typeof stats, 'object'); + +const heapFields = [ + 'total_heap_size', 'total_heap_size_executable', 'total_physical_size', + 'total_available_size', 'total_global_handles_size', 'used_global_handles_size', + 'used_heap_size', 'heap_size_limit', 'malloced_memory', + 'peak_malloced_memory', 'does_zap_garbage', 'number_of_native_contexts', + 'number_of_detached_contexts', 'total_heap_blinded_size' +]; +for (const field of heapFields) { + test(`getHeapStatistics has ${field}`, typeof stats[field], 'number'); +} + +test('used_heap_size > 0', stats.used_heap_size > 0, true); +test('total_heap_size >= used_heap_size', stats.total_heap_size >= stats.used_heap_size, true); +test('total_physical_size > 0', stats.total_physical_size > 0, true); +test('heap_size_limit > 0', stats.heap_size_limit > 0, true); +test('number_of_native_contexts >= 1', stats.number_of_native_contexts >= 1, true); + +console.log('\ngetHeapSpaceStatistics\n'); + +const spaces = v8.getHeapSpaceStatistics(); +test('getHeapSpaceStatistics returns array', Array.isArray(spaces), true); +test('getHeapSpaceStatistics has spaces', spaces.length >= 1, true); + +const spaceFields = ['space_name', 'space_size', 'space_used_size', 'space_available_size', 'physical_space_size']; +for (const space of spaces) { + test(`space has name`, typeof space.space_name, 'string'); + for (const field of spaceFields) { + test(`${space.space_name} has ${field}`, typeof space[field], field === 'space_name' ? 'string' : 'number'); + } +} + +console.log('\ngetHeapCodeStatistics\n'); + +const code = v8.getHeapCodeStatistics(); +test('getHeapCodeStatistics returns object', typeof code, 'object'); +test('has code_and_metadata_size', typeof code.code_and_metadata_size, 'number'); +test('has bytecode_and_metadata_size', typeof code.bytecode_and_metadata_size, 'number'); +test('has external_script_source_size', typeof code.external_script_source_size, 'number'); +test('has cpu_profiler_metadata_size', typeof code.cpu_profiler_metadata_size, 'number'); +test('code_and_metadata_size >= bytecode_and_metadata_size', code.code_and_metadata_size >= code.bytecode_and_metadata_size, true); +test('external_script_source_size > 0', code.external_script_source_size > 0, true); + +console.log('\nserialize / deserialize\n'); + +const roundtrip = (label, value) => { + const buf = v8.serialize(value); + test(`${label}: serialize returns Uint8Array`, buf instanceof Uint8Array, true); + test(`${label}: buffer is non-empty`, buf.length > 0, true); + const out = v8.deserialize(buf); + test(`${label}: round-trip typeof`, typeof out, typeof value); + return out; +}; + +roundtrip('undefined', undefined); +roundtrip('null', null); +roundtrip('true', true); +roundtrip('false', false); + +const n = roundtrip('integer', 42); +test('integer value preserved', n, 42); + +const f = roundtrip('float', 3.14); +test('float value preserved', Math.abs(f - 3.14) < 1e-10, true); + +const buf = v8.serialize(NaN); +const nan = v8.deserialize(buf); +test('NaN round-trips', Number.isNaN(nan), true); + +const s = roundtrip('string', 'hello world'); +test('string value preserved', s, 'hello world'); + +const empty = roundtrip('empty string', ''); +test('empty string preserved', empty, ''); + +const arr = v8.deserialize(v8.serialize([1, 2, 3])); +test('array round-trips', Array.isArray(arr), true); +test('array length preserved', arr.length, 3); +test('array[0]', arr[0], 1); +test('array[2]', arr[2], 3); + +const obj = v8.deserialize(v8.serialize({ x: 1, y: 'two', z: true })); +test('object round-trips', typeof obj, 'object'); +test('object.x', obj.x, 1); +test('object.y', obj.y, 'two'); +test('object.z', obj.z, true); + +const nested = v8.deserialize(v8.serialize({ a: [1, { b: 2 }], c: null })); +test('nested object.a is array', Array.isArray(nested.a), true); +test('nested object.a[1].b', nested.a[1].b, 2); +test('nested object.c is null', nested.c, null); + +let threw = false; +try { v8.deserialize(new Uint8Array([0x00, 0x00])); } catch (e) { threw = true; } +test('deserialize rejects bad magic', threw, true); + +console.log('\nno-op methods\n'); + +test('setFlagsFromString is a function', typeof v8.setFlagsFromString, 'function'); +test('setFlagsFromString returns undefined', v8.setFlagsFromString('--harmony'), undefined); +test('stopCoverage is a function', typeof v8.stopCoverage, 'function'); +test('stopCoverage returns undefined', v8.stopCoverage(), undefined); +test('takeCoverage is a function', typeof v8.takeCoverage, 'function'); +test('takeCoverage returns undefined', v8.takeCoverage(), undefined); + +console.log('\nconstants\n'); + +test('constants is an object', typeof v8.constants, 'object'); + +summary(); diff --git a/include/modules/assert.h b/include/modules/assert.h new file mode 100644 index 0000000..66c235e --- /dev/null +++ b/include/modules/assert.h @@ -0,0 +1,8 @@ +#ifndef ANT_ASSERT_MODULE_H +#define ANT_ASSERT_MODULE_H + +#include "types.h" + +ant_value_t assert_library(ant_t *js); + +#endif diff --git a/include/modules/v8.h b/include/modules/v8.h new file mode 100644 index 0000000..337c49b --- /dev/null +++ b/include/modules/v8.h @@ -0,0 +1,8 @@ +#ifndef ANT_V8_MODULE_H +#define ANT_V8_MODULE_H + +#include "types.h" + +ant_value_t v8_library(ant_t *js); + +#endif diff --git a/src/main.c b/src/main.c index c3b5149..8cbe1d5 100644 --- a/src/main.c +++ b/src/main.c @@ -68,6 +68,8 @@ #include "modules/async_hooks.h" #include "modules/net.h" #include "modules/dns.h" +#include "modules/assert.h" +#include "modules/v8.h" #include "modules/worker_threads.h" int js_result = EXIT_SUCCESS; @@ -606,6 +608,7 @@ int main(int argc, char *argv[]) { ant_standard_library("util", util_library); ant_standard_library("net", net_library); ant_standard_library("dns", dns_library); + ant_standard_library("assert", assert_library); ant_standard_library("module", module_library); ant_standard_library("buffer", buffer_library); ant_standard_library("path", path_library); @@ -623,6 +626,7 @@ int main(int argc, char *argv[]) { ant_standard_library("child_process", child_process_library); ant_standard_library("worker_threads", worker_threads_library); ant_standard_library("async_hooks", async_hooks_library); + ant_standard_library("v8", v8_library); ant_value_t snapshot_result = ant_load_snapshot(js); if (vtype(snapshot_result) == T_ERR) { diff --git a/src/modules/assert.c b/src/modules/assert.c new file mode 100644 index 0000000..c9a59d8 --- /dev/null +++ b/src/modules/assert.c @@ -0,0 +1,304 @@ +#include +#include + +#include "ant.h" +#include "errors.h" +#include "internal.h" +#include "silver/engine.h" + +static ant_value_t assertion_error(ant_t *js, const char *msg, ant_value_t msg_val) { + if (vtype(msg_val) == T_STR) { + char *s = js_getstr(js, msg_val, NULL); + if (s) return js_mkerr(js, "%s", s); + } + return js_mkerr(js, "%s", msg); +} + +// assert(value, message) / assert.ok +static ant_value_t assert_ok(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !js_truthy(js, args[0])) + return assertion_error(js, "The expression evaluated to a falsy value", nargs >= 2 ? args[1] : js_mkundef()); + return js_mkundef(); +} + +// assert.fail(message) +static ant_value_t assert_fail(ant_t *js, ant_value_t *args, int nargs) { + if (nargs >= 1 && vtype(args[0]) == T_STR) { + char *msg = js_getstr(js, args[0], NULL); + if (msg) return js_mkerr(js, "%s", msg); + } + return js_mkerr(js, "Assertion failed"); +} + +// assert.ifError(value) +static ant_value_t assert_if_error(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkundef(); + uint8_t t = vtype(args[0]); + if (t == T_NULL || t == T_UNDEF) return js_mkundef(); + if (is_err(args[0])) return args[0]; + char *msg = js_getstr(js, args[0], NULL); + return js_mkerr(js, "ifError got unwanted exception: %s", msg ? msg : "(unknown)"); +} + +// TODO: make into global helper +static bool values_strict_equal(ant_t *js, ant_value_t a, ant_value_t b) { + uint8_t ta = vtype(a), tb = vtype(b); + if (ta != tb) return false; + if (ta == T_NULL || ta == T_UNDEF) return true; + if (ta == T_BOOL) return a == b; + if (ta == T_NUM) { + double na = js_getnum(a), nb = js_getnum(b); + return (na == nb) || (isnan(na) && isnan(nb)); + } + if (ta == T_STR) { + char *sa = js_getstr(js, a, NULL); + char *sb = js_getstr(js, b, NULL); + return sa && sb && strcmp(sa, sb) == 0; + } + return vdata(a) == vdata(b); +} + +// TODO: make into global helper +static bool values_loose_equal(ant_t *js, ant_value_t a, ant_value_t b) { + uint8_t ta = vtype(a), tb = vtype(b); + if (ta == tb) return values_strict_equal(js, a, b); + if ((ta == T_NULL && tb == T_UNDEF) || (ta == T_UNDEF && tb == T_NULL)) return true; + if (ta == T_NUM && tb == T_STR) { + char *sb = js_getstr(js, b, NULL); + return sb && js_getnum(a) == strtod(sb, NULL); + } + if (ta == T_STR && tb == T_NUM) { + char *sa = js_getstr(js, a, NULL); + return sa && strtod(sa, NULL) == js_getnum(b); + } + return false; +} + +// TODO: make into global helper +static bool deep_equal(ant_t *js, ant_value_t a, ant_value_t b, bool strict, int depth) { + if (depth > 64) return false; + uint8_t ta = vtype(a), tb = vtype(b); + + if (ta == T_ARR && tb == T_ARR) { + ant_offset_t la = js_arr_len(js, a), lb = js_arr_len(js, b); + if (la != lb) return false; + for (ant_offset_t i = 0; i < la; i++) { + if (!deep_equal(js, js_arr_get(js, a, i), js_arr_get(js, b, i), strict, depth + 1)) + return false; + } + return true; + } + + if (ta == T_OBJ && tb == T_OBJ) { + if (vdata(a) == vdata(b)) return true; + ant_iter_t iter = js_prop_iter_begin(js, a); + const char *key; size_t key_len; ant_value_t va; + while (js_prop_iter_next(&iter, &key, &key_len, &va)) { + ant_value_t vb = js_get(js, b, key); + if (!deep_equal(js, va, vb, strict, depth + 1)) { + js_prop_iter_end(&iter); + return false; + } + } + js_prop_iter_end(&iter); + ant_iter_t iter2 = js_prop_iter_begin(js, b); + while (js_prop_iter_next(&iter2, &key, &key_len, &va)) { + ant_value_t va2 = js_get(js, a, key); + if (vtype(va2) == T_UNDEF && vtype(va) != T_UNDEF) { + js_prop_iter_end(&iter2); + return false; + } + } + js_prop_iter_end(&iter2); + return true; + } + + return strict ? values_strict_equal(js, a, b) : values_loose_equal(js, a, b); +} + +static ant_value_t assert_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (!values_loose_equal(js, args[0], args[1])) + return assertion_error(js, "Expected values to be equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_not_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (values_loose_equal(js, args[0], args[1])) + return assertion_error(js, "Expected values to not be equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_strict_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (!values_strict_equal(js, args[0], args[1])) + return assertion_error(js, "Expected values to be strictly equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_not_strict_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (values_strict_equal(js, args[0], args[1])) + return assertion_error(js, "Expected values to not be strictly equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_deep_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (!deep_equal(js, args[0], args[1], false, 0)) + return assertion_error(js, "Expected values to be deeply equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_not_deep_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (deep_equal(js, args[0], args[1], false, 0)) + return assertion_error(js, "Expected values to not be deeply equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_deep_strict_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (!deep_equal(js, args[0], args[1], true, 0)) + return assertion_error(js, "Expected values to be deeply strictly equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_not_deep_strict_equal(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + if (deep_equal(js, args[0], args[1], true, 0)) + return assertion_error(js, "Expected values to not be deeply strictly equal", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_throws(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) != T_FUNC) + return js_mkerr(js, "assert.throws: first argument must be a function"); + ant_value_t result = sv_vm_call(js->vm, js, args[0], js_mkundef(), NULL, 0, NULL, false); + if (!is_err(result)) + return js_mkerr(js, "Missing expected exception"); + return js_mkundef(); +} + +static ant_value_t assert_does_not_throw(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) != T_FUNC) + return js_mkerr(js, "assert.doesNotThrow: first argument must be a function"); + ant_value_t result = sv_vm_call(js->vm, js, args[0], js_mkundef(), NULL, 0, NULL, false); + if (is_err(result)) + return js_mkerr(js, "Got unwanted exception: %s", js_str(js, result)); + return js_mkundef(); +} + +static bool promise_was_rejected(ant_value_t result) { + if (vtype(result) != T_PROMISE) return false; + ant_object_t *obj = js_obj_ptr(js_as_obj(result)); + return obj && obj->promise_state && obj->promise_state->state == 2; +} + +static bool promise_was_fulfilled(ant_value_t result) { + if (vtype(result) != T_PROMISE) return false; + ant_object_t *obj = js_obj_ptr(js_as_obj(result)); + return obj && obj->promise_state && obj->promise_state->state == 1; +} + +static ant_value_t assert_rejects(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "assert.rejects: first argument required"); + ant_value_t promise = js_mkpromise(js); + ant_value_t result = vtype(args[0]) == T_FUNC + ? sv_vm_call(js->vm, js, args[0], js_mkundef(), NULL, 0, NULL, false) + : args[0]; + if (is_err(result) || promise_was_rejected(result)) + js_resolve_promise(js, promise, js_mkundef()); + else + js_reject_promise(js, promise, js_mkerr(js, "Missing expected rejection")); + return promise; +} + +static ant_value_t assert_does_not_reject(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "assert.doesNotReject: first argument required"); + ant_value_t promise = js_mkpromise(js); + ant_value_t result = vtype(args[0]) == T_FUNC + ? sv_vm_call(js->vm, js, args[0], js_mkundef(), NULL, 0, NULL, false) + : args[0]; + if (is_err(result) || promise_was_rejected(result)) + js_reject_promise(js, promise, js_mkerr(js, "Got unwanted rejection")); + else + js_resolve_promise(js, promise, js_mkundef()); + return promise; +} + +static ant_value_t assert_match(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + ant_value_t test_fn = js_getprop_fallback(js, args[1], "test"); + if (vtype(test_fn) != T_FUNC) return js_mkerr(js, "assert.match: second argument must be a RegExp"); + ant_value_t test_args[1] = {args[0]}; + ant_value_t result = sv_vm_call(js->vm, js, test_fn, args[1], test_args, 1, NULL, false); + if (!js_truthy(js, result)) + return assertion_error(js, "Value does not match the regular expression", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_does_not_match(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkundef(); + ant_value_t test_fn = js_getprop_fallback(js, args[1], "test"); + if (vtype(test_fn) != T_FUNC) return js_mkerr(js, "assert.doesNotMatch: second argument must be a RegExp"); + ant_value_t test_args[1] = {args[0]}; + ant_value_t result = sv_vm_call(js->vm, js, test_fn, args[1], test_args, 1, NULL, false); + if (js_truthy(js, result)) + return assertion_error(js, "Value matches the regular expression", nargs >= 3 ? args[2] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t assert_assertion_error_ctor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t self = js_getthis(js); + js_set(js, self, "name", js_mkstr(js, "AssertionError", 14)); + if (nargs >= 1 && vtype(args[0]) == T_OBJ) { + const char *fields[] = {"message", "actual", "expected", "operator"}; + for (int i = 0; i < 4; i++) { + ant_value_t v = js_get(js, args[0], fields[i]); + if (vtype(v) != T_UNDEF) js_set(js, self, fields[i], v); + } + } + return js_mkundef(); +} + +ant_value_t assert_library(ant_t *js) { + ant_value_t assert_obj = js_mkobj(js); + js_set_slot(assert_obj, SLOT_CFUNC, js_mkfun(assert_ok)); + + js_set(js, assert_obj, "ok", js_mkfun(assert_ok)); + js_set(js, assert_obj, "fail", js_mkfun(assert_fail)); + js_set(js, assert_obj, "ifError", js_mkfun(assert_if_error)); + js_set(js, assert_obj, "equal", js_mkfun(assert_equal)); + js_set(js, assert_obj, "notEqual", js_mkfun(assert_not_equal)); + js_set(js, assert_obj, "strictEqual", js_mkfun(assert_strict_equal)); + js_set(js, assert_obj, "notStrictEqual", js_mkfun(assert_not_strict_equal)); + js_set(js, assert_obj, "deepEqual", js_mkfun(assert_deep_equal)); + js_set(js, assert_obj, "notDeepEqual", js_mkfun(assert_not_deep_equal)); + js_set(js, assert_obj, "deepStrictEqual", js_mkfun(assert_deep_strict_equal)); + js_set(js, assert_obj, "notDeepStrictEqual", js_mkfun(assert_not_deep_strict_equal)); + js_set(js, assert_obj, "throws", js_mkfun(assert_throws)); + js_set(js, assert_obj, "doesNotThrow", js_mkfun(assert_does_not_throw)); + js_set(js, assert_obj, "rejects", js_mkfun(assert_rejects)); + js_set(js, assert_obj, "doesNotReject", js_mkfun(assert_does_not_reject)); + js_set(js, assert_obj, "match", js_mkfun(assert_match)); + js_set(js, assert_obj, "doesNotMatch", js_mkfun(assert_does_not_match)); + + ant_value_t ae_ctor = js_mkobj(js); + ant_value_t ae_proto = js_mkobj(js); + + js_set(js, ae_proto, "name", js_mkstr(js, "AssertionError", 14)); + js_set_slot(ae_ctor, SLOT_CFUNC, js_mkfun(assert_assertion_error_ctor)); + js_mkprop_fast(js, ae_ctor, "prototype", 9, ae_proto); + js_mkprop_fast(js, ae_ctor, "name", 4, js_mkstr(js, "AssertionError", 14)); + + ant_value_t ae_fn = js_obj_to_func(ae_ctor); + js_set(js, ae_proto, "constructor", ae_fn); + js_set(js, assert_obj, "AssertionError", ae_fn); + + ant_value_t lib = js_obj_to_func(assert_obj); + js_set(js, lib, "default", lib); + + return lib; +} diff --git a/src/modules/v8.c b/src/modules/v8.c new file mode 100644 index 0000000..c9dc615 --- /dev/null +++ b/src/modules/v8.c @@ -0,0 +1,378 @@ +// stub: functional where possible + +#include +#include +#include +#include +#include + +#include "arena.h" +#include "errors.h" +#include "internal.h" +#include "modules/buffer.h" + +// serialize / deserialize +// +// wire format: +// [0xAB 0xCD] — magic header (2 bytes) +// — recursively encoded +// +// value tags (1 byte): +// 'U' undefined +// 'N' null +// 'T' true +// 'F' false +// 'Z' NaN +// 'D' double (8 bytes little-endian) +// 'S' string (uint32 length LE + UTF-8 bytes) +// 'A' array (uint32 count LE + …) +// 'O' object (uint32 count LE + (uint32 klen + key bytes + )…) + +#define SER_MAGIC_0 0xAB +#define SER_MAGIC_1 0xCD + +typedef struct { + uint8_t *data; + size_t len; + size_t cap; +} enc_t; + +static bool enc_grow(enc_t *e, size_t need) { + if (e->len + need <= e->cap) return true; + size_t nc = e->cap ? e->cap * 2 : 64; + while (nc < e->len + need) nc *= 2; + uint8_t *p = realloc(e->data, nc); + if (!p) return false; + e->data = p; + e->cap = nc; + return true; +} + +static bool enc_u8(enc_t *e, uint8_t b) { + if (!enc_grow(e, 1)) return false; + e->data[e->len++] = b; + return true; +} + +static bool enc_u32(enc_t *e, uint32_t v) { + if (!enc_grow(e, 4)) return false; + e->data[e->len++] = (uint8_t)(v); + e->data[e->len++] = (uint8_t)(v >> 8); + e->data[e->len++] = (uint8_t)(v >> 16); + e->data[e->len++] = (uint8_t)(v >> 24); + return true; +} + +static bool enc_bytes(enc_t *e, const void *src, size_t n) { + if (!enc_grow(e, n)) return false; + memcpy(e->data + e->len, src, n); + e->len += n; + return true; +} + +static bool ser_val(ant_t *js, enc_t *e, ant_value_t val, int depth) { + if (depth > 64) return false; + uint8_t t = vtype(val); + + if (t == T_UNDEF) return enc_u8(e, 'U'); + if (t == T_NULL) return enc_u8(e, 'N'); + if (t == T_BOOL) return enc_u8(e, (val == js_true) ? 'T' : 'F'); + + if (t == T_NUM) { + double d = js_getnum(val); + if (isnan(d)) return enc_u8(e, 'Z'); + if (!enc_u8(e, 'D')) return false; + return enc_bytes(e, &d, 8); + } + + if (t == T_STR) { + size_t slen; + const char *s = js_getstr(js, val, &slen); + if (!s) { s = ""; slen = 0; } + if (!enc_u8(e, 'S')) return false; + if (!enc_u32(e, (uint32_t)slen)) return false; + return enc_bytes(e, s, slen); + } + + if (t == T_ARR) { + ant_offset_t n = js_arr_len(js, val); + if (!enc_u8(e, 'A')) return false; + if (!enc_u32(e, (uint32_t)n)) return false; + for (ant_offset_t i = 0; i < n; i++) { + if (!ser_val(js, e, js_arr_get(js, val, i), depth + 1)) return false; + } + return true; + } + + if (t == T_OBJ) { + uint32_t count = 0; + ant_iter_t it = js_prop_iter_begin(js, val); + const char *k; size_t klen; ant_value_t v; + while (js_prop_iter_next(&it, &k, &klen, &v)) count++; + js_prop_iter_end(&it); + + if (!enc_u8(e, 'O')) return false; + if (!enc_u32(e, count)) return false; + + it = js_prop_iter_begin(js, val); + while (js_prop_iter_next(&it, &k, &klen, &v)) { + if (!enc_u32(e, (uint32_t)klen) || !enc_bytes(e, k, klen)) { + js_prop_iter_end(&it); + return false; + } + if (!ser_val(js, e, v, depth + 1)) { + js_prop_iter_end(&it); + return false; + } + } + js_prop_iter_end(&it); + return true; + } + + return enc_u8(e, 'U'); +} + +static ant_value_t v8_serialize(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "serialize: value required"); + + enc_t e = {0}; + if (!enc_u8(&e, SER_MAGIC_0) || !enc_u8(&e, SER_MAGIC_1)) goto oom; + if (!ser_val(js, &e, args[0], 0)) goto oom; + + ArrayBufferData *ab = create_array_buffer_data(e.len); + if (!ab) goto oom; + memcpy(ab->data, e.data, e.len); + free(e.data); + return create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, e.len, "Buffer"); + +oom: + free(e.data); + return js_mkerr(js, "serialize: out of memory"); +} + +typedef struct { + const uint8_t *data; + size_t len; + size_t pos; +} dec_t; + +static bool dec_u8(dec_t *d, uint8_t *out) { + if (d->pos >= d->len) return false; + *out = d->data[d->pos++]; + return true; +} + +static bool dec_u32(dec_t *d, uint32_t *out) { + if (d->pos + 4 > d->len) return false; + *out = (uint32_t)d->data[d->pos] + | ((uint32_t)d->data[d->pos+1] << 8) + | ((uint32_t)d->data[d->pos+2] << 16) + | ((uint32_t)d->data[d->pos+3] << 24); + d->pos += 4; + return true; +} + +static ant_value_t des_val(ant_t *js, dec_t *d, int depth) { + if (depth > 64) return js_mkerr(js, "deserialize: maximum depth exceeded"); + + uint8_t tag; + if (!dec_u8(d, &tag)) return js_mkundef(); + + switch (tag) { + case 'U': return js_mkundef(); + case 'N': return js_mknull(); + case 'T': return js_true; + case 'F': return js_false; + case 'Z': return js_mknum((double)NAN); + case 'D': { + if (d->pos + 8 > d->len) return js_mkundef(); + double v; + memcpy(&v, d->data + d->pos, 8); + d->pos += 8; + return js_mknum(v); + } + case 'S': { + uint32_t n; + if (!dec_u32(d, &n) || d->pos + n > d->len) return js_mkundef(); + ant_value_t s = js_mkstr(js, (const char *)(d->data + d->pos), n); + d->pos += n; + return s; + } + case 'A': { + uint32_t n; + if (!dec_u32(d, &n)) return js_mkundef(); + ant_value_t arr = js_mkarr(js); + for (uint32_t i = 0; i < n; i++) { + ant_value_t elem = des_val(js, d, depth + 1); + if (is_err(elem)) return elem; + js_arr_push(js, arr, elem); + } + return arr; + } + case 'O': { + uint32_t n; + if (!dec_u32(d, &n)) return js_mkundef(); + ant_value_t obj = js_mkobj(js); + char kbuf[4096]; + for (uint32_t i = 0; i < n; i++) { + uint32_t klen; + if (!dec_u32(d, &klen) || klen >= sizeof(kbuf) || d->pos + klen > d->len) + break; + memcpy(kbuf, d->data + d->pos, klen); + kbuf[klen] = '\0'; + d->pos += klen; + ant_value_t v = des_val(js, d, depth + 1); + if (is_err(v)) return v; + js_set(js, obj, kbuf, v); + } + return obj; + } + default: + return js_mkundef(); + } +} + +static ant_value_t v8_deserialize(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "deserialize: Buffer required"); + + ant_value_t buf_slot = js_get_slot(args[0], SLOT_BUFFER); + TypedArrayData *ta = (TypedArrayData *)js_gettypedarray(buf_slot); + if (!ta || !ta->buffer) return js_mkerr(js, "deserialize: expected a Buffer"); + + const uint8_t *data = ta->buffer->data + ta->byte_offset; + size_t len = ta->byte_length; + + if (len < 2 || data[0] != SER_MAGIC_0 || data[1] != SER_MAGIC_1) + return js_mkerr(js, "deserialize: invalid or incompatible buffer"); + + dec_t d = { .data = data, .len = len, .pos = 2 }; + return des_val(js, &d, 0); +} + +static ant_value_t v8_get_heap_statistics(ant_t *js, ant_value_t *args, int nargs) { + size_t rss = 0; + uv_resident_set_memory(&rss); + + size_t arena_committed = js->obj_arena.committed; + size_t arena_reserved = js->obj_arena.reserved; + size_t arena_live_bytes = js->obj_arena.live_count * js->obj_arena.elem_size; + + size_t pool_live = js->gc_pool_last_live; + size_t pool_alloc = js->gc_pool_alloc; + + size_t closure_live = js->closure_arena.live_count * js->closure_arena.elem_size; + size_t extra_alloc = js->alloc_bytes.closures + js->alloc_bytes.upvalues; + + size_t used_heap = arena_live_bytes + pool_live + closure_live; + size_t total_heap = arena_committed + pool_alloc + extra_alloc; + size_t heap_limit = arena_reserved; + + ant_value_t obj = js_mkobj(js); + js_set(js, obj, "total_heap_size", js_mknum((double)total_heap)); + js_set(js, obj, "total_heap_size_executable", js_mknum(0)); + js_set(js, obj, "total_physical_size", js_mknum((double)rss)); + js_set(js, obj, "total_available_size", js_mknum((double)(heap_limit > used_heap ? heap_limit - used_heap : 0))); + js_set(js, obj, "total_global_handles_size", js_mknum(0)); + js_set(js, obj, "used_global_handles_size", js_mknum(0)); + js_set(js, obj, "used_heap_size", js_mknum((double)used_heap)); + js_set(js, obj, "heap_size_limit", js_mknum((double)heap_limit)); + js_set(js, obj, "malloced_memory", js_mknum((double)extra_alloc)); + js_set(js, obj, "peak_malloced_memory", js_mknum((double)extra_alloc)); + js_set(js, obj, "does_zap_garbage", js_mknum(0)); + js_set(js, obj, "number_of_native_contexts", js_mknum(1)); + js_set(js, obj, "number_of_detached_contexts", js_mknum(0)); + js_set(js, obj, "total_heap_blinded_size", js_mknum(0)); + + return obj; +} + +static ant_value_t v8_get_heap_space_statistics(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t nursery = js_mkobj(js); + size_t arena_committed = js->obj_arena.committed; + size_t arena_live_bytes = js->obj_arena.live_count * js->obj_arena.elem_size; + double arena_available = (double)(arena_committed / 2 > arena_live_bytes / 2 ? arena_committed / 2 - arena_live_bytes / 2 : 0); + + js_set(js, nursery, "space_name", js_mkstr(js, "new_space", 9)); + js_set(js, nursery, "space_size", js_mknum((double)arena_committed / 2)); + js_set(js, nursery, "space_used_size", js_mknum((double)arena_live_bytes / 2)); + js_set(js, nursery, "space_available_size", js_mknum(arena_available)); + js_set(js, nursery, "physical_space_size", js_mknum((double)arena_committed / 2)); + + ant_value_t oldspace = js_mkobj(js); + size_t old_live = js->old_live_count * js->obj_arena.elem_size; + double old_size = (double)(arena_committed / 2 > old_live ? arena_committed / 2 - old_live : 0); + + js_set(js, oldspace, "space_name", js_mkstr(js, "old_space", 9)); + js_set(js, oldspace, "space_size", js_mknum((double)arena_committed / 2)); + js_set(js, oldspace, "space_used_size", js_mknum((double)old_live)); + js_set(js, oldspace, "space_available_size", js_mknum(old_size)); + js_set(js, oldspace, "physical_space_size", js_mknum((double)arena_committed / 2)); + + ant_value_t arr = js_mkarr(js); + js_arr_push(js, arr, nursery); + js_arr_push(js, arr, oldspace); + + return arr; +} + +static ant_value_t v8_noop(ant_t *js, ant_value_t *args, int nargs) { + return js_mkundef(); +} + +static ant_value_t v8_noop_false(ant_t *js, ant_value_t *args, int nargs) { + return js_false; +} + +static ant_value_t v8_write_heap_snapshot(ant_t *js, ant_value_t *args, int nargs) { + return js_mkstr(js, "", 0); +} + +static ant_value_t v8_get_heap_snapshot(ant_t *js, ant_value_t *args, int nargs) { + return js_mkundef(); +} + +static ant_value_t v8_get_heap_code_statistics(ant_t *js, ant_value_t *args, int nargs) { + size_t closure_structs = js->closure_arena.live_count * js->closure_arena.elem_size; + size_t bytecode_alloc = js->alloc_bytes.closures; + + ant_pool_stats_t rope_stats = js_pool_stats(&js->pool.rope); + ant_value_t obj = js_mkobj(js); + + js_set(js, obj, "code_and_metadata_size", js_mknum((double)(closure_structs + bytecode_alloc))); + js_set(js, obj, "bytecode_and_metadata_size", js_mknum((double)bytecode_alloc)); + js_set(js, obj, "external_script_source_size", js_mknum((double)rope_stats.used)); + js_set(js, obj, "cpu_profiler_metadata_size", js_mknum(0)); + + return obj; +} + +static ant_value_t v8_cached_data_version_tag(ant_t *js, ant_value_t *args, int nargs) { + return js_mknum(0xA0A0A0); +} + +ant_value_t v8_library(ant_t *js) { + ant_value_t lib = js_mkobj(js); + + js_set(js, lib, "serialize", js_mkfun(v8_serialize)); + js_set(js, lib, "deserialize", js_mkfun(v8_deserialize)); + js_set(js, lib, "writeHeapSnapshot", js_mkfun(v8_write_heap_snapshot)); + js_set(js, lib, "getHeapSnapshot", js_mkfun(v8_get_heap_snapshot)); + js_set(js, lib, "getHeapStatistics", js_mkfun(v8_get_heap_statistics)); + js_set(js, lib, "getHeapSpaceStatistics", js_mkfun(v8_get_heap_space_statistics)); + js_set(js, lib, "getHeapCodeStatistics", js_mkfun(v8_get_heap_code_statistics)); + js_set(js, lib, "cachedDataVersionTag", js_mkfun(v8_cached_data_version_tag)); + js_set(js, lib, "setFlagsFromString", js_mkfun(v8_noop)); + js_set(js, lib, "stopCoverage", js_mkfun(v8_noop)); + js_set(js, lib, "takeCoverage", js_mkfun(v8_noop)); + js_set(js, lib, "promiseEvents", js_mkfun(v8_noop)); + + ant_value_t snapshot = js_mkobj(js); + js_set(js, snapshot, "isBuildingSnapshot", js_mkfun(v8_noop_false)); + js_set(js, snapshot, "addSerializeCallback", js_mkfun(v8_noop)); + js_set(js, snapshot, "addDeserializeCallback", js_mkfun(v8_noop)); + + js_set(js, lib, "startupSnapshot", snapshot); + js_set(js, lib, "constants", js_mkobj(js)); + + return lib; +}