/* * brintOS on-target duktape payload (BRINTOS-203). * * Driven by tests/86-duktape.test. Everything the ENGINE does is checked here, * in ONE duk process, because a duk start-up costs ~1 s on this machine (wasm * instantiation of a 4.4 MB module) against the runner's 60 s per-suite cap -- * an assertion-per-invocation suite does not fit. The .test file keeps only the * cases that genuinely need their own process (exit codes, stdin, file args, * bytecode) and turns this payload's output into result lines. * * Output contract: one `DUKCASE ok` / `DUKCASE FAIL ` line * per case, then `ALL-DUK-OK` if every case passed and `DUK-COUNT ` always. * ES5 syntax only -- duktape 2.7 is ES5.1 plus selected later features, and this * file should not itself be the thing that fails on a syntax it did not need. * * NOTHING HERE MAY LOOP WITHOUT A BOUND. A compute-bound duk is never preempted * on this kernel: the console stops responding, an on-target `timeout` around it * does not fire, and the machine has to be reloaded in the browser. Every loop * and every recursion below has a fixed count -- see the tail-call pair. */ var fails = 0; var count = 0; /* Every case is a THUNK, and an exception inside one is that case's failure -- * never the payload's. An uncaught throw here would exit duk on the spot and * silently drop every case after it, which is exactly how the first draft of * this file lost the 30 cases that followed its tail-call case. */ function t(name, fn, want) { var got; count++; try { got = fn(); } catch (e) { fails++; print('DUKCASE ' + name + ' FAIL threw=' + (e && e.name) + ':' + (e && e.message)); return; } if (String(got) === String(want)) { print('DUKCASE ' + name + ' ok'); } else { fails++; print('DUKCASE ' + name + ' FAIL got=[' + got + '] want=[' + want + ']'); } } /* ---- identity ------------------------------------------------------------ */ t('version', function () { return Duktape.version; }, 20700); /* Duktape.env is the build profile string. "generic" is the architecture: wasm32 * matches none of duk_config.h's architecture families, so it falls back to the * portable defaults -- which is exactly why this port needed no source change. */ t('env-generic', function () { return /(^| )generic( |$)/.test(Duktape.env); }, true); t('env-linux', function () { return /(^| )linux( |$)/.test(Duktape.env); }, true); t('env-clang', function () { return /(^| )clang( |$)/.test(Duktape.env); }, true); /* ---- numerics ------------------------------------------------------------ */ /* IEEE-754 doubles and duktape's own number->string conversion, which is its own * code rather than the platform's printf -- so these say the wasm32 build got the * arithmetic AND the shortest-representation formatting right. */ t('num-third', function () { return 1 / 3; }, '0.3333333333333333'); t('num-sum', function () { return 0.1 + 0.2; }, '0.30000000000000004'); t('num-pi', function () { return Math.PI; }, '3.141592653589793'); t('num-2p53', function () { return Math.pow(2, 53); }, '9007199254740992'); t('num-int-max-safe', function () { return Math.pow(2, 53) + 1 === Math.pow(2, 53); }, true); t('num-nan', function () { return isNaN(0 / 0); }, true); t('num-inf', function () { return 1 / 0; }, 'Infinity'); t('num-neg-zero', function () { return 1 / -0; }, '-Infinity'); t('num-parse', function () { return parseFloat('3.5e2'); }, 350); t('num-tofixed', function () { return (1234.5678).toFixed(2); }, '1234.57'); t('num-radix', function () { return (255).toString(16); }, 'ff'); /* ---- language core ------------------------------------------------------- */ t('closure', function () { var mk = function () { var n = 0; return function () { return ++n; }; }; var c = mk(); c(); return c(); }, 2); t('proto', function () { function A() {} A.prototype.v = function () { return 'proto-ok'; }; return new A().v(); }, 'proto-ok'); t('strict', function () { 'use strict'; try { undefinedGlobal = 1; return 'no-throw'; } catch (e) { return e.name; } }, 'ReferenceError'); /* ---- string / array / object builtins ------------------------------------ */ t('str-upper', function () { return 'abc'.toUpperCase(); }, 'ABC'); t('str-slice', function () { return 'abcdef'.slice(1, 4); }, 'bcd'); t('str-split', function () { return 'a,b,c'.split(',').length; }, 3); t('str-replace', function () { return 'aXbXc'.replace(/X/g, '-'); }, 'a-b-c'); t('str-charcode', function () { return 'A'.charCodeAt(0); }, 65); t('arr-sort-num', function () { return [10, 9, 100].sort(function (a, b) { return a - b; }).join(','); }, '9,10,100'); t('arr-sort-default', function () { return [10, 9, 100].sort().join(','); }, '10,100,9'); t('arr-map', function () { return [1, 2, 3].map(function (x) { return x * 2; }).join('|'); }, '2|4|6'); t('arr-filter-reduce', function () { return [1, 2, 3, 4].filter(function (x) { return x % 2 === 0; }) .reduce(function (a, b) { return a + b; }, 0); }, 6); t('obj-keys', function () { return Object.keys({ a: 1, b: 2 }).sort().join(','); }, 'a,b'); /* A sort big enough to drive the comparator repeatedly, small enough to stay * well inside the suite budget (see the no-unbounded-loops note above). */ t('arr-sort-bulk', function () { var a = [], i; for (i = 0; i < 2000; i++) { a.push((i * 7919) % 2011); } a.sort(function (x, y) { return x - y; }); return a[0] + ':' + a[1999] + ':' + a.length; }, '0:2010:2000'); /* ---- JSON ---------------------------------------------------------------- */ t('json-stringify', function () { return JSON.stringify({ a: 1, b: [1, 2, 3], c: 'x' }); }, '{"a":1,"b":[1,2,3],"c":"x"}'); t('json-roundtrip', function () { return JSON.parse(JSON.stringify({ b: [1, 2, 3] })).b[2]; }, 3); t('json-escape', function () { return JSON.stringify('a"b\nc'); }, '"a\\"b\\nc"'); t('json-nested', function () { var o = { l1: { l2: { l3: [1, { l4: 'deep' }] } } }; return JSON.parse(JSON.stringify(o)).l1.l2.l3[1].l4; }, 'deep'); /* ---- RegExp -------------------------------------------------------------- */ t('re-exec', function () { return /(\d+)-(\d+)/.exec('ab 12-34 cd').slice(1).join(':'); }, '12:34'); t('re-global', function () { return 'a1b22c333'.match(/\d+/g).join(','); }, '1,22,333'); t('re-class', function () { return /^[a-f0-9]+$/.test('deadbeef'); }, true); t('re-nomatch', function () { return /zzz/.exec('abc'); }, 'null'); /* ---- errors and the longjmp path ----------------------------------------- */ /* duktape unwinds with _setjmp/_longjmp; on this arm that runs through wasmld's * HWJS-3 coro machinery. If that were broken these would not merely fail, they * would not return at all -- so they are the load-bearing cases of the port. */ t('err-typeerror', function () { try { null.foo; return 'no-throw'; } catch (e) { return e.name; } }, 'TypeError'); t('err-custom', function () { try { throw new Error('boom'); } catch (e) { return e.message; } }, 'boom'); t('err-finally', function () { var s = ''; try { throw new Error('x'); } catch (e) { s += 'c'; } finally { s += 'f'; } return s; }, 'cf'); t('err-rethrow-depth', function () { function inner() { throw new RangeError('deep'); } function mid() { inner(); } try { mid(); return 'no-throw'; } catch (e) { return e.name; } }, 'RangeError'); t('err-stack', function () { try { null.foo; } catch (e) { return typeof e.stack === 'string' && e.stack.length > 0; } }, true); /* ---- recursion, and the tail-call distinction this port had to learn ------ */ /* NOT in tail position (the `1 +` forces a frame), so this is real stack growth * and duktape's callstack limit stops it -- the default 10000. */ t('rec-nontail-rangeerror', function () { function f(n) { return 1 + f(n + 1); } try { f(0); return 'no-throw'; } catch (e) { return e.name; } }, 'RangeError'); t('rec-depth-9000', function () { function f(n) { return n <= 0 ? 0 : 1 + f(n - 1); } return f(9000); }, 9000); t('rec-depth-15000-throws', function () { function f(n) { return n <= 0 ? 0 : 1 + f(n - 1); } try { f(15000); return 'no-throw'; } catch (e) { return e.name; } }, 'RangeError'); /* A call that IS in tail position: duktape reuses the frame, so 100000 -- ten * times the callstack limit -- returns rather than throwing. This is why * `function f(n){return f(n+1);} f(0)` is an INFINITE LOOP on this image and not * a RangeError, and why that form must never appear in a test here: it wedges the * machine with no way back short of reloading the tab. */ t('tail-call-flat', function () { function f(n) { if (n <= 0) { return 'flat'; } return f(n - 1); } return f(100000); }, 'flat'); /* ...and the same call written as a ternary is NOT optimised: duktape only takes * the tail-call path for a call that is the whole of a return statement, so this * one really recurses and really hits the limit. Worth pinning: the two forms * look interchangeable and behave completely differently, which is what cost this * suite its first run. */ t('tail-call-ternary-not-flat', function () { function f(n) { return n <= 0 ? 'flat' : f(n - 1); } try { f(100000); return 'no-throw'; } catch (e) { return e.name; } }, 'RangeError'); /* ---- Date ---------------------------------------------------------------- */ t('date-epoch-iso', function () { return new Date(0).toISOString(); }, '1970-01-01T00:00:00.000Z'); t('date-parse', function () { return new Date('2020-01-02T03:04:05Z').getTime(); }, 1577934245000); t('date-fields', function () { var d = new Date(Date.UTC(2020, 0, 2)); return d.getUTCFullYear() + '-' + d.getUTCMonth() + '-' + d.getUTCDate(); }, '2020-0-2'); /* Date.now() must be a plausible wall clock, not 0 -- it comes from the host * clock through gettimeofday, and a broken one would read as 1970 here. */ t('date-now-sane', function () { return Date.now() > 1600000000000; }, true); /* ---- buffers / typed arrays ---------------------------------------------- */ t('ta-uint8', function () { var b = new Uint8Array(4); b[0] = 255; b[3] = 1; return b[0] + ':' + b[3] + ':' + b.length; }, '255:1:4'); t('ta-wrap', function () { var b = new Uint8Array(1); b[0] = 256; return b[0]; }, 0); t('ta-arraybuffer', function () { return new Uint32Array(new ArrayBuffer(8)).length; }, 2); t('ta-dataview', function () { var dv = new DataView(new ArrayBuffer(4)); dv.setUint16(0, 0x1234); return dv.getUint16(0).toString(16); }, '1234'); /* ---- Duktape-specific API ------------------------------------------------ */ t('duk-enc-hex', function () { return Duktape.enc('hex', 'AB'); }, '4142'); t('duk-enc-base64', function () { return Duktape.enc('base64', 'hello'); }, 'aGVsbG8='); /* Duktape.dec returns a plain BUFFER, not a string -- String() on it yields * "[object Uint8Array]" -- so the bytes are walked out explicitly. */ t('duk-dec-base64', function () { var b = Duktape.dec('base64', 'aGVsbG8='), s = '', i; for (i = 0; i < b.length; i++) { s += String.fromCharCode(b[i]); } return s; }, 'hello'); t('duk-gc', function () { Duktape.gc(); return 'gc-ok'; }, 'gc-ok'); t('duk-act', function () { return typeof Duktape.act; }, 'function'); t('duk-info', function () { return typeof Duktape.info; }, 'function'); /* ---- the cmdline extras compiled into duk -------------------------------- */ /* These are the -DDUK_CMDLINE_*_SUPPORT switches in upstream's Makefile.cmdline * showing up at runtime; they are how you tell this duk from a bare engine. */ t('extra-console', function () { return typeof console === 'object' && typeof console.log === 'function'; }, true); t('extra-console-methods', function () { var ms = ['log', 'warn', 'error', 'info'], i; for (i = 0; i < ms.length; i++) { if (typeof console[ms[i]] !== 'function') { return 'missing:' + ms[i]; } } return true; }, true); t('extra-print', function () { return typeof print; }, 'function'); t('extra-alert', function () { return typeof alert; }, 'function'); t('extra-logger', function () { return typeof Duktape.Logger; }, 'function'); t('extra-modsearch-writable', function () { Duktape.modSearch = function () { return ''; }; return typeof Duktape.modSearch; }, 'function'); /* ---- selected post-ES5.1 features duktape 2.x carries -------------------- */ t('es6-proxy', function () { return new Proxy({}, { get: function () { return 'trapped'; } }).anything; }, 'trapped'); t('es6-symbol', function () { return typeof Symbol('x'); }, 'symbol'); t('es6-object-assign', function () { return JSON.stringify(Object.assign({ a: 1 }, { b: 2 })); }, '{"a":1,"b":2}'); t('es6-string-repeat', function () { return 'ab'.repeat(3); }, 'ababab'); t('es6-array-isarray', function () { return Array.isArray([]); }, true); /* ---- compile / eval at runtime ------------------------------------------- */ t('eval-runtime', function () { return eval('1 + 2 * 3'); }, 7); t('func-ctor', function () { return new Function('a', 'b', 'return a*b;')(6, 7); }, 42); t('eval-syntaxerror', function () { try { eval('function ('); return 'no-throw'; } catch (e) { return e.name; } }, 'SyntaxError'); print('DUK-COUNT ' + count); if (fails === 0) { print('ALL-DUK-OK'); } else { print('DUK-FAILURES ' + fails); }