@opencode/codemode 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -26,16 +26,19 @@ export const executeProgram = (code, prepared, limits, hooks, globals) => {
|
|
|
26
26
|
const program = parseProgram(code);
|
|
27
27
|
const pending = new Pending(scope, builtins.Promise);
|
|
28
28
|
const ctx = new Interpreter({ tools, pending, builtins, logs, globals });
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
// The main job holds the turn until leftover promise work is interrupted.
|
|
30
|
+
return yield* pending.turn.hold(Effect.gen(function* () {
|
|
31
|
+
const result = (yield* toBoundary(ctx, yield* ctx.run(program))) ?? null;
|
|
32
|
+
returned = { value: result, pending };
|
|
33
|
+
const warnings = yield* pending.interrupt();
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
value: result,
|
|
37
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
38
|
+
...logged(),
|
|
39
|
+
toolCalls: tools.calls,
|
|
40
|
+
};
|
|
41
|
+
}));
|
|
39
42
|
}), (scope, exit) => Scope.close(scope, exit));
|
|
40
43
|
const timeoutMs = limits.timeoutMs;
|
|
41
44
|
const operation = timeoutMs === undefined
|
|
@@ -140,7 +140,7 @@ export const extensionGlobals = (ctx, extensions) => {
|
|
|
140
140
|
}
|
|
141
141
|
if (!(result instanceof Promise))
|
|
142
142
|
return settle(Effect.succeed(result));
|
|
143
|
-
return ctx.pending.
|
|
143
|
+
return ctx.pending.createHost(settle(Effect.tryPromise({ try: () => result, catch: (reason) => reason })));
|
|
144
144
|
});
|
|
145
145
|
return extensions.flatMap((extension) => Object.entries(extension.globals).map(([name, value]) => [name, wrap(value, name, (args) => ({ extension: extension.name, name, args }))]));
|
|
146
146
|
};
|
|
@@ -286,18 +286,24 @@ class Frame {
|
|
|
286
286
|
return value;
|
|
287
287
|
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())));
|
|
288
288
|
}
|
|
289
|
-
// Fork at the call site so admission and hooks occur when the call is made.
|
|
289
|
+
// Fork at the call site so admission and hooks occur when the call is made. Arguments cross on the caller's turn,
|
|
290
|
+
// since `toJSON` is program code; the host call runs without it, so tool calls stay concurrent.
|
|
290
291
|
createToolCallPromise(path, args) {
|
|
291
|
-
|
|
292
|
+
const pending = this.ctx.pending;
|
|
293
|
+
return pending.create(this.ctx.tool((json) => pending.turn.leave(this.ctx.tools.execute(path, json)), args));
|
|
292
294
|
}
|
|
293
295
|
// Fiber exits make settlement idempotent; yielding prevents inline continuation.
|
|
294
296
|
await(promise) {
|
|
295
297
|
const pending = this.ctx.pending;
|
|
296
298
|
return Effect.suspend(() => {
|
|
297
299
|
pending.markObserved(promise);
|
|
298
|
-
return Effect.flatMap(pending.await(promise), (exit) => Effect.
|
|
300
|
+
return Effect.flatten(pending.turn.suspend(Effect.flatMap(pending.await(promise), (exit) => Effect.as(Effect.yieldNow, exit))));
|
|
299
301
|
});
|
|
300
302
|
}
|
|
303
|
+
// End this job; the program continues in a later one, after the jobs already waiting.
|
|
304
|
+
nextJob() {
|
|
305
|
+
return this.ctx.pending.turn.suspend(Effect.yieldNow);
|
|
306
|
+
}
|
|
301
307
|
evaluateStatement(node) {
|
|
302
308
|
switch (node.type) {
|
|
303
309
|
case "ExpressionStatement":
|
|
@@ -549,7 +555,7 @@ class Frame {
|
|
|
549
555
|
const close = () => iterator
|
|
550
556
|
? self.closeIterator(iterator, node, awaiting)
|
|
551
557
|
: awaiting
|
|
552
|
-
? Effect.andThen(cursor?.close ?? Effect.void,
|
|
558
|
+
? Effect.andThen(cursor?.close ?? Effect.void, self.nextJob())
|
|
553
559
|
: (cursor?.close ?? Effect.void);
|
|
554
560
|
if (left.type === "RestElement" || left.type === "AssignmentPattern") {
|
|
555
561
|
throw typeError("Unsupported for...of binding.", left);
|
|
@@ -675,7 +681,7 @@ class Frame {
|
|
|
675
681
|
const called = yield* Effect.exit(self.call(iterator.next, iterator.iterator, [], node));
|
|
676
682
|
if (!Exit.isSuccess(called)) {
|
|
677
683
|
if (awaiting)
|
|
678
|
-
yield*
|
|
684
|
+
yield* self.nextJob();
|
|
679
685
|
return yield* Effect.failCause(called.cause);
|
|
680
686
|
}
|
|
681
687
|
const captured = yield* Effect.exit(Effect.sync(() => {
|
|
@@ -684,7 +690,7 @@ class Frame {
|
|
|
684
690
|
}));
|
|
685
691
|
if (!Exit.isSuccess(captured)) {
|
|
686
692
|
if (awaiting)
|
|
687
|
-
yield*
|
|
693
|
+
yield* self.nextJob();
|
|
688
694
|
return yield* Effect.failCause(captured.cause);
|
|
689
695
|
}
|
|
690
696
|
return {
|
|
@@ -697,8 +703,9 @@ class Frame {
|
|
|
697
703
|
}
|
|
698
704
|
closeIterator(iterator, node, awaiting = true) {
|
|
699
705
|
const close = get(iterator.iterator, "return");
|
|
700
|
-
if (close === undefined || close === null)
|
|
701
|
-
return iterator.asynchronous || !awaiting ? Effect.void :
|
|
706
|
+
if (close === undefined || close === null) {
|
|
707
|
+
return iterator.asynchronous || !awaiting ? Effect.void : this.nextJob();
|
|
708
|
+
}
|
|
702
709
|
const self = this;
|
|
703
710
|
return Effect.gen(function* () {
|
|
704
711
|
const method = self.requireIteratorMethod(close, "Iterator return", node);
|
|
@@ -709,13 +716,13 @@ class Frame {
|
|
|
709
716
|
const called = yield* Effect.exit(self.call(method, iterator.iterator, [], node));
|
|
710
717
|
if (!Exit.isSuccess(called)) {
|
|
711
718
|
if (awaiting)
|
|
712
|
-
yield*
|
|
719
|
+
yield* self.nextJob();
|
|
713
720
|
return yield* Effect.failCause(called.cause);
|
|
714
721
|
}
|
|
715
722
|
const captured = yield* Effect.exit(Effect.sync(() => get(self.requireIteratorObject(called.value, "Iterator return() result", node), "value")));
|
|
716
723
|
if (!Exit.isSuccess(captured)) {
|
|
717
724
|
if (awaiting)
|
|
718
|
-
yield*
|
|
725
|
+
yield* self.nextJob();
|
|
719
726
|
return yield* Effect.failCause(captured.cause);
|
|
720
727
|
}
|
|
721
728
|
if (awaiting)
|
|
@@ -1506,8 +1513,14 @@ class Frame {
|
|
|
1506
1513
|
invocation.generatorAsync = asynchronous;
|
|
1507
1514
|
const builtins = this.ctx.builtins;
|
|
1508
1515
|
const result = (value, done) => record(builtins.Object, { value, done });
|
|
1516
|
+
const turn = this.ctx.pending.turn;
|
|
1509
1517
|
const request = (kind, value) => {
|
|
1510
1518
|
const request = { kind, value, response: Deferred.makeUnsafe() };
|
|
1519
|
+
// A body that ran on the caller's turn has answered or suspended when it hands the turn back. An answer still
|
|
1520
|
+
// pending can only be an async generator's: its promise settles when the body answers, without the turn.
|
|
1521
|
+
const answer = Effect.suspend(() => Deferred.isDoneUnsafe(request.response)
|
|
1522
|
+
? Deferred.await(request.response)
|
|
1523
|
+
: turn.leave(Deferred.await(request.response)));
|
|
1511
1524
|
if (!asynchronous && state.active)
|
|
1512
1525
|
return Effect.die(typeError("Generator is already running."));
|
|
1513
1526
|
if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
|
|
@@ -1515,11 +1528,11 @@ class Frame {
|
|
|
1515
1528
|
state.completed = true;
|
|
1516
1529
|
state.pending.push(request);
|
|
1517
1530
|
if (state.draining)
|
|
1518
|
-
return
|
|
1531
|
+
return answer;
|
|
1519
1532
|
state.draining = true;
|
|
1520
1533
|
return Effect.andThen(this.ctx.pending.fork(invocation
|
|
1521
1534
|
.completeGeneratorRequests(state, true)
|
|
1522
|
-
.pipe(Effect.ensuring(Effect.sync(() => (state.draining = false))))),
|
|
1535
|
+
.pipe(Effect.ensuring(Effect.sync(() => (state.draining = false))))), answer);
|
|
1523
1536
|
}
|
|
1524
1537
|
if (state.completed) {
|
|
1525
1538
|
if (kind === "throw")
|
|
@@ -1534,9 +1547,10 @@ class Frame {
|
|
|
1534
1547
|
}
|
|
1535
1548
|
state.pending.push(request);
|
|
1536
1549
|
if (state.available) {
|
|
1550
|
+
// The body is parked waiting for a request: it runs this step on the caller's turn.
|
|
1537
1551
|
const available = state.available;
|
|
1538
1552
|
state.available = undefined;
|
|
1539
|
-
Deferred.doneUnsafe(available, Exit.succeed(undefined));
|
|
1553
|
+
return Effect.andThen(turn.handOff(Effect.sync(() => Deferred.doneUnsafe(available, Exit.succeed(undefined)))), answer);
|
|
1540
1554
|
}
|
|
1541
1555
|
if (!state.started) {
|
|
1542
1556
|
state.started = true;
|
|
@@ -1555,9 +1569,9 @@ class Frame {
|
|
|
1555
1569
|
yield* invocation.completeGeneratorRequests(state, asynchronous);
|
|
1556
1570
|
state.completed = true;
|
|
1557
1571
|
});
|
|
1558
|
-
return Effect.andThen(this.ctx.pending.fork(body),
|
|
1572
|
+
return Effect.andThen(this.ctx.pending.fork(body), answer);
|
|
1559
1573
|
}
|
|
1560
|
-
return
|
|
1574
|
+
return answer;
|
|
1561
1575
|
};
|
|
1562
1576
|
const proto = get(fn, "prototype");
|
|
1563
1577
|
return new GeneratorObj(proto instanceof Obj ? proto : builtins.Generator, asynchronous, request);
|
|
@@ -1588,7 +1602,7 @@ class Frame {
|
|
|
1588
1602
|
if (next)
|
|
1589
1603
|
return Effect.succeed(next);
|
|
1590
1604
|
state.available = Deferred.makeUnsafe();
|
|
1591
|
-
return Effect.andThen(Deferred.await(state.available), Effect.sync(() => this.dequeueGeneratorRequest(state)));
|
|
1605
|
+
return Effect.andThen(this.ctx.pending.turn.park(Deferred.await(state.available)), Effect.sync(() => this.dequeueGeneratorRequest(state)));
|
|
1592
1606
|
}
|
|
1593
1607
|
dequeueGeneratorRequest(state) {
|
|
1594
1608
|
const request = state.pending[state.pendingIndex];
|
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
import { Effect, Exit, Scope } from "effect";
|
|
1
|
+
import { Effect, Exit, Fiber, Scope } from "effect";
|
|
2
2
|
import type { Diagnostic } from "../codemode.js";
|
|
3
3
|
import { Obj, PromiseObj, type Value } from "./objects.js";
|
|
4
4
|
import type { Interpreter } from "./interpreter.js";
|
|
5
|
+
/**
|
|
6
|
+
* The right to run program code. JavaScript runs one job at a time: code between two awaits finishes before another
|
|
7
|
+
* job starts. Effect preempts a busy fiber so timeouts and host work still run, so only the fiber holding the turn may
|
|
8
|
+
* run program code. A synchronous call into another fiber (an async function's body up to its first await, a
|
|
9
|
+
* generator step) lends that fiber the turn and gets it back when the callee suspends or ends. An await gives the turn
|
|
10
|
+
* to the next waiting job and queues to take it back. Work that only settles a promise or runs host code leaves
|
|
11
|
+
* without it, so tool calls stay concurrent and resolvers settle in the call, as in JavaScript.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Turn {
|
|
14
|
+
private holder;
|
|
15
|
+
private readonly handbacks;
|
|
16
|
+
private first;
|
|
17
|
+
private last;
|
|
18
|
+
private nesting;
|
|
19
|
+
readonly take: Effect.Effect<void>;
|
|
20
|
+
hold<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
|
|
21
|
+
release(fiber: Fiber.Fiber<unknown, unknown>): void;
|
|
22
|
+
suspend<A, E, R>(wait: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
|
|
23
|
+
leave<A, E, R>(wait: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
|
|
24
|
+
handOff<A, E, R>(start: Effect.Effect<A, E, R>): Effect.Effect<void, E, R>;
|
|
25
|
+
park<A, E, R>(wait: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
|
|
26
|
+
fork<A, E, R>(effect: Effect.Effect<A, E, R>, scope: Scope.Scope): Effect.Effect<Fiber.Fiber<A, E>, never, R>;
|
|
27
|
+
private handBack;
|
|
28
|
+
private nest;
|
|
29
|
+
}
|
|
5
30
|
export declare class Pending<R> {
|
|
6
31
|
private readonly scope;
|
|
7
32
|
private readonly proto;
|
|
33
|
+
readonly turn: Turn;
|
|
8
34
|
private readonly active;
|
|
9
35
|
private readonly ids;
|
|
10
36
|
private readonly observed;
|
|
@@ -15,6 +41,8 @@ export declare class Pending<R> {
|
|
|
15
41
|
promise?: PromiseObj;
|
|
16
42
|
}) => Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R>;
|
|
17
43
|
create(effect: Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R>;
|
|
44
|
+
createHost(effect: Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R>;
|
|
45
|
+
private register;
|
|
18
46
|
markObserved(promise: PromiseObj): void;
|
|
19
47
|
await(promise: PromiseObj): Effect.Effect<Exit.Exit<Value, unknown>>;
|
|
20
48
|
fork(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<void, never, R>;
|
|
@@ -11,10 +11,165 @@ const capability = (ctx, name, settle) => fn(ctx.builtins, name, 1, (_, args) =>
|
|
|
11
11
|
settle(args[0]);
|
|
12
12
|
return undefined;
|
|
13
13
|
});
|
|
14
|
+
// Program fibers that synchronous starts and handbacks may nest on the host stack. Deeper ones start and resume
|
|
15
|
+
// through the scheduler, so deep recursion through async calls cannot overflow the host stack.
|
|
16
|
+
const MAX_NESTING = 100;
|
|
17
|
+
/**
|
|
18
|
+
* The right to run program code. JavaScript runs one job at a time: code between two awaits finishes before another
|
|
19
|
+
* job starts. Effect preempts a busy fiber so timeouts and host work still run, so only the fiber holding the turn may
|
|
20
|
+
* run program code. A synchronous call into another fiber (an async function's body up to its first await, a
|
|
21
|
+
* generator step) lends that fiber the turn and gets it back when the callee suspends or ends. An await gives the turn
|
|
22
|
+
* to the next waiting job and queues to take it back. Work that only settles a promise or runs host code leaves
|
|
23
|
+
* without it, so tool calls stay concurrent and resolvers settle in the call, as in JavaScript.
|
|
24
|
+
*/
|
|
25
|
+
export class Turn {
|
|
26
|
+
holder;
|
|
27
|
+
// Callers waiting for a callee to hand the turn back, innermost last.
|
|
28
|
+
handbacks = [];
|
|
29
|
+
// Jobs waiting to take the turn, in the order they became ready.
|
|
30
|
+
first;
|
|
31
|
+
last;
|
|
32
|
+
nesting = 0;
|
|
33
|
+
// Take the turn when it is free, after the jobs already waiting.
|
|
34
|
+
take = Effect.withFiber((fiber) => {
|
|
35
|
+
if (this.holder === undefined) {
|
|
36
|
+
this.holder = fiber;
|
|
37
|
+
return Effect.void;
|
|
38
|
+
}
|
|
39
|
+
return Effect.callback((resume) => {
|
|
40
|
+
const waiter = { fiber, resume, removed: false };
|
|
41
|
+
if (this.last === undefined)
|
|
42
|
+
this.first = waiter;
|
|
43
|
+
else
|
|
44
|
+
this.last.next = waiter;
|
|
45
|
+
this.last = waiter;
|
|
46
|
+
// Interrupted while waiting, or after the turn was granted but before it ran.
|
|
47
|
+
return Effect.sync(() => {
|
|
48
|
+
if (this.holder === fiber)
|
|
49
|
+
this.release(fiber);
|
|
50
|
+
else
|
|
51
|
+
waiter.removed = true;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// Run `effect` holding the turn, as the program's main job does.
|
|
56
|
+
hold(effect) {
|
|
57
|
+
return Effect.withFiber((fiber) => Effect.andThen(this.take, effect).pipe(Effect.ensuring(Effect.sync(() => this.release(fiber)))));
|
|
58
|
+
}
|
|
59
|
+
// Give up the turn, back to the innermost caller waiting for it or to the next waiting job.
|
|
60
|
+
release(fiber) {
|
|
61
|
+
if (this.holder !== fiber)
|
|
62
|
+
return;
|
|
63
|
+
const back = this.handbacks.pop();
|
|
64
|
+
if (back !== undefined) {
|
|
65
|
+
this.holder = back.fiber;
|
|
66
|
+
this.handBack(back);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let next = this.first;
|
|
70
|
+
while (next !== undefined && next.removed)
|
|
71
|
+
next = next.next;
|
|
72
|
+
this.first = next?.next;
|
|
73
|
+
if (this.first === undefined)
|
|
74
|
+
this.last = undefined;
|
|
75
|
+
this.holder = next?.fiber;
|
|
76
|
+
// Resume in the scheduler, as a yield would: release can run inside another fiber's step.
|
|
77
|
+
if (next !== undefined)
|
|
78
|
+
next.fiber.currentDispatcher.scheduleTask(() => next.resume(Effect.void), 0);
|
|
79
|
+
}
|
|
80
|
+
// Wait without the turn, then queue for it like any job that became ready. An interrupted fiber stops without it.
|
|
81
|
+
suspend(wait) {
|
|
82
|
+
return Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
|
|
83
|
+
this.release(fiber);
|
|
84
|
+
return Effect.flatMap(Effect.exit(restore(wait)), (exit) => Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) ? exit : Effect.andThen(restore(this.take), exit));
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
// Wait without the turn and finish without it, for work that only settles a promise or runs host code.
|
|
88
|
+
leave(wait) {
|
|
89
|
+
return Effect.withFiber((fiber) => {
|
|
90
|
+
this.release(fiber);
|
|
91
|
+
return wait;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Run `start`, which wakes a fiber parked with `park`, and continue once it hands the turn back.
|
|
95
|
+
handOff(start) {
|
|
96
|
+
return Effect.withFiber((fiber) => {
|
|
97
|
+
const back = { fiber, returned: false };
|
|
98
|
+
this.handbacks.push(back);
|
|
99
|
+
return Effect.andThen(this.nest(start), awaitHandback(back));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Wait without the turn for a caller to hand it over with `handOff`.
|
|
103
|
+
park(wait) {
|
|
104
|
+
return Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
|
|
105
|
+
this.release(fiber);
|
|
106
|
+
// The wake runs inside the caller's step; when that is deep, yield so the body continues on its own stack.
|
|
107
|
+
return Effect.tap(restore(wait), () => {
|
|
108
|
+
this.holder = fiber;
|
|
109
|
+
return this.nesting > MAX_NESTING ? Effect.yieldNow : Effect.void;
|
|
110
|
+
});
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
// Fork a fiber that runs program code. It starts with the caller's turn, and the caller continues once the fiber
|
|
114
|
+
// suspends or ends; the fiber gives up the turn whenever it ends.
|
|
115
|
+
fork(effect, scope) {
|
|
116
|
+
return Effect.withFiber((caller) => {
|
|
117
|
+
const back = { fiber: caller, returned: false };
|
|
118
|
+
this.handbacks.push(back);
|
|
119
|
+
let started = false;
|
|
120
|
+
const body = Effect.withFiber((fiber) => {
|
|
121
|
+
started = true;
|
|
122
|
+
this.holder = fiber;
|
|
123
|
+
return effect;
|
|
124
|
+
});
|
|
125
|
+
const forked = this.nesting < MAX_NESTING
|
|
126
|
+
? this.nest(Effect.forkIn(body, scope, { startImmediately: true }))
|
|
127
|
+
: Effect.forkIn(body, scope);
|
|
128
|
+
return Effect.flatMap(forked, (fiber) => {
|
|
129
|
+
fiber.addObserver(() => {
|
|
130
|
+
if (started)
|
|
131
|
+
return this.release(fiber);
|
|
132
|
+
// Forked into a closing scope: it never took the turn, so the caller keeps it.
|
|
133
|
+
const index = this.handbacks.lastIndexOf(back);
|
|
134
|
+
if (index >= 0)
|
|
135
|
+
this.handbacks.splice(index, 1);
|
|
136
|
+
this.handBack(back);
|
|
137
|
+
});
|
|
138
|
+
// A callee that suspends without blocking hands the turn back before `forkIn` returns.
|
|
139
|
+
return back.returned ? Effect.succeed(fiber) : Effect.as(awaitHandback(back), fiber);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Resume a parked caller in the scheduler: resumed inline, it would run to its next await and queue before the callee
|
|
144
|
+
// that just suspended has queued its own continuation.
|
|
145
|
+
handBack(back) {
|
|
146
|
+
back.returned = true;
|
|
147
|
+
const resume = back.resume;
|
|
148
|
+
if (resume !== undefined)
|
|
149
|
+
back.fiber.currentDispatcher.scheduleTask(() => resume(Effect.void), 0);
|
|
150
|
+
}
|
|
151
|
+
// Run `effect`, which can run another program fiber inside the current step, counting the nesting.
|
|
152
|
+
nest(effect) {
|
|
153
|
+
return Effect.suspend(() => {
|
|
154
|
+
this.nesting++;
|
|
155
|
+
return Effect.ensuring(effect, Effect.sync(() => {
|
|
156
|
+
this.nesting--;
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// The check runs when the caller suspends: it can be preempted after deciding to wait.
|
|
162
|
+
const awaitHandback = (back) => Effect.callback((resume) => {
|
|
163
|
+
if (back.returned)
|
|
164
|
+
resume(Effect.void);
|
|
165
|
+
else
|
|
166
|
+
back.resume = resume;
|
|
167
|
+
});
|
|
14
168
|
// Observation only controls rejection reporting; program completion interrupts all promise work.
|
|
15
169
|
export class Pending {
|
|
16
170
|
scope;
|
|
17
171
|
proto;
|
|
172
|
+
turn = new Turn();
|
|
18
173
|
active = new Set();
|
|
19
174
|
ids = new WeakMap();
|
|
20
175
|
observed = new WeakSet();
|
|
@@ -32,7 +187,15 @@ export class Pending {
|
|
|
32
187
|
return promise;
|
|
33
188
|
});
|
|
34
189
|
}
|
|
190
|
+
// A promise whose work runs program code, on the turn. Host work that runs no program code uses `createHost`.
|
|
35
191
|
create(effect) {
|
|
192
|
+
return this.register(effect, (body) => this.turn.fork(body, this.scope));
|
|
193
|
+
}
|
|
194
|
+
// Starts now, without the turn, so host work stays concurrent and attaches to host promises at once.
|
|
195
|
+
createHost(effect) {
|
|
196
|
+
return this.register(effect, (body) => Effect.forkIn(body, this.scope, { startImmediately: true }));
|
|
197
|
+
}
|
|
198
|
+
register(effect, fork) {
|
|
36
199
|
return Effect.flatMap(CallSite, (site) => {
|
|
37
200
|
if (this.active.size >= MAX_PENDING_PROMISES) {
|
|
38
201
|
throw rangeError(`Too many pending promises (limit ${MAX_PENDING_PROMISES}); await promises before creating more.`);
|
|
@@ -40,7 +203,7 @@ export class Pending {
|
|
|
40
203
|
// Allocate before forking so reruns get distinct IDs and diagnostics retain creation order.
|
|
41
204
|
const id = this.nextID++;
|
|
42
205
|
const body = Effect.catchDefect(effect, (defect) => Effect.die(locate(defect, site.node)));
|
|
43
|
-
return Effect.map(
|
|
206
|
+
return Effect.map(fork(body), (fiber) => {
|
|
44
207
|
const promise = new PromiseObj(this.proto, fiber);
|
|
45
208
|
this.active.add(promise);
|
|
46
209
|
this.ids.set(promise, id);
|
|
@@ -72,7 +235,7 @@ export class Pending {
|
|
|
72
235
|
return Fiber.await(promise.fiber);
|
|
73
236
|
}
|
|
74
237
|
fork(effect) {
|
|
75
|
-
return Effect.asVoid(
|
|
238
|
+
return Effect.asVoid(this.turn.fork(effect, this.scope));
|
|
76
239
|
}
|
|
77
240
|
diagnostics() {
|
|
78
241
|
return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure);
|
|
@@ -101,7 +264,7 @@ export const resolvePromiseValue = (ctx, value, own) => {
|
|
|
101
264
|
return Effect.succeed(value);
|
|
102
265
|
return Effect.gen(function* () {
|
|
103
266
|
// Promise resolution invokes a thenable's method in a later job.
|
|
104
|
-
yield* Effect.yieldNow;
|
|
267
|
+
yield* ctx.pending.turn.suspend(Effect.yieldNow);
|
|
105
268
|
const deferred = Deferred.makeUnsafe();
|
|
106
269
|
const resolve = capability(ctx, "resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result)));
|
|
107
270
|
const reject = capability(ctx, "reject", (reason) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(reason))));
|
|
@@ -111,7 +274,7 @@ export const resolvePromiseValue = (ctx, value, own) => {
|
|
|
111
274
|
return yield* Effect.failCause(executed.cause);
|
|
112
275
|
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)));
|
|
113
276
|
}
|
|
114
|
-
return yield* resolvePromiseValue(ctx, yield* Deferred.await(deferred), own);
|
|
277
|
+
return yield* resolvePromiseValue(ctx, yield* ctx.pending.turn.leave(Deferred.await(deferred)), own);
|
|
115
278
|
});
|
|
116
279
|
};
|
|
117
280
|
export const resolvePromise = (ctx, value) => {
|
|
@@ -157,31 +320,33 @@ const invokePromiseMethod = (ctx, name, args) => {
|
|
|
157
320
|
items.push(item);
|
|
158
321
|
}
|
|
159
322
|
if (name === "all") {
|
|
160
|
-
return new Arr(ctx.builtins.Array, yield* settleAfterTurn(Effect.all(items.map((item) => Effect.flatten(ctx.pending.await(item))), { concurrency: "unbounded" })));
|
|
323
|
+
return new Arr(ctx.builtins.Array, yield* settleAfterTurn(ctx, Effect.all(items.map((item) => Effect.flatten(ctx.pending.await(item))), { concurrency: "unbounded" })));
|
|
161
324
|
}
|
|
162
325
|
if (name === "allSettled") {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
326
|
+
return yield* ctx.pending.turn.suspend(Effect.gen(function* () {
|
|
327
|
+
const outcomes = [];
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
const exit = yield* ctx.pending.await(item);
|
|
330
|
+
if (Exit.isSuccess(exit)) {
|
|
331
|
+
outcomes.push(record(ctx.builtins.Object, { status: "fulfilled", value: exit.value }));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (Cause.hasInterruptsOnly(exit.cause))
|
|
335
|
+
return yield* Effect.failCause(exit.cause);
|
|
336
|
+
outcomes.push(record(ctx.builtins.Object, {
|
|
337
|
+
status: "rejected",
|
|
338
|
+
reason: materialize(ctx, Cause.squash(exit.cause)),
|
|
339
|
+
}));
|
|
169
340
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
status: "rejected",
|
|
174
|
-
reason: materialize(ctx, Cause.squash(exit.cause)),
|
|
175
|
-
}));
|
|
176
|
-
}
|
|
177
|
-
yield* Effect.yieldNow;
|
|
178
|
-
return new Arr(ctx.builtins.Array, outcomes);
|
|
341
|
+
yield* Effect.yieldNow;
|
|
342
|
+
return new Arr(ctx.builtins.Array, outcomes);
|
|
343
|
+
}));
|
|
179
344
|
}
|
|
180
345
|
if (name === "race") {
|
|
181
346
|
if (items.length === 0) {
|
|
182
347
|
throw typeError("Promise.race([]) would never settle; provide at least one promise or value.");
|
|
183
348
|
}
|
|
184
|
-
return yield* settleAfterTurn(Effect.flatten(Effect.raceAll(items.map((item) => ctx.pending.await(item)))));
|
|
349
|
+
return yield* settleAfterTurn(ctx, Effect.flatten(Effect.raceAll(items.map((item) => ctx.pending.await(item)))));
|
|
185
350
|
}
|
|
186
351
|
const flipped = items.map((item) => Effect.flatMap(ctx.pending.await(item), (exit) => {
|
|
187
352
|
if (Exit.isSuccess(exit))
|
|
@@ -190,7 +355,7 @@ const invokePromiseMethod = (ctx, name, args) => {
|
|
|
190
355
|
return Effect.failCause(exit.cause);
|
|
191
356
|
return Effect.succeed(materialize(ctx, Cause.squash(exit.cause)));
|
|
192
357
|
}));
|
|
193
|
-
return yield* settleAfterTurn(Effect.all(flipped, { concurrency: "unbounded" }).pipe(Effect.flatMap((reasons) => Effect.fail(new Throw(createAggregateErrorValue(ctx, reasons, "All promises were rejected")))), Effect.catch((error) => error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error))));
|
|
358
|
+
return yield* settleAfterTurn(ctx, Effect.all(flipped, { concurrency: "unbounded" }).pipe(Effect.flatMap((reasons) => Effect.fail(new Throw(createAggregateErrorValue(ctx, reasons, "All promises were rejected")))), Effect.catch((error) => error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error))));
|
|
194
359
|
}));
|
|
195
360
|
};
|
|
196
361
|
const instanceMethod = (ctx, name, thisValue, args) => {
|
|
@@ -207,7 +372,7 @@ const instanceMethod = (ctx, name, thisValue, args) => {
|
|
|
207
372
|
/** NewPromiseCapability: a pending promise with the resolve/reject callables that settle it exactly once. */
|
|
208
373
|
const promiseCapability = (ctx) => Effect.gen(function* () {
|
|
209
374
|
const deferred = Deferred.makeUnsafe();
|
|
210
|
-
const promise = yield* ctx.pending.createWithSelf((self) => Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(ctx, value, self)));
|
|
375
|
+
const promise = yield* ctx.pending.createWithSelf((self) => Effect.flatMap(ctx.pending.turn.leave(Deferred.await(deferred)), (value) => resolvePromiseValue(ctx, value, self)));
|
|
211
376
|
const resolve = capability(ctx, "resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)));
|
|
212
377
|
const reject = capability(ctx, "reject", (value) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(value))));
|
|
213
378
|
return { promise, resolve, reject, deferred };
|
|
@@ -227,8 +392,9 @@ const constructPromise = (ctx, executor) => {
|
|
|
227
392
|
return made.promise;
|
|
228
393
|
});
|
|
229
394
|
};
|
|
230
|
-
// Settle one reaction turn after the deciding member, after its existing reactions.
|
|
231
|
-
|
|
395
|
+
// Settle one reaction turn after the deciding member, after its existing reactions. That reaction is a job: it queues
|
|
396
|
+
// for the turn behind the jobs ready before it and settles holding it.
|
|
397
|
+
const settleAfterTurn = (ctx, body) => Effect.flatten(ctx.pending.turn.suspend(Effect.flatMap(Effect.exit(body), (exit) => Effect.as(Effect.yieldNow, exit))));
|
|
232
398
|
class PromiseAnyFulfilled {
|
|
233
399
|
value;
|
|
234
400
|
constructor(value) {
|
|
@@ -244,13 +410,13 @@ const reactionHandler = (value, method) => {
|
|
|
244
410
|
return undefined;
|
|
245
411
|
};
|
|
246
412
|
// Teardown bypasses handlers; settled reactions yield once so handlers never run inline.
|
|
247
|
-
const reactionExit = (ctx, source) => Effect.gen(function* () {
|
|
413
|
+
const reactionExit = (ctx, source) => ctx.pending.turn.suspend(Effect.gen(function* () {
|
|
248
414
|
const exit = yield* ctx.pending.await(source);
|
|
249
415
|
if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause))
|
|
250
416
|
return yield* Effect.failCause(exit.cause);
|
|
251
417
|
yield* Effect.yieldNow;
|
|
252
418
|
return exit;
|
|
253
|
-
});
|
|
419
|
+
}));
|
|
254
420
|
const chainReaction = (ctx, source, onFulfilled, onRejected, method) => {
|
|
255
421
|
return ctx.pending.createWithSelf((self) => Effect.gen(function* () {
|
|
256
422
|
const exit = yield* reactionExit(ctx, source);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode/codemode",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.3",
|
|
5
5
|
"description": "Effect-native confined code execution over schema-described tools",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@tsconfig/bun": "1.0.9",
|
|
36
|
-
"@types/bun": "1.4.
|
|
36
|
+
"@types/bun": "1.4.3",
|
|
37
37
|
"@typescript/native": "npm:typescript@7.0.2"
|
|
38
38
|
}
|
|
39
39
|
}
|