From 9cc8ec9b86a20954422ded5942e6766a6cbfab32 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 5 Nov 2026 15:28:31 +0000 Subject: [PATCH] refactor(codemode): move iterator consumers, Iterator.from, the JSON reviver, and string replace callbacks to the prelude; drop Drain and consumers --- packages/codemode/src/machine/frame.ts | 11 +- packages/codemode/src/machine/kinds.ts | 15 +-- packages/codemode/src/machine/native-kind.ts | 38 +----- packages/codemode/src/machine/natives.ts | 128 ------------------ packages/codemode/src/prelude/helpers.ts | 15 ++- packages/codemode/src/prelude/source.ts | 87 +++++++++++- packages/codemode/src/snapshot/codec.ts | 15 +-- packages/codemode/src/stdlib/iterator.ts | 131 +------------------ packages/codemode/src/stdlib/json.ts | 64 +-------- packages/codemode/src/stdlib/string.ts | 46 ++----- packages/codemode/test/kinds.test.ts | 9 +- packages/codemode/test/machine.test.ts | 9 +- 12 files changed, 131 insertions(+), 437 deletions(-) delete mode 100644 packages/codemode/src/machine/natives.ts diff --git a/packages/codemode/src/machine/frame.ts b/packages/codemode/src/machine/frame.ts index 6709662c914..961d3075b22 100644 --- a/packages/codemode/src/machine/frame.ts +++ b/packages/codemode/src/machine/frame.ts @@ -66,27 +66,22 @@ export const kinds = new Map() let sealed = false -/** Closes the kind and consumer registries: `machine/kinds.ts` does it once every module that defines one has loaded. */ +/** Closes the kind registry: `machine/kinds.ts` does it once every module that defines one has loaded. */ export const sealKinds = () => { sealed = true } -/** A runtime is made only once the registries are sealed, so the kinds a saved state names are the package's own. */ +/** A runtime is made only once the registry is sealed, so the kinds a saved state names are the package's own. */ export const requireSealedKinds = () => { if (!sealed) throw new Error("Frame kinds aren't sealed: import src/machine/kinds.ts before making a runtime.") } -/** Throws once the registries are sealed: `what` (frame kinds, consumers) are only defined while the package loads. */ -export const unsealed = (what: string, name: string) => { - if (sealed) throw new Error(`${what} are sealed: "${name}" can't be registered after Code Mode has loaded.`) -} - export const kind = ( name: string, step: Kind["step"], options?: Pick, ): Kind => { - unsealed("Frame kinds", name) + if (sealed) throw new Error(`Frame kinds are sealed: "${name}" can't be registered after Code Mode has loaded.`) if (kinds.has(name)) throw new Error(`Frame kind "${name}" is defined twice.`) const made = { name, step, ...options } kinds.set(name, made) diff --git a/packages/codemode/src/machine/kinds.ts b/packages/codemode/src/machine/kinds.ts index baa35f6b668..8dbdec3c8c0 100644 --- a/packages/codemode/src/machine/kinds.ts +++ b/packages/codemode/src/machine/kinds.ts @@ -1,28 +1,19 @@ -// Every module that defines a frame kind or a consumer, then the seal: once this module has loaded, `kind()` and -// `consumer()` throw, so the kinds a saved state names, and the snapshot build id hashes, are the package's own and -// never depend on what else a process imported, or when. The runtime factory (`interpreter/execute.ts`) imports it, +// Every module that defines a frame kind, then the seal: once this module has loaded, `kind()` throws, so the kinds a +// saved state names, and the snapshot build id hashes, are the package's own and never depend on what else a process +// imported, or when. The runtime factory (`interpreter/execute.ts`) imports it, // and a runtime refuses to be made before it has loaded. Nothing it imports may import it back: the seal must run only // after every kind module has finished loading. Add a module here when it starts defining kinds. import "../data.js" -import "../interpreter/errors.js" import "../interpreter/extensions.js" import "../interpreter/interpreter.js" import "../interpreter/promises.js" import "../stdlib/array.js" -import "../stdlib/bytes.js" -import "../stdlib/collections.js" import "../stdlib/iterator.js" -import "../stdlib/json.js" -import "../stdlib/math.js" -import "../stdlib/object.js" -import "../stdlib/string.js" -import "../stdlib/url.js" import "./calls.js" import "./expressions.js" import "./generators.js" import "./helpers.js" import "./iteration.js" -import "./natives.js" import "./patterns.js" import "./statements.js" import "./task.js" diff --git a/packages/codemode/src/machine/native-kind.ts b/packages/codemode/src/machine/native-kind.ts index e49c13d7619..f681d323735 100644 --- a/packages/codemode/src/machine/native-kind.ts +++ b/packages/codemode/src/machine/native-kind.ts @@ -1,6 +1,6 @@ import type { AstNode } from "../interpreter/model.js" import type { Value } from "../interpreter/objects.js" -import { type Frame, kind, type Kind, type Out, unsealed } from "./frame.js" +import { type Frame, kind, type Kind, type Out } from "./frame.js" import type { MachineTask } from "./task.js" // Built-ins that call program code run as frames whose state is plain data: a kind (a module constant, named once) @@ -17,39 +17,3 @@ export const nativeKind = ( /** The call site a built-in's frame runs at, for the operations that report failures there. */ export const site = (frame: Frame) => frame.node as AstNode - -/** - * What a `Drain` (machine/natives.ts) does with each value it pulls. `each` and `resume` may call program code: `each` - * starts the work for one value, and `resume` continues it with what the work gave back (`frame.slots[SUB]` is theirs - * for the place it got to); either finishing (anything but `PENDING`) takes the next value, and `EXIT` stops early. A - * throw from either closes the iterator first. `take` takes a value without closing the iterator when it fails. - * `done` makes the result once the iterator is exhausted. Consumers are module constants, named once like kinds. - */ -export type Consumer = { - readonly name: string - /** What a source that is not iterable gives: usually a TypeError it throws. Not needed when it is known to be. */ - readonly notIterable?: (task: MachineTask, frame: Frame, source: Value) => Out - readonly take?: (frame: Frame, value: Value, index: number) => void - readonly each?: (task: MachineTask, frame: Frame, value: Value, index: number) => Out - readonly resume?: (task: MachineTask, frame: Frame, input: unknown, index: number) => Out - readonly done: (task: MachineTask, frame: Frame) => Out -} - -/** Every consumer, by name, so a saved `Drain` frame can name its consumer. */ -export const consumers = new Map() - -/** Defines a consumer, once per name. */ -export const consumer = (made: Consumer): Consumer => { - unsealed("Consumers", made.name) - if (consumers.has(made.name)) throw new Error(`Consumer "${made.name}" is defined twice.`) - consumers.set(made.name, made) - return made -} - -/** What `each` or `resume` returns to stop early: the iterator is closed, then `done` makes the result. */ -export const EXIT: unique symbol = Symbol("codemode.exit") - -/** The first slot a consumer keeps its own state in. */ -export const STATE = 5 -/** The slot a consumer keeps its place within one value's work in. */ -export const SUB = 4 diff --git a/packages/codemode/src/machine/natives.ts b/packages/codemode/src/machine/natives.ts deleted file mode 100644 index 5a127c9ab64..00000000000 --- a/packages/codemode/src/machine/natives.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { locate } from "../interpreter/errors.js" -import { type AstNode, GeneratorReturn } from "../interpreter/model.js" -import type { Value } from "../interpreter/objects.js" -import { Interruption } from "../interpreter/signals.js" -import { type Frame, type Out, PENDING } from "./frame.js" -import { type Iter, iterate, iteratorClose, iteratorStep } from "./iteration.js" -import { type Consumer, EXIT, nativeKind, site, SUB } from "./native-kind.js" -import type { MachineTask } from "./task.js" - -// Phases: open the iterator, have it, step it, have the step, continue a value's work, close after a failure, closed, -// finished, and closed after stopping early. -const OPENED = 1 -const STEP = 2 -const STEPPED = 3 -const CONSUMING = 4 -const CLOSING = 5 -const CLOSED = 6 -const FINISHED = 7 -const EXITED = 8 - -/** - * Iterates `source` to its end, handing each value to `consumer`: `Array.from`, the collection constructors, - * `Object.fromEntries`, and the others that consume an iterable. Slots: the consumer, the source, the iterator, the - * index, the consumer's place, then the consumer's own state (from `STATE`). A failure in the consumer's work closes - * the iterator, and wins over a failing close; a failure of the iterator itself does not close it. - */ -export const drain = (consumer: Consumer, source: Value, state: Array) => - [consumer, source, undefined, 0, 0, ...state] as Array - -/** `drain`'s slots for an iterator already open (GetIteratorDirect). */ -export const drainOpen = (consumer: Consumer, iterator: Iter, state: Array) => - [consumer, undefined, iterator, 0, 0, ...state] as Array - -export const Drain = nativeKind( - "Drain", - (task, frame, input) => { - const slots = frame.slots - const consumer = slots[0] as Consumer - const node = site(frame) - let phase = frame.phase - if (phase === CLOSING) { - frame.phase = CLOSED - if (iteratorClose(task, slots[2] as Iter, node) === PENDING) return PENDING - phase = CLOSED - } - if (phase === CLOSED) throw slots[3] - if (phase === EXITED) { - frame.phase = FINISHED - return task.tail(frame, consumer.done(task, frame)) - } - if (phase === 0 && slots[2] !== undefined) phase = STEP - if (phase === 0) { - const opened = iterate(task, slots[1] as Value, node) - if (opened === PENDING) { - frame.phase = OPENED - return PENDING - } - input = opened - phase = OPENED - } - if (phase === OPENED) { - if (input === undefined) { - frame.phase = FINISHED - return task.tail(frame, consumer.notIterable!(task, frame, slots[1] as Value)) - } - slots[2] = input - phase = STEP - } - const iterator = slots[2] as Iter - if (phase === CONSUMING) { - const out = consumer.resume!(task, frame, input, slots[3] as number) - if (out === PENDING) return PENDING - if (out === EXIT) return exit(task, frame, iterator, node) - slots[3] = (slots[3] as number) + 1 - phase = STEP - } - while (true) { - if (phase === STEP) { - frame.phase = STEP - const step = iteratorStep(task, iterator, node) - if (step === PENDING) { - frame.phase = STEPPED - return PENDING - } - input = step - } - const step = input as { done: boolean; value: Value } - if (step.done) { - frame.phase = FINISHED - return task.tail(frame, consumer.done(task, frame)) - } - consumer.take?.(frame, step.value, slots[3] as number) - if (consumer.each !== undefined) { - frame.phase = CONSUMING - slots[SUB] = 0 - const out = consumer.each(task, frame, step.value, slots[3] as number) - if (out === PENDING) return PENDING - if (out === EXIT) return exit(task, frame, iterator, node) - } - slots[3] = (slots[3] as number) + 1 - phase = STEP - } - }, - { - unwind: (_task, frame, error) => { - if (frame.phase === CONSUMING && !(error instanceof Interruption)) { - frame.phase = CLOSING - frame.slots[3] = locate(error, frame.node) - return true - } - if (frame.phase === CLOSING || frame.phase === CLOSED) { - // A generator's return() is a return completion, so a failing close wins over it, as after `break`. - throw frame.slots[3] instanceof GeneratorReturn && !(error instanceof Interruption) - ? locate(error, frame.node) - : frame.slots[3] - } - throw locate(error, frame.node) - }, - }, -) - -// Stops early: closes the iterator (a failure to close is the result), then the consumer makes the result. -function exit(task: MachineTask, frame: Frame, iterator: Iter, node: AstNode): Out { - frame.phase = EXITED - if (iteratorClose(task, iterator, node) === PENDING) return PENDING - frame.phase = FINISHED - return task.tail(frame, (frame.slots[0] as Consumer).done(task, frame)) -} diff --git a/packages/codemode/src/prelude/helpers.ts b/packages/codemode/src/prelude/helpers.ts index 4aeb58f6395..467c1ba9f8a 100644 --- a/packages/codemode/src/prelude/helpers.ts +++ b/packages/codemode/src/prelude/helpers.ts @@ -15,6 +15,8 @@ import { data, define, hidden, + hasPrototype, + keys, getOwn, IteratorObj, positionIterator, @@ -83,6 +85,11 @@ export const helpers = (ctx: Interpreter): Array describeValue(value)], + $Inherits: [2, ([value, proto]) => hasPrototype(value, proto as Obj)], + /** An iterator wrapped as `Iterator.from` returns it. */ + $Wrap: [1, ([value]) => new IteratorObj(builtins.IteratorHelper, iterateDirect(value, undefined))], + /** An object's own enumerable string keys. */ + $Keys: [1, ([value]) => new Arr(builtins.Array, keys(value as Obj))], /** Defines an own data property, enumerable or hidden, and returns the object. */ $Define: [ 4, @@ -108,11 +115,11 @@ export const helpers = (ctx: Interpreter): Array isSupportedCallback(value)], - /** An iterator object as `for...of` steps it, reading `next` once (GetIteratorDirect); an array's values. */ + /** An iterator object as `for...of` steps it, reading `next` once (GetIteratorDirect); with `arrays`, an array's values. */ $Direct: [ - 1, - ([value]) => - value instanceof Arr + 2, + ([value, arrays]) => + arrays === true && value instanceof Arr ? positionIterator(builtins, value, "values") : new IteratorObj(builtins.Iterator, iterateDirect(value, undefined)), ], diff --git a/packages/codemode/src/prelude/source.ts b/packages/codemode/src/prelude/source.ts index 69a8fa04b01..a31a7bcefc7 100644 --- a/packages/codemode/src/prelude/source.ts +++ b/packages/codemode/src/prelude/source.ts @@ -125,7 +125,7 @@ function setMethod(target, other, name) { } const iterator = $Call(keys, other) if (!$IsObject(iterator)) $Throw("TypeError", "Set." + name + " expects 'keys' to return an iterator object.") - for (const item of $Direct(iterator)) { + for (const item of $Direct(iterator, true)) { found = $Table("has", target, item) if (name === "union") $Table("add", result, item) else if (name === "intersection") { @@ -200,4 +200,89 @@ function pairsForEach(target, pairs, callback, thisArg) { index++ } } +function Iterator$from(value) { + if ($IsObject(value) && $Inherits(value, Iterator.prototype)) return value + if (typeof value !== "string" && !$IsObject(value)) $Throw("TypeError", "Iterator.from expects an iterable or iterator, received " + $Describe(value) + ".") + const method = value[Symbol.iterator] + return $Wrap(method === undefined || method === null ? value : $Call(method, value)) +} + +function Iterator$prototype$toArray() { + const result = [] + for (const item of $Direct(this, false)) result[result.length] = item + return result +} + +function Iterator$prototype$reduce(callback, ...initial) { + const iterator = $Direct(this, false) + $Callback(callback, "Iterator.prototype.reduce") + let accumulator = initial[0] + let started = initial.length > 0 + let index = 0 + for (const item of iterator) { + if (started) accumulator = callback(accumulator, item, index) + else accumulator = item + started = true + index++ + } + if (!started) $Throw("TypeError", "Iterator.prototype.reduce of an empty iterator with no initial value.") + return accumulator +} + +function Iterator$prototype$forEach(callback) { + iteratorTest(this, callback, "forEach") +} +function Iterator$prototype$some(callback) { + return iteratorTest(this, callback, "some") +} +function Iterator$prototype$every(callback) { + return iteratorTest(this, callback, "every") +} +function Iterator$prototype$find(callback) { + return iteratorTest(this, callback, "find") +} +// Calls the callback with each value and its index until its result decides; leaving early closes the iterator. +function iteratorTest(self, callback, name) { + const iterator = $Direct(self, false) + $Callback(callback, "Iterator.prototype." + name) + let index = 0 + let result + for (const item of iterator) { + result = callback(item, index++) + if (name === "some" && result) return true + if (name === "every" && !result) return false + if (name === "find" && result) return item + } + return name === "every" ? true : name === "some" ? false : undefined +} + +// JSON.parse's reviver: children are revived first, each replacing (or, when the reviver gives undefined, removing) +// its property; then the reviver sees the value itself. +function revive(holder, key, reviver) { + const value = holder[key] + if ($IsObject(value)) { + const names = $Keys(value) + let index = 0 + let revived + while (index < names.length) { + revived = revive(value, names[index], reviver) + if (revived === undefined) delete value[names[index]] + else value[names[index]] = revived + index++ + } + } + return $Call(reviver, holder, key, value) +} + +// String replace with a callback, once the matches are found: each is the text before it and the callback's +// arguments; the text after the last one follows. +function replaceWith(matches, rest, callback) { + let result = "" + let index = 0 + while (index < matches.length) { + result += matches[index][0] + String(callback(...matches[index][1])) + index++ + } + return result + rest +} ` diff --git a/packages/codemode/src/snapshot/codec.ts b/packages/codemode/src/snapshot/codec.ts index 88983fd6013..920081c5286 100644 --- a/packages/codemode/src/snapshot/codec.ts +++ b/packages/codemode/src/snapshot/codec.ts @@ -44,7 +44,6 @@ import { deleted, OrderedTable } from "../interpreter/table.js" import { Frame, kinds, PENDING } from "../machine/frame.js" import { AsyncFromSyncIterator } from "../machine/generators.js" import { Machine } from "../machine/machine.js" -import { consumers, EXIT } from "../machine/native-kind.js" import { HostCall, Resolving } from "../machine/scheduler.js" import { MachineTask } from "../machine/task.js" import { TextDecoderObj } from "../stdlib/bytes.js" @@ -52,7 +51,7 @@ import { ToolError, toolError } from "../tool-error.js" import { ToolReference, ToolRuntimeError } from "../tool-runtime.js" // A graph codec for a run's state: every object reachable from the roots becomes one record, so identity, sharing, -// and cycles survive, and every value inside a record is tagged JSON. Built-ins, frame kinds, consumers, AST nodes, +// and cycles survive, and every value inside a record is tagged JSON. Built-ins, frame kinds, AST nodes, // and the machine's own objects are named rather than copied; a value with no data form (a live host function, say) // becomes a `Lost` placeholder. Nothing here evaluates anything: decoding only allocates and assigns. @@ -63,7 +62,7 @@ export type Json = null | boolean | number | string | ReadonlyArray | { re * A value inside a record: a JSON primitive, or a tag (an array whose first item says what it is): * - `["u"]` undefined, `["n", "NaN" | "Infinity" | "-Infinity" | "-0"]` a number JSON cannot spell, `["-"]` an absent * field, `["h"]` an array hole; - * - `["s", name]` an interpreter symbol, `["k", name]` a frame kind, `["c", name]` a consumer; + * - `["s", name]` an interpreter symbol, `["k", name]` a frame kind; * - `["i", name]` a built-in or host global, unchanged since the runtime was made; * - `["a", source, index]` an AST node, `["A", source, index, key]` an array the AST owns; * - `["r", id]` another record. @@ -109,7 +108,6 @@ const symbols = new Map([ ["iterator", IteratorSymbol], ["asyncIterator", AsyncIteratorSymbol], ["pending", PENDING], - ["exit", EXIT], ["optionalShortCircuit", OptionalShortCircuit], ["slice", Slice], ["deleted", deleted], @@ -308,16 +306,14 @@ const errorClasses = new Map([ const errorNames = new Map([...errorClasses].map(([name, ctor]) => [ctor, name])) let kindNames: Map | undefined -let consumerNames: Map | undefined /** - * The names of every frame kind and consumer, and of every class with a codec: a build that saves a different set + * The names of every frame kind, and of every class with a codec: a build that saves a different set * cannot read this one's snapshots. */ export const codecShape = (): string => JSON.stringify([ [...kinds.keys()].sort(), - [...consumers.keys()].sort(), specs.map((spec) => [spec.name, spec.fields]), [...symbols.keys()], ]) @@ -345,7 +341,6 @@ class Encoder { constructor(private readonly env: EncodeEnv) { kindNames ??= new Map([...kinds].map(([name, kind]) => [kind, name])) - consumerNames ??= new Map([...consumers].map(([name, consumer]) => [consumer, name])) } value(value: unknown): Encoded { @@ -370,8 +365,6 @@ class Encoder { const object = value as object const kind = kindNames!.get(object) if (kind !== undefined) return ["k", kind] - const consumer = consumerNames!.get(object) - if (consumer !== undefined) return ["c", consumer] if (object instanceof Obj) { const name = this.env.nameOf(object) if (name !== undefined && this.env.unchanged(object, name)) return ["i", name] @@ -844,8 +837,6 @@ class Decoder { return symbols.get(text(value[1], "a symbol")) ?? invalid(`unknown symbol ${String(value[1])}.`) case "k": return kinds.get(text(value[1], "a kind")) ?? invalid(`unknown frame kind ${String(value[1])}.`) - case "c": - return consumers.get(text(value[1], "a consumer")) ?? invalid(`unknown consumer ${String(value[1])}.`) case "i": { const name = text(value[1], "a built-in name") return this.env.named(name) ?? invalid(`this runtime has no built-in or host global named ${name}.`) diff --git a/packages/codemode/src/stdlib/iterator.ts b/packages/codemode/src/stdlib/iterator.ts index 9fc47a3bc0f..3a008f01a3f 100644 --- a/packages/codemode/src/stdlib/iterator.ts +++ b/packages/codemode/src/stdlib/iterator.ts @@ -1,30 +1,22 @@ -import { consumer, type Consumer, EXIT, nativeKind, site, STATE } from "../machine/native-kind.js" +import { nativeKind, site } from "../machine/native-kind.js" import { requireCallback } from "../interpreter/callback.js" import type { Interpreter } from "../interpreter/interpreter.js" import { rangeError, typeError } from "../interpreter/model.js" -import { constructor, fn, methods, receiver, requiresNew } from "../interpreter/native.js" +import { constructor, methods, receiver, requiresNew } from "../interpreter/native.js" import { - Arr, - type Callable, coerceToNumber, type Cursor, - define, - hasPrototype, Helper, type HelperOp, - hidden, iteratorResult, IteratorObj, - Obj, Position, type Step, type Value, } from "../interpreter/objects.js" import { Push } from "../interpreter/signals.js" -import { type Frame, PENDING } from "../machine/frame.js" -import { flattenable } from "../machine/helpers.js" +import { PENDING } from "../machine/frame.js" import { iterateDirect, iteratorClose, iteratorStep } from "../machine/iteration.js" -import { Drain, drainOpen } from "../machine/natives.js" const method = (name: string) => `Iterator.prototype.${name}` @@ -53,21 +45,6 @@ export const iteratorGlobals = (ctx: Interpreter) => { return new IteratorObj(builtins.IteratorHelper, new Helper(op, source, callback, 0)) }, ] as const - // A consuming method: drives the iterator to its end, or until the callback decides the result. - const consuming = (name: string, consumer: Consumer, needsCallback = true) => - [ - name, - name === "toArray" ? 0 : 1, - (thisValue: Value, args: Array) => { - const source = iterateDirect(thisValue, undefined) - const callback = needsCallback ? requireCallback(args[0], method(name)) : undefined - return new Push( - Drain, - drainOpen(consumer, source, [callback, args[1], name === "reduce" && args.length >= 2, undefined]), - ) - }, - ] as const - methods(builtins, proto, [ [ "next", @@ -86,12 +63,6 @@ export const iteratorGlobals = (ctx: Interpreter) => { ["take", 1, (thisValue, args) => helper("take", thisValue, undefined, limit(args[0], "take"))], ["drop", 1, (thisValue, args) => helper("drop", thisValue, undefined, limit(args[0], "drop"))], lazy("flatMap"), - consuming("reduce", Reduce), - consuming("toArray", ToArray, false), - consuming("forEach", ForEach), - consuming("some", Some), - consuming("every", Every), - consuming("find", Find), ]) // Helpers and `Iterator.from` wrappers can be closed; collection iterators, as in JS, cannot. @@ -111,15 +82,6 @@ export const iteratorGlobals = (ctx: Interpreter) => { throw typeError("Iterator is abstract; use Iterator.from(...) or a built-in iterator.") }, }) - define( - iterator, - "from", - fn(builtins, "from", 1, (_, args) => { - if (args[0] instanceof Obj && hasPrototype(args[0], proto)) return args[0] - return new Push(IteratorFrom, [args[0]]) - }), - hidden, - ) return iterator } @@ -144,90 +106,3 @@ const IteratorReturn = nativeKind("%IteratorHelper%.return", (task, frame) => { } return iteratorResult(task.ctx.builtins.Object, undefined, true) }) - -// Slots: the value to iterate. -const IteratorFrom = nativeKind("Iterator.from", (task, frame, input) => { - let cursor = input as Cursor - if (frame.phase === 0) { - const opened = flattenable(task, frame.slots[0] as Value, "Iterator.from", true, site(frame)) - if (opened === PENDING) { - frame.phase = 1 - return PENDING - } - cursor = opened - } - return new IteratorObj(task.ctx.builtins.IteratorHelper, cursor) -}) - -// The consuming methods' state: the callback, the initial value (reduce), whether there is an accumulator yet -// (reduce), and the result. -const CALLBACK = STATE -const ACCUMULATOR = STATE + 1 -const HAS = STATE + 2 -const RESULT = STATE + 3 - -const Reduce = consumer({ - name: "Iterator.prototype.reduce", - each: (task, frame, value, index) => { - const slots = frame.slots - if (!slots[HAS]) { - slots[HAS] = true - slots[ACCUMULATOR] = value - return undefined - } - const result = task.call( - slots[CALLBACK] as Callable, - undefined, - [slots[ACCUMULATOR] as Value, value, index], - undefined, - ) - if (result === PENDING) return PENDING - slots[ACCUMULATOR] = result - return undefined - }, - resume: (_task, frame, input) => { - frame.slots[ACCUMULATOR] = input - }, - done: (_task, frame) => { - if (!frame.slots[HAS]) throw typeError("Iterator.prototype.reduce of an empty iterator with no initial value.") - return frame.slots[ACCUMULATOR] as Value - }, -}) - -const ToArray = consumer({ - name: "Iterator.prototype.toArray", - take: (frame, value) => { - ;((frame.slots[RESULT] ??= []) as Array).push(value) - }, - done: (task, frame) => new Arr(task.ctx.builtins.Array, (frame.slots[RESULT] ?? []) as Array), -}) - -// A consumer that calls the callback with each value and its index, and decides by the result. -const deciding = ( - name: string, - decide: (frame: Frame, value: Value, result: Value) => typeof EXIT | undefined, - otherwise: Value, -): Consumer => - consumer({ - name: method(name), - each: (task, frame, value, index) => { - frame.slots[RESULT] = value - const result = task.call(frame.slots[CALLBACK] as Callable, undefined, [value, index], undefined) - if (result === PENDING) return PENDING - return decide(frame, frame.slots[RESULT] as Value, result) - }, - resume: (_task, frame, input) => decide(frame, frame.slots[RESULT] as Value, input as Value), - done: (_task, frame) => (frame.slots[HAS] ? (frame.slots[RESULT] as Value) : otherwise), - }) - -// Stops with `result` as the method's result. -const stop = (frame: Frame, result: Value): typeof EXIT => { - frame.slots[HAS] = true - frame.slots[RESULT] = result - return EXIT -} - -const ForEach = deciding("forEach", () => undefined, undefined) -const Some = deciding("some", (frame, _value, result) => (result ? stop(frame, true) : undefined), false) -const Every = deciding("every", (frame, _value, result) => (result ? undefined : stop(frame, false)), true) -const Find = deciding("find", (frame, value, result) => (result ? stop(frame, value) : undefined), undefined) diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index e0061bf2085..039c4426168 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -1,24 +1,11 @@ -import { nativeKind } from "../machine/native-kind.js" import { methods } from "../interpreter/native.js" import { requireCallback } from "../interpreter/callback.js" import type { Interpreter } from "../interpreter/interpreter.js" import { syntaxError } from "../interpreter/model.js" import { typeofValue } from "../interpreter/references.js" import { fromJson, JsonWalk, walk } from "../data.js" -import { - get, - keys, - Arr, - type Callable, - Obj, - coerceToString, - record, - remove, - set, - type Value, -} from "../interpreter/objects.js" -import { Push } from "../interpreter/signals.js" -import { type Frame, PENDING } from "../machine/frame.js" +import { Arr, Obj, coerceToString, record, type Value } from "../interpreter/objects.js" +import { Invoke, Push } from "../interpreter/signals.js" export const jsonGlobal = (ctx: Interpreter) => { const json = new Obj(ctx.builtins.Object) @@ -41,54 +28,9 @@ const parse = (ctx: Interpreter, args: Array) => { })() if (typeofValue(args[1]) !== "function") return parsed const reviver = requireCallback(args[1], "JSON.parse") - return new Push(Revive, [reviver, [revival(record(ctx.builtins.Object, { "": parsed }), "")]]) + return new Invoke(ctx.prelude.fn("revive"), undefined, [record(ctx.builtins.Object, { "": parsed }), "", reviver], undefined) } -/** One value the reviver walk is at: its holder and key, the value, and its property names and the next one. */ -type Revival = { - readonly holder: Obj - readonly key: string - readonly value: Value - names: Array - index: number -} - -const revival = (holder: Obj, key: string): Revival => { - const value = get(holder, key) - return { holder, key, value, names: value instanceof Obj ? keys(value) : [], index: 0 } -} - -// Slots: the reviver, and the values being revived, innermost last. Children are revived first, each replacing (or, -// when the reviver gives undefined, removing) its property; then the reviver sees the value itself. -const Revive = nativeKind("JSON.parse reviver", (task, frame: Frame, input) => { - const reviver = frame.slots[0] as Callable - const stack = frame.slots[1] as Array - let resumed = frame.phase === 1 - while (true) { - const current = stack[stack.length - 1]! - let revived: Value - if (resumed) { - resumed = false - revived = input as Value - } else { - if (current.index < current.names.length) { - stack.push(revival(current.value as Obj, current.names[current.index++]!)) - continue - } - frame.phase = 1 - const result = task.call(reviver, current.holder, [current.key, current.value], undefined) - if (result === PENDING) return PENDING - revived = result - } - stack.pop() - const parent = stack[stack.length - 1] - if (parent === undefined) return revived - const name = parent.names[parent.index - 1]! - if (revived === undefined) remove(parent.value as Obj, name) - else set(parent.value as Obj, name, revived) - } -}) - const stringify = (ctx: Interpreter, args: Array) => { const space = args[2] const indent = typeof space === "number" || typeof space === "string" ? space : undefined diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts index 96df88b4be3..00e059e3b78 100644 --- a/packages/codemode/src/stdlib/string.ts +++ b/packages/codemode/src/stdlib/string.ts @@ -1,6 +1,4 @@ -import { nativeKind } from "../machine/native-kind.js" -import { type Hint, Push } from "../interpreter/signals.js" -import { PENDING } from "../machine/frame.js" +import { type Hint, Invoke } from "../interpreter/signals.js" import { constructor, fn, type Method, methods } from "../interpreter/native.js" import { checkArrayLength, checkStringLength } from "../interpreter/limits.js" import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js" @@ -47,7 +45,7 @@ const replaceWithCallback = ( value: string, name: "replace" | "replaceAll", args: Array, -): Push => { +): Invoke => { const builtins = ctx.builtins const callback = requireCallback(args[1], `String.${name}`) const matches: Array = [] @@ -78,40 +76,20 @@ const replaceWithCallback = ( if (name === "replace") value.replace(search, collect) else value.replaceAll(search, collect) } - return new Push(ReplaceWithCallback, [value, matches, callback, 0, []]) + const pieces = matches.map( + (match, index) => + new Arr(builtins.Array, [ + value.slice(index === 0 ? 0 : matches[index - 1]!.offset + matches[index - 1]!.match.length, match.offset), + new Arr(builtins.Array, match.args), + ]), + ) + const last = matches.at(-1) + const rest = value.slice(last === undefined ? 0 : last.offset + last.match.length) + return new Invoke(ctx.prelude.fn("replaceWith"), undefined, [new Arr(builtins.Array, pieces), rest, callback], undefined) } type Match = { readonly match: string; readonly offset: number; readonly args: Array } -// Slots: the string, its matches, the callback, the next match, and the output so far. -const ReplaceWithCallback = nativeKind("String.prototype.replace callback", (task, frame, input) => { - const slots = frame.slots - const value = slots[0] as string - const matches = slots[1] as Array - const output = slots[4] as Array - let index = slots[3] as number - if (frame.phase === 1) { - output.push(coerceToString(input as Value)) - index++ - } - for (; index < matches.length; index++) { - const match = matches[index]! - output.push( - value.slice(index === 0 ? 0 : matches[index - 1]!.offset + matches[index - 1]!.match.length, match.offset), - ) - const replacement = task.call(slots[2] as Value, undefined, match.args, frame.node) - if (replacement === PENDING) { - slots[3] = index - frame.phase = 1 - return PENDING - } - output.push(coerceToString(replacement)) - } - const last = matches.at(-1) - output.push(value.slice(last === undefined ? 0 : last.offset + last.match.length)) - return output.join("") -}) - export const stringGlobal = (ctx: Interpreter) => { const builtins = ctx.builtins const string = constructor(builtins, builtins.String, { diff --git a/packages/codemode/test/kinds.test.ts b/packages/codemode/test/kinds.test.ts index ac023f14616..b7374d50cd7 100644 --- a/packages/codemode/test/kinds.test.ts +++ b/packages/codemode/test/kinds.test.ts @@ -2,9 +2,9 @@ import { describe, expect, test } from "bun:test" import path from "node:path" import "../src/machine/kinds.js" import { kind } from "../src/machine/frame.js" -import { consumer, nativeKind } from "../src/machine/native-kind.js" +import { nativeKind } from "../src/machine/native-kind.js" -// Frame kinds and consumers are sealed once Code Mode has loaded, so the snapshot build id hashes only the package's +// Frame kinds are sealed once Code Mode has loaded, so the snapshot build id hashes only the package's // own and is the same in every process, whatever it imported first. const root = path.join(import.meta.dir, "..") @@ -17,16 +17,13 @@ const output = async (command: Array) => { } describe("sealed frame kinds", () => { - test("a kind or consumer registered after Code Mode has loaded throws", () => { + test("a kind registered after Code Mode has loaded throws", () => { expect(() => kind("late", () => undefined)).toThrow( 'Frame kinds are sealed: "late" can\'t be registered after Code Mode has loaded.', ) expect(() => nativeKind("late native", () => undefined)).toThrow( 'Frame kinds are sealed: "late native" can\'t be registered after Code Mode has loaded.', ) - expect(() => consumer({ name: "late", done: () => undefined })).toThrow( - 'Consumers are sealed: "late" can\'t be registered after Code Mode has loaded.', - ) }) test("the build id is the same whatever a process imported first", async () => { diff --git a/packages/codemode/test/machine.test.ts b/packages/codemode/test/machine.test.ts index 71278cdb8b7..fc5eb51de40 100644 --- a/packages/codemode/test/machine.test.ts +++ b/packages/codemode/test/machine.test.ts @@ -9,7 +9,6 @@ import { GeneratorObj, type Native, nativeRule, Obj, PromiseObj, type Value } fr import { normal } from "../src/interpreter/signals.js" import { Frame, type Kind, kinds } from "../src/machine/frame.js" import { Machine } from "../src/machine/machine.js" -import { type Consumer, consumers } from "../src/machine/native-kind.js" import { ScopeStack } from "../src/interpreter/scope.js" import { AsyncFromSyncIterator } from "../src/machine/generators.js" import { stats } from "../src/machine/task.js" @@ -70,8 +69,8 @@ const programBinding = (machine: Machine, name: string): Value => { } // Throws unless `value` is plain data: no host function or generator is reachable from it. Program values, scopes, -// AST nodes, and the machine (a back-reference from what settles a promise) are data of their own; frame kinds and -// consumers are module constants named once. +// AST nodes, and the machine (a back-reference from what settles a promise) are data of their own; frame kinds are +// module constants named once. const requireData = (value: unknown, path: string, seen: Set): void => { if (typeof value === "function") throw new Error(`a host function at ${path}`) if (value === null || typeof value !== "object" || seen.has(value)) return @@ -80,7 +79,7 @@ const requireData = (value: unknown, path: string, seen: Set): void => if (ArrayBuffer.isView(value)) return const node = value as { type?: unknown; start?: unknown } if (typeof node.type === "string" && typeof node.start === "number") return - if (kinds.get((value as Kind).name) === value || consumers.get((value as Consumer).name) === value) return + if (kinds.get((value as Kind).name) === value) return const generator = value as { next?: unknown; throw?: unknown } if (typeof generator.next === "function" && typeof generator.throw === "function") { throw new Error(`a host generator at ${path}`) @@ -408,7 +407,6 @@ describe("built-ins as data", () => { for (const name of [ "Array callback", "Call", - "Drain", "Convert", "IteratorHelper.next", "JSON.stringify walk", @@ -416,7 +414,6 @@ describe("built-ins as data", () => { "Promise combinator", "Array sort", "PromiseReactionJob", - "JSON.parse reviver", "search", ]) { expect(seenKinds).toContain(name)