refactor(codemode): move iterator consumers, Iterator.from, the JSON reviver, and string replace callbacks to the prelude; drop Drain and consumers

This commit is contained in:
James Long
2026-11-05 15:28:31 +00:00
Unverified
parent fa7b94f2fe
commit 9cc8ec9b86
12 changed files with 131 additions and 437 deletions
+3 -8
View File
@@ -66,27 +66,22 @@ export const kinds = new Map<string, Kind>()
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, "unwind" | "cut" | "yielded" | "counts" | "locates">,
): 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)
+3 -12
View File
@@ -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"
+1 -37
View File
@@ -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<Value>
readonly take?: (frame: Frame, value: Value, index: number) => void
readonly each?: (task: MachineTask, frame: Frame, value: Value, index: number) => Out<unknown>
readonly resume?: (task: MachineTask, frame: Frame, input: unknown, index: number) => Out<unknown>
readonly done: (task: MachineTask, frame: Frame) => Out<Value>
}
/** Every consumer, by name, so a saved `Drain` frame can name its consumer. */
export const consumers = new Map<string, Consumer>()
/** 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
-128
View File
@@ -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<unknown>) =>
[consumer, source, undefined, 0, 0, ...state] as Array<unknown>
/** `drain`'s slots for an iterator already open (GetIteratorDirect). */
export const drainOpen = (consumer: Consumer, iterator: Iter, state: Array<unknown>) =>
[consumer, undefined, iterator, 0, 0, ...state] as Array<unknown>
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<Value> {
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))
}
+11 -4
View File
@@ -15,6 +15,8 @@ import {
data,
define,
hidden,
hasPrototype,
keys,
getOwn,
IteratorObj,
positionIterator,
@@ -83,6 +85,11 @@ export const helpers = (ctx: Interpreter<unknown>): Array<readonly [string, Valu
},
],
$Describe: [1, ([value]) => 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<unknown>): Array<readonly [string, Valu
buildPairs({ kind: kind as "Headers", proto: proto as Obj }, value, String(label)),
],
$IsCallable: [1, ([value]) => 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)),
],
+86 -1
View File
@@ -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
}
`
+3 -12
View File
@@ -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<Json> | { 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<string, symbol>([
["iterator", IteratorSymbol],
["asyncIterator", AsyncIteratorSymbol],
["pending", PENDING],
["exit", EXIT],
["optionalShortCircuit", OptionalShortCircuit],
["slice", Slice],
["deleted", deleted],
@@ -308,16 +306,14 @@ const errorClasses = new Map<string, Function>([
const errorNames = new Map([...errorClasses].map(([name, ctor]) => [ctor, name]))
let kindNames: Map<object, string> | undefined
let consumerNames: Map<object, string> | 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}.`)
+3 -128
View File
@@ -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 = <R>(ctx: Interpreter<R>) => {
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<Value>) => {
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 = <R>(ctx: Interpreter<R>) => {
["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 = <R>(ctx: Interpreter<R>) => {
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<Value>).push(value)
},
done: (task, frame) => new Arr(task.ctx.builtins.Array, (frame.slots[RESULT] ?? []) as Array<Value>),
})
// 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)
+3 -61
View File
@@ -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 = <R>(ctx: Interpreter<R>) => {
const json = new Obj(ctx.builtins.Object)
@@ -41,54 +28,9 @@ const parse = <R>(ctx: Interpreter<R>, args: Array<Value>) => {
})()
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<string>
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<Revival>
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 = <R>(ctx: Interpreter<R>, args: Array<Value>) => {
const space = args[2]
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
+12 -34
View File
@@ -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 = <R>(
value: string,
name: "replace" | "replaceAll",
args: Array<Value>,
): Push => {
): Invoke => {
const builtins = ctx.builtins
const callback = requireCallback(args[1], `String.${name}`)
const matches: Array<Match> = []
@@ -78,40 +76,20 @@ const replaceWithCallback = <R>(
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<Value> }
// 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<Match>
const output = slots[4] as Array<string>
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 = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const string = constructor(builtins, builtins.String, {
+3 -6
View File
@@ -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<string>) => {
}
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 () => {
+3 -6
View File
@@ -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<unknown>): 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<unknown>): 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)