From b42e7e9331b25d71585884b1070c4e8d3c82edf5 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Mon, 27 Apr 2026 15:13:33 -0700 Subject: [PATCH] Implement explicit resource management support Add parser/compiler/runtime support for throw expressions, using and await using declarations, disposal stacks, dispose symbols, and SuppressedError handling. Wire disposal cleanup through blocks, returns, breaks, continues, and for-of loops, including async disposal paths and suppressed errors. Also add DisposableStack and AsyncDisposableStack builtins, AsyncIterator helper plumbing, and focused tests/spec coverage for resource management, suppression, throw expressions, and async iterators. --- examples/results.txt | 72 +- examples/spec/async_iterators.js | 225 ++++ examples/spec/explicit_resource_management.js | 331 ++++++ examples/spec/throw_expressions.js | 39 + include/common.h | 4 +- include/internal.h | 1 + include/modules/iterator.h | 1 + include/modules/symbol.h | 2 + include/silver/ast.h | 2 + include/silver/compiler.h | 12 + include/silver/opcode.h | 10 + src/ant.c | 394 ++++++- src/esm/library.c | 10 +- src/modules/generator.c | 13 +- src/modules/iterator.c | 1049 +++++++++++++++-- src/modules/symbol.c | 3 + src/silver/ast.c | 61 +- src/silver/compile_ctx.c | 3 + src/silver/compiler.c | 366 +++++- src/silver/engine.c | 14 +- src/silver/ops/using.h | 410 +++++++ tests/test_disposable_stack.cjs | 118 ++ tests/test_suppressed_error_symbols.cjs | 28 + tests/test_throw_expressions.cjs | 48 + 24 files changed, 3039 insertions(+), 177 deletions(-) create mode 100644 examples/spec/async_iterators.js create mode 100644 examples/spec/explicit_resource_management.js create mode 100644 examples/spec/throw_expressions.js create mode 100644 src/silver/ops/using.h create mode 100644 tests/test_disposable_stack.cjs create mode 100644 tests/test_suppressed_error_symbols.cjs create mode 100644 tests/test_throw_expressions.cjs diff --git a/examples/results.txt b/examples/results.txt index 0049279..73d4f9a 100644 --- a/examples/results.txt +++ b/examples/results.txt @@ -1141,7 +1141,7 @@ compat-table/es6/typed-arrays.of.js: failed compat-table/es6/typed-arrays.prototype.Symbol.iterator.js: OK compat-table/es6/typed-arrays.prototype.copyWithin.js: OK compat-table/es6/typed-arrays.prototype.entries.js: OK -compat-table/es6/typed-arrays.prototype.every.js: failed +compat-table/es6/typed-arrays.prototype.every.js: OK compat-table/es6/typed-arrays.prototype.fill.js: OK compat-table/es6/typed-arrays.prototype.filter.js: failed compat-table/es6/typed-arrays.prototype.findIndex.js: failed @@ -1470,45 +1470,45 @@ compat-table/intl/Number.prototype.toLocaleString.js: OK compat-table/intl/Object.prototype.toLocaleString.js: OK compat-table/intl/String.prototype.localeCompare.js: OK compat-table/next/Array.isTemplateObject.js: TypeError: undefined is not a function -compat-table/next/AsyncDisposableStack.js: ReferenceError: 'AsyncDisposableStack' is not defined -compat-table/next/AsyncIterator.extends.js: ReferenceError: 'AsyncIterator' is not defined -compat-table/next/AsyncIterator.from.async-iterable.js: ReferenceError: 'AsyncIterator' is not defined -compat-table/next/AsyncIterator.from.iterable.js: ReferenceError: 'AsyncIterator' is not defined -compat-table/next/AsyncIterator.from.iterator.js: ReferenceError: 'AsyncIterator' is not defined -compat-table/next/AsyncIterator.instanceof.js: ReferenceError: 'AsyncIterator' is not defined -compat-table/next/AsyncIterator.prototype.Symbol.toStringTag.js: ReferenceError: 'AsyncIterator' is not defined -compat-table/next/AsyncIterator.prototype.drop.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.every.js: TypeError: undefined is not a function -compat-table/next/AsyncIterator.prototype.filter.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.find.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.flatMap.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.forEach.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.map.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.reduce.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.some.js: TIMEOUT: >5s -compat-table/next/AsyncIterator.prototype.take.js: failed -compat-table/next/AsyncIterator.prototype.toArray.js: TIMEOUT: >5s -compat-table/next/DisposableStack.js: ReferenceError: 'DisposableStack' is not defined +compat-table/next/AsyncDisposableStack.js: OK +compat-table/next/AsyncIterator.extends.js: OK +compat-table/next/AsyncIterator.from.async-iterable.js: OK +compat-table/next/AsyncIterator.from.iterable.js: OK +compat-table/next/AsyncIterator.from.iterator.js: OK +compat-table/next/AsyncIterator.instanceof.js: OK +compat-table/next/AsyncIterator.prototype.Symbol.toStringTag.js: OK +compat-table/next/AsyncIterator.prototype.drop.js: OK +compat-table/next/AsyncIterator.prototype.every.js: OK +compat-table/next/AsyncIterator.prototype.filter.js: OK +compat-table/next/AsyncIterator.prototype.find.js: OK +compat-table/next/AsyncIterator.prototype.flatMap.js: OK +compat-table/next/AsyncIterator.prototype.forEach.js: OK +compat-table/next/AsyncIterator.prototype.map.js: OK +compat-table/next/AsyncIterator.prototype.reduce.js: OK +compat-table/next/AsyncIterator.prototype.some.js: OK +compat-table/next/AsyncIterator.prototype.take.js: OK +compat-table/next/AsyncIterator.prototype.toArray.js: OK +compat-table/next/DisposableStack.js: OK compat-table/next/Map.prototype.upsert.js: OK compat-table/next/RegExp.1-9.js: OK compat-table/next/RegExp.lastMatch.js: OK compat-table/next/ShadowRealm.js: failed -compat-table/next/SuppressedError.js: ReferenceError: 'SuppressedError' is not defined -compat-table/next/Symbol.dispose.bound.js: ReferenceError: 'using' is not defined -compat-table/next/Uint8Array.fromBase64.js: TypeError: undefined is not a function -compat-table/next/Uint8Array.fromHex.js: TypeError: undefined is not a function -compat-table/next/Uint8Array.setFromBase64.js: TypeError: undefined is not a function -compat-table/next/Uint8Array.setFromHex.js: TypeError: undefined is not a function -compat-table/next/Uint8Array.toBase64.js: TypeError: undefined is not a function -compat-table/next/Uint8Array.toHex.js: TypeError: undefined is not a function +compat-table/next/SuppressedError.js: OK +compat-table/next/Symbol.dispose.bound.js: OK +compat-table/next/Uint8Array.fromBase64.js: OK +compat-table/next/Uint8Array.fromHex.js: OK +compat-table/next/Uint8Array.setFromBase64.js: OK +compat-table/next/Uint8Array.setFromHex.js: OK +compat-table/next/Uint8Array.toBase64.js: OK +compat-table/next/Uint8Array.toHex.js: OK compat-table/next/WeakMap.prototype.upsert.js: OK -compat-table/next/await-using.for-of.js: ReferenceError: 'using' is not defined -compat-table/next/await-using.js: ReferenceError: 'using' is not defined +compat-table/next/await-using.for-of.js: OK +compat-table/next/await-using.js: OK compat-table/next/class-decorators.js: failed compat-table/next/function.sent.js: failed -compat-table/next/throw-expr.arrow.js: SyntaxError: Unexpected token 'throw' -compat-table/next/throw-expr.conditional.js: SyntaxError: Unexpected token 'throw' -compat-table/next/throw-expr.logical.js: SyntaxError: Unexpected token 'throw' -compat-table/next/throw-expr.param-init.js: SyntaxError: Unexpected token 'throw' -compat-table/next/using.for-of.js: ReferenceError: 'using' is not defined -compat-table/next/using.js: ReferenceError: 'using' is not defined +compat-table/next/throw-expr.arrow.js: OK +compat-table/next/throw-expr.conditional.js: OK +compat-table/next/throw-expr.logical.js: OK +compat-table/next/throw-expr.param-init.js: OK +compat-table/next/using.for-of.js: OK +compat-table/next/using.js: OK diff --git a/examples/spec/async_iterators.js b/examples/spec/async_iterators.js new file mode 100644 index 0000000..b9dcdd0 --- /dev/null +++ b/examples/spec/async_iterators.js @@ -0,0 +1,225 @@ +import { test, testDeep, summary } from './helpers.js'; + +console.log('Async Iterator Tests\n'); + +test('AsyncIterator global exists', typeof AsyncIterator, 'function'); +test('AsyncIterator prototype async iterator tag', AsyncIterator.prototype[Symbol.toStringTag], 'AsyncIterator'); + +class CustomAsyncIterator extends AsyncIterator { + constructor(values) { + super(); + this.values = values; + this.index = 0; + } + + next() { + if (this.index >= this.values.length) return Promise.resolve({ done: true }); + return Promise.resolve({ done: false, value: this.values[this.index++] }); + } +} + +const custom = new CustomAsyncIterator([4, 5]); +test('class extends AsyncIterator', custom[Symbol.asyncIterator](), custom); +test('class instance instanceof AsyncIterator', custom instanceof AsyncIterator, true); +testDeep('custom async iterator toArray', await custom.toArray(), [4, 5]); + +const asyncSource = AsyncIterator.from( + (async function* () { + yield 1; + yield 2; + yield 3; + })() +); +test('AsyncIterator.from async iterable instanceof', asyncSource instanceof AsyncIterator, true); +testDeep('AsyncIterator.from async iterable values', await asyncSource.toArray(), [1, 2, 3]); + +testDeep('AsyncIterator.from iterable values', await AsyncIterator.from([1, 2, 3]).toArray(), [1, 2, 3]); +testDeep('AsyncIterator.from iterator values', await AsyncIterator.from([1, 2, 3].values()).toArray(), [1, 2, 3]); + +const promisedStep = await AsyncIterator.from([Promise.resolve(7)]).next(); +test('AsyncIterator.from sync iterable awaits promised value', promisedStep.value, 7); +test('AsyncIterator.from sync iterable promised value done', promisedStep.done, false); + +let rejectedValueCaught; +try { + await AsyncIterator.from([Promise.reject(new Error('sync value rejection'))]).next(); +} catch (e) { + rejectedValueCaught = e; +} +test('AsyncIterator.from sync iterable rejects promised value', rejectedValueCaught.message, 'sync value rejection'); + +const syncReturnSource = { + next() { + return { done: false, value: 0 }; + }, + return() { + return { done: true, value: Promise.resolve(7) }; + }, + [Symbol.iterator]() { + return this; + }, +}; +test('AsyncIterator.from sync return awaits value', (await AsyncIterator.from(syncReturnSource).return()).value, 7); + +const syncThrowSource = { + next() { + return { done: false, value: 0 }; + }, + throw() { + return { done: true, value: Promise.resolve(8) }; + }, + [Symbol.iterator]() { + return this; + }, +}; +test('AsyncIterator.from sync throw awaits value', (await AsyncIterator.from(syncThrowSource).throw(new Error('x'))).value, 8); + +testDeep( + 'async iterator map', + await AsyncIterator.from([1, 2, 3]) + .map(x => x * x) + .toArray(), + [1, 4, 9] +); +testDeep( + 'async iterator map awaits callback', + await AsyncIterator.from([1, 2]) + .map(async x => x + 1) + .toArray(), + [2, 3] +); +testDeep( + 'async iterator filter', + await AsyncIterator.from([1, 2, 3, 4]) + .filter(x => x % 2) + .toArray(), + [1, 3] +); +testDeep( + 'async iterator filter awaits callback', + await AsyncIterator.from([1, 2, 3, 4]) + .filter(async x => x > 2) + .toArray(), + [3, 4] +); +testDeep('async iterator take', await AsyncIterator.from([1, 2, 3]).take(2).toArray(), [1, 2]); +testDeep('async iterator drop', await AsyncIterator.from([1, 2, 3]).drop(1).toArray(), [2, 3]); +testDeep( + 'async iterator flatMap sync inner', + await AsyncIterator.from([1, 2, 3]) + .flatMap(x => [x, 0]) + .toArray(), + [1, 0, 2, 0, 3, 0] +); +testDeep( + 'async iterator flatMap awaits callback', + await AsyncIterator.from([1, 2]) + .flatMap(async x => [x, x + 10]) + .toArray(), + [1, 11, 2, 12] +); + +async function* doubleAsync(x) { + yield x; + yield x * 2; +} + +testDeep('async iterator flatMap async inner', await AsyncIterator.from([2, 3]).flatMap(doubleAsync).toArray(), [2, 4, 3, 6]); + +test('async iterator every true', await AsyncIterator.from([1, 2, 3]).every(x => typeof x === 'number'), true); +test('async iterator every awaits callback', await AsyncIterator.from([1, 2, 3]).every(async x => x < 4), true); +test('async iterator some true', await AsyncIterator.from([1, 2, 3]).some(x => x === 2), true); +test('async iterator some awaits callback', await AsyncIterator.from([1, 2, 3]).some(async x => x === 3), true); +test('async iterator find', await AsyncIterator.from([1, 2, 3]).find(x => x > 1), 2); +test('async iterator find awaits callback', await AsyncIterator.from([1, 2, 3]).find(async x => x > 2), 3); + +function closableAsyncIterator(log) { + return { + index: 0, + next() { + return Promise.resolve({ done: false, value: ++this.index }); + }, + return() { + log.push('closed'); + return Promise.resolve({ done: true }); + }, + [Symbol.asyncIterator]() { + return this; + }, + }; +} + +let everyCloseLog = []; +test('async iterator every closes on false', await AsyncIterator.from(closableAsyncIterator(everyCloseLog)).every(x => x < 1), false); +testDeep('async iterator every close log', everyCloseLog, ['closed']); + +let someCloseLog = []; +test('async iterator some closes on match', await AsyncIterator.from(closableAsyncIterator(someCloseLog)).some(x => x === 1), true); +testDeep('async iterator some close log', someCloseLog, ['closed']); + +let findCloseLog = []; +test('async iterator find closes on match', await AsyncIterator.from(closableAsyncIterator(findCloseLog)).find(x => x === 1), 1); +testDeep('async iterator find close log', findCloseLog, ['closed']); + +let throwCloseLog = []; +let throwCaught = false; +try { + await AsyncIterator.from(closableAsyncIterator(throwCloseLog)).some(() => { + throw new Error('sync callback failure'); + }); +} catch (err) { + throwCaught = err.message === 'sync callback failure'; +} +test('async iterator closes on sync callback throw', throwCaught, true); +testDeep('async iterator sync callback throw close log', throwCloseLog, ['closed']); + +let rejectCloseLog = []; +let rejectCaught = false; +try { + await AsyncIterator.from(closableAsyncIterator(rejectCloseLog)).find(async () => { + throw new Error('async callback failure'); + }); +} catch (err) { + rejectCaught = err.message === 'async callback failure'; +} +test('async iterator closes on async callback rejection', rejectCaught, true); +testDeep('async iterator async callback rejection close log', rejectCloseLog, ['closed']); + +const syncAsyncLike = { + index: 0, + next() { + if (this.index >= 5000) return { done: true }; + return { done: false, value: this.index++ }; + }, + [Symbol.asyncIterator]() { + return this; + }, +}; +test('async iterator handles sync next results without recursive stack growth', (await AsyncIterator.from(syncAsyncLike).toArray()).length, 5000); + +let forEachSum = 0; +await AsyncIterator.from([1, 2, 3]).forEach(x => { + forEachSum += x; +}); +test('async iterator forEach', forEachSum, 6); + +let asyncForEachSum = 0; +await AsyncIterator.from([1, 2, 3]).forEach(async x => { + asyncForEachSum += x; +}); +test('async iterator forEach awaits callback', asyncForEachSum, 6); + +test('async iterator reduce', await AsyncIterator.from([1, 2, 3]).reduce((a, b) => a + b), 6); +test('async iterator reduce awaits callback', await AsyncIterator.from([1, 2, 3]).reduce(async (a, b) => a + b), 6); +testDeep( + 'async generator helpers inherited', + await (async function* () { + yield 2; + yield 4; + })() + .map(x => x + 1) + .toArray(), + [3, 5] +); + +summary(); diff --git a/examples/spec/explicit_resource_management.js b/examples/spec/explicit_resource_management.js new file mode 100644 index 0000000..a470381 --- /dev/null +++ b/examples/spec/explicit_resource_management.js @@ -0,0 +1,331 @@ +import { test, testDeep, summary } from './helpers.js'; + +console.log('Explicit Resource Management Tests\n'); + +test('Symbol.dispose exists', typeof Symbol.dispose, 'symbol'); +test('Symbol.asyncDispose exists', typeof Symbol.asyncDispose, 'symbol'); +test('SuppressedError constructor exists', typeof SuppressedError, 'function'); + +const suppressed = new SuppressedError('error', 'suppressed', 'message'); +test('SuppressedError name', suppressed.name, 'SuppressedError'); +test('SuppressedError message', suppressed.message, 'message'); +test('SuppressedError error property', suppressed.error, 'error'); +test('SuppressedError suppressed property', suppressed.suppressed, 'suppressed'); +test('SuppressedError instanceof Error', suppressed instanceof Error, true); +test('SuppressedError callable', SuppressedError('e', 's') instanceof SuppressedError, true); + +let stackLog = []; +const stack = new DisposableStack(); +const used = { + [Symbol.dispose]() { + stackLog.push('use'); + } +}; +test('DisposableStack use returns resource', stack.use(used), used); +test( + 'DisposableStack adopt returns value', + stack.adopt('value', value => stackLog.push('adopt:' + value)), + 'value' +); +test( + 'DisposableStack defer returns undefined', + stack.defer(() => stackLog.push('defer')), + undefined +); +test('DisposableStack move returns stack', stack.move() instanceof DisposableStack, true); + +const stack2 = new DisposableStack(); +stack2.use({ + [Symbol.dispose]() { + stackLog.push('second use'); + } +}); +stack2.adopt('second', value => stackLog.push(value)); +stack2.defer(() => stackLog.push('second defer')); +stack2.dispose(); +testDeep('DisposableStack disposes LIFO', stackLog.slice(-3), ['second defer', 'second', 'second use']); + +let suppressedErr1 = new Error('first'); +let suppressedErr2 = new Error('second'); +let suppressedCaught; +try { + const throwingStack = new DisposableStack(); + throwingStack.use({ + [Symbol.dispose]() { + throw suppressedErr2; + } + }); + throwingStack.use({ + [Symbol.dispose]() { + throw suppressedErr1; + } + }); + throwingStack.dispose(); +} catch (e) { + suppressedCaught = e; +} +test('DisposableStack suppression error', suppressedCaught.error, suppressedErr2); +test('DisposableStack suppression suppressed', suppressedCaught.suppressed, suppressedErr1); + +let asyncLog = []; +const asyncStack = new AsyncDisposableStack(); +const asyncResource = { + async [Symbol.asyncDispose]() { + asyncLog.push('use'); + } +}; +test('AsyncDisposableStack use returns resource', asyncStack.use(asyncResource), asyncResource); +test( + 'AsyncDisposableStack adopt returns value', + asyncStack.adopt('value', async value => asyncLog.push('adopt:' + value)), + 'value' +); +test( + 'AsyncDisposableStack defer returns undefined', + asyncStack.defer(async () => asyncLog.push('defer')), + undefined +); +await asyncStack.disposeAsync(); +testDeep('AsyncDisposableStack disposes LIFO', asyncLog, ['defer', 'adopt:value', 'use']); + +function usingBlock(resource) { + { + using _ = resource; + } +} + +const usingResource = { + disposed: false, + [Symbol.dispose]() { + this.disposed = true; + } +}; +usingBlock(usingResource); +test('using block disposes', usingResource.disposed, true); + +function usingReturn(resource) { + { + using _ = resource; + return 'done'; + } +} + +const returnResource = { + disposed: false, + [Symbol.dispose]() { + this.disposed = true; + } +}; +test('using return value', usingReturn(returnResource), 'done'); +test('using return disposes', returnResource.disposed, true); + +let throwingReturnCalls = 0; +const throwingReturnError = new Error('dispose return'); +try { + (function () { + { + using _ = { + [Symbol.dispose]() { + throwingReturnCalls++; + throw throwingReturnError; + } + }; + return 'skipped'; + } + })(); +} catch (e) { + test('using return cleanup throws original error', e, throwingReturnError); +} +test('using return cleanup throws once', throwingReturnCalls, 1); + +function usingFunctionScope(resource) { + using _ = resource; + test('using function scope alive before return', resource.disposed, false); + return 'function'; +} + +const functionScopeResource = { + disposed: false, + [Symbol.dispose]() { + this.disposed = true; + } +}; +test('using function scope return value', usingFunctionScope(functionScopeResource), 'function'); +test('using function scope disposes on return', functionScopeResource.disposed, true); + +let breakResource; +while (true) { + { + using _ = (breakResource = { + disposed: false, + [Symbol.dispose]() { + this.disposed = true; + } + }); + break; + } +} +test('using break disposes', breakResource.disposed, true); + +let continueResource; +let continueCount = 0; +while (continueCount++ < 1) { + { + using _ = (continueResource = { + disposed: false, + [Symbol.dispose]() { + this.disposed = true; + } + }); + continue; + } +} +test('using continue disposes', continueResource.disposed, true); + +async function awaitUsingBlock(resource) { + { + await using _ = resource; + } +} + +const awaitUsingResource = { + disposed: false, + async [Symbol.asyncDispose]() { + this.disposed = true; + } +}; +await awaitUsingBlock(awaitUsingResource); +test('await using block disposes', awaitUsingResource.disposed, true); + +async function awaitUsingReturn(resource) { + { + await using _ = resource; + return 'async'; + } +} + +const awaitReturnResource = { + disposed: false, + async [Symbol.asyncDispose]() { + this.disposed = true; + } +}; +test('await using return value', await awaitUsingReturn(awaitReturnResource), 'async'); +test('await using return disposes', awaitReturnResource.disposed, true); + +let throwingAwaitReturnCalls = 0; +const throwingAwaitReturnError = new Error('async dispose return'); +try { + await (async function () { + { + await using _ = { + async [Symbol.asyncDispose]() { + throwingAwaitReturnCalls++; + throw throwingAwaitReturnError; + } + }; + return 'skipped'; + } + })(); +} catch (e) { + test('await using return cleanup throws original error', e, throwingAwaitReturnError); +} +test('await using return cleanup throws once', throwingAwaitReturnCalls, 1); + +const forOfLog = []; +for (using item of [ + { + [Symbol.dispose]() { + forOfLog.push('a'); + } + }, + { + [Symbol.dispose]() { + forOfLog.push('b'); + } + } +]); +testDeep('using for-of disposes each iteration', forOfLog, ['a', 'b']); + +const forOfBreakLog = []; +for (using item of [ + { + [Symbol.dispose]() { + forOfBreakLog.push('break'); + } + } +]) { + break; +} +testDeep('using for-of break disposes current item', forOfBreakLog, ['break']); + +const forOfThrowLog = []; +try { + for (using item of [ + { + [Symbol.dispose]() { + forOfThrowLog.push('throw'); + } + } + ]) { + throw new Error('body'); + } +} catch (e) { + test('using for-of body throw preserved', e.message, 'body'); +} +testDeep('using for-of throw disposes current item', forOfThrowLog, ['throw']); + +const awaitForOfLog = []; +for (await using item of [ + { + async [Symbol.asyncDispose]() { + awaitForOfLog.push('a'); + } + }, + { + async [Symbol.asyncDispose]() { + awaitForOfLog.push('b'); + } + } +]); +testDeep('await using for-of disposes each iteration', awaitForOfLog, ['a', 'b']); + +const awaitForOfBreakLog = []; +for (await using item of [ + { + async [Symbol.asyncDispose]() { + awaitForOfBreakLog.push('break'); + } + } +]) { + break; +} +testDeep('await using for-of break disposes current item', awaitForOfBreakLog, ['break']); + +const err1 = new Error('dispose 1'); +const err2 = new Error('dispose 2'); +const err3 = new Error('body'); +let usingSuppressed; +try { + { + using _1 = { + [Symbol.dispose]() { + throw err1; + } + }, + _2 = { + [Symbol.dispose]() { + throw err2; + } + }; + throw err3; + } +} catch (e) { + usingSuppressed = e; +} + +test('using suppression outer error', usingSuppressed.error, err1); +test('using suppression inner error', usingSuppressed.suppressed.error, err2); +test('using suppression body error', usingSuppressed.suppressed.suppressed, err3); + +summary(); diff --git a/examples/spec/throw_expressions.js b/examples/spec/throw_expressions.js new file mode 100644 index 0000000..7db15be --- /dev/null +++ b/examples/spec/throw_expressions.js @@ -0,0 +1,39 @@ +import { test, testThrows, summary } from './helpers.js'; + +console.log('Throw Expression Tests\n'); + +const must = value => value || throw new Error('missing'); +test('throw expression in arrow skip', must('ok'), 'ok'); +testThrows('throw expression in arrow throws', () => must('')); + +function fallback(value) { + return value ?? throw new Error('nullish'); +} + +test('throw expression in nullish skip', fallback('value'), 'value'); +testThrows('throw expression in nullish throws', () => fallback(null)); + +function choose(value) { + return value ? value : throw new Error('bad'); +} + +test('throw expression in conditional skip', choose(7), 7); +testThrows('throw expression in conditional throws', () => choose(0)); + +function defaultParam(value = throw new Error('default')) { + return value; +} + +test('throw expression in parameter initializer skip', defaultParam(3), 3); +testThrows('throw expression in parameter initializer throws', () => defaultParam()); + +let caught; +try { + const value = false || throw 'sentinel'; + void value; +} catch (e) { + caught = e; +} +test('throw expression throws raw value', caught, 'sentinel'); + +summary(); diff --git a/include/common.h b/include/common.h index fcb0cff..ef73d37 100644 --- a/include/common.h +++ b/include/common.h @@ -129,7 +129,9 @@ typedef enum { BRAND_MODULE_NAMESPACE, BRAND_ABORT_SIGNAL, BRAND_EVENTEMITTER, - BRAND_EVENTTARGET + BRAND_EVENTTARGET, + BRAND_DISPOSABLE_STACK, + BRAND_ASYNC_DISPOSABLE_STACK } object_brand_id_t; static inline void *mantissa_chk(void *p, const char *func) { diff --git a/include/internal.h b/include/internal.h index 38b9963..af22846 100644 --- a/include/internal.h +++ b/include/internal.h @@ -199,6 +199,7 @@ struct ant_isolate_t { ant_value_t array_iterator_proto; ant_value_t string_iterator_proto; ant_value_t generator_proto; + ant_value_t async_iterator_proto; } sym; ant_offset_t max_size; diff --git a/include/modules/iterator.h b/include/modules/iterator.h index ef9632d..b7c174e 100644 --- a/include/modules/iterator.h +++ b/include/modules/iterator.h @@ -2,5 +2,6 @@ #define ITERATOR_H void init_iterator_module(void); +void init_async_iterator_helpers(void); #endif diff --git a/include/modules/symbol.h b/include/modules/symbol.h index 8be1d07..61ed4fb 100644 --- a/include/modules/symbol.h +++ b/include/modules/symbol.h @@ -46,6 +46,8 @@ ant_value_t maybe_call_symbol_method( X(split, "Symbol.split") \ X(matchAll, "Symbol.matchAll") \ X(isConcatSpreadable, "Symbol.isConcatSpreadable") \ + X(dispose, "Symbol.dispose") \ + X(asyncDispose, "Symbol.asyncDispose") \ X(observable, "Symbol.observable") \ X(toPrimitive, "Symbol.toPrimitive") \ X(species, "Symbol.species") \ diff --git a/include/silver/ast.h b/include/silver/ast.h index f8755aa..eaf3e77 100644 --- a/include/silver/ast.h +++ b/include/silver/ast.h @@ -84,6 +84,8 @@ typedef enum { SV_VAR_VAR, SV_VAR_LET, SV_VAR_CONST, + SV_VAR_USING, + SV_VAR_AWAIT_USING, } sv_var_kind_t; enum { diff --git a/include/silver/compiler.h b/include/silver/compiler.h index 9e3bbd4..94646e2 100644 --- a/include/silver/compiler.h +++ b/include/silver/compiler.h @@ -40,6 +40,12 @@ typedef struct { uint32_t len; } sv_deferred_export_t; +typedef struct { + int stack_local; + int scope_depth; + bool is_async; +} sv_using_cleanup_t; + typedef struct { int loop_start; sv_patch_list_t breaks; @@ -116,6 +122,12 @@ typedef struct sv_compiler { int strict_args_local; int new_target_local; int super_local; + int using_stack_local; + int using_cleanup_count; + int using_cleanup_cap; + + bool using_stack_async; + sv_using_cleanup_t *using_cleanups; const char *pending_label; uint32_t pending_label_len; diff --git a/include/silver/opcode.h b/include/silver/opcode.h index 60cbdc8..d48df7a 100644 --- a/include/silver/opcode.h +++ b/include/silver/opcode.h @@ -187,6 +187,16 @@ OP_DEF( FINALLY, 5, 0, 0, label) /* enter finally block */ OP_DEF( FINALLY_RET, 1, 1, 0, none) /* return from finally */ OP_DEF( NIP_CATCH, 1, 2, 1, none) /* catch ... a -> a */ +OP_DEF( USING_PUSH, 1, 2, 1, none) /* entries resource -> resource */ +OP_DEF( USING_PUSH_ASYNC, 1, 2, 1, none) /* entries resource -> resource */ +OP_DEF( DISPOSE_RESOURCE, 1, 1, 1, none) /* resource -> completion */ +OP_DEF( DISPOSE_RESOURCE_ASYNC, 1, 1, 1, none) /* resource -> promise/completion */ + +OP_DEF( USING_DISPOSE, 1, 1, 1, none) /* entries -> completion */ +OP_DEF( USING_DISPOSE_ASYNC, 1, 1, 1, none) /* entries -> promise/completion */ +OP_DEF( USING_DISPOSE_SUPPRESSED, 1, 2, 1, none) /* entries completion -> completion */ +OP_DEF( USING_DISPOSE_ASYNC_SUPPRESSED, 1, 2, 1, none) /* entries completion -> promise/completion */ + OP_DEF( FOR_IN, 1, 1, 1, none) /* obj -> iterator */ OP_DEF( FOR_OF, 1, 1, 3, none) /* iterable -> iter next catch_off */ OP_DEF( FOR_AWAIT_OF, 1, 1, 3, none) /* async iterable -> iter next catch_off */ diff --git a/src/ant.c b/src/ant.c index f325e9c..d814120 100644 --- a/src/ant.c +++ b/src/ant.c @@ -31,6 +31,7 @@ #include "silver/lexer.h" #include "silver/compiler.h" #include "silver/engine.h" +#include "silver/ops/using.h" #include #include @@ -559,6 +560,11 @@ ant_offset_t vstrlen(ant_t *js, ant_value_t v) { return flat ? flat->len : 0; } +static ant_value_t make_data_cfunc( + ant_t *js, ant_value_t data, + ant_value_t (*fn)(ant_t *, ant_value_t *, int) +); + static ant_value_t proxy_read_target(ant_t *js, ant_value_t obj); static ant_offset_t proxy_aware_length(ant_t *js, ant_value_t obj); static ant_value_t proxy_aware_get_elem(ant_t *js, ant_value_t obj, const char *key, size_t key_len); @@ -5979,6 +5985,337 @@ static ant_value_t builtin_AggregateError(ant_t *js, ant_value_t *args, int narg return this_val; } +static ant_value_t builtin_SuppressedError(ant_t *js, ant_value_t *args, int nargs) { + bool is_new = (vtype(js->new_target) != T_UNDEF); + ant_value_t this_val = js->this_val; + + if (!is_new) { + this_val = js_mkobj(js); + ant_offset_t proto_off = lkp_interned(js, js_func_obj(js->current_func), js->intern.prototype, 9); + if (proto_off) js_set_proto_init(this_val, propref_load(js, proto_off)); + else js_set_proto_init(this_val, get_ctor_proto(js, "SuppressedError", 15)); + } + + ant_value_t error = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t suppressed = nargs > 1 ? args[1] : js_mkundef(); + + js_mkprop_fast(js, this_val, "error", 5, error); + js_mkprop_fast(js, this_val, "suppressed", 10, suppressed); + + if (nargs > 2 && vtype(args[2]) != T_UNDEF) { + ant_value_t msg = args[2]; + if (vtype(msg) != T_STR) { + const char *str = js_str(js, msg); + msg = js_mkstr(js, str, strlen(str)); + } + js_mkprop_fast(js, this_val, "message", 7, msg); + } + + js_mkprop_fast(js, this_val, "name", 4, ANT_STRING("SuppressedError")); + set_slot(this_val, SLOT_ERROR_BRAND, js_true); + js_capture_stack(js, this_val); + + return this_val; +} + +static ant_value_t disposable_stack_init(ant_t *js, ant_value_t obj, int brand, ant_value_t proto) { + if (vtype(obj) != T_OBJ) obj = js_mkobj(js); + if (is_err(obj)) return obj; + if (is_object_type(proto)) js_set_proto_init(obj, proto); + + ant_value_t entries = js_mkarr(js); + if (is_err(entries)) return entries; + set_slot(obj, SLOT_BRAND, js_mknum((double)brand)); + set_slot(obj, SLOT_ENTRIES, entries); + set_slot(obj, SLOT_SETTLED, js_false); + + return obj; +} + +static ant_value_t builtin_DisposableStack(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) { + return js_mkerr_typed(js, JS_ERR_TYPE, "DisposableStack constructor requires 'new'"); + } + + ant_value_t proto = js_get_ctor_proto(js, "DisposableStack", 15); + ant_value_t instance_proto = js_instance_proto_from_new_target(js, proto); + + return disposable_stack_init(js, js->this_val, BRAND_DISPOSABLE_STACK, instance_proto); +} + +static ant_value_t builtin_AsyncDisposableStack(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) { + return js_mkerr_typed(js, JS_ERR_TYPE, "AsyncDisposableStack constructor requires 'new'"); + } + + ant_value_t proto = js_get_ctor_proto(js, "AsyncDisposableStack", 20); + ant_value_t instance_proto = js_instance_proto_from_new_target(js, proto); + + return disposable_stack_init(js, js->this_val, BRAND_ASYNC_DISPOSABLE_STACK, instance_proto); +} + +static bool disposable_stack_has_brand(ant_value_t obj, int brand) { + if (!is_object_type(obj)) return false; + ant_value_t actual = get_slot(js_as_obj(obj), SLOT_BRAND); + return vtype(actual) == T_NUM && (int)js_getnum(actual) == brand; +} + +static ant_value_t disposable_stack_entries_checked(ant_t *js, ant_value_t stack, int brand, const char *name) { + if (!disposable_stack_has_brand(stack, brand)) { + return js_mkerr_typed(js, JS_ERR_TYPE, "%s method called on incompatible receiver", name); + } + + if (get_slot(js_as_obj(stack), SLOT_SETTLED) == js_true) { + return js_mkerr_typed(js, JS_ERR_TYPE, "%s is already disposed", name); + } + + ant_value_t entries = get_slot(js_as_obj(stack), SLOT_ENTRIES); + if (vtype(entries) != T_ARR) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid %s", name); + return entries; +} + +static ant_value_t disposable_stack_push_record( + ant_t *js, ant_value_t stack, int brand, const char *name, + sv_disposal_record_kind_t kind, ant_value_t value, ant_value_t method +) { + if (!is_callable(method)) { + return js_mkerr_typed(js, JS_ERR_TYPE, "%s requires a callable disposer", name); + } + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, stack); + GC_ROOT_PIN(js, value); + GC_ROOT_PIN(js, method); + + ant_value_t entries = disposable_stack_entries_checked(js, stack, brand, name); + GC_ROOT_PIN(js, entries); + if (is_err(entries)) { + GC_ROOT_RESTORE(js, root_mark); + return entries; + } + + ant_value_t record = js_mkarr(js); + GC_ROOT_PIN(js, record); + if (is_err(record)) { + GC_ROOT_RESTORE(js, root_mark); + return record; + } + + js_arr_push(js, record, js_mknum((double)kind)); + js_arr_push(js, record, value); + js_arr_push(js, record, method); + js_arr_push(js, entries, record); + + GC_ROOT_RESTORE(js, root_mark); + return js_mkundef(); +} + +static ant_value_t builtin_DisposableStack_defer(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t fn = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t result = disposable_stack_push_record( + js, js->this_val, BRAND_DISPOSABLE_STACK, + "DisposableStack", SV_DISPOSAL_RECORD_DEFER, js_mkundef(), fn + ); + return is_err(result) ? result : js_mkundef(); +} + +static ant_value_t builtin_DisposableStack_adopt(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t value = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t fn = nargs > 1 ? args[1] : js_mkundef(); + ant_value_t result = disposable_stack_push_record( + js, js->this_val, BRAND_DISPOSABLE_STACK, + "DisposableStack", SV_DISPOSAL_RECORD_ADOPT, value, fn + ); + return is_err(result) ? result : value; +} + +static ant_value_t builtin_AsyncDisposableStack_defer(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t fn = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t result = disposable_stack_push_record( + js, js->this_val, BRAND_ASYNC_DISPOSABLE_STACK, + "AsyncDisposableStack", SV_DISPOSAL_RECORD_DEFER, js_mkundef(), fn + ); + return is_err(result) ? result : js_mkundef(); +} + +static ant_value_t builtin_AsyncDisposableStack_adopt(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t value = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t fn = nargs > 1 ? args[1] : js_mkundef(); + ant_value_t result = disposable_stack_push_record( + js, js->this_val, BRAND_ASYNC_DISPOSABLE_STACK, + "AsyncDisposableStack", SV_DISPOSAL_RECORD_ADOPT, value, fn + ); + return is_err(result) ? result : value; +} + +static ant_value_t disposable_stack_use( + ant_t *js, ant_value_t stack, int brand, const char *name, + ant_value_t resource, ant_value_t dispose_sym, ant_value_t fallback_sym +) { + ant_value_t entries = disposable_stack_entries_checked(js, stack, brand, name); + if (is_err(entries)) return entries; + + if (vtype(resource) == T_NULL || vtype(resource) == T_UNDEF) return resource; + + ant_value_t method = js_get_sym(js, resource, dispose_sym); + if ((vtype(method) == T_UNDEF || vtype(method) == T_NULL) && vtype(fallback_sym) == T_SYMBOL) { + method = js_get_sym(js, resource, fallback_sym); + } + if (!is_callable(method)) { + return js_mkerr_typed(js, JS_ERR_TYPE, "%s resource is not disposable", name); + } + + ant_value_t result = disposable_stack_push_record( + js, stack, brand, name, SV_DISPOSAL_RECORD_USE, resource, method + ); + return is_err(result) ? result : resource; +} + +static ant_value_t builtin_DisposableStack_use(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t resource = nargs > 0 ? args[0] : js_mkundef(); + return disposable_stack_use( + js, js->this_val, BRAND_DISPOSABLE_STACK, "DisposableStack", resource, get_dispose_sym(), js_mkundef() + ); +} + +static ant_value_t builtin_AsyncDisposableStack_use(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t resource = nargs > 0 ? args[0] : js_mkundef(); + return disposable_stack_use( + js, js->this_val, BRAND_ASYNC_DISPOSABLE_STACK, "AsyncDisposableStack", + resource, get_asyncDispose_sym(), get_dispose_sym() + ); +} + +static ant_value_t disposable_stack_move(ant_t *js, int brand, const char *name) { + GC_ROOT_SAVE(root_mark, js); + ant_value_t stack = js->this_val; + GC_ROOT_PIN(js, stack); + + ant_value_t entries = disposable_stack_entries_checked(js, stack, brand, name); + GC_ROOT_PIN(js, entries); + if (is_err(entries)) { + GC_ROOT_RESTORE(js, root_mark); + return entries; + } + + ant_value_t proto = js_get_ctor_proto(js, name, strlen(name)); + GC_ROOT_PIN(js, proto); + ant_value_t moved = disposable_stack_init(js, js_mkundef(), brand, proto); + GC_ROOT_PIN(js, moved); + if (is_err(moved)) { + GC_ROOT_RESTORE(js, root_mark); + return moved; + } + + set_slot(moved, SLOT_ENTRIES, entries); + set_slot(js_as_obj(stack), SLOT_ENTRIES, js_mkarr(js)); + set_slot(js_as_obj(stack), SLOT_SETTLED, js_true); + + GC_ROOT_RESTORE(js, root_mark); + return moved; +} + +static ant_value_t builtin_DisposableStack_move(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + return disposable_stack_move(js, BRAND_DISPOSABLE_STACK, "DisposableStack"); +} + +static ant_value_t builtin_AsyncDisposableStack_move(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + return disposable_stack_move(js, BRAND_ASYNC_DISPOSABLE_STACK, "AsyncDisposableStack"); +} + +static ant_value_t builtin_DisposableStack_dispose(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stack = js->this_val; + if (!disposable_stack_has_brand(stack, BRAND_DISPOSABLE_STACK)) { + return js_mkerr_typed(js, JS_ERR_TYPE, "DisposableStack method called on incompatible receiver"); + } + if (get_slot(js_as_obj(stack), SLOT_SETTLED) == js_true) return js_mkundef(); + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, stack); + ant_value_t entries = get_slot(js_as_obj(stack), SLOT_ENTRIES); + + GC_ROOT_PIN(js, entries); + ant_value_t completion = js_mkundef(); + + GC_ROOT_PIN(js, completion); + set_slot(js_as_obj(stack), SLOT_SETTLED, js_true); + + ant_offset_t len = vtype(entries) == T_ARR ? js_arr_len(js, entries) : 0; + for (ant_offset_t i = len; i > 0; i--) { + ant_value_t record = js_arr_get(js, entries, i - 1); + GC_ROOT_PIN(js, record); + ant_value_t result = sv_disposal_record_call(js, record); + + if (is_err(result) || js->thrown_exists) { + ant_value_t error = sv_disposal_error_value(js, result); + GC_ROOT_PIN(js, error); + completion = sv_suppress_disposal_error(js, error, completion); + + if (is_err(completion)) { + GC_ROOT_RESTORE(js, root_mark); + return completion; + } + } + } + + set_slot(js_as_obj(stack), SLOT_ENTRIES, js_mkarr(js)); + if (vtype(completion) != T_UNDEF) { + ant_value_t thrown = js_throw(js, completion); + GC_ROOT_RESTORE(js, root_mark); + return thrown; + } + + GC_ROOT_RESTORE(js, root_mark); + return js_mkundef(); +} + +static ant_value_t builtin_AsyncDisposableStack_disposeAsync(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stack = js->this_val; + ant_value_t result_promise = js_mkpromise(js); + if (is_err(result_promise)) return result_promise; + + if (!disposable_stack_has_brand(stack, BRAND_ASYNC_DISPOSABLE_STACK)) { + ant_value_t error = js_make_error_silent( + js, JS_ERR_TYPE, "AsyncDisposableStack method called on incompatible receiver" + ); + js_reject_promise(js, result_promise, error); + return result_promise; + } + + if (get_slot(js_as_obj(stack), SLOT_SETTLED) == js_true) { + js_resolve_promise(js, result_promise, js_mkundef()); + return result_promise; + } + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, stack); + GC_ROOT_PIN(js, result_promise); + + ant_value_t entries = get_slot(js_as_obj(stack), SLOT_ENTRIES); + GC_ROOT_PIN(js, entries); + set_slot(js_as_obj(stack), SLOT_SETTLED, js_true); + + ant_value_t state = js_mkobj(js); + GC_ROOT_PIN(js, state); + if (is_err(state)) { + GC_ROOT_RESTORE(js, root_mark); + return state; + } + + set_slot(state, SLOT_ENTRIES, entries); + set_slot(state, SLOT_DATA, result_promise); + set_slot(state, SLOT_AUX, js_mkundef()); + set_slot(state, SLOT_ITER_STATE, js_mknum((double)(vtype(entries) == T_ARR ? js_arr_len(js, entries) : 0))); + set_slot(js_as_obj(stack), SLOT_ENTRIES, js_mkarr(js)); + + ant_value_t result = sv_async_dispose_continue(js, state, false, js_mkundef()); + GC_ROOT_RESTORE(js, root_mark); + + return result; +} + typedef ant_value_t (*dynamic_kv_mapper_fn)( ant_t *js, @@ -13177,7 +13514,7 @@ static ant_value_t handle_proxy_instanceof(ant_t *js, ant_value_t l, ant_value_t return mkval(T_BOOL, 0); } - if (ltype != T_OBJ && ltype != T_ARR && ltype != T_FUNC && ltype != T_PROMISE) { + if (ltype != T_OBJ && ltype != T_ARR && ltype != T_FUNC && ltype != T_PROMISE && ltype != T_GENERATOR) { return mkval(T_BOOL, 0); } @@ -13313,7 +13650,7 @@ ant_value_t do_instanceof(ant_t *js, ant_value_t l, ant_value_t r) { return mkval(T_BOOL, vdata(ctor_proto) == vdata(type_proto) ? 1 : 0); } - if (ltype != T_OBJ && ltype != T_ARR && ltype != T_FUNC && ltype != T_PROMISE) { + if (ltype != T_OBJ && ltype != T_ARR && ltype != T_FUNC && ltype != T_PROMISE && ltype != T_GENERATOR) { return mkval(T_BOOL, 0); } @@ -14513,6 +14850,59 @@ ant_t *js_create(void *buf, size_t len) { js_setprop(js, proto, ANT_STRING("constructor"), js_obj_to_func(ctor)); js_set_descriptor(js, proto, "constructor", 11, JS_DESC_W | JS_DESC_C); js_setprop(js, glob, ANT_STRING("AggregateError"), js_obj_to_func(ctor)); + + ant_value_t suppressed_proto = js_mkobj(js); + set_proto(js, suppressed_proto, error_proto); + js_setprop(js, suppressed_proto, ANT_STRING("name"), ANT_STRING("SuppressedError")); + + ant_value_t suppressed_ctor = mkobj(js, 0); + set_proto(js, suppressed_ctor, function_proto); + set_slot(suppressed_ctor, SLOT_CFUNC, js_mkfun(builtin_SuppressedError)); + js_setprop_nonconfigurable(js, suppressed_ctor, "prototype", 9, suppressed_proto); + js_setprop(js, suppressed_ctor, ANT_STRING("name"), ANT_STRING("SuppressedError")); + + ant_value_t suppressed_ctor_func = js_obj_to_func(suppressed_ctor); + js_setprop(js, suppressed_proto, ANT_STRING("constructor"), suppressed_ctor_func); + js_set_descriptor(js, suppressed_proto, "constructor", 11, JS_DESC_W | JS_DESC_C); + js_setprop(js, glob, ANT_STRING("SuppressedError"), suppressed_ctor_func); + + ant_value_t disposable_stack_proto = js_mkobj(js); + set_proto(js, disposable_stack_proto, object_proto); + defmethod(js, disposable_stack_proto, "use", 3, js_mkfun(builtin_DisposableStack_use)); + defmethod(js, disposable_stack_proto, "adopt", 5, js_mkfun(builtin_DisposableStack_adopt)); + defmethod(js, disposable_stack_proto, "defer", 5, js_mkfun(builtin_DisposableStack_defer)); + defmethod(js, disposable_stack_proto, "move", 4, js_mkfun(builtin_DisposableStack_move)); + defmethod(js, disposable_stack_proto, "dispose", 7, js_mkfun(builtin_DisposableStack_dispose)); + + ant_value_t disposable_stack_ctor = mkobj(js, 0); + set_proto(js, disposable_stack_ctor, function_proto); + set_slot(disposable_stack_ctor, SLOT_CFUNC, js_mkfun(builtin_DisposableStack)); + js_setprop_nonconfigurable(js, disposable_stack_ctor, "prototype", 9, disposable_stack_proto); + js_setprop(js, disposable_stack_ctor, ANT_STRING("name"), ANT_STRING("DisposableStack")); + + ant_value_t disposable_stack_ctor_func = js_obj_to_func(disposable_stack_ctor); + js_setprop(js, disposable_stack_proto, ANT_STRING("constructor"), disposable_stack_ctor_func); + js_set_descriptor(js, disposable_stack_proto, "constructor", 11, JS_DESC_W | JS_DESC_C); + js_setprop(js, glob, ANT_STRING("DisposableStack"), disposable_stack_ctor_func); + + ant_value_t async_disposable_stack_proto = js_mkobj(js); + set_proto(js, async_disposable_stack_proto, object_proto); + defmethod(js, async_disposable_stack_proto, "use", 3, js_mkfun(builtin_AsyncDisposableStack_use)); + defmethod(js, async_disposable_stack_proto, "adopt", 5, js_mkfun(builtin_AsyncDisposableStack_adopt)); + defmethod(js, async_disposable_stack_proto, "defer", 5, js_mkfun(builtin_AsyncDisposableStack_defer)); + defmethod(js, async_disposable_stack_proto, "move", 4, js_mkfun(builtin_AsyncDisposableStack_move)); + defmethod(js, async_disposable_stack_proto, "disposeAsync", 12, js_mkfun(builtin_AsyncDisposableStack_disposeAsync)); + + ant_value_t async_disposable_stack_ctor = mkobj(js, 0); + set_proto(js, async_disposable_stack_ctor, function_proto); + set_slot(async_disposable_stack_ctor, SLOT_CFUNC, js_mkfun(builtin_AsyncDisposableStack)); + js_setprop_nonconfigurable(js, async_disposable_stack_ctor, "prototype", 9, async_disposable_stack_proto); + js_setprop(js, async_disposable_stack_ctor, ANT_STRING("name"), ANT_STRING("AsyncDisposableStack")); + + ant_value_t async_disposable_stack_ctor_func = js_obj_to_func(async_disposable_stack_ctor); + js_setprop(js, async_disposable_stack_proto, ANT_STRING("constructor"), async_disposable_stack_ctor_func); + js_set_descriptor(js, async_disposable_stack_proto, "constructor", 11, JS_DESC_W | JS_DESC_C); + js_setprop(js, glob, ANT_STRING("AsyncDisposableStack"), async_disposable_stack_ctor_func); ant_value_t promise_proto = js_mkobj(js); set_proto(js, promise_proto, object_proto); diff --git a/src/esm/library.c b/src/esm/library.c index 90b88ea..bec1803 100644 --- a/src/esm/library.c +++ b/src/esm/library.c @@ -13,6 +13,7 @@ typedef struct ant_library_entry { ant_library_init_fn init_fn; ant_value_t cached_ns; bool ns_initialized; + bool root_registered; struct ant_library_entry *canonical; UT_hash_handle hh; } ant_library_entry_t; @@ -80,11 +81,16 @@ ant_value_t js_esm_load_registered_library(ant_t *js, const char *specifier, siz if (loaded) *loaded = false; return js_mkundef(); } - if (loaded) *loaded = true; + if (loaded) *loaded = true; ant_library_entry_t *canon = lib->canonical; if (canon->ns_initialized) return canon->cached_ns; + if (!canon->root_registered) { + gc_register_root(&canon->cached_ns); + canon->root_registered = true; + } + canon->cached_ns = canon->init_fn(js); if (is_object_type(canon->cached_ns)) { const char *display_name = canon->display_name[0] ? canon->display_name : canon->name; @@ -94,7 +100,5 @@ ant_value_t js_esm_load_registered_library(ant_t *js, const char *specifier, siz } canon->ns_initialized = true; - gc_register_root(&canon->cached_ns); - return canon->cached_ns; } diff --git a/src/modules/generator.c b/src/modules/generator.c index a20574c..dc38281 100644 --- a/src/modules/generator.c +++ b/src/modules/generator.c @@ -11,6 +11,7 @@ #include "gc/roots.h" #include "silver/engine.h" #include "modules/generator.h" +#include "modules/iterator.h" #include "modules/symbol.h" enum { GENERATOR_NATIVE_TAG = 0x47454e52u }; // GENR @@ -481,11 +482,13 @@ void init_generator_module(void) { js->sym.generator_proto = proto; js_set_proto_init(proto, js->sym.iterator_proto); + js_set_proto_wb(js, js->sym.async_iterator_proto, proto); js_set(js, proto, "next", js_mkfun(generator_next)); js_set(js, proto, "return", js_mkfun(generator_return)); js_set(js, proto, "throw", js_mkfun(generator_throw)); js_set_sym(js, proto, get_toStringTag_sym(), js_mkstr(js, "Generator", 9)); + init_async_iterator_helpers(); } ant_value_t sv_call_generator_closure_dispatch( @@ -574,10 +577,12 @@ ant_value_t sv_call_generator_closure_dispatch( js_set_native_tag(gen, GENERATOR_NATIVE_TAG); js_set_finalizer(gen, generator_finalize); - ant_value_t instance_proto = js_get(js, callee_func, "prototype"); - if (is_object_type(instance_proto)) js_set_proto_wb(js, gen, instance_proto); - if (data->is_async) - js_set_sym(js, gen, get_asyncIterator_sym(), js_mkfun(sym_this_cb)); + if (data->is_async && is_object_type(js->sym.async_iterator_proto)) + js_set_proto_wb(js, gen, js->sym.async_iterator_proto); + else { + ant_value_t instance_proto = js_get(js, callee_func, "prototype"); + if (is_object_type(instance_proto)) js_set_proto_wb(js, gen, instance_proto); + } return gen; } diff --git a/src/modules/iterator.c b/src/modules/iterator.c index fecec69..8d06d61 100644 --- a/src/modules/iterator.c +++ b/src/modules/iterator.c @@ -5,9 +5,11 @@ #include "errors.h" #include "runtime.h" #include "internal.h" +#include "ptr.h" #include "silver/engine.h" #include "descriptors.h" +#include "modules/assert.h" #include "modules/iterator.h" #include "modules/symbol.h" @@ -17,9 +19,40 @@ enum { WRAP_TAKE = 2, WRAP_DROP = 3, WRAP_FLATMAP = 4, + WRAP_PASS = 5, + WRAP_FROM_SYNC = 6, +}; + +enum { + ASYNC_TERM_EVERY = 0, + ASYNC_TERM_SOME = 1, + ASYNC_TERM_FIND = 2, + ASYNC_TERM_FOREACH = 3, + ASYNC_TERM_REDUCE = 4, + ASYNC_TERM_TOARRAY = 5, }; static ant_value_t g_wrap_iter_proto = 0; +static ant_value_t g_async_wrap_iter_proto = 0; + +enum { ASYNC_TERMINAL_STATE_TAG = 0x41544954u }; // ATIT + +typedef struct { + double index; + int mode; + bool has_acc; +} async_terminal_state_t; + +static inline ant_value_t call_indexed_callback(ant_t *js, ant_value_t fn, ant_value_t value, double index) { + ant_value_t call_args[2] = { value, js_mknum(index) }; + return sv_vm_call(js->vm, js, fn, js_mkundef(), call_args, 2, NULL, false); +} + +static inline ant_value_t set_iter_result(ant_t *js, ant_value_t result, ant_value_t value, bool done) { + js_set(js, result, "done", done ? js_true : js_false); + js_set(js, result, "value", value); + return result; +} static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { ant_value_t self = js->this_val; @@ -29,9 +62,9 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { uint32_t state = (vtype(state_v) == T_NUM) ? (uint32_t)js_getnum(state_v) : 0; uint32_t kind = ITER_STATE_KIND(state); uint32_t count = ITER_STATE_INDEX(state); - ant_value_t cb = js_get_slot(self, SLOT_CTOR); - + ant_value_t result = js_mkobj(js); + ant_value_t cb = js_get_slot(self, SLOT_CTOR); ant_value_t next_fn = js_getprop_fallback(js, source, "next"); for (;;) { @@ -42,12 +75,11 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { ant_value_t inner_next = js_getprop_fallback(js, inner, "next"); ant_value_t inner_step = sv_vm_call(js->vm, js, inner_next, inner, NULL, 0, NULL, false); if (!is_err(inner_step)) { - ant_value_t inner_done = js_getprop_fallback(js, inner_step, "done"); - if (!js_truthy(js, inner_done)) { - js_set(js, result, "done", js_false); - js_set(js, result, "value", js_getprop_fallback(js, inner_step, "value")); - return result; - }} + ant_value_t inner_done = js_getprop_fallback(js, inner_step, "done"); + if (!js_truthy(js, inner_done)) return set_iter_result( + js, result, js_getprop_fallback(js, inner_step, "value" + ), false); + } js_set_slot(self, SLOT_ENTRIES, js_mkundef()); }} @@ -70,10 +102,7 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { js_set_slot(self, SLOT_ENTRIES, js_mkundef()); }} - js_set(js, result, "done", js_true); - js_set(js, result, "value", js_mkundef()); - - return result; + return set_iter_result(js, result, js_mkundef(), true); } ant_value_t value = js_getprop_fallback(js, step, "value"); @@ -82,29 +111,22 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { case WRAP_MAP: { ant_value_t out_val; if (is_callable(cb)) { - ant_value_t call_args[2] = { value, js_mknum((double)count) }; - out_val = sv_vm_call(js->vm, js, cb, js_mkundef(), call_args, 2, NULL, false); + out_val = call_indexed_callback(js, cb, value, (double)count); if (is_err(out_val)) return out_val; } else out_val = value; count++; js_set_slot(self, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count))); - js_set(js, result, "done", js_false); - js_set(js, result, "value", out_val); - - return result; + return set_iter_result(js, result, out_val, false); } case WRAP_FILTER: { - ant_value_t call_args[2] = { value, js_mknum((double)count) }; - ant_value_t test = sv_vm_call(js->vm, js, cb, js_mkundef(), call_args, 2, NULL, false); + ant_value_t test = call_indexed_callback(js, cb, value, (double)count); if (is_err(test)) return test; count++; js_set_slot(self, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count))); if (js_truthy(js, test)) { - js_set(js, result, "done", js_false); - js_set(js, result, "value", value); - return result; + return set_iter_result(js, result, value, false); } continue; } @@ -112,14 +134,10 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { case WRAP_TAKE: { uint32_t limit = (vtype(cb) == T_NUM) ? (uint32_t)js_getnum(cb) : 0; if (count >= limit) { - js_set(js, result, "done", js_true); - js_set(js, result, "value", js_mkundef()); - return result; + return set_iter_result(js, result, js_mkundef(), true); } js_set_slot(self, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count + 1))); - js_set(js, result, "done", js_false); - js_set(js, result, "value", value); - return result; + return set_iter_result(js, result, value, false); } case WRAP_DROP: { @@ -127,23 +145,18 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { count++; js_set_slot(self, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count))); if (count <= limit) continue; - js_set(js, result, "done", js_false); - js_set(js, result, "value", value); - return result; + return set_iter_result(js, result, value, false); } case WRAP_FLATMAP: { - ant_value_t call_args[2] = { value, js_mknum((double)count) }; - ant_value_t mapped = sv_vm_call(js->vm, js, cb, js_mkundef(), call_args, 2, NULL, false); + ant_value_t mapped = call_indexed_callback(js, cb, value, (double)count); if (is_err(mapped)) return mapped; count++; js_set_slot(self, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count))); ant_value_t iter_fn = js_get_sym(js, mapped, get_iterator_sym()); if (!is_callable(iter_fn)) { - js_set(js, result, "done", js_false); - js_set(js, result, "value", mapped); - return result; + return set_iter_result(js, result, mapped, false); } ant_value_t inner = sv_vm_call(js->vm, js, iter_fn, mapped, NULL, 0, NULL, false); @@ -155,17 +168,13 @@ static ant_value_t wrap_iter_next(ant_t *js, ant_value_t *args, int nargs) { ant_value_t inner_done = js_getprop_fallback(js, inner_step, "done"); if (!js_truthy(js, inner_done)) { js_set_slot_wb(js, self, SLOT_ENTRIES, inner); - js_set(js, result, "done", js_false); - js_set(js, result, "value", js_getprop_fallback(js, inner_step, "value")); - return result; + return set_iter_result(js, result, js_getprop_fallback(js, inner_step, "value"), false); } continue; } default: - js_set(js, result, "done", js_false); - js_set(js, result, "value", value); - return result; + return set_iter_result(js, result, value, false); } } } @@ -192,44 +201,52 @@ static ant_value_t get_source_iter(ant_t *js) { return sv_vm_call(js->vm, js, iter_fn, self, NULL, 0, NULL, false); } -static ant_value_t iter_map(ant_t *js, ant_value_t *args, int nargs) { - if (nargs < 1 || !is_callable(args[0])) - return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator.prototype.map requires a callable"); +static ant_value_t iter_make_helper(ant_t *js, int kind, ant_value_t cb) { ant_value_t source = get_source_iter(js); if (is_err(source)) return source; - return make_wrap_iter(js, source, WRAP_MAP, args[0]); + return make_wrap_iter(js, source, kind, cb); } -static ant_value_t iter_filter(ant_t *js, ant_value_t *args, int nargs) { +static ant_value_t iter_make_callable_helper( + ant_t *js, + ant_value_t *args, + int nargs, + int kind, + const char *method +) { if (nargs < 1 || !is_callable(args[0])) - return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator.prototype.filter requires a callable"); - ant_value_t source = get_source_iter(js); - if (is_err(source)) return source; - return make_wrap_iter(js, source, WRAP_FILTER, args[0]); + return js_mkerr_typed(js, JS_ERR_TYPE, "%s requires a callable", method); + return iter_make_helper(js, kind, args[0]); } -static ant_value_t iter_take(ant_t *js, ant_value_t *args, int nargs) { +static ant_value_t iter_make_count_helper( + ant_t *js, ant_value_t *args, + int nargs, int kind, + const char *method +) { double limit = (nargs >= 1 && vtype(args[0]) == T_NUM) ? js_getnum(args[0]) : 0; - if (limit < 0) return js_mkerr(js, "Iterator.prototype.take requires a non-negative number"); - ant_value_t source = get_source_iter(js); - if (is_err(source)) return source; - return make_wrap_iter(js, source, WRAP_TAKE, js_mknum(limit)); + if (limit < 0) return js_mkerr(js, "%s requires a non-negative number", method); + return iter_make_helper(js, kind, js_mknum(limit)); +} + +static ant_value_t iter_map(ant_t *js, ant_value_t *args, int nargs) { + return iter_make_callable_helper(js, args, nargs, WRAP_MAP, "Iterator.prototype.map"); +} + +static ant_value_t iter_filter(ant_t *js, ant_value_t *args, int nargs) { + return iter_make_callable_helper(js, args, nargs, WRAP_FILTER, "Iterator.prototype.filter"); +} + +static ant_value_t iter_take(ant_t *js, ant_value_t *args, int nargs) { + return iter_make_count_helper(js, args, nargs, WRAP_TAKE, "Iterator.prototype.take"); } static ant_value_t iter_drop(ant_t *js, ant_value_t *args, int nargs) { - double limit = (nargs >= 1 && vtype(args[0]) == T_NUM) ? js_getnum(args[0]) : 0; - if (limit < 0) return js_mkerr(js, "Iterator.prototype.drop requires a non-negative number"); - ant_value_t source = get_source_iter(js); - if (is_err(source)) return source; - return make_wrap_iter(js, source, WRAP_DROP, js_mknum(limit)); + return iter_make_count_helper(js, args, nargs, WRAP_DROP, "Iterator.prototype.drop"); } static ant_value_t iter_flatMap(ant_t *js, ant_value_t *args, int nargs) { - if (nargs < 1 || !is_callable(args[0])) - return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator.prototype.flatMap requires a callable"); - ant_value_t source = get_source_iter(js); - if (is_err(source)) return source; - return make_wrap_iter(js, source, WRAP_FLATMAP, args[0]); + return iter_make_callable_helper(js, args, nargs, WRAP_FLATMAP, "Iterator.prototype.flatMap"); } static ant_value_t iter_every(ant_t *js, ant_value_t *args, int nargs) { @@ -244,8 +261,7 @@ static ant_value_t iter_every(ant_t *js, ant_value_t *args, int nargs) { ant_value_t value; uint32_t counter = 0; while (js_iter_next(js, &it, &value)) { - ant_value_t call_args[2] = { value, js_mknum((double)counter++) }; - ant_value_t test = sv_vm_call(js->vm, js, fn, js_mkundef(), call_args, 2, NULL, false); + ant_value_t test = call_indexed_callback(js, fn, value, (double)counter++); if (is_err(test)) { js_iter_close(js, &it); return test; } if (!js_truthy(js, test)) { js_iter_close(js, &it); return js_false; } } @@ -264,8 +280,7 @@ static ant_value_t iter_some(ant_t *js, ant_value_t *args, int nargs) { ant_value_t value; uint32_t counter = 0; while (js_iter_next(js, &it, &value)) { - ant_value_t call_args[2] = { value, js_mknum((double)counter++) }; - ant_value_t test = sv_vm_call(js->vm, js, fn, js_mkundef(), call_args, 2, NULL, false); + ant_value_t test = call_indexed_callback(js, fn, value, (double)counter++); if (is_err(test)) { js_iter_close(js, &it); return test; } if (js_truthy(js, test)) { js_iter_close(js, &it); return js_true; } } @@ -284,8 +299,7 @@ static ant_value_t iter_find(ant_t *js, ant_value_t *args, int nargs) { ant_value_t value; uint32_t counter = 0; while (js_iter_next(js, &it, &value)) { - ant_value_t call_args[2] = { value, js_mknum((double)counter++) }; - ant_value_t test = sv_vm_call(js->vm, js, fn, js_mkundef(), call_args, 2, NULL, false); + ant_value_t test = call_indexed_callback(js, fn, value, (double)counter++); if (is_err(test)) { js_iter_close(js, &it); return test; } if (js_truthy(js, test)) { js_iter_close(js, &it); return value; } } @@ -304,8 +318,7 @@ static ant_value_t iter_forEach(ant_t *js, ant_value_t *args, int nargs) { ant_value_t value; uint32_t counter = 0; while (js_iter_next(js, &it, &value)) { - ant_value_t call_args[2] = { value, js_mknum((double)counter++) }; - ant_value_t r = sv_vm_call(js->vm, js, fn, js_mkundef(), call_args, 2, NULL, false); + ant_value_t r = call_indexed_callback(js, fn, value, (double)counter++); if (is_err(r)) { js_iter_close(js, &it); return r; } } return js_mkundef(); @@ -381,6 +394,845 @@ static ant_value_t iter_ctor(ant_t *js, ant_value_t *args, int nargs) { return obj; } +static ant_value_t async_iter_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "AsyncIterator constructor requires 'new'"); + + ant_value_t obj = js_mkobj(js); + ant_value_t proto = js_instance_proto_from_new_target(js, js->sym.async_iterator_proto); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + + return obj; +} + +static inline ant_value_t iter_result(ant_t *js, ant_value_t value, bool done) { + ant_value_t result = js_mkobj(js); + js_set(js, result, "done", done ? js_true : js_false); + js_set(js, result, "value", value); + return result; +} + +static inline ant_value_t fulfilled_promise(ant_t *js, ant_value_t value) { + ant_value_t promise = js_mkpromise(js); + js_resolve_promise(js, promise, value); + return promise; +} + +static inline ant_value_t rejected_promise(ant_t *js, ant_value_t reason) { + ant_value_t promise = js_mkpromise(js); + js_reject_promise(js, promise, reason); + return promise; +} + +static inline ant_value_t promise_from_call_result(ant_t *js, ant_value_t result) { + if (vtype(result) == T_PROMISE) return result; + if (is_err(result)) return rejected_promise(js, result); + return fulfilled_promise(js, result); +} + +static ant_value_t async_iter_call_method( + ant_t *js, ant_value_t receiver, + const char *name, ant_value_t *args, int nargs, bool *missing +) { + ant_value_t fn = js_getprop_fallback(js, receiver, name); + if (missing) *missing = !is_callable(fn); + if (!is_callable(fn)) return js_mkundef(); + return sv_vm_call(js->vm, js, fn, receiver, args, nargs, NULL, false); +} + +static ant_value_t make_async_wrap_iter(ant_t *js, ant_value_t source, int kind, ant_value_t cb) { + ant_value_t iter = js_mkobj(js); + js_set_proto_init(iter, g_async_wrap_iter_proto); + js_set_slot_wb(js, iter, SLOT_DATA, source); + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, 0))); + js_set_slot_wb(js, iter, SLOT_CTOR, cb); + js_set_slot(iter, SLOT_ENTRIES, js_mkundef()); + return iter; +} + +static ant_value_t async_wrap_advance( + ant_t *js, + ant_value_t iter, + ant_value_t promise +); + +static ant_value_t async_wrap_handle_step( + ant_t *js, + ant_value_t iter, + ant_value_t promise, + ant_value_t step +); + +static ant_value_t async_wrap_handle_callback_result( + ant_t *js, + ant_value_t iter, + ant_value_t promise, + uint32_t kind, + ant_value_t value, + ant_value_t result +); + +static ant_value_t async_wrap_on_step(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t iter = js_get(js, state, "iter"); + ant_value_t promise = js_get(js, state, "promise"); + ant_value_t step = nargs > 0 ? args[0] : js_mkundef(); + return async_wrap_handle_step(js, iter, promise, step); +} + +static ant_value_t async_wrap_on_reject(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t promise = js_get(js, state, "promise"); + js_reject_promise(js, promise, nargs > 0 ? args[0] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t async_wrap_on_callback(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t iter = js_get(js, state, "iter"); + ant_value_t promise = js_get(js, state, "promise"); + uint32_t kind = (uint32_t)js_getnum(js_get(js, state, "kind")); + ant_value_t value = js_get(js, state, "value"); + ant_value_t result = nargs > 0 ? args[0] : js_mkundef(); + return async_wrap_handle_callback_result(js, iter, promise, kind, value, result); +} + +static ant_value_t async_wrap_on_sync_value(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t promise = js_get(js, state, "promise"); + bool done = js_truthy(js, js_get(js, state, "done")); + ant_value_t value = nargs > 0 ? args[0] : js_mkundef(); + js_resolve_promise(js, promise, iter_result(js, value, done)); + return js_mkundef(); +} + +static bool async_wrap_chain_step(ant_t *js, ant_value_t iter, ant_value_t promise, ant_value_t next_result) { + if (is_err(next_result)) { + js_reject_promise(js, promise, next_result); + return true; + } + + if (vtype(next_result) != T_PROMISE) { + async_wrap_handle_step(js, iter, promise, next_result); + return true; + } + + ant_value_t state = js_mkobj(js); + js_set(js, state, "iter", iter); + js_set(js, state, "promise", promise); + ant_value_t on_resolve = js_heavy_mkfun(js, async_wrap_on_step, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_wrap_on_reject, state); + ant_value_t then_result = js_promise_then(js, next_result, on_resolve, on_reject); + promise_mark_handled(then_result); + return true; +} + +static ant_value_t async_wrap_handle_inner_step( + ant_t *js, + ant_value_t iter, + ant_value_t promise, + ant_value_t step +) { + if (!is_object_type(step)) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "iterator result is not an object")); + return js_mkundef(); + } + + if (!js_truthy(js, js_getprop_fallback(js, step, "done"))) { + js_resolve_promise(js, promise, iter_result(js, js_getprop_fallback(js, step, "value"), false)); + return js_mkundef(); + } + + js_set_slot(iter, SLOT_ENTRIES, js_mkundef()); + async_wrap_advance(js, iter, promise); + return js_mkundef(); +} + +static ant_value_t async_wrap_on_inner_step(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t iter = js_get(js, state, "iter"); + ant_value_t promise = js_get(js, state, "promise"); + ant_value_t step = nargs > 0 ? args[0] : js_mkundef(); + return async_wrap_handle_inner_step(js, iter, promise, step); +} + +static ant_value_t async_wrap_advance_inner(ant_t *js, ant_value_t iter, ant_value_t promise) { + ant_value_t inner = js_get_slot(iter, SLOT_ENTRIES); + if (vtype(inner) == T_UNDEF || vtype(inner) == T_NULL) return async_wrap_advance(js, iter, promise); + + bool missing = false; + ant_value_t next_result = async_iter_call_method(js, inner, "next", NULL, 0, &missing); + if (missing) { + js_set_slot(iter, SLOT_ENTRIES, js_mkundef()); + return async_wrap_advance(js, iter, promise); + } + + if (is_err(next_result)) { + js_reject_promise(js, promise, next_result); + return js_mkundef(); + } + + if (vtype(next_result) == T_PROMISE) { + ant_value_t state = js_mkobj(js); + js_set(js, state, "iter", iter); + js_set(js, state, "promise", promise); + ant_value_t on_resolve = js_heavy_mkfun(js, async_wrap_on_inner_step, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_wrap_on_reject, state); + ant_value_t then_result = js_promise_then(js, next_result, on_resolve, on_reject); + promise_mark_handled(then_result); + return js_mkundef(); + } + + return async_wrap_handle_inner_step(js, iter, promise, next_result); +} + +static ant_value_t async_wrap_handle_step( + ant_t *js, + ant_value_t iter, + ant_value_t promise, + ant_value_t step +) { + if (!is_object_type(step)) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "iterator result is not an object")); + return js_mkundef(); + } + + bool done = js_truthy(js, js_getprop_fallback(js, step, "done")); + ant_value_t state_v = js_get_slot(iter, SLOT_ITER_STATE); + + uint32_t state = (vtype(state_v) == T_NUM) ? (uint32_t)js_getnum(state_v) : 0; + uint32_t kind = ITER_STATE_KIND(state); + uint32_t count = ITER_STATE_INDEX(state); + ant_value_t value = js_getprop_fallback(js, step, "value"); + + if (kind == WRAP_FROM_SYNC && vtype(value) == T_PROMISE) { + ant_value_t state_obj = js_mkobj(js); + js_set(js, state_obj, "promise", promise); + js_set(js, state_obj, "done", done ? js_true : js_false); + + ant_value_t on_resolve = js_heavy_mkfun(js, async_wrap_on_sync_value, state_obj); + ant_value_t on_reject = js_heavy_mkfun(js, async_wrap_on_reject, state_obj); + ant_value_t then_result = js_promise_then(js, value, on_resolve, on_reject); + + promise_mark_handled(then_result); + return js_mkundef(); + } + + if (done) { + js_resolve_promise( + js, promise, + iter_result(js, kind == WRAP_FROM_SYNC ? value : js_mkundef(), true) + ); + return js_mkundef(); + } + + ant_value_t cb = js_get_slot(iter, SLOT_CTOR); + + switch (kind) { + case WRAP_MAP: { + ant_value_t mapped = call_indexed_callback(js, cb, value, (double)count); + if (is_err(mapped)) { + js_reject_promise(js, promise, mapped); + return js_mkundef(); + } + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count + 1))); + return async_wrap_handle_callback_result(js, iter, promise, kind, value, mapped); + } + + case WRAP_FILTER: { + ant_value_t test = call_indexed_callback(js, cb, value, (double)count); + if (is_err(test)) { + js_reject_promise(js, promise, test); + return js_mkundef(); + } + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count + 1))); + return async_wrap_handle_callback_result(js, iter, promise, kind, value, test); + } + + case WRAP_TAKE: + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count + 1))); + js_resolve_promise(js, promise, iter_result(js, value, false)); + return js_mkundef(); + + case WRAP_DROP: { + double limit = (vtype(cb) == T_NUM) ? js_getnum(cb) : 0; + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count + 1))); + if ((double)(count + 1) <= limit) async_wrap_advance(js, iter, promise); + else js_resolve_promise(js, promise, iter_result(js, value, false)); + return js_mkundef(); + } + + case WRAP_FLATMAP: { + ant_value_t mapped = call_indexed_callback(js, cb, value, (double)count); + if (is_err(mapped)) { + js_reject_promise(js, promise, mapped); + return js_mkundef(); + } + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)ITER_STATE_PACK(kind, count + 1))); + return async_wrap_handle_callback_result(js, iter, promise, kind, value, mapped); + } + + default: + js_resolve_promise(js, promise, iter_result(js, value, false)); + return js_mkundef(); + } +} + +static ant_value_t async_wrap_handle_callback_result( + ant_t *js, + ant_value_t iter, + ant_value_t promise, + uint32_t kind, + ant_value_t value, + ant_value_t result +) { + if (vtype(result) == T_PROMISE) { + ant_value_t state = js_mkobj(js); + js_set(js, state, "iter", iter); + js_set(js, state, "promise", promise); + js_set(js, state, "kind", js_mknum((double)kind)); + js_set(js, state, "value", value); + ant_value_t on_resolve = js_heavy_mkfun(js, async_wrap_on_callback, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_wrap_on_reject, state); + ant_value_t then_result = js_promise_then(js, result, on_resolve, on_reject); + promise_mark_handled(then_result); + return js_mkundef(); + } + + if (kind == WRAP_MAP) { + js_resolve_promise(js, promise, iter_result(js, result, false)); + return js_mkundef(); + } + + if (kind == WRAP_FILTER) { + if (js_truthy(js, result)) js_resolve_promise(js, promise, iter_result(js, value, false)); + else async_wrap_advance(js, iter, promise); + return js_mkundef(); + } + + if (kind == WRAP_FLATMAP) { + ant_value_t iter_fn = js_get_sym(js, result, get_asyncIterator_sym()); + if (!is_callable(iter_fn)) iter_fn = js_get_sym(js, result, get_iterator_sym()); + if (!is_callable(iter_fn)) { + js_resolve_promise(js, promise, iter_result(js, result, false)); + return js_mkundef(); + } + + ant_value_t inner = sv_vm_call(js->vm, js, iter_fn, result, NULL, 0, NULL, false); + if (is_err(inner)) { + js_reject_promise(js, promise, inner); + return js_mkundef(); + } + js_set_slot_wb(js, iter, SLOT_ENTRIES, inner); + return async_wrap_advance_inner(js, iter, promise); + } + + js_resolve_promise(js, promise, iter_result(js, result, false)); + return js_mkundef(); +} + +static ant_value_t async_wrap_advance(ant_t *js, ant_value_t iter, ant_value_t promise) { + ant_value_t state_v = js_get_slot(iter, SLOT_ITER_STATE); + uint32_t state = (vtype(state_v) == T_NUM) ? (uint32_t)js_getnum(state_v) : 0; + uint32_t kind = ITER_STATE_KIND(state); + ant_value_t cb = js_get_slot(iter, SLOT_CTOR); + + if (kind == WRAP_TAKE) { + double limit = (vtype(cb) == T_NUM) ? js_getnum(cb) : 0; + if ((double)ITER_STATE_INDEX(state) >= limit) { + js_resolve_promise(js, promise, iter_result(js, js_mkundef(), true)); + return js_mkundef(); + } + } + + if (kind == WRAP_FLATMAP && vtype(js_get_slot(iter, SLOT_ENTRIES)) != T_UNDEF) + return async_wrap_advance_inner(js, iter, promise); + + ant_value_t source = js_get_slot(iter, SLOT_DATA); + bool missing = false; + ant_value_t next_result = async_iter_call_method(js, source, "next", NULL, 0, &missing); + if (missing) js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "object is not async iterable")); + else async_wrap_chain_step(js, iter, promise, next_result); + return js_mkundef(); +} + +static ant_value_t async_wrap_next(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + ant_value_t promise = js_mkpromise(js); + async_wrap_advance(js, js->this_val, promise); + return promise; +} + +static ant_value_t async_wrap_return(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t source = js_get_slot(js->this_val, SLOT_DATA); + bool missing = false; + ant_value_t result = async_iter_call_method(js, source, "return", args, nargs, &missing); + if (missing) return fulfilled_promise(js, iter_result(js, nargs > 0 ? args[0] : js_mkundef(), true)); + ant_value_t state_v = js_get_slot(js->this_val, SLOT_ITER_STATE); + uint32_t state = (vtype(state_v) == T_NUM) ? (uint32_t)js_getnum(state_v) : 0; + if (ITER_STATE_KIND(state) == WRAP_FROM_SYNC) { + ant_value_t promise = js_mkpromise(js); + async_wrap_chain_step(js, js->this_val, promise, result); + return promise; + } + return promise_from_call_result(js, result); +} + +static ant_value_t async_wrap_throw(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t source = js_get_slot(js->this_val, SLOT_DATA); + bool missing = false; + ant_value_t result = async_iter_call_method(js, source, "throw", args, nargs, &missing); + if (missing) return rejected_promise(js, nargs > 0 ? args[0] : js_mkundef()); + ant_value_t state_v = js_get_slot(js->this_val, SLOT_ITER_STATE); + uint32_t state = (vtype(state_v) == T_NUM) ? (uint32_t)js_getnum(state_v) : 0; + if (ITER_STATE_KIND(state) == WRAP_FROM_SYNC) { + ant_value_t promise = js_mkpromise(js); + async_wrap_chain_step(js, js->this_val, promise, result); + return promise; + } + return promise_from_call_result(js, result); +} + +static ant_value_t async_iter_from(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) == T_UNDEF || vtype(args[0]) == T_NULL) + return js_mkerr_typed(js, JS_ERR_TYPE, "AsyncIterator.from requires an object"); + + ant_value_t obj = args[0]; + ant_value_t iter_fn = js_get_sym(js, obj, get_asyncIterator_sym()); + if (is_callable(iter_fn)) { + ant_value_t iterator = sv_vm_call(js->vm, js, iter_fn, obj, NULL, 0, NULL, false); + if (is_err(iterator)) return iterator; + return make_async_wrap_iter(js, iterator, WRAP_PASS, js_mkundef()); + } + + iter_fn = js_get_sym(js, obj, get_iterator_sym()); + if (is_callable(iter_fn)) { + ant_value_t iterator = sv_vm_call(js->vm, js, iter_fn, obj, NULL, 0, NULL, false); + if (is_err(iterator)) return iterator; + return make_async_wrap_iter(js, iterator, WRAP_FROM_SYNC, js_mkundef()); + } + + ant_value_t next = js_getprop_fallback(js, obj, "next"); + if (is_callable(next)) return make_async_wrap_iter(js, obj, WRAP_FROM_SYNC, js_mkundef()); + + return js_mkerr_typed(js, JS_ERR_TYPE, "object is not async iterable"); +} + +static ant_value_t get_async_source_iter(ant_t *js) { + ant_value_t self = js->this_val; + ant_value_t next = js_getprop_fallback(js, self, "next"); + if (is_callable(next)) return self; + + ant_value_t iter_fn = js_get_sym(js, self, get_asyncIterator_sym()); + if (!is_callable(iter_fn)) return js_mkerr_typed(js, JS_ERR_TYPE, "object is not async iterable"); + + return sv_vm_call(js->vm, js, iter_fn, self, NULL, 0, NULL, false); +} + +static ant_value_t async_iter_make_helper(ant_t *js, int kind, ant_value_t cb) { + ant_value_t source = get_async_source_iter(js); + if (is_err(source)) return source; + return make_async_wrap_iter(js, source, kind, cb); +} + +static ant_value_t async_iter_make_callable_helper( + ant_t *js, + ant_value_t *args, + int nargs, + int kind, + const char *method +) { + if (nargs < 1 || !is_callable(args[0])) + return js_mkerr_typed(js, JS_ERR_TYPE, "%s requires a callable", method); + return async_iter_make_helper(js, kind, args[0]); +} + +static ant_value_t async_iter_make_count_helper( + ant_t *js, + ant_value_t *args, + int nargs, + int kind, + const char *method +) { + double limit = (nargs >= 1 && vtype(args[0]) == T_NUM) ? js_getnum(args[0]) : 0; + if (limit < 0) return js_mkerr_typed(js, JS_ERR_TYPE, "%s requires a non-negative number", method); + return async_iter_make_helper(js, kind, js_mknum(limit)); +} + +static ant_value_t async_iter_map(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_make_callable_helper(js, args, nargs, WRAP_MAP, "AsyncIterator.prototype.map"); +} + +static ant_value_t async_iter_filter(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_make_callable_helper(js, args, nargs, WRAP_FILTER, "AsyncIterator.prototype.filter"); +} + +static ant_value_t async_iter_take(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_make_count_helper(js, args, nargs, WRAP_TAKE, "AsyncIterator.prototype.take"); +} + +static ant_value_t async_iter_drop(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_make_count_helper(js, args, nargs, WRAP_DROP, "AsyncIterator.prototype.drop"); +} + +static ant_value_t async_iter_flatMap(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_make_callable_helper(js, args, nargs, WRAP_FLATMAP, "AsyncIterator.prototype.flatMap"); +} + +static ant_value_t async_terminal_advance(ant_t *js, ant_value_t state); +static ant_value_t async_terminal_finish_callback(ant_t *js, ant_value_t state, ant_value_t result); +static void async_terminal_close_and_reject(ant_t *js, ant_value_t state, ant_value_t reason); + +static void async_terminal_state_finalize(ant_t *js, ant_object_t *obj) { + free(obj->native.ptr); + obj->native.ptr = NULL; +} + +static inline async_terminal_state_t *async_terminal_state(ant_value_t state) { + if (!js_check_native_tag(state, ASYNC_TERMINAL_STATE_TAG)) return NULL; + return (async_terminal_state_t *)js_get_native_ptr(state); +} + +static inline int async_terminal_mode(ant_value_t state) { + async_terminal_state_t *st = async_terminal_state(state); + return st ? st->mode : ASYNC_TERM_TOARRAY; +} + +static inline double async_terminal_index(ant_value_t state) { + async_terminal_state_t *st = async_terminal_state(state); + return st ? st->index : 0; +} + +static inline bool async_terminal_has_acc(ant_value_t state) { + async_terminal_state_t *st = async_terminal_state(state); + return st && st->has_acc; +} + +static ant_value_t async_terminal_on_reject(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + js_reject_promise(js, promise, nargs > 0 ? args[0] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t async_terminal_on_callback_reject(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + async_terminal_close_and_reject(js, state, nargs > 0 ? args[0] : js_mkundef()); + return js_mkundef(); +} + +static ant_value_t async_terminal_on_callback(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t result = nargs > 0 ? args[0] : js_mkundef(); + return async_terminal_finish_callback(js, state, result); +} + +static ant_value_t async_terminal_on_close(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + js_resolve_promise(js, promise, js_get_slot(state, SLOT_AUX)); + return js_mkundef(); +} + +static ant_value_t async_terminal_on_close_reject(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + js_reject_promise(js, promise, js_get_slot(state, SLOT_AUX)); + return js_mkundef(); +} + +static void async_terminal_close_and_resolve(ant_t *js, ant_value_t state, ant_value_t value) { + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + ant_value_t iter = js_get_slot(state, SLOT_DATA); + js_set_slot_wb(js, state, SLOT_AUX, value); + + bool missing = false; + ant_value_t result = async_iter_call_method(js, iter, "return", NULL, 0, &missing); + if (missing) { + js_resolve_promise(js, promise, value); + return; + } + + if (is_err(result)) { + js_reject_promise(js, promise, result); + return; + } + + if (vtype(result) == T_PROMISE) { + ant_value_t on_resolve = js_heavy_mkfun(js, async_terminal_on_close, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_terminal_on_reject, state); + ant_value_t then_result = js_promise_then(js, result, on_resolve, on_reject); + promise_mark_handled(then_result); + return; + } + + js_resolve_promise(js, promise, value); +} + +static void async_terminal_close_and_reject(ant_t *js, ant_value_t state, ant_value_t reason) { + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + ant_value_t iter = js_get_slot(state, SLOT_DATA); + js_set_slot_wb(js, state, SLOT_AUX, reason); + + bool missing = false; + ant_value_t result = async_iter_call_method(js, iter, "return", NULL, 0, &missing); + if (missing) { + js_reject_promise(js, promise, reason); + return; + } + + if (is_err(result)) { + js_reject_promise(js, promise, result); + return; + } + + if (vtype(result) == T_PROMISE) { + ant_value_t on_resolve = js_heavy_mkfun(js, async_terminal_on_close_reject, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_terminal_on_reject, state); + ant_value_t then_result = js_promise_then(js, result, on_resolve, on_reject); + promise_mark_handled(then_result); + return; + } + + js_reject_promise(js, promise, reason); +} + +static bool async_terminal_apply_callback_result(ant_t *js, ant_value_t state, ant_value_t result) { + int mode = async_terminal_mode(state); + + if (mode == ASYNC_TERM_REDUCE) { + js_set_slot_wb(js, state, SLOT_SET, result); + async_terminal_state_t *st = async_terminal_state(state); + if (st) st->has_acc = true; + return true; + } + + switch (mode) { + case ASYNC_TERM_EVERY: + if (!js_truthy(js, result)) { + async_terminal_close_and_resolve(js, state, js_false); + return false; + } + return true; + case ASYNC_TERM_SOME: + if (js_truthy(js, result)) { + async_terminal_close_and_resolve(js, state, js_true); + return false; + } + return true; + case ASYNC_TERM_FIND: + if (js_truthy(js, result)) { + async_terminal_close_and_resolve(js, state, js_get_slot(state, SLOT_AUX)); + return false; + } + return true; + default: + return true; + } +} + +static bool async_terminal_handle_step(ant_t *js, ant_value_t state, ant_value_t step) { + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + + if (!is_object_type(step)) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "iterator result is not an object")); + return false; + } + + int mode = async_terminal_mode(state); + ant_value_t done = js_getprop_fallback(js, step, "done"); + ant_value_t value = js_getprop_fallback(js, step, "value"); + + if (js_truthy(js, done)) { + switch (mode) { + case ASYNC_TERM_EVERY: js_resolve_promise(js, promise, js_true); break; + case ASYNC_TERM_SOME: js_resolve_promise(js, promise, js_false); break; + + case ASYNC_TERM_FIND: js_resolve_promise(js, promise, js_mkundef()); break; + case ASYNC_TERM_FOREACH: js_resolve_promise(js, promise, js_mkundef()); break; + + case ASYNC_TERM_REDUCE: + if (!async_terminal_has_acc(state)) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "reduce of empty iterator with no initial value")); + } else js_resolve_promise(js, promise, js_get_slot(state, SLOT_SET)); + break; + + default: + js_resolve_promise(js, promise, js_get_slot(state, SLOT_ENTRIES)); + break; + } + + return false; + } + + double index = async_terminal_index(state); + async_terminal_state_t *st = async_terminal_state(state); + if (st) st->index = index + 1; + + if (mode == ASYNC_TERM_TOARRAY) { + js_arr_push(js, js_get_slot(state, SLOT_ENTRIES), value); + return true; + } + + ant_value_t fn = js_get_slot(state, SLOT_MAP); + if (!is_callable(fn)) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "callback is not callable")); + return false; + } + + if (mode == ASYNC_TERM_REDUCE) { + if (!async_terminal_has_acc(state)) { + js_set_slot_wb(js, state, SLOT_SET, value); + if (st) st->has_acc = true; + return true; + } + + ant_value_t call_args[3] = { js_get_slot(state, SLOT_SET), value, js_mknum(index) }; + ant_value_t next_acc = sv_vm_call(js->vm, js, fn, js_mkundef(), call_args, 3, NULL, false); + if (is_err(next_acc)) { + async_terminal_close_and_reject(js, state, next_acc); + return false; + } + + if (vtype(next_acc) == T_PROMISE) { + ant_value_t on_resolve = js_heavy_mkfun(js, async_terminal_on_callback, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_terminal_on_callback_reject, state); + ant_value_t then_result = js_promise_then(js, next_acc, on_resolve, on_reject); + promise_mark_handled(then_result); + return false; + } + return async_terminal_apply_callback_result(js, state, next_acc); + } + + ant_value_t result = call_indexed_callback(js, fn, value, (double)index); + if (is_err(result)) { + async_terminal_close_and_reject(js, state, result); + return false; + } + + js_set_slot_wb(js, state, SLOT_AUX, value); + if (vtype(result) == T_PROMISE) { + ant_value_t on_resolve = js_heavy_mkfun(js, async_terminal_on_callback, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_terminal_on_callback_reject, state); + ant_value_t then_result = js_promise_then(js, result, on_resolve, on_reject); + promise_mark_handled(then_result); + return false; + } + + return async_terminal_apply_callback_result(js, state, result); +} + +static ant_value_t async_terminal_on_step(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t step = nargs > 0 ? args[0] : js_mkundef(); + if (async_terminal_handle_step(js, state, step)) return async_terminal_advance(js, state); + return js_mkundef(); +} + +static ant_value_t async_terminal_finish_callback( + ant_t *js, + ant_value_t state, + ant_value_t result +) { + if (vtype(result) == T_PROMISE) { + ant_value_t on_resolve = js_heavy_mkfun(js, async_terminal_on_callback, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_terminal_on_callback_reject, state); + ant_value_t then_result = js_promise_then(js, result, on_resolve, on_reject); + promise_mark_handled(then_result); + return js_mkundef(); + } + + if (async_terminal_apply_callback_result(js, state, result)) return async_terminal_advance(js, state); + return js_mkundef(); +} + +static ant_value_t async_terminal_advance(ant_t *js, ant_value_t state) { +for (;;) { + ant_value_t iter = js_get_slot(state, SLOT_DATA); + ant_value_t promise = js_get_slot(state, SLOT_CTOR); + bool missing = false; + ant_value_t next_result = async_iter_call_method(js, iter, "next", NULL, 0, &missing); + + if (missing) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "object is not async iterable")); + return js_mkundef(); + } + + if (is_err(next_result)) { + js_reject_promise(js, promise, next_result); + return js_mkundef(); + } + + if (vtype(next_result) == T_PROMISE) { + ant_value_t on_resolve = js_heavy_mkfun(js, async_terminal_on_step, state); + ant_value_t on_reject = js_heavy_mkfun(js, async_terminal_on_reject, state); + ant_value_t then_result = js_promise_then(js, next_result, on_resolve, on_reject); + promise_mark_handled(then_result); + return js_mkundef(); + } + + if (!async_terminal_handle_step(js, state, next_result)) return js_mkundef(); +}} + +static ant_value_t async_iter_terminal(ant_t *js, ant_value_t *args, int nargs, int mode) { + if (mode != ASYNC_TERM_TOARRAY && (nargs < 1 || !is_callable(args[0]))) + return js_mkerr_typed(js, JS_ERR_TYPE, "AsyncIterator helper requires a callable"); + + ant_value_t iter = get_async_source_iter(js); + if (is_err(iter)) return rejected_promise(js, iter); + + ant_value_t promise = js_mkpromise(js); + ant_value_t state = js_mkobj(js); + async_terminal_state_t *st = calloc(1, sizeof(*st)); + + if (!st) { + js_reject_promise(js, promise, js_mkerr(js, "out of memory")); + return promise; + } + + st->mode = mode; + st->index = 0; + st->has_acc = (mode == ASYNC_TERM_REDUCE && nargs > 1); + + js_set_native_tag(state, ASYNC_TERMINAL_STATE_TAG); + js_set_native_ptr(state, st); + js_set_finalizer(state, async_terminal_state_finalize); + + js_set_slot_wb(js, state, SLOT_DATA, iter); + js_set_slot_wb(js, state, SLOT_CTOR, promise); + js_set_slot_wb(js, state, SLOT_MAP, mode == ASYNC_TERM_TOARRAY ? js_mkundef() : args[0]); + js_set_slot_wb(js, state, SLOT_ENTRIES, js_mkarr(js)); + js_set_slot_wb(js, state, SLOT_SET, (mode == ASYNC_TERM_REDUCE && nargs > 1) ? args[1] : js_mkundef()); + js_set_slot(state, SLOT_AUX, js_mkundef()); + + async_terminal_advance(js, state); + return promise; +} + +static ant_value_t async_iter_every(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_terminal(js, args, nargs, ASYNC_TERM_EVERY); +} + +static ant_value_t async_iter_some(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_terminal(js, args, nargs, ASYNC_TERM_SOME); +} + +static ant_value_t async_iter_find(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_terminal(js, args, nargs, ASYNC_TERM_FIND); +} + +static ant_value_t async_iter_forEach(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_terminal(js, args, nargs, ASYNC_TERM_FOREACH); +} + +static ant_value_t async_iter_reduce(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_terminal(js, args, nargs, ASYNC_TERM_REDUCE); +} + +static ant_value_t async_iter_toArray(ant_t *js, ant_value_t *args, int nargs) { + return async_iter_terminal(js, args, nargs, ASYNC_TERM_TOARRAY); +} + void init_iterator_module(void) { ant_t *js = rt->js; ant_value_t g = js_glob(js); @@ -408,9 +1260,52 @@ void init_iterator_module(void) { js_mkprop_fast(js, ctor_obj, "prototype", 9, iter_proto); js_mkprop_fast(js, ctor_obj, "name", 4, js_mkstr(js, "Iterator", 8)); js_set_descriptor(js, ctor_obj, "name", 4, 0); - js_set(js, ctor_obj, "from", js_mkfun(iter_from)); - + ant_value_t ctor = js_obj_to_func(ctor_obj); + js_set(js, ctor, "from", js_mkfun(iter_from)); js_set(js, iter_proto, "constructor", ctor); js_set(js, g, "Iterator", ctor); + + ant_value_t async_iter_proto = js_mkobj(js); + js->sym.async_iterator_proto = async_iter_proto; + js_set_proto_init(async_iter_proto, js->sym.object_proto); + js_set_sym(js, async_iter_proto, get_asyncIterator_sym(), js_mkfun(sym_this_cb)); + js_set_sym(js, async_iter_proto, get_toStringTag_sym(), js_mkstr(js, "AsyncIterator", 13)); + + ant_value_t async_ctor_obj = js_mkobj(js); + js_set_slot(async_ctor_obj, SLOT_CFUNC, js_mkfun(async_iter_ctor)); + js_mkprop_fast(js, async_ctor_obj, "prototype", 9, async_iter_proto); + js_mkprop_fast(js, async_ctor_obj, "name", 4, js_mkstr(js, "AsyncIterator", 13)); + js_set_descriptor(js, async_ctor_obj, "name", 4, 0); + + ant_value_t async_ctor = js_obj_to_func(async_ctor_obj); + js_set(js, async_iter_proto, "constructor", async_ctor); + js_set(js, g, "AsyncIterator", async_ctor); + + g_async_wrap_iter_proto = js_mkobj(js); + js_set_proto_init(g_async_wrap_iter_proto, async_iter_proto); + js_set(js, g_async_wrap_iter_proto, "next", js_mkfun(async_wrap_next)); + js_set(js, g_async_wrap_iter_proto, "return", js_mkfun(async_wrap_return)); + js_set(js, g_async_wrap_iter_proto, "throw", js_mkfun(async_wrap_throw)); +} + +void init_async_iterator_helpers(void) { + ant_t *js = rt->js; + ant_value_t g = js_glob(js); + + ant_value_t ctor = js_get(js, g, "AsyncIterator"); + ant_value_t proto = js->sym.async_iterator_proto; + + js_set(js, ctor, "from", js_mkfun(async_iter_from)); + js_set(js, proto, "map", js_mkfun(async_iter_map)); + js_set(js, proto, "filter", js_mkfun(async_iter_filter)); + js_set(js, proto, "take", js_mkfun(async_iter_take)); + js_set(js, proto, "drop", js_mkfun(async_iter_drop)); + js_set(js, proto, "flatMap", js_mkfun(async_iter_flatMap)); + js_set(js, proto, "every", js_mkfun(async_iter_every)); + js_set(js, proto, "some", js_mkfun(async_iter_some)); + js_set(js, proto, "find", js_mkfun(async_iter_find)); + js_set(js, proto, "forEach", js_mkfun(async_iter_forEach)); + js_set(js, proto, "reduce", js_mkfun(async_iter_reduce)); + js_set(js, proto, "toArray", js_mkfun(async_iter_toArray)); } diff --git a/src/modules/symbol.c b/src/modules/symbol.c index f8d9ad7..f61c5da 100644 --- a/src/modules/symbol.c +++ b/src/modules/symbol.c @@ -305,11 +305,13 @@ void init_symbol_module(void) { js->sym.array_iterator_proto = js_mkundef(); js->sym.string_iterator_proto = js_mkundef(); js->sym.generator_proto = js_mkundef(); + js->sym.async_iterator_proto = js_mkundef(); gc_register_root(&js->sym.iterator_proto); gc_register_root(&js->sym.array_iterator_proto); gc_register_root(&js->sym.string_iterator_proto); gc_register_root(&js->sym.generator_proto); + gc_register_root(&js->sym.async_iterator_proto); #define INIT_SYM(name, desc) g_##name = js_mksym_well_known(js, desc); WELLKNOWN_SYMBOLS(INIT_SYM) @@ -380,6 +382,7 @@ void gc_mark_symbols(ant_t *js, gc_mark_fn mark) { mark(js, js->sym.array_iterator_proto); mark(js, js->sym.string_iterator_proto); mark(js, js->sym.generator_proto); + mark(js, js->sym.async_iterator_proto); #define GC_SYM(name, _desc) mark(js, g_##name); WELLKNOWN_SYMBOLS(GC_SYM) diff --git a/src/silver/ast.c b/src/silver/ast.c index b0b08c7..bc00051 100644 --- a/src/silver/ast.c +++ b/src/silver/ast.c @@ -210,6 +210,10 @@ static inline bool is_private_ident_like_tok(uint8_t tok) { return tok >= TOK_IDENTIFIER && tok < TOK_IDENT_LIKE_END; } +static inline bool is_using_tok(P) { + return TOK == TOK_IDENTIFIER && TLEN == 5 && memcmp(tok_str(p), "using", 5) == 0; +} + static inline bool sv_strict_forbidden_binding_ident(const char *s, uint32_t len) { return is_eval_or_arguments_name(s, len) || is_strict_reserved_name(s, len); } @@ -1172,6 +1176,12 @@ static sv_ast_t *parse_unary(P) { n->flags = 1; return n; } + if (la == TOK_THROW) { + CONSUME(); + sv_ast_t *n = mk(N_THROW); + n->right = parse_assign(p); + return n; + } return parse_postfix(p); } @@ -1524,11 +1534,10 @@ static sv_ast_t *parse_var_decl(P, sv_var_kind_t kind, bool allow_uninit_const) sv_strict_check_binding_ident(p, decl->left->str, decl->left->len); CONSUME(); } - if (NEXT() == TOK_ASSIGN) { CONSUME(); decl->right = parse_assign(p); - } else if (kind == SV_VAR_CONST && !allow_uninit_const) { + } else if ((kind == SV_VAR_CONST || kind == SV_VAR_USING || kind == SV_VAR_AWAIT_USING) && !allow_uninit_const) { SV_MKERR_TYPED(JS, JS_ERR_SYNTAX, "Missing initializer in const declaration"); } sv_ast_list_push(&var->args, decl); @@ -1871,6 +1880,27 @@ static sv_ast_t *parse_stmt(P) { [TOK_IMPORT] = &&l_import, }; + if (is_using_tok(p)) { + CONSUME(); + sv_ast_t *n = parse_var_decl(p, SV_VAR_USING, false); + if (NEXT() == TOK_SEMICOLON) CONSUME(); + return n; + } + + if (TOK == TOK_AWAIT) { + sv_lexer_state_t saved; + sv_lexer_save_state(&p->lx, &saved); + CONSUME(); + NEXT(); + if (is_using_tok(p)) { + CONSUME(); + sv_ast_t *n = parse_var_decl(p, SV_VAR_AWAIT_USING, false); + if (NEXT() == TOK_SEMICOLON) CONSUME(); + return n; + } + sv_lexer_restore_state(&p->lx, &saved); + } + if (TOK < TOK_MAX && dispatch[TOK]) goto *dispatch[TOK]; goto l_expr_stmt; @@ -1944,7 +1974,25 @@ static sv_ast_t *parse_stmt(P) { sv_ast_t *init_node = NULL; NEXT(); - if (TOK == TOK_VAR || TOK == TOK_LET || TOK == TOK_CONST) { + if (TOK == TOK_AWAIT) { + sv_lexer_state_t saved; + sv_lexer_save_state(&p->lx, &saved); + CONSUME(); + NEXT(); + if (is_using_tok(p)) { + CONSUME(); + p->no_in = true; + init_node = parse_var_decl(p, SV_VAR_AWAIT_USING, true); + p->no_in = false; + } else sv_lexer_restore_state(&p->lx, &saved); + } + + if (!init_node && is_using_tok(p)) { + CONSUME(); + p->no_in = true; + init_node = parse_var_decl(p, SV_VAR_USING, true); + p->no_in = false; + } else if (!init_node && (TOK == TOK_VAR || TOK == TOK_LET || TOK == TOK_CONST)) { sv_var_kind_t kind = ( TOK == TOK_VAR) ? SV_VAR_VAR : (TOK == TOK_LET) ? SV_VAR_LET : SV_VAR_CONST; @@ -1952,7 +2000,7 @@ static sv_ast_t *parse_stmt(P) { p->no_in = true; init_node = parse_var_decl(p, kind, true); p->no_in = false; - } else if (TOK != TOK_SEMICOLON) { + } else if (!init_node && TOK != TOK_SEMICOLON) { p->no_in = true; init_node = parse_expr(p); p->no_in = false; @@ -1979,7 +2027,10 @@ static sv_ast_t *parse_stmt(P) { return n; } - if (init_node && init_node->type == N_VAR && init_node->var_kind == SV_VAR_CONST) { + if (init_node && init_node->type == N_VAR && ( + init_node->var_kind == SV_VAR_CONST || + init_node->var_kind == SV_VAR_USING || + init_node->var_kind == SV_VAR_AWAIT_USING)) { for (int i = 0; i < init_node->args.count; i++) { sv_ast_t *decl = init_node->args.items[i]; if (decl && decl->type == N_VARDECL && !decl->right) { diff --git a/src/silver/compile_ctx.c b/src/silver/compile_ctx.c index 33df2e5..2d95994 100644 --- a/src/silver/compile_ctx.c +++ b/src/silver/compile_ctx.c @@ -41,6 +41,7 @@ void sv_compile_ctx_init_root( ctx->strict_args_local = -1; ctx->new_target_local = -1; ctx->super_local = -1; + ctx->using_stack_local = -1; ctx->line_table = line_table; } @@ -65,6 +66,7 @@ void sv_compile_ctx_init_child( ctx->strict_args_local = -1; ctx->new_target_local = -1; ctx->super_local = -1; + ctx->using_stack_local = -1; ctx->param_count = node ? node->args.count : 0; } @@ -79,6 +81,7 @@ void sv_compile_ctx_cleanup(sv_compiler_t *ctx) { free(ctx->srcpos); free(ctx->slot_types); free(ctx->deferred_exports); + free(ctx->using_cleanups); const_dedup_entry_t *entry; const_dedup_entry_t *tmp; diff --git a/src/silver/compiler.c b/src/silver/compiler.c index 5e9af58..788e317 100644 --- a/src/silver/compiler.c +++ b/src/silver/compiler.c @@ -1164,7 +1164,10 @@ static void hoist_lexical_decls(sv_compiler_t *c, sv_ast_list_t *stmts) { if (!decl_node) continue; if (decl_node->type == N_VAR && decl_node->var_kind != SV_VAR_VAR) { - bool is_const = (decl_node->var_kind == SV_VAR_CONST); + bool is_const = + (decl_node->var_kind == SV_VAR_CONST || + decl_node->var_kind == SV_VAR_USING || + decl_node->var_kind == SV_VAR_AWAIT_USING); int lb = c->local_count; for (int j = 0; j < decl_node->args.count; j++) { sv_ast_t *decl = decl_node->args.items[j]; @@ -1505,6 +1508,11 @@ void compile_expr(sv_compiler_t *c, sv_ast_t *node) { } break; + case N_THROW: + compile_expr(c, node->right); + emit_op(c, OP_THROW); + break; + case N_TAGGED_TEMPLATE: { compile_expr(c, node->left); sv_ast_t *tpl = node->right; @@ -2777,32 +2785,67 @@ static void compile_destructure_pattern( if (consume_source) emit_op(c, OP_POP); } -void compile_array_destructure(sv_compiler_t *c, sv_ast_t *pat, - bool keep) { +void compile_array_destructure(sv_compiler_t *c, sv_ast_t *pat, bool keep) { compile_destructure_pattern(c, pat, keep, true, DESTRUCTURE_ASSIGN, SV_VAR_LET); } -void compile_object_destructure(sv_compiler_t *c, sv_ast_t *pat, - bool keep) { +void compile_object_destructure(sv_compiler_t *c, sv_ast_t *pat, bool keep) { compile_destructure_pattern(c, pat, keep, true, DESTRUCTURE_ASSIGN, SV_VAR_LET); } static bool is_tail_callable(sv_compiler_t *c, sv_ast_t *node) { if (c->try_depth > 0) return false; + if (c->using_cleanup_count > 0) return false; if (node->type != N_CALL) return false; if (call_has_spread_arg(node)) return false; + sv_ast_t *callee = node->left; - if (callee->type == N_IDENT && callee->len == 5 && memcmp(callee->str, "super", 5) == 0) return false; + if ( + callee->type == N_IDENT && + callee->len == 5 && memcmp(callee->str, "super", 5) == 0 + ) return false; + return true; } +static void emit_using_dispose_call( + sv_compiler_t *c, + int stack_local, + int completion_local, + bool is_async, + bool suppressed_completion +) { + emit_get_local(c, stack_local); + if (suppressed_completion) { + emit_get_local(c, completion_local); + } + if (is_async && suppressed_completion) emit_op(c, OP_USING_DISPOSE_ASYNC_SUPPRESSED); + else if (is_async) emit_op(c, OP_USING_DISPOSE_ASYNC); + else if (suppressed_completion) emit_op(c, OP_USING_DISPOSE_SUPPRESSED); + else emit_op(c, OP_USING_DISPOSE); + if (is_async) emit_op(c, OP_AWAIT); +} + +static void emit_using_cleanups_to_depth(sv_compiler_t *c, int target_depth) { +for (int i = c->using_cleanup_count - 1; i >= 0; i--) { + sv_using_cleanup_t *cleanup = &c->using_cleanups[i]; + if (cleanup->scope_depth <= target_depth) break; + emit_using_dispose_call(c, cleanup->stack_local, -1, cleanup->is_async, false); + emit_op(c, OP_POP); +}} + +static void emit_return_from_stack(sv_compiler_t *c) { + emit_using_cleanups_to_depth(c, -1); + emit_close_upvals(c); + emit_op(c, OP_RETURN); +} + static void compile_tail_call(sv_compiler_t *c, sv_ast_t *node) { sv_ast_t *callee = node->left; if (callee->type == N_OPTIONAL) { compile_call(c, node); - emit_close_upvals(c); - emit_op(c, OP_RETURN); + emit_return_from_stack(c); return; } @@ -2826,10 +2869,10 @@ void compile_tail_return_expr(sv_compiler_t *c, sv_ast_t *expr) { return; } - if (expr->type == N_AWAIT && c->is_async && c->try_depth == 0 && !c->is_tla) { + if (expr->type == N_AWAIT && c->is_async && c->try_depth == 0 && !c->is_tla && + c->using_cleanup_count == 0) { compile_expr(c, expr->right); - emit_close_upvals(c); - emit_op(c, OP_RETURN); + emit_return_from_stack(c); return; } @@ -2839,13 +2882,120 @@ void compile_tail_return_expr(sv_compiler_t *c, sv_ast_t *expr) { } compile_expr(c, expr); - emit_close_upvals(c); - emit_op(c, OP_RETURN); + emit_return_from_stack(c); } void compile_stmts(sv_compiler_t *c, sv_ast_list_t *list) { - for (int i = 0; i < list->count; i++) - compile_stmt(c, list->items[i]); + for (int i = 0; i < list->count; i++) compile_stmt(c, list->items[i]); +} + +static bool stmt_list_has_using_decl(sv_ast_list_t *list, bool *has_await_using) { + bool found = false; + for (int i = 0; i < list->count; i++) { + sv_ast_t *node = list->items[i]; + if (!node) continue; + + sv_ast_t *decl = (node->type == N_EXPORT) ? node->left : node; + if (!decl || decl->type != N_VAR) continue; + + if (decl->var_kind == SV_VAR_USING || decl->var_kind == SV_VAR_AWAIT_USING) { + if (decl->var_kind == SV_VAR_AWAIT_USING && has_await_using) *has_await_using = true; + found = true; + } + } + + return found; +} + +static void emit_empty_disposal_stack(sv_compiler_t *c) { + emit_op(c, OP_ARRAY); + emit_u16(c, 0); +} + +static void emit_using_push(sv_compiler_t *c, bool is_async) { + emit_op(c, is_async ? OP_USING_PUSH_ASYNC : OP_USING_PUSH); +} + +static void pop_using_cleanup(sv_compiler_t *c) { + if (c->using_cleanup_count > 0) c->using_cleanup_count--; +} + +static void emit_dispose_resource(sv_compiler_t *c, bool is_async) { + emit_op(c, is_async ? OP_DISPOSE_RESOURCE_ASYNC : OP_DISPOSE_RESOURCE); + if (is_async) emit_op(c, OP_AWAIT); +} + +static void push_using_cleanup( + sv_compiler_t *c, + int stack_local, + int scope_depth, + bool is_async +) { + if (c->using_cleanup_count >= c->using_cleanup_cap) { + int cap = c->using_cleanup_cap ? c->using_cleanup_cap * 2 : 4; + c->using_cleanups = realloc(c->using_cleanups, (size_t)cap * sizeof(sv_using_cleanup_t)); + c->using_cleanup_cap = cap; + } + + c->using_cleanups[c->using_cleanup_count++] = (sv_using_cleanup_t){ + .stack_local = stack_local, + .scope_depth = scope_depth, + .is_async = is_async, + }; +} + +static void compile_block_with_using(sv_compiler_t *c, sv_ast_t *node) { + bool has_await_using = false; + bool has_using = stmt_list_has_using_decl(&node->args, &has_await_using); + + begin_scope(c); + hoist_lexical_decls(c, &node->args); + hoist_func_decls(c, &node->args); + + if (!has_using) { + compile_stmts(c, &node->args); + end_scope(c); + return; + } + + emit_empty_disposal_stack(c); + int stack_local = add_local(c, "", 0, false, c->scope_depth); + + emit_put_local(c, stack_local); + int err_local = add_local(c, "", 0, false, c->scope_depth); + + int old_using_stack = c->using_stack_local; + bool old_using_async = c->using_stack_async; + + c->using_stack_local = stack_local; + c->using_stack_async = has_await_using; + push_using_cleanup(c, stack_local, c->scope_depth, has_await_using); + + c->try_depth++; + int try_jump = emit_jump(c, OP_TRY_PUSH); + compile_stmts(c, &node->args); + emit_op(c, OP_TRY_POP); + c->try_depth--; + + emit_using_dispose_call(c, stack_local, -1, has_await_using, false); + emit_op(c, OP_POP); + int end_jump = emit_jump(c, OP_JMP); + + patch_jump(c, try_jump); + int catch_tag = emit_jump(c, OP_CATCH); + + emit_put_local(c, err_local); + emit_using_dispose_call(c, stack_local, err_local, has_await_using, true); + + if (!has_await_using) emit_op(c, OP_THROW); + patch_jump(c, catch_tag); + patch_jump(c, end_jump); + + c->using_stack_local = old_using_stack; + c->using_stack_async = old_using_async; + + pop_using_cleanup(c); + end_scope(c); } void compile_stmt(sv_compiler_t *c, sv_ast_t *node) { @@ -2858,11 +3008,7 @@ void compile_stmt(sv_compiler_t *c, sv_ast_t *node) { break; case N_BLOCK: - begin_scope(c); - hoist_lexical_decls(c, &node->args); - hoist_func_decls(c, &node->args); - compile_stmts(c, &node->args); - end_scope(c); + compile_block_with_using(c, node); break; case N_VAR: @@ -2910,8 +3056,8 @@ void compile_stmt(sv_compiler_t *c, sv_ast_t *node) { if (node->right) { compile_tail_return_expr(c, node->right); } else { - emit_close_upvals(c); - emit_op(c, OP_RETURN_UNDEF); + emit_op(c, OP_UNDEF); + emit_return_from_stack(c); } break; @@ -3187,7 +3333,9 @@ void compile_export_decl(sv_compiler_t *c, sv_ast_t *node) { void compile_var_decl(sv_compiler_t *c, sv_ast_t *node) { sv_var_kind_t kind = node->var_kind; - bool is_const = (kind == SV_VAR_CONST); + bool is_using = (kind == SV_VAR_USING || kind == SV_VAR_AWAIT_USING); + bool is_await_using = (kind == SV_VAR_AWAIT_USING); + bool is_const = (kind == SV_VAR_CONST || is_using); bool repl_top = is_repl_top_level(c); for (int i = 0; i < node->args.count; i++) { @@ -3237,6 +3385,17 @@ void compile_var_decl(sv_compiler_t *c, sv_ast_t *node) { if (decl->right || !is_const) { emit_put_local_typed(c, idx, init_type); c->locals[idx].is_tdz = false; + if (is_using) { + if (c->using_stack_local >= 0) { + emit_get_local(c, c->using_stack_local); + emit_get_local(c, idx); + emit_using_push(c, is_await_using); + } else { + emit_get_local(c, idx); + emit_dispose_resource(c, is_await_using); + } + emit_op(c, OP_POP); + } } } else { if (decl->right) { @@ -3517,7 +3676,9 @@ static void compile_for_each_assign_target(sv_compiler_t *c, sv_ast_t *lhs) { if (target->type == N_IDENT) { int loc = resolve_local(c, target->str, target->len); if (loc == -1) { - bool is_const = (lhs->var_kind == SV_VAR_CONST); + bool is_const = (lhs->var_kind == SV_VAR_CONST || + lhs->var_kind == SV_VAR_USING || + lhs->var_kind == SV_VAR_AWAIT_USING); loc = add_local(c, target->str, target->len, is_const, c->scope_depth); } emit_put_local(c, loc); @@ -3538,6 +3699,37 @@ static void compile_for_each_assign_target(sv_compiler_t *c, sv_ast_t *lhs) { compile_lhs_set(c, lhs, false); } +static void compile_using_dispose_target(sv_compiler_t *c, sv_ast_t *lhs) { + if (!lhs || lhs->type != N_VAR) return; + bool is_await_using = lhs->var_kind == SV_VAR_AWAIT_USING; + if (lhs->var_kind != SV_VAR_USING && !is_await_using) return; + if (lhs->args.count == 0) return; + + sv_ast_t *decl = lhs->args.items[0]; + if (!decl || decl->type != N_VARDECL || !decl->left || decl->left->type != N_IDENT) return; + + emit_get_var(c, decl->left->str, decl->left->len); + emit_dispose_resource(c, is_await_using); + emit_op(c, OP_POP); +} + +static bool compile_using_push_target(sv_compiler_t *c, sv_ast_t *lhs, int stack_local) { + if (!lhs || lhs->type != N_VAR) return false; + bool is_await_using = lhs->var_kind == SV_VAR_AWAIT_USING; + if (lhs->var_kind != SV_VAR_USING && !is_await_using) return false; + if (lhs->args.count == 0) return false; + + sv_ast_t *decl = lhs->args.items[0]; + if (!decl || decl->type != N_VARDECL || !decl->left || decl->left->type != N_IDENT) return false; + + emit_get_local(c, stack_local); + emit_get_var(c, decl->left->str, decl->left->len); + emit_using_push(c, is_await_using); + emit_op(c, OP_POP); + + return true; +} + static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { begin_scope(c); @@ -3547,7 +3739,10 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { if (node->left && node->left->type == N_VAR && node->left->var_kind != SV_VAR_VAR) { - bool is_const = (node->left->var_kind == SV_VAR_CONST); + bool is_const = + (node->left->var_kind == SV_VAR_CONST || + node->left->var_kind == SV_VAR_USING || + node->left->var_kind == SV_VAR_AWAIT_USING); int lb = c->local_count; for (int i = 0; i < node->left->args.count; i++) { sv_ast_t *decl = node->left->args.items[i]; @@ -3588,12 +3783,24 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { int iter_err_local = -1; int break_close_slot = -1; int iter_inner_start = -1; + int using_stack_local = -1; bool is_for_await = (node->type == N_FOR_AWAIT_OF); + bool is_using_loop = node->left && node->left->type == N_VAR && + (node->left->var_kind == SV_VAR_USING || node->left->var_kind == SV_VAR_AWAIT_USING); + bool is_await_using_loop = is_using_loop && node->left->var_kind == SV_VAR_AWAIT_USING; + int old_using_stack = c->using_stack_local; + bool old_using_async = c->using_stack_async; uint8_t iter_hint = 0; if (is_for_of && !is_for_await) iter_hint = iter_hint_for_type(infer_expr_type(c, node->right)); + if (is_using_loop) { + emit_empty_disposal_stack(c); + using_stack_local = add_local(c, "", 0, false, c->scope_depth); + emit_put_local(c, using_stack_local); + } + compile_expr(c, node->right); if (is_for_of) { emit_op(c, is_for_await ? OP_FOR_AWAIT_OF : OP_FOR_OF); @@ -3613,6 +3820,11 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { int loop_start = c->code_len; push_loop(c, loop_start, NULL, 0, false); + if (is_using_loop) { + emit_empty_disposal_stack(c); + emit_put_local(c, using_stack_local); + } + if (is_for_of) { if (is_for_await) { emit_op(c, OP_AWAIT_ITER_NEXT); @@ -3648,12 +3860,27 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { } } - compile_stmt(c, node->body); + if (is_using_loop) { + c->using_stack_local = using_stack_local; + c->using_stack_async = is_await_using_loop; + push_using_cleanup(c, using_stack_local, c->scope_depth, is_await_using_loop); + compile_using_push_target(c, node->left, using_stack_local); + } + compile_stmt(c, node->body); sv_loop_t *loop = &c->loops[c->loop_count - 1]; for (int i = 0; i < loop->continues.count; i++) patch_jump(c, loop->continues.offsets[i]); + if (is_using_loop) { + emit_using_dispose_call(c, using_stack_local, -1, is_await_using_loop, false); + emit_op(c, OP_POP); + + c->using_stack_local = old_using_stack; + c->using_stack_async = old_using_async; + pop_using_cleanup(c); + } else compile_using_dispose_target(c, node->left); + if (iter_count > 0) { for (int i = 0; i < iter_count; i++) { int inner_idx = iter_inner_start + i; @@ -3694,7 +3921,13 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { patch_jump(c, try_jump_for_of); int catch_tag = emit_jump(c, OP_CATCH); - emit_put_local(c, iter_err_local); + emit_put_local(c, iter_err_local); + + if (is_using_loop) { + emit_using_dispose_call(c, using_stack_local, iter_err_local, is_await_using_loop, true); + emit_put_local(c, iter_err_local); + } + emit_op(c, OP_ITER_CLOSE); emit_get_local(c, iter_err_local); emit_op(c, OP_THROW); @@ -3734,40 +3967,39 @@ void compile_break(sv_compiler_t *c, sv_ast_t *node) { if (c->loop_count == 0) return; int target = c->loop_count - 1; - if (node->str) { - for (int i = c->loop_count - 1; i >= 0; i--) { - if (c->loops[i].label && - c->loops[i].label_len == node->len && - memcmp(c->loops[i].label, node->str, node->len) == 0) { - target = i; - break; - } - } - } + if (node->str) for (int i = c->loop_count - 1; i >= 0; i--) if ( + c->loops[i].label && + c->loops[i].label_len == node->len && + memcmp(c->loops[i].label, node->str, node->len) == 0 + ) { target = i; break; } emit_close_upvals_to_depth(c, c->loops[target].scope_depth); + emit_using_cleanups_to_depth(c, c->loops[target].scope_depth); + int offset = emit_jump(c, OP_JMP); patch_list_add(&c->loops[target].breaks, offset); } void compile_continue(sv_compiler_t *c, sv_ast_t *node) { - for (int i = c->loop_count - 1; i >= 0; i--) { - if (node->str) { + for (int i = c->loop_count - 1; i >= 0; i--) if (node->str) { if ( c->loops[i].label && c->loops[i].label_len == node->len && - memcmp(c->loops[i].label, node->str, node->len) == 0) { + memcmp(c->loops[i].label, node->str, node->len) == 0 + ) { emit_close_upvals_to_depth(c, c->loops[i].scope_depth); + emit_using_cleanups_to_depth(c, c->loops[i].scope_depth); patch_list_add(&c->loops[i].continues, emit_jump(c, OP_JMP)); return; } } else if (!c->loops[i].is_switch) { emit_close_upvals_to_depth(c, c->loops[i].scope_depth); + emit_using_cleanups_to_depth(c, c->loops[i].scope_depth); patch_list_add(&c->loops[i].continues, emit_jump(c, OP_JMP)); return; } -}} +} static void compile_finally_block(sv_compiler_t *c, sv_ast_t *finally_body) { int finally_jump = emit_jump(c, OP_FINALLY); @@ -4576,6 +4808,28 @@ sv_func_t *compile_function_body( emit_field_inits(&comp, enclosing->field_inits, enclosing->field_init_count); } + bool body_has_await_using = false; + bool body_has_using = node->body && node->body->type == N_BLOCK && + stmt_list_has_using_decl(&node->body->args, &body_has_await_using); + int body_using_try_jump = -1; + int body_using_err_local = -1; + int old_using_stack = comp.using_stack_local; + bool old_using_async = comp.using_stack_async; + + if (body_has_using) { + emit_empty_disposal_stack(&comp); + int body_using_stack = add_local(&comp, "", 0, false, comp.scope_depth); + emit_put_local(&comp, body_using_stack); + body_using_err_local = add_local(&comp, "", 0, false, comp.scope_depth); + + comp.using_stack_local = body_using_stack; + comp.using_stack_async = body_has_await_using; + push_using_cleanup(&comp, body_using_stack, comp.scope_depth, body_has_await_using); + + comp.try_depth++; + body_using_try_jump = emit_jump(&comp, OP_TRY_PUSH); + } + if (node->body) { if (node->body->type == N_BLOCK) { int last_expr_idx = -1; @@ -4584,16 +4838,12 @@ sv_func_t *compile_function_body( if (sv_ast_can_be_expression_statement(last)) last_expr_idx = node->body->args.count - 1; } - for (int i = 0; i < node->body->args.count; i++) { sv_ast_t *stmt = node->body->args.items[i]; if (i == last_expr_idx) { compile_expr(&comp, stmt); - emit_close_upvals(&comp); - emit_op(&comp, OP_RETURN); - } else { - compile_stmt(&comp, stmt); - } + emit_return_from_stack(&comp); + } else compile_stmt(&comp, stmt); } } else compile_tail_return_expr(&comp, node->body); } @@ -4602,7 +4852,29 @@ sv_func_t *compile_function_body( sv_deferred_export_t *e = &comp.deferred_exports[i]; compile_export_emit(&comp, e->name, e->len); } + + if (body_has_using) { + emit_op(&comp, OP_TRY_POP); + comp.try_depth--; + + emit_using_dispose_call(&comp, comp.using_stack_local, -1, body_has_await_using, false); + emit_op(&comp, OP_POP); + int end_jump = emit_jump(&comp, OP_JMP); + + patch_jump(&comp, body_using_try_jump); + int catch_tag = emit_jump(&comp, OP_CATCH); + emit_put_local(&comp, body_using_err_local); + emit_using_dispose_call(&comp, comp.using_stack_local, body_using_err_local, body_has_await_using, true); + if (!body_has_await_using) emit_op(&comp, OP_THROW); + patch_jump(&comp, catch_tag); + patch_jump(&comp, end_jump); + + pop_using_cleanup(&comp); + comp.using_stack_local = old_using_stack; + comp.using_stack_async = old_using_async; + } + emit_using_cleanups_to_depth(&comp, -1); emit_close_upvals(&comp); emit_op(&comp, OP_RETURN_UNDEF); diff --git a/src/silver/engine.c b/src/silver/engine.c index ea722a9..5276972 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -25,6 +25,7 @@ #include "ops/exceptions.h" #include "ops/async.h" #include "ops/iteration.h" +#include "ops/using.h" #include "ops/objects.h" #include "ops/coercion.h" @@ -1680,7 +1681,8 @@ ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant L_TRY_POP: { sv_op_try_pop(vm); NEXT(1); } L_CATCH: { sv_op_catch(vm, sv_err, ip); NEXT(5); } L_FINALLY: { VM_CHECK(sv_op_finally(vm, js, ip)); NEXT(5); } - + L_NIP_CATCH: { sv_op_nip_catch(vm); NEXT(1); } + L_FINALLY_RET: { uint8_t *resume_ip = NULL; ant_value_t completion = js_mkundef(); @@ -1712,7 +1714,15 @@ ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant DISPATCH(); } - L_NIP_CATCH: { sv_op_nip_catch(vm); NEXT(1); } + L_USING_PUSH: { VM_CHECK(sv_op_using_push(vm, js, false)); NEXT(1); } + L_USING_PUSH_ASYNC: { VM_CHECK(sv_op_using_push(vm, js, true)); NEXT(1); } + L_DISPOSE_RESOURCE: { VM_CHECK(sv_op_dispose_resource(vm, js, false)); NEXT(1); } + L_DISPOSE_RESOURCE_ASYNC: { VM_CHECK(sv_op_dispose_resource(vm, js, true)); NEXT(1); } + L_USING_DISPOSE: { VM_CHECK(sv_op_using_dispose(vm, js, false, false)); NEXT(1); } + L_USING_DISPOSE_ASYNC: { VM_CHECK(sv_op_using_dispose(vm, js, true, false)); NEXT(1); } + L_USING_DISPOSE_SUPPRESSED: { VM_CHECK(sv_op_using_dispose(vm, js, false, true)); NEXT(1); } + L_USING_DISPOSE_ASYNC_SUPPRESSED: { VM_CHECK(sv_op_using_dispose(vm, js, true, true)); NEXT(1); } + L_FOR_IN: { VM_CHECK(sv_op_for_in(vm, js)); NEXT(1); } L_FOR_OF: { VM_CHECK(sv_op_for_of(vm, js)); NEXT(1); } L_FOR_AWAIT_OF: { VM_CHECK(sv_op_for_await_of(vm, js)); NEXT(1); } diff --git a/src/silver/ops/using.h b/src/silver/ops/using.h new file mode 100644 index 0000000..1ee2998 --- /dev/null +++ b/src/silver/ops/using.h @@ -0,0 +1,410 @@ +#ifndef SV_USING_H +#define SV_USING_H + +#include "silver/engine.h" +#include "errors.h" +#include "gc/roots.h" +#include "modules/symbol.h" + +typedef enum { + SV_DISPOSAL_RECORD_DEFER = 0, + SV_DISPOSAL_RECORD_ADOPT = 1, + SV_DISPOSAL_RECORD_USE = 2 +} sv_disposal_record_kind_t; + +static inline void sv_using_array_clear(ant_value_t arr) { + ant_object_t *ptr = js_obj_ptr(js_as_obj(arr)); + if (ptr && ptr->type_tag == T_ARR) ptr->u.array.len = 0; +} + +static inline ant_value_t sv_make_suppressed_error_value( + ant_t *js, ant_value_t error, ant_value_t suppressed +) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, error); + GC_ROOT_PIN(js, suppressed); + + ant_value_t obj = js_mkobj(js); + GC_ROOT_PIN(js, obj); + if (is_err(obj)) { + GC_ROOT_RESTORE(js, root_mark); + return obj; + } + + ant_value_t proto = js_get_ctor_proto(js, "SuppressedError", 15); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + + js_mkprop_fast(js, obj, "error", 5, error); + js_mkprop_fast(js, obj, "suppressed", 10, suppressed); + js_mkprop_fast(js, obj, "message", 7, ANT_STRING("An error was suppressed during disposal.")); + js_mkprop_fast(js, obj, "name", 4, ANT_STRING("SuppressedError")); + js_set_slot(obj, SLOT_ERROR_BRAND, js_true); + js_capture_stack(js, obj); + + GC_ROOT_RESTORE(js, root_mark); + return obj; +} + +static inline ant_value_t sv_disposal_error_value(ant_t *js, ant_value_t result) { + if (js->thrown_exists) { + ant_value_t thrown = js->thrown_value; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); + js->thrown_exists = false; + return thrown; + } + + if (is_err(result)) { + if (vdata(result) != 0) return mkval(T_OBJ, vdata(result)); + return js_make_error_silent(js, JS_ERR_INTERNAL, "unknown disposal error"); + } + + return result; +} + +static inline ant_value_t sv_suppress_disposal_error( + ant_t *js, ant_value_t error, ant_value_t previous +) { + if (vtype(previous) == T_UNDEF) return error; + return sv_make_suppressed_error_value(js, error, previous); +} + +static inline ant_value_t sv_disposal_record_call(ant_t *js, ant_value_t record) { + ant_value_t kind_v = js_arr_get(js, record, 0); + ant_value_t value = js_arr_get(js, record, 1); + ant_value_t method = js_arr_get(js, record, 2); + + int kind = vtype(kind_v) == T_NUM + ? (int)js_getnum(kind_v) : -1; + + ant_value_t this_arg = kind == SV_DISPOSAL_RECORD_USE ? value : js_mkundef(); + ant_value_t arg = value; + + ant_value_t *args = kind == SV_DISPOSAL_RECORD_ADOPT ? &arg : NULL; + int nargs = kind == SV_DISPOSAL_RECORD_ADOPT ? 1 : 0; + + if (!is_callable(method)) + return js_mkerr_typed(js, JS_ERR_TYPE, "disposer is not callable"); + + if (vtype(method) == T_CFUNC) { + ant_value_t saved_this = js->this_val; + js->this_val = this_arg; + ant_value_t result = js_as_cfunc(method)(js, args, nargs); + js->this_val = saved_this; + return result; + } + + return sv_vm_call(js->vm, js, method, this_arg, args, nargs, NULL, false); +} + +static inline ant_value_t sv_dispose_resource(ant_t *js, ant_value_t resource, bool is_async) { + if (vtype(resource) == T_NULL || vtype(resource) == T_UNDEF) return js_mkundef(); + + ant_value_t method = js_get_sym(js, resource, is_async ? get_asyncDispose_sym() : get_dispose_sym()); + if (is_async && (vtype(method) == T_UNDEF || vtype(method) == T_NULL)) method = js_get_sym(js, resource, get_dispose_sym()); + + if (!is_callable(method)) return js_mkerr_typed( + js, JS_ERR_TYPE, is_async + ? "resource is not async disposable" + : "resource is not disposable" + ); + + if (vtype(method) == T_CFUNC) { + ant_value_t saved_this = js->this_val; + js->this_val = resource; + ant_value_t result = js_as_cfunc(method)(js, NULL, 0); + js->this_val = saved_this; + return result; + } + + return sv_vm_call(js->vm, js, method, resource, NULL, 0, NULL, false); +} + +static inline ant_value_t sv_using_push( + ant_t *js, ant_value_t entries, ant_value_t resource, bool is_async +) { + if (vtype(resource) == T_NULL || vtype(resource) == T_UNDEF) + return resource; + + if (vtype(entries) != T_ARR) + return js_mkerr_typed(js, JS_ERR_TYPE, "invalid using disposal stack"); + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, entries); + GC_ROOT_PIN(js, resource); + + ant_value_t method = js_get_sym(js, resource, is_async ? get_asyncDispose_sym() : get_dispose_sym()); + if (is_async && (vtype(method) == T_UNDEF || vtype(method) == T_NULL)) method = js_get_sym(js, resource, get_dispose_sym()); + + GC_ROOT_PIN(js, method); + if (!is_callable(method)) { + GC_ROOT_RESTORE(js, root_mark); + return js_mkerr_typed(js, JS_ERR_TYPE, "resource is not disposable"); + } + + ant_value_t record = js_mkarr(js); + GC_ROOT_PIN(js, record); + if (is_err(record)) { + GC_ROOT_RESTORE(js, root_mark); + return record; + } + + js_arr_push(js, record, js_mknum((double)SV_DISPOSAL_RECORD_USE)); + js_arr_push(js, record, resource); + js_arr_push(js, record, method); + js_arr_push(js, entries, record); + GC_ROOT_RESTORE(js, root_mark); + + return resource; +} + +static inline ant_value_t sv_using_dispose_sync( + ant_t *js, ant_value_t entries, ant_value_t completion, bool throw_completion +) { + if (vtype(entries) != T_ARR) return js_mkerr_typed(js, JS_ERR_TYPE, "invalid using disposal stack"); + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, entries); + GC_ROOT_PIN(js, completion); + + ant_offset_t len = js_arr_len(js, entries); + ant_value_t work = js_mkarr(js); + GC_ROOT_PIN(js, work); + + if (is_err(work)) { + GC_ROOT_RESTORE(js, root_mark); + return work; + } + + for (ant_offset_t i = 0; i < len; i++) { + GC_ROOT_SAVE(copy_mark, js); + ant_value_t record = js_arr_get(js, entries, i); + GC_ROOT_PIN(js, record); + js_arr_push(js, work, record); + GC_ROOT_RESTORE(js, copy_mark); + } + + sv_using_array_clear(entries); + len = js_arr_len(js, work); + + for (ant_offset_t i = len; i > 0; i--) { + GC_ROOT_SAVE(iter_mark, js); + ant_value_t record = js_arr_get(js, work, i - 1); + + GC_ROOT_PIN(js, record); + ant_value_t result = sv_disposal_record_call(js, record); + + if (is_err(result) || js->thrown_exists) { + ant_value_t error = sv_disposal_error_value(js, result); + GC_ROOT_PIN(js, error); + completion = sv_suppress_disposal_error(js, error, completion); + + if (is_err(completion)) { + GC_ROOT_RESTORE(js, root_mark); + return completion; + } + } + + GC_ROOT_RESTORE(js, iter_mark); + } + + if (throw_completion && vtype(completion) != T_UNDEF) { + ant_value_t thrown = js_throw(js, completion); + GC_ROOT_RESTORE(js, root_mark); + return thrown; + } + + GC_ROOT_RESTORE(js, root_mark); + return completion; +} + +static inline ant_value_t sv_async_dispose_continue( + ant_t *js, + ant_value_t state, + bool rejected, + ant_value_t reason +); + +static inline ant_value_t sv_async_dispose_on_fulfilled( + ant_t *js, + ant_value_t *args, + int nargs +) { + (void)args; (void)nargs; + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + return sv_async_dispose_continue(js, state, false, js_mkundef()); +} + +static inline ant_value_t sv_async_dispose_on_rejected( + ant_t *js, + ant_value_t *args, + int nargs +) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + return sv_async_dispose_continue(js, state, true, nargs > 0 ? args[0] : js_mkundef()); +} + +static inline ant_value_t sv_async_dispose_continue( + ant_t *js, + ant_value_t state, + bool rejected, + ant_value_t reason +) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, state); + GC_ROOT_PIN(js, reason); + + ant_value_t entries = js_get_slot(state, SLOT_ENTRIES); + ant_value_t result_promise = js_get_slot(state, SLOT_DATA); + ant_value_t completion = js_get_slot(state, SLOT_AUX); + GC_ROOT_PIN(js, entries); + GC_ROOT_PIN(js, result_promise); + GC_ROOT_PIN(js, completion); + + if (rejected) { + completion = sv_suppress_disposal_error(js, reason, completion); + GC_ROOT_PIN(js, completion); + js_set_slot(state, SLOT_AUX, completion); + } + + ant_value_t idx_v = js_get_slot(state, SLOT_ITER_STATE); + ant_offset_t idx = vtype(idx_v) == T_NUM ? (ant_offset_t)js_getnum(idx_v) : 0; + + while (idx > 0) { + GC_ROOT_SAVE(iter_mark, js); + idx--; + js_set_slot(state, SLOT_ITER_STATE, js_mknum((double)idx)); + + ant_value_t record = js_arr_get(js, entries, idx); + GC_ROOT_PIN(js, record); + ant_value_t result = sv_disposal_record_call(js, record); + GC_ROOT_PIN(js, result); + + if (is_err(result) || js->thrown_exists) { + ant_value_t error = sv_disposal_error_value(js, result); + GC_ROOT_PIN(js, error); + completion = sv_suppress_disposal_error(js, error, completion); + js_set_slot(state, SLOT_AUX, completion); + GC_ROOT_RESTORE(js, iter_mark); + continue; + } + + if (vtype(result) == T_PROMISE) { + ant_value_t on_fulfilled = js_heavy_mkfun(js, sv_async_dispose_on_fulfilled, state); + GC_ROOT_PIN(js, on_fulfilled); + ant_value_t on_rejected = js_heavy_mkfun(js, sv_async_dispose_on_rejected, state); + GC_ROOT_PIN(js, on_rejected); + js_promise_then(js, result, on_fulfilled, on_rejected); + GC_ROOT_RESTORE(js, iter_mark); + GC_ROOT_RESTORE(js, root_mark); + return result_promise; + } + + GC_ROOT_RESTORE(js, iter_mark); + } + + if (vtype(completion) != T_UNDEF) js_reject_promise(js, result_promise, completion); + else js_resolve_promise(js, result_promise, js_mkundef()); + + GC_ROOT_RESTORE(js, root_mark); + return result_promise; +} + +static inline ant_value_t sv_using_dispose_async( + ant_t *js, + ant_value_t entries, + ant_value_t completion +) { + ant_value_t result_promise = js_mkpromise(js); + if (is_err(result_promise)) return result_promise; + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, result_promise); + GC_ROOT_PIN(js, completion); + + if (vtype(entries) != T_ARR) { + ant_value_t error = js_make_error_silent(js, JS_ERR_TYPE, "invalid using disposal stack"); + GC_ROOT_PIN(js, error); + js_reject_promise(js, result_promise, error); + GC_ROOT_RESTORE(js, root_mark); + return result_promise; + } + + GC_ROOT_PIN(js, entries); + + ant_offset_t len = js_arr_len(js, entries); + ant_value_t work = js_mkarr(js); + GC_ROOT_PIN(js, work); + if (is_err(work)) { + GC_ROOT_RESTORE(js, root_mark); + return work; + } + + for (ant_offset_t i = 0; i < len; i++) { + GC_ROOT_SAVE(copy_mark, js); + ant_value_t record = js_arr_get(js, entries, i); + GC_ROOT_PIN(js, record); + js_arr_push(js, work, record); + GC_ROOT_RESTORE(js, copy_mark); + } + + sv_using_array_clear(entries); + ant_value_t state = js_mkobj(js); + + GC_ROOT_PIN(js, state); + if (is_err(state)) { + GC_ROOT_RESTORE(js, root_mark); + return state; + } + + js_set_slot(state, SLOT_ENTRIES, work); + js_set_slot(state, SLOT_DATA, result_promise); + js_set_slot(state, SLOT_AUX, completion); + js_set_slot(state, SLOT_ITER_STATE, js_mknum((double)js_arr_len(js, work))); + + ant_value_t result = sv_async_dispose_continue(js, state, false, js_mkundef()); + GC_ROOT_RESTORE(js, root_mark); + return result; +} + +static inline ant_value_t sv_using_dispose( + ant_t *js, + ant_value_t entries, + ant_value_t completion, + bool is_async, + bool suppressed +) { + ant_value_t actual_completion = suppressed ? completion : js_mkundef(); + if (is_async) return sv_using_dispose_async(js, entries, actual_completion); + return sv_using_dispose_sync(js, entries, actual_completion, !suppressed); +} + +static inline ant_value_t sv_op_using_push(sv_vm_t *vm, ant_t *js, bool is_async) { + ant_value_t resource = vm->stack[--vm->sp]; + ant_value_t entries = vm->stack[--vm->sp]; + ant_value_t result = sv_using_push(js, entries, resource, is_async); + if (!is_err(result)) vm->stack[vm->sp++] = result; + return result; +} + +static inline ant_value_t sv_op_dispose_resource(sv_vm_t *vm, ant_t *js, bool is_async) { + ant_value_t resource = vm->stack[--vm->sp]; + ant_value_t result = sv_dispose_resource(js, resource, is_async); + if (!is_err(result)) vm->stack[vm->sp++] = result; + return result; +} + +static inline ant_value_t sv_op_using_dispose( + sv_vm_t *vm, + ant_t *js, + bool is_async, + bool suppressed +) { + ant_value_t completion = suppressed ? vm->stack[--vm->sp] : js_mkundef(); + ant_value_t entries = vm->stack[--vm->sp]; + ant_value_t result = sv_using_dispose(js, entries, completion, is_async, suppressed); + if (!is_err(result)) vm->stack[vm->sp++] = result; + return result; +} + +#endif diff --git a/tests/test_disposable_stack.cjs b/tests/test_disposable_stack.cjs new file mode 100644 index 0000000..3277cc3 --- /dev/null +++ b/tests/test_disposable_stack.cjs @@ -0,0 +1,118 @@ +const assert = require("assert"); + +{ + const calls = []; + const stack = new DisposableStack(); + const first = { disposed: false }; + const second = { disposed: false }; + const third = { + disposed: false, + [Symbol.dispose]() { + this.disposed = true; + calls.push("third"); + }, + }; + + assert.strictEqual(stack.adopt(first, (value) => { + value.disposed = true; + calls.push("first"); + }), first); + + assert.strictEqual(stack.defer(() => { + second.disposed = true; + calls.push("second"); + }), undefined); + + const moved = stack.move(); + assert.strictEqual(moved.use(third), third); + let movedStackRejected = false; + try { + stack.defer(() => {}); + } catch (error) { + movedStackRejected = /already disposed/.test(error.message); + } + assert(movedStackRejected); + + assert.strictEqual(moved.dispose(), undefined); + assert.deepStrictEqual(calls, ["third", "second", "first"]); + assert(first.disposed); + assert(second.disposed); + assert(third.disposed); + assert.strictEqual(moved.dispose(), undefined); +} + +{ + const stack = new DisposableStack(); + stack.defer(() => { + throw new Error("first"); + }); + stack.defer(() => { + throw new Error("second"); + }); + + assert.throws(() => stack.dispose(), (error) => { + assert(error instanceof SuppressedError); + assert.strictEqual(error.error.message, "first"); + assert.strictEqual(error.suppressed.message, "second"); + return true; + }); +} + +{ + const calls = []; + const stack = new AsyncDisposableStack(); + const first = { disposed: false }; + const second = { disposed: false }; + const third = { + disposed: false, + async [Symbol.asyncDispose]() { + this.disposed = true; + calls.push("third"); + }, + }; + + assert.strictEqual(stack.adopt(first, async (value) => { + value.disposed = true; + calls.push("first"); + }), first); + + assert.strictEqual(stack.defer(async () => { + second.disposed = true; + calls.push("second"); + }), undefined); + + const moved = stack.move(); + assert.strictEqual(moved.use(third), third); + + moved.disposeAsync().then(() => { + assert.deepStrictEqual(calls, ["third", "second", "first"]); + assert(first.disposed); + assert(second.disposed); + assert(third.disposed); + }); +} + +{ + const stack = new AsyncDisposableStack(); + stack.defer(async () => { + throw new Error("async first"); + }); + stack.defer(async () => { + throw new Error("async second"); + }); + + stack.disposeAsync().then( + () => { + throw new Error("disposeAsync should reject"); + }, + (error) => { + assert(error instanceof SuppressedError); + assert.strictEqual(error.error.message, "async first"); + assert.strictEqual(error.suppressed.message, "async second"); + } + ); +} + +Promise.resolve().then(() => { + console.log("DisposableStack tests completed!"); +}); diff --git a/tests/test_suppressed_error_symbols.cjs b/tests/test_suppressed_error_symbols.cjs new file mode 100644 index 0000000..75207a5 --- /dev/null +++ b/tests/test_suppressed_error_symbols.cjs @@ -0,0 +1,28 @@ +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +assert(typeof Symbol.dispose === "symbol", "Symbol.dispose exists"); +assert(typeof Symbol.asyncDispose === "symbol", "Symbol.asyncDispose exists"); +assert(Symbol.dispose.description === "Symbol.dispose", "Symbol.dispose description"); +assert(Symbol.asyncDispose.description === "Symbol.asyncDispose", "Symbol.asyncDispose description"); + +const error = new Error("inner"); +const suppressed = new TypeError("outer"); +const err = new SuppressedError(error, suppressed, "cleanup failed"); + +assert(err instanceof SuppressedError, "instanceof SuppressedError"); +assert(err instanceof Error, "instanceof Error"); +assert(Error.isError(err), "Error.isError recognizes SuppressedError"); +assert(err.name === "SuppressedError", "name is SuppressedError"); +assert(err.message === "cleanup failed", "message is set"); +assert(err.error === error, "error field is set"); +assert(err.suppressed === suppressed, "suppressed field is set"); +assert(typeof err.stack === "string", "stack is captured"); + +const called = SuppressedError(error, suppressed); +assert(called instanceof SuppressedError, "SuppressedError is callable"); +assert(called.error === error, "callable error field"); +assert(called.suppressed === suppressed, "callable suppressed field"); + +console.log("SuppressedError and dispose symbol tests completed!"); diff --git a/tests/test_throw_expressions.cjs b/tests/test_throw_expressions.cjs new file mode 100644 index 0000000..d835e95 --- /dev/null +++ b/tests/test_throw_expressions.cjs @@ -0,0 +1,48 @@ +const assert = require("assert"); + +{ + const fn = () => throw 42; + try { + fn(); + assert(false, "arrow throw expression should throw"); + } catch (error) { + assert.strictEqual(error, 42); + } +} + +{ + true ? 1 : throw 2; + try { + false ? 1 : throw 2; + assert(false, "conditional throw expression should throw"); + } catch (error) { + assert.strictEqual(error, 2); + } +} + +{ + let a; + try { + a = 19 || throw 77; + 88 && throw 23; + assert(false, "logical throw expression should throw"); + } catch (error) { + assert.strictEqual(a + error, 42); + } +} + +{ + function fn(arg = throw 42) { + return arg; + } + + assert.strictEqual(fn(21), 21); + try { + fn(); + assert(false, "parameter initializer throw expression should throw"); + } catch (error) { + assert.strictEqual(error, 42); + } +} + +console.log("throw expression tests completed!"); -- 2.51.2