@neurocode-ai/codemode 1.18.8
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.
- package/AGENTS.md +23 -0
- package/README.md +369 -0
- package/codemode.md +176 -0
- package/package.json +25 -0
- package/src/codemode.ts +159 -0
- package/src/index.ts +4 -0
- package/src/interpreter/model.ts +201 -0
- package/src/interpreter/runtime.ts +3465 -0
- package/src/openapi/TODO.md +19 -0
- package/src/openapi/index.ts +130 -0
- package/src/openapi/runtime.ts +326 -0
- package/src/openapi/spec.ts +511 -0
- package/src/openapi/types.ts +112 -0
- package/src/stdlib/collections.ts +51 -0
- package/src/stdlib/console.ts +4 -0
- package/src/stdlib/date.ts +94 -0
- package/src/stdlib/json.ts +42 -0
- package/src/stdlib/math.ts +65 -0
- package/src/stdlib/number.ts +66 -0
- package/src/stdlib/object.ts +77 -0
- package/src/stdlib/promise.ts +6 -0
- package/src/stdlib/regexp.ts +74 -0
- package/src/stdlib/string.ts +52 -0
- package/src/stdlib/url.ts +90 -0
- package/src/stdlib/value.ts +86 -0
- package/src/tool-error.ts +11 -0
- package/src/tool-runtime.ts +806 -0
- package/src/tool-schema.ts +301 -0
- package/src/tool.ts +96 -0
- package/src/values.ts +49 -0
- package/sst-env.d.ts +10 -0
- package/test/codemode.test.ts +1163 -0
- package/test/enumeration.test.ts +159 -0
- package/test/fixtures/openapi-happy-path.json +230 -0
- package/test/fixtures/opencode-v2-openapi.json +23730 -0
- package/test/openapi.test.ts +964 -0
- package/test/parity.test.ts +425 -0
- package/test/promise.test.ts +456 -0
- package/test/signature.test.ts +449 -0
- package/test/stdlib.test.ts +715 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,3465 @@
|
|
|
1
|
+
import { parse } from "acorn"
|
|
2
|
+
import { Cause, Effect, Exit, Fiber, Semaphore } from "effect"
|
|
3
|
+
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
|
|
4
|
+
import {
|
|
5
|
+
copyIn,
|
|
6
|
+
copyOut,
|
|
7
|
+
isBlockedMember,
|
|
8
|
+
ToolReference,
|
|
9
|
+
ToolRuntime,
|
|
10
|
+
ToolRuntimeError,
|
|
11
|
+
type HostTools,
|
|
12
|
+
type SafeObject,
|
|
13
|
+
type Services,
|
|
14
|
+
} from "../tool-runtime.js"
|
|
15
|
+
import { ToolError } from "../tool-error.js"
|
|
16
|
+
import type {
|
|
17
|
+
DataValue,
|
|
18
|
+
Diagnostic,
|
|
19
|
+
DiagnosticKind,
|
|
20
|
+
ExecuteOptions,
|
|
21
|
+
ResolvedExecutionLimits,
|
|
22
|
+
Result,
|
|
23
|
+
} from "../codemode.js"
|
|
24
|
+
import {
|
|
25
|
+
type AstNode,
|
|
26
|
+
asNode,
|
|
27
|
+
type Binding,
|
|
28
|
+
CodeModeFunction,
|
|
29
|
+
CoercionFunction,
|
|
30
|
+
ComputedValue,
|
|
31
|
+
ErrorConstructorReference,
|
|
32
|
+
GlobalMethodReference,
|
|
33
|
+
GlobalNamespace,
|
|
34
|
+
type GlobalNamespaceName,
|
|
35
|
+
formatLocation,
|
|
36
|
+
getArray,
|
|
37
|
+
getBoolean,
|
|
38
|
+
getNode,
|
|
39
|
+
getOptionalNode,
|
|
40
|
+
getString,
|
|
41
|
+
IntrinsicReference,
|
|
42
|
+
InterpreterRuntimeError,
|
|
43
|
+
isRecord,
|
|
44
|
+
type MemberReference,
|
|
45
|
+
OptionalShortCircuit,
|
|
46
|
+
PromiseMethodReference,
|
|
47
|
+
type PromiseMethodName,
|
|
48
|
+
PromiseNamespace,
|
|
49
|
+
ProgramThrow,
|
|
50
|
+
type ProgramNode,
|
|
51
|
+
type StatementResult,
|
|
52
|
+
sourceLocation,
|
|
53
|
+
supportedSyntaxMessage,
|
|
54
|
+
unsupportedSyntax,
|
|
55
|
+
UriFunction,
|
|
56
|
+
} from "./model.js"
|
|
57
|
+
import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js"
|
|
58
|
+
import { consoleMethods, MAX_CONSOLE_DEPTH } from "../stdlib/console.js"
|
|
59
|
+
import { dateMethods, dateStatics, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
|
|
60
|
+
import { invokeJsonMethod } from "../stdlib/json.js"
|
|
61
|
+
import { invokeMathMethod, mathConstants } from "../stdlib/math.js"
|
|
62
|
+
import {
|
|
63
|
+
invokeNumberMethod,
|
|
64
|
+
invokeNumberStatic,
|
|
65
|
+
numberConstants,
|
|
66
|
+
numberMethods,
|
|
67
|
+
numberStatics,
|
|
68
|
+
} from "../stdlib/number.js"
|
|
69
|
+
import { invokeObjectMethod } from "../stdlib/object.js"
|
|
70
|
+
import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js"
|
|
71
|
+
import {
|
|
72
|
+
escapeRegexHint,
|
|
73
|
+
invokeRegExpMethod,
|
|
74
|
+
matchToValue,
|
|
75
|
+
regexpMethods,
|
|
76
|
+
regexpProperties,
|
|
77
|
+
regexFailureReason,
|
|
78
|
+
toHostRegex,
|
|
79
|
+
} from "../stdlib/regexp.js"
|
|
80
|
+
import { invokeStringStatic, stringMethods, stringStatics } from "../stdlib/string.js"
|
|
81
|
+
import {
|
|
82
|
+
urlMethods,
|
|
83
|
+
urlProperties,
|
|
84
|
+
urlSearchParamsMethods,
|
|
85
|
+
urlStatics,
|
|
86
|
+
urlWritableProperties,
|
|
87
|
+
invokeUriFunction,
|
|
88
|
+
invokeURLMethod,
|
|
89
|
+
invokeURLStatic,
|
|
90
|
+
uriArgument,
|
|
91
|
+
urlArgument,
|
|
92
|
+
} from "../stdlib/url.js"
|
|
93
|
+
import {
|
|
94
|
+
boundedData,
|
|
95
|
+
coerceToNumber,
|
|
96
|
+
coerceToString,
|
|
97
|
+
compoundOperators,
|
|
98
|
+
createErrorValue,
|
|
99
|
+
errorBrandName,
|
|
100
|
+
errorConstructors,
|
|
101
|
+
invokeCoercion,
|
|
102
|
+
valueConstructors,
|
|
103
|
+
} from "../stdlib/value.js"
|
|
104
|
+
import {
|
|
105
|
+
isSandboxValue,
|
|
106
|
+
SandboxDate,
|
|
107
|
+
SandboxMap,
|
|
108
|
+
SandboxPromise,
|
|
109
|
+
SandboxRegExp,
|
|
110
|
+
SandboxSet,
|
|
111
|
+
SandboxURL,
|
|
112
|
+
SandboxURLSearchParams,
|
|
113
|
+
} from "../values.js"
|
|
114
|
+
|
|
115
|
+
const parseProgram = (code: string): ProgramNode => {
|
|
116
|
+
const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, {
|
|
117
|
+
reportDiagnostics: true,
|
|
118
|
+
compilerOptions: {
|
|
119
|
+
target: ScriptTarget.ESNext,
|
|
120
|
+
module: ModuleKind.ESNext,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
|
|
124
|
+
|
|
125
|
+
if (diagnostic) {
|
|
126
|
+
throw new InterpreterRuntimeError(
|
|
127
|
+
`Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
|
|
128
|
+
undefined,
|
|
129
|
+
"ParseError",
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const bodyStart = transpiled.outputText.indexOf("{") + 1
|
|
134
|
+
const bodyEnd = transpiled.outputText.lastIndexOf("}")
|
|
135
|
+
const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd)
|
|
136
|
+
const parsed = parse(executableCode, {
|
|
137
|
+
ecmaVersion: "latest",
|
|
138
|
+
sourceType: "script",
|
|
139
|
+
allowReturnOutsideFunction: true,
|
|
140
|
+
allowAwaitOutsideFunction: true,
|
|
141
|
+
locations: true,
|
|
142
|
+
}) as unknown
|
|
143
|
+
|
|
144
|
+
if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) {
|
|
145
|
+
throw new InterpreterRuntimeError("Failed to parse script as a Program node.")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parsed as ProgramNode
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const publicErrorMessage = (message: string): string =>
|
|
152
|
+
message.replace(/\/(?:Users|home|private|tmp|var\/folders)\/[^\s"'`]+/g, "<redacted-path>")
|
|
153
|
+
|
|
154
|
+
const normalizeError = (error: unknown): Diagnostic => {
|
|
155
|
+
if (error instanceof InterpreterRuntimeError) {
|
|
156
|
+
return {
|
|
157
|
+
kind: error.kind,
|
|
158
|
+
message: `${error.message}${formatLocation(error.node)}`,
|
|
159
|
+
...(error.node?.loc ? { location: sourceLocation(error.node) } : {}),
|
|
160
|
+
...(error.suggestions ? { suggestions: error.suggestions } : {}),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (error instanceof ToolRuntimeError) {
|
|
165
|
+
return {
|
|
166
|
+
kind: error.kind,
|
|
167
|
+
message: error.message,
|
|
168
|
+
...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (error instanceof ToolError) {
|
|
173
|
+
return { kind: "ToolFailure", message: publicErrorMessage(error.message) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (error instanceof ProgramThrow) {
|
|
177
|
+
const value = error.value
|
|
178
|
+
let message: string
|
|
179
|
+
if (containsRuntimeReference(value)) {
|
|
180
|
+
// A thrown tool/function reference must not leak its internal structure.
|
|
181
|
+
message = "a non-data value"
|
|
182
|
+
} else if (typeof value === "string") {
|
|
183
|
+
message = value
|
|
184
|
+
} else if (
|
|
185
|
+
value !== null &&
|
|
186
|
+
typeof value === "object" &&
|
|
187
|
+
typeof (value as { message?: unknown }).message === "string"
|
|
188
|
+
) {
|
|
189
|
+
message = (value as { message: string }).message
|
|
190
|
+
} else {
|
|
191
|
+
try {
|
|
192
|
+
message = JSON.stringify(copyOut(value)) ?? String(value)
|
|
193
|
+
} catch {
|
|
194
|
+
message = String(value)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { kind: "ExecutionFailure", message: `Uncaught: ${message}` }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) {
|
|
201
|
+
return {
|
|
202
|
+
kind: "ExecutionFailure",
|
|
203
|
+
message: "Execution exceeded the maximum nesting depth.",
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (error instanceof Error) {
|
|
208
|
+
return {
|
|
209
|
+
kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure",
|
|
210
|
+
message: publicErrorMessage(error.message),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// A non-Error thrown by a host tool (raw string / number / Symbol) still routes through
|
|
215
|
+
// path redaction so filesystem paths can never leak through the catch-all branch.
|
|
216
|
+
return {
|
|
217
|
+
kind: "ExecutionFailure",
|
|
218
|
+
message: publicErrorMessage(String(error)),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers.
|
|
223
|
+
const caughtErrorValue = (thrown: unknown): unknown => {
|
|
224
|
+
if (thrown instanceof ProgramThrow) return thrown.value
|
|
225
|
+
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
|
|
226
|
+
const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error"
|
|
227
|
+
return createErrorValue(name, normalizeError(thrown).message)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const isRuntimeReference = (value: unknown): boolean =>
|
|
231
|
+
value instanceof CodeModeFunction ||
|
|
232
|
+
value instanceof ToolReference ||
|
|
233
|
+
value instanceof IntrinsicReference ||
|
|
234
|
+
value instanceof GlobalNamespace ||
|
|
235
|
+
value instanceof GlobalMethodReference ||
|
|
236
|
+
value instanceof PromiseNamespace ||
|
|
237
|
+
value instanceof PromiseMethodReference ||
|
|
238
|
+
value instanceof SandboxPromise ||
|
|
239
|
+
value instanceof CoercionFunction ||
|
|
240
|
+
value instanceof UriFunction ||
|
|
241
|
+
value instanceof ErrorConstructorReference ||
|
|
242
|
+
isSandboxValue(value)
|
|
243
|
+
|
|
244
|
+
const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => {
|
|
245
|
+
if (isRuntimeReference(value)) return true
|
|
246
|
+
if (value === null || typeof value !== "object") return false
|
|
247
|
+
if (seen.has(value)) return false
|
|
248
|
+
seen.add(value)
|
|
249
|
+
const contains = Array.isArray(value)
|
|
250
|
+
? value.some((item) => containsRuntimeReference(item, seen))
|
|
251
|
+
: Object.values(value).some((item) => containsRuntimeReference(item, seen))
|
|
252
|
+
seen.delete(value)
|
|
253
|
+
return contains
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Like containsRuntimeReference, but sandbox standard-library values count as data:
|
|
257
|
+
// operators and switch treat them as ordinary object operands (identity equality, ToPrimitive
|
|
258
|
+
// coercion) rather than rejecting them as opaque interpreter machinery.
|
|
259
|
+
const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => {
|
|
260
|
+
if (isSandboxValue(value)) return false
|
|
261
|
+
if (isRuntimeReference(value)) return true
|
|
262
|
+
if (value === null || typeof value !== "object") return false
|
|
263
|
+
if (seen.has(value)) return false
|
|
264
|
+
seen.add(value)
|
|
265
|
+
const contains = Array.isArray(value)
|
|
266
|
+
? value.some((item) => containsOpaqueReference(item, seen))
|
|
267
|
+
: Object.values(value).some((item) => containsOpaqueReference(item, seen))
|
|
268
|
+
seen.delete(value)
|
|
269
|
+
return contains
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// `typeof` never throws in JS; map every interpreter value to its JS-visible category.
|
|
273
|
+
// A SandboxPromise falls through to the final `typeof value` and reports "object", exactly
|
|
274
|
+
// like a real JS promise.
|
|
275
|
+
const typeofValue = (value: unknown): string => {
|
|
276
|
+
if (
|
|
277
|
+
value instanceof CodeModeFunction ||
|
|
278
|
+
value instanceof CoercionFunction ||
|
|
279
|
+
value instanceof IntrinsicReference ||
|
|
280
|
+
value instanceof GlobalMethodReference ||
|
|
281
|
+
value instanceof PromiseMethodReference ||
|
|
282
|
+
value instanceof PromiseNamespace ||
|
|
283
|
+
value instanceof ErrorConstructorReference
|
|
284
|
+
)
|
|
285
|
+
return "function"
|
|
286
|
+
if (value instanceof UriFunction) return "function"
|
|
287
|
+
if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
|
|
288
|
+
if (value instanceof GlobalNamespace) {
|
|
289
|
+
return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function"
|
|
290
|
+
}
|
|
291
|
+
return typeof value
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// `x instanceof C` against the constructors CodeMode knows. Like `typeof`, it observes any
|
|
295
|
+
// left-hand value (opaque references included) without coercing it. Error checks use the
|
|
296
|
+
// error brand: `instanceof Error` accepts every branded error; a specific error type matches
|
|
297
|
+
// its own brand only (as in JS, where TypeError instances are also Error instances).
|
|
298
|
+
const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
|
|
299
|
+
if (rhs instanceof ErrorConstructorReference) {
|
|
300
|
+
const brand = errorBrandName(lhs)
|
|
301
|
+
return brand !== undefined && (rhs.name === "Error" || brand === rhs.name)
|
|
302
|
+
}
|
|
303
|
+
if (rhs instanceof GlobalNamespace) {
|
|
304
|
+
switch (rhs.name) {
|
|
305
|
+
case "Date":
|
|
306
|
+
return lhs instanceof SandboxDate
|
|
307
|
+
case "RegExp":
|
|
308
|
+
return lhs instanceof SandboxRegExp
|
|
309
|
+
case "Map":
|
|
310
|
+
return lhs instanceof SandboxMap
|
|
311
|
+
case "Set":
|
|
312
|
+
return lhs instanceof SandboxSet
|
|
313
|
+
case "URL":
|
|
314
|
+
return lhs instanceof SandboxURL
|
|
315
|
+
case "URLSearchParams":
|
|
316
|
+
return lhs instanceof SandboxURLSearchParams
|
|
317
|
+
case "Array":
|
|
318
|
+
return Array.isArray(lhs)
|
|
319
|
+
case "Object":
|
|
320
|
+
return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function")
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise
|
|
324
|
+
// Number/String/Boolean wrap primitives in JS; no boxed values exist in CodeMode, so
|
|
325
|
+
// `x instanceof Number` is always false - exactly what it is for primitives in JS.
|
|
326
|
+
if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) {
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
throw new InterpreterRuntimeError(
|
|
330
|
+
"The right-hand side of 'instanceof' must be a constructor CodeMode knows: Error (or a specific error type like TypeError), Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, or Promise.",
|
|
331
|
+
node,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const invokeStringMethod = (value: string, name: string, args: Array<unknown>, node: AstNode): unknown => {
|
|
336
|
+
const str = (index: number): string => {
|
|
337
|
+
const arg = args[index]
|
|
338
|
+
if (typeof arg !== "string")
|
|
339
|
+
throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node)
|
|
340
|
+
return arg
|
|
341
|
+
}
|
|
342
|
+
const num = (index: number): number => {
|
|
343
|
+
const arg = args[index]
|
|
344
|
+
if (typeof arg !== "number")
|
|
345
|
+
throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node)
|
|
346
|
+
return arg
|
|
347
|
+
}
|
|
348
|
+
const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index))
|
|
349
|
+
const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index))
|
|
350
|
+
|
|
351
|
+
let result: unknown
|
|
352
|
+
switch (name) {
|
|
353
|
+
case "toLowerCase":
|
|
354
|
+
result = value.toLowerCase()
|
|
355
|
+
break
|
|
356
|
+
case "toUpperCase":
|
|
357
|
+
result = value.toUpperCase()
|
|
358
|
+
break
|
|
359
|
+
case "trim":
|
|
360
|
+
result = value.trim()
|
|
361
|
+
break
|
|
362
|
+
// trimLeft/trimRight are the legacy aliases of trimStart/trimEnd, kept because models write them.
|
|
363
|
+
case "trimStart":
|
|
364
|
+
case "trimLeft":
|
|
365
|
+
result = value.trimStart()
|
|
366
|
+
break
|
|
367
|
+
case "trimEnd":
|
|
368
|
+
case "trimRight":
|
|
369
|
+
result = value.trimEnd()
|
|
370
|
+
break
|
|
371
|
+
// Locale/options arguments are ignored: comparison runs with the host default locale, and
|
|
372
|
+
// the common use is a sort comparator where any consistent order works.
|
|
373
|
+
case "localeCompare":
|
|
374
|
+
result = value.localeCompare(str(0))
|
|
375
|
+
break
|
|
376
|
+
case "normalize": {
|
|
377
|
+
const form = optStr(0)
|
|
378
|
+
try {
|
|
379
|
+
result = value.normalize(form)
|
|
380
|
+
} catch {
|
|
381
|
+
throw new InterpreterRuntimeError(
|
|
382
|
+
`String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`,
|
|
383
|
+
node,
|
|
384
|
+
).as("RangeError")
|
|
385
|
+
}
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
case "split": {
|
|
389
|
+
if (args.length === 0) {
|
|
390
|
+
result = [value]
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
if (args[0] instanceof SandboxRegExp) {
|
|
394
|
+
result = value.split((args[0] as SandboxRegExp).regex, optNum(1))
|
|
395
|
+
break
|
|
396
|
+
}
|
|
397
|
+
const requestedLimit = optNum(1)
|
|
398
|
+
result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0)
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
case "slice":
|
|
402
|
+
result = value.slice(optNum(0), optNum(1))
|
|
403
|
+
break
|
|
404
|
+
case "includes":
|
|
405
|
+
result = value.includes(str(0), optNum(1))
|
|
406
|
+
break
|
|
407
|
+
case "startsWith":
|
|
408
|
+
result = value.startsWith(str(0), optNum(1))
|
|
409
|
+
break
|
|
410
|
+
case "endsWith":
|
|
411
|
+
result = value.endsWith(str(0), optNum(1))
|
|
412
|
+
break
|
|
413
|
+
case "indexOf":
|
|
414
|
+
result = value.indexOf(str(0), optNum(1))
|
|
415
|
+
break
|
|
416
|
+
case "lastIndexOf":
|
|
417
|
+
result = value.lastIndexOf(str(0), optNum(1))
|
|
418
|
+
break
|
|
419
|
+
case "replace":
|
|
420
|
+
case "replaceAll": {
|
|
421
|
+
if (args[0] instanceof SandboxRegExp) {
|
|
422
|
+
const pattern = (args[0] as SandboxRegExp).regex
|
|
423
|
+
const replacement = str(1)
|
|
424
|
+
if (name === "replaceAll" && !pattern.global) {
|
|
425
|
+
throw new InterpreterRuntimeError(
|
|
426
|
+
`String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`,
|
|
427
|
+
node,
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement)
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
if (name === "replace") {
|
|
434
|
+
result = value.replace(str(0), str(1))
|
|
435
|
+
break
|
|
436
|
+
}
|
|
437
|
+
result = value.replaceAll(str(0), str(1))
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
case "match": {
|
|
441
|
+
const pattern = toHostRegex(args[0], name, node)
|
|
442
|
+
const matched = value.match(pattern)
|
|
443
|
+
if (matched === null) return null
|
|
444
|
+
// A global match is a plain array of matched strings; a non-global match carries
|
|
445
|
+
// index/groups own properties, so bypass the copying data checkpoint to keep them.
|
|
446
|
+
if (pattern.global) return boundedData(matched, "String.match result")
|
|
447
|
+
return matchToValue(matched)
|
|
448
|
+
}
|
|
449
|
+
case "matchAll": {
|
|
450
|
+
const pattern = toHostRegex(args[0], name, node, "g")
|
|
451
|
+
if (!pattern.global) {
|
|
452
|
+
throw new InterpreterRuntimeError(
|
|
453
|
+
`String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`,
|
|
454
|
+
node,
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
// Materialized as an array (not an iterator); each entry is a match array with
|
|
458
|
+
// index/groups own properties. Match count is bounded by the subject length.
|
|
459
|
+
return Array.from(value.matchAll(pattern), matchToValue)
|
|
460
|
+
}
|
|
461
|
+
case "search": {
|
|
462
|
+
result = value.search(toHostRegex(args[0], name, node))
|
|
463
|
+
break
|
|
464
|
+
}
|
|
465
|
+
case "repeat": {
|
|
466
|
+
const count = num(0)
|
|
467
|
+
if (!Number.isFinite(count) || count < 0)
|
|
468
|
+
throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node)
|
|
469
|
+
result = value.repeat(count)
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
case "padStart":
|
|
473
|
+
result = value.padStart(num(0), optStr(1))
|
|
474
|
+
break
|
|
475
|
+
case "padEnd":
|
|
476
|
+
result = value.padEnd(num(0), optStr(1))
|
|
477
|
+
break
|
|
478
|
+
case "charAt":
|
|
479
|
+
result = value.charAt(optNum(0) ?? 0)
|
|
480
|
+
break
|
|
481
|
+
case "at":
|
|
482
|
+
result = value.at(optNum(0) ?? 0)
|
|
483
|
+
break
|
|
484
|
+
case "substring":
|
|
485
|
+
result = value.substring(optNum(0) ?? 0, optNum(1))
|
|
486
|
+
break
|
|
487
|
+
case "substr":
|
|
488
|
+
result = value.substr(optNum(0) ?? 0, optNum(1))
|
|
489
|
+
break
|
|
490
|
+
// JS charCodeAt returns NaN out of range; NaN flows as an ordinary in-sandbox value
|
|
491
|
+
// (normalized to null only at the data boundary - see copyOut), so return it as-is.
|
|
492
|
+
case "charCodeAt":
|
|
493
|
+
result = value.charCodeAt(optNum(0) ?? 0)
|
|
494
|
+
break
|
|
495
|
+
case "codePointAt":
|
|
496
|
+
result = value.codePointAt(optNum(0) ?? 0)
|
|
497
|
+
break
|
|
498
|
+
case "toString":
|
|
499
|
+
result = value
|
|
500
|
+
break
|
|
501
|
+
case "concat": {
|
|
502
|
+
result = value.concat(...args.map((_, index) => str(index)))
|
|
503
|
+
break
|
|
504
|
+
}
|
|
505
|
+
default:
|
|
506
|
+
throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node)
|
|
507
|
+
}
|
|
508
|
+
return boundedData(result, `String.${name} result`)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => {
|
|
512
|
+
switch (name) {
|
|
513
|
+
case "isArray":
|
|
514
|
+
return Array.isArray(args[0])
|
|
515
|
+
case "of":
|
|
516
|
+
return [...args]
|
|
517
|
+
case "from": {
|
|
518
|
+
if (args.length > 1) {
|
|
519
|
+
throw new InterpreterRuntimeError(
|
|
520
|
+
"Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.",
|
|
521
|
+
node,
|
|
522
|
+
"UnsupportedSyntax",
|
|
523
|
+
[supportedSyntaxMessage],
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
// Map/Set materialize directly (the data checkpoint would serialize them to {}).
|
|
527
|
+
if (args[0] instanceof SandboxMap)
|
|
528
|
+
return Array.from((args[0] as SandboxMap).map.entries(), ([key, item]) => [key, item])
|
|
529
|
+
if (args[0] instanceof SandboxSet) return Array.from((args[0] as SandboxSet).set.values())
|
|
530
|
+
if (args[0] instanceof SandboxURLSearchParams) {
|
|
531
|
+
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
|
532
|
+
}
|
|
533
|
+
const source = boundedData(args[0], "Array.from input")
|
|
534
|
+
if (typeof source === "string") return Array.from(source)
|
|
535
|
+
if (Array.isArray(source)) return [...source]
|
|
536
|
+
if (
|
|
537
|
+
source !== null &&
|
|
538
|
+
typeof source === "object" &&
|
|
539
|
+
typeof (source as { length?: unknown }).length === "number"
|
|
540
|
+
) {
|
|
541
|
+
return Array.from(source as ArrayLike<unknown>)
|
|
542
|
+
}
|
|
543
|
+
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
|
|
544
|
+
}
|
|
545
|
+
default:
|
|
546
|
+
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unknown>, node: AstNode): unknown => {
|
|
551
|
+
if (ref.namespace === "console")
|
|
552
|
+
throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node)
|
|
553
|
+
if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node)
|
|
554
|
+
if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node)
|
|
555
|
+
if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node)
|
|
556
|
+
if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node)
|
|
557
|
+
if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node)
|
|
558
|
+
if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node)
|
|
559
|
+
if (ref.namespace === "Date") {
|
|
560
|
+
if (!dateStatics.has(ref.name))
|
|
561
|
+
throw new InterpreterRuntimeError(`Date.${ref.name} is not available in CodeMode.`, node)
|
|
562
|
+
return invokeDateStatic(ref.name, args, node)
|
|
563
|
+
}
|
|
564
|
+
if (
|
|
565
|
+
ref.namespace === "RegExp" ||
|
|
566
|
+
ref.namespace === "Map" ||
|
|
567
|
+
ref.namespace === "Set" ||
|
|
568
|
+
ref.namespace === "URLSearchParams"
|
|
569
|
+
) {
|
|
570
|
+
throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node)
|
|
571
|
+
}
|
|
572
|
+
return invokeJsonMethod(ref.name, args, node)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Every identifier a parameter pattern binds, used to seed TDZ slots before defaults run.
|
|
576
|
+
const collectPatternNames = (pattern: AstNode, out: Array<string> = []): Array<string> => {
|
|
577
|
+
switch (pattern.type) {
|
|
578
|
+
case "Identifier":
|
|
579
|
+
out.push(getString(pattern, "name"))
|
|
580
|
+
break
|
|
581
|
+
case "AssignmentPattern":
|
|
582
|
+
collectPatternNames(getNode(pattern, "left"), out)
|
|
583
|
+
break
|
|
584
|
+
case "RestElement":
|
|
585
|
+
collectPatternNames(getNode(pattern, "argument"), out)
|
|
586
|
+
break
|
|
587
|
+
case "ArrayPattern":
|
|
588
|
+
for (const element of getArray(pattern, "elements")) {
|
|
589
|
+
if (element !== null) collectPatternNames(asNode(element, "elements"), out)
|
|
590
|
+
}
|
|
591
|
+
break
|
|
592
|
+
case "ObjectPattern":
|
|
593
|
+
for (const property of getArray(pattern, "properties")) {
|
|
594
|
+
const prop = asNode(property, "properties")
|
|
595
|
+
collectPatternNames(prop.type === "RestElement" ? getNode(prop, "argument") : getNode(prop, "value"), out)
|
|
596
|
+
}
|
|
597
|
+
break
|
|
598
|
+
}
|
|
599
|
+
return out
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
class Interpreter<R> {
|
|
603
|
+
private scopes: Array<Map<string, Binding>>
|
|
604
|
+
private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
|
605
|
+
// Enumerable namespace/tool names at a node of the host tool tree, threaded from
|
|
606
|
+
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
|
|
607
|
+
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
|
608
|
+
private readonly logs: Array<string>
|
|
609
|
+
private lastValue: unknown
|
|
610
|
+
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
|
|
611
|
+
private readonly callPermits: Semaphore.Semaphore
|
|
612
|
+
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
|
|
613
|
+
// program completion drains these (like a runtime waiting on in-flight work at exit) and
|
|
614
|
+
// surfaces a never-awaited failure as an unhandled-rejection diagnostic.
|
|
615
|
+
private readonly pendingSettlements = new Set<SandboxPromise>()
|
|
616
|
+
|
|
617
|
+
constructor(
|
|
618
|
+
invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
|
619
|
+
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
|
620
|
+
logs: Array<string> = [],
|
|
621
|
+
) {
|
|
622
|
+
const globalScope = new Map<string, Binding>()
|
|
623
|
+
this.scopes = [globalScope]
|
|
624
|
+
this.invokeTool = invokeTool
|
|
625
|
+
this.toolKeys = toolKeys
|
|
626
|
+
this.logs = logs
|
|
627
|
+
this.lastValue = undefined
|
|
628
|
+
this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
|
|
629
|
+
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
|
630
|
+
globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() })
|
|
631
|
+
globalScope.set("undefined", { mutable: false, value: undefined })
|
|
632
|
+
globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") })
|
|
633
|
+
globalScope.set("Math", { mutable: false, value: new GlobalNamespace("Math") })
|
|
634
|
+
globalScope.set("JSON", { mutable: false, value: new GlobalNamespace("JSON") })
|
|
635
|
+
globalScope.set("Number", { mutable: false, value: new CoercionFunction("Number") })
|
|
636
|
+
globalScope.set("String", { mutable: false, value: new CoercionFunction("String") })
|
|
637
|
+
globalScope.set("Boolean", { mutable: false, value: new CoercionFunction("Boolean") })
|
|
638
|
+
globalScope.set("Array", { mutable: false, value: new GlobalNamespace("Array") })
|
|
639
|
+
globalScope.set("console", { mutable: false, value: new GlobalNamespace("console") })
|
|
640
|
+
globalScope.set("parseInt", { mutable: false, value: new CoercionFunction("parseInt") })
|
|
641
|
+
globalScope.set("parseFloat", { mutable: false, value: new CoercionFunction("parseFloat") })
|
|
642
|
+
globalScope.set("Date", { mutable: false, value: new GlobalNamespace("Date") })
|
|
643
|
+
globalScope.set("RegExp", { mutable: false, value: new GlobalNamespace("RegExp") })
|
|
644
|
+
globalScope.set("Map", { mutable: false, value: new GlobalNamespace("Map") })
|
|
645
|
+
globalScope.set("Set", { mutable: false, value: new GlobalNamespace("Set") })
|
|
646
|
+
globalScope.set("URL", { mutable: false, value: new GlobalNamespace("URL") })
|
|
647
|
+
globalScope.set("URLSearchParams", { mutable: false, value: new GlobalNamespace("URLSearchParams") })
|
|
648
|
+
globalScope.set("encodeURI", { mutable: false, value: new UriFunction("encodeURI") })
|
|
649
|
+
globalScope.set("encodeURIComponent", { mutable: false, value: new UriFunction("encodeURIComponent") })
|
|
650
|
+
globalScope.set("decodeURI", { mutable: false, value: new UriFunction("decodeURI") })
|
|
651
|
+
globalScope.set("decodeURIComponent", { mutable: false, value: new UriFunction("decodeURIComponent") })
|
|
652
|
+
// Error constructors are real values, so `x instanceof Error` works and `Error("msg")`
|
|
653
|
+
// (with or without `new`) constructs a branded { name, message } error object.
|
|
654
|
+
for (const name of errorConstructors) {
|
|
655
|
+
globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) })
|
|
656
|
+
}
|
|
657
|
+
// NaN/Infinity flow as ordinary in-sandbox values (normalized to null only at the data
|
|
658
|
+
// boundary - see copyOut), so their global bindings must exist too, e.g. `reduce(max, -Infinity)`.
|
|
659
|
+
globalScope.set("NaN", { mutable: false, value: NaN })
|
|
660
|
+
globalScope.set("Infinity", { mutable: false, value: Infinity })
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
run(program: ProgramNode): Effect.Effect<unknown, unknown, R> {
|
|
664
|
+
const self = this
|
|
665
|
+
// Run the program body in its own module scope on top of the builtin global scope, so
|
|
666
|
+
// top-level declarations (`let undefined = 5`, `const Object = ...`) shadow builtins like
|
|
667
|
+
// JS module scope, instead of colliding with the seeded globals.
|
|
668
|
+
this.pushScope()
|
|
669
|
+
return Effect.gen(function* () {
|
|
670
|
+
self.hoistFunctions(program.body)
|
|
671
|
+
let value: unknown = undefined
|
|
672
|
+
let returned = false
|
|
673
|
+
for (const statement of program.body) {
|
|
674
|
+
const result = yield* self.evaluateStatement(statement)
|
|
675
|
+
|
|
676
|
+
if (result.kind === "return") {
|
|
677
|
+
value = result.value
|
|
678
|
+
returned = true
|
|
679
|
+
break
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (result.kind === "break" || result.kind === "continue") {
|
|
683
|
+
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (result.kind === "value") {
|
|
687
|
+
self.lastValue = result.value
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (!returned) value = self.lastValue
|
|
691
|
+
|
|
692
|
+
// The program body runs inside an implicit async function, so a returned promise
|
|
693
|
+
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
|
694
|
+
// without an explicit await, exactly as in JS.
|
|
695
|
+
if (value instanceof SandboxPromise) value = yield* self.settlePromise(value)
|
|
696
|
+
yield* self.drainPendingSettlements()
|
|
697
|
+
return value
|
|
698
|
+
}).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
|
|
702
|
+
// their work completes before the execution ends - mirroring a JS runtime waiting on
|
|
703
|
+
// in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
|
|
704
|
+
// diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).
|
|
705
|
+
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
|
|
706
|
+
const self = this
|
|
707
|
+
return Effect.gen(function* () {
|
|
708
|
+
for (const promise of [...self.pendingSettlements]) {
|
|
709
|
+
const exit = yield* self.observePromise(promise)
|
|
710
|
+
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue
|
|
711
|
+
const failure = normalizeError(Cause.squash(exit.cause))
|
|
712
|
+
throw new InterpreterRuntimeError(
|
|
713
|
+
`Unhandled rejection from an un-awaited tool call: ${failure.message}`,
|
|
714
|
+
undefined,
|
|
715
|
+
failure.kind,
|
|
716
|
+
["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."],
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Eagerly starts a tool call on a supervised child fiber (so the execution timeout and
|
|
723
|
+
// scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a
|
|
724
|
+
// first-class promise value. `startImmediately` makes the runtime admit the call - charging
|
|
725
|
+
// the tool-call budget and firing onToolCallStart - at the call site, before any await.
|
|
726
|
+
private createToolCallPromise(
|
|
727
|
+
path: ReadonlyArray<string>,
|
|
728
|
+
args: Array<unknown>,
|
|
729
|
+
): Effect.Effect<SandboxPromise, never, R> {
|
|
730
|
+
const self = this
|
|
731
|
+
return Effect.map(
|
|
732
|
+
Effect.forkChild(this.callPermits.withPermit(Effect.suspend(() => self.invokeTool(path, args))), {
|
|
733
|
+
startImmediately: true,
|
|
734
|
+
}),
|
|
735
|
+
(fiber) => {
|
|
736
|
+
const promise = new SandboxPromise(fiber)
|
|
737
|
+
self.pendingSettlements.add(promise)
|
|
738
|
+
return promise
|
|
739
|
+
},
|
|
740
|
+
)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// The promise's settlement as an Exit, marking it observed for unhandled-rejection tracking.
|
|
744
|
+
// Fiber settlement is idempotent, so observing the same promise repeatedly (await twice,
|
|
745
|
+
// Promise.all([p, p])) never re-runs the underlying call.
|
|
746
|
+
private observePromise(promise: SandboxPromise): Effect.Effect<Exit.Exit<unknown, unknown>> {
|
|
747
|
+
this.pendingSettlements.delete(promise)
|
|
748
|
+
return promise.fiber !== undefined ? Fiber.await(promise.fiber) : Effect.exit(promise.immediate ?? Effect.void)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch
|
|
752
|
+
// observes it exactly like a synchronous throw at the await site.
|
|
753
|
+
private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect<unknown, unknown, never> {
|
|
754
|
+
const self = this
|
|
755
|
+
return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private unwrapPromiseExit(
|
|
759
|
+
promise: SandboxPromise | undefined,
|
|
760
|
+
exit: Exit.Exit<unknown, unknown>,
|
|
761
|
+
node?: AstNode,
|
|
762
|
+
): Effect.Effect<unknown, unknown> {
|
|
763
|
+
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
|
|
764
|
+
// A call Promise.race interrupted after losing settles as a catchable program failure;
|
|
765
|
+
// any other interruption is execution teardown (timeout/host) and must keep propagating
|
|
766
|
+
// as interruption rather than becoming program-visible data.
|
|
767
|
+
if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) {
|
|
768
|
+
return Effect.fail(
|
|
769
|
+
new InterpreterRuntimeError(
|
|
770
|
+
"This tool call was interrupted because another value settled a Promise.race first.",
|
|
771
|
+
node,
|
|
772
|
+
),
|
|
773
|
+
)
|
|
774
|
+
}
|
|
775
|
+
return Effect.failCause(exit.cause)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
779
|
+
switch (node.type) {
|
|
780
|
+
case "ExpressionStatement":
|
|
781
|
+
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value }))
|
|
782
|
+
case "VariableDeclaration":
|
|
783
|
+
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
|
|
784
|
+
case "ReturnStatement": {
|
|
785
|
+
const argumentNode = getOptionalNode(node, "argument")
|
|
786
|
+
return argumentNode
|
|
787
|
+
? Effect.map(this.evaluateExpression(argumentNode), (value) => ({ kind: "return", value }))
|
|
788
|
+
: Effect.succeed({ kind: "return", value: undefined })
|
|
789
|
+
}
|
|
790
|
+
case "BlockStatement":
|
|
791
|
+
return this.evaluateBlock(node)
|
|
792
|
+
case "IfStatement":
|
|
793
|
+
return this.evaluateIfStatement(node)
|
|
794
|
+
case "SwitchStatement":
|
|
795
|
+
return this.evaluateSwitchStatement(node)
|
|
796
|
+
case "WhileStatement":
|
|
797
|
+
return this.evaluateWhileStatement(node)
|
|
798
|
+
case "DoWhileStatement":
|
|
799
|
+
return this.evaluateDoWhileStatement(node)
|
|
800
|
+
case "ForStatement":
|
|
801
|
+
return this.evaluateForStatement(node)
|
|
802
|
+
case "ForOfStatement":
|
|
803
|
+
return this.evaluateForOfStatement(node)
|
|
804
|
+
case "ForInStatement":
|
|
805
|
+
return this.evaluateForInStatement(node)
|
|
806
|
+
case "BreakStatement":
|
|
807
|
+
return Effect.succeed(this.evaluateBreakStatement(node))
|
|
808
|
+
case "ContinueStatement":
|
|
809
|
+
return Effect.succeed(this.evaluateContinueStatement(node))
|
|
810
|
+
case "ThrowStatement":
|
|
811
|
+
return this.evaluateThrowStatement(node)
|
|
812
|
+
case "TryStatement":
|
|
813
|
+
return this.evaluateTryStatement(node)
|
|
814
|
+
case "EmptyStatement":
|
|
815
|
+
return Effect.succeed({ kind: "none" })
|
|
816
|
+
case "FunctionDeclaration":
|
|
817
|
+
return Effect.succeed({ kind: "none" }) // bound ahead of time by hoistFunctions
|
|
818
|
+
default:
|
|
819
|
+
throw unsupportedSyntax(node.type, node)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private evaluateBlock(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
824
|
+
this.pushScope()
|
|
825
|
+
const self = this
|
|
826
|
+
return Effect.gen(function* () {
|
|
827
|
+
const body = getArray(node, "body")
|
|
828
|
+
self.hoistFunctions(body)
|
|
829
|
+
|
|
830
|
+
for (const statementValue of body) {
|
|
831
|
+
const statement = asNode(statementValue, "body")
|
|
832
|
+
const result = yield* self.evaluateStatement(statement)
|
|
833
|
+
|
|
834
|
+
if (result.kind === "value") {
|
|
835
|
+
self.lastValue = result.value
|
|
836
|
+
continue
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (result.kind !== "none") {
|
|
840
|
+
return result
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return { kind: "none" } satisfies StatementResult
|
|
845
|
+
}).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private createFunction(node: AstNode): CodeModeFunction {
|
|
849
|
+
if (node.generator === true) {
|
|
850
|
+
throw new InterpreterRuntimeError(
|
|
851
|
+
"Generator functions are not supported in CodeMode.",
|
|
852
|
+
node,
|
|
853
|
+
"UnsupportedSyntax",
|
|
854
|
+
[supportedSyntaxMessage],
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
return new CodeModeFunction(
|
|
858
|
+
getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
|
|
859
|
+
getNode(node, "body"),
|
|
860
|
+
this.scopes.slice(),
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Function declarations are hoisted: bound in their scope before the body runs, so a
|
|
865
|
+
// program can call a helper defined further down (matching JavaScript).
|
|
866
|
+
private hoistFunctions(statements: Array<unknown>): void {
|
|
867
|
+
for (const statementValue of statements) {
|
|
868
|
+
if (!isRecord(statementValue) || statementValue.type !== "FunctionDeclaration") continue
|
|
869
|
+
const node = statementValue as AstNode
|
|
870
|
+
this.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private evaluateIfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
875
|
+
const testNode = getNode(node, "test")
|
|
876
|
+
const consequentNode = getNode(node, "consequent")
|
|
877
|
+
const alternateNode = getOptionalNode(node, "alternate")
|
|
878
|
+
|
|
879
|
+
return Effect.flatMap(this.evaluateExpression(testNode), (test) =>
|
|
880
|
+
test
|
|
881
|
+
? this.evaluateStatement(consequentNode)
|
|
882
|
+
: alternateNode
|
|
883
|
+
? this.evaluateStatement(alternateNode)
|
|
884
|
+
: Effect.succeed({ kind: "none" }),
|
|
885
|
+
)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
889
|
+
const self = this
|
|
890
|
+
this.pushScope()
|
|
891
|
+
return Effect.gen(function* () {
|
|
892
|
+
const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant"))
|
|
893
|
+
if (containsOpaqueReference(discriminant)) {
|
|
894
|
+
throw new InterpreterRuntimeError(
|
|
895
|
+
"Switch discriminants must be data values in CodeMode.",
|
|
896
|
+
node,
|
|
897
|
+
"InvalidDataValue",
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`))
|
|
901
|
+
let defaultIndex: number | undefined
|
|
902
|
+
let selected: number | undefined
|
|
903
|
+
for (const [index, branch] of cases.entries()) {
|
|
904
|
+
const test = getOptionalNode(branch, "test")
|
|
905
|
+
if (!test) {
|
|
906
|
+
defaultIndex = index
|
|
907
|
+
continue
|
|
908
|
+
}
|
|
909
|
+
const candidate = yield* self.evaluateExpression(test)
|
|
910
|
+
if (containsOpaqueReference(candidate)) {
|
|
911
|
+
throw new InterpreterRuntimeError(
|
|
912
|
+
"Switch case values must be data values in CodeMode.",
|
|
913
|
+
test,
|
|
914
|
+
"InvalidDataValue",
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
if (candidate === discriminant) {
|
|
918
|
+
selected = index
|
|
919
|
+
break
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const start = selected ?? defaultIndex
|
|
923
|
+
if (start === undefined) return { kind: "none" } satisfies StatementResult
|
|
924
|
+
for (let index = start; index < cases.length; index += 1) {
|
|
925
|
+
for (const statementValue of getArray(cases[index]!, "consequent")) {
|
|
926
|
+
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
|
927
|
+
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
|
928
|
+
if (result.kind === "return" || result.kind === "continue") return result
|
|
929
|
+
if (result.kind === "value") self.lastValue = result.value
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { kind: "none" } satisfies StatementResult
|
|
933
|
+
}).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private evaluateWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
937
|
+
const testNode = getNode(node, "test")
|
|
938
|
+
const bodyNode = getNode(node, "body")
|
|
939
|
+
|
|
940
|
+
const self = this
|
|
941
|
+
return Effect.gen(function* () {
|
|
942
|
+
while (yield* self.evaluateExpression(testNode)) {
|
|
943
|
+
const result = yield* self.evaluateStatement(bodyNode)
|
|
944
|
+
|
|
945
|
+
if (result.kind === "continue") {
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (result.kind === "break") {
|
|
950
|
+
return { kind: "none" } satisfies StatementResult
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (result.kind === "return") {
|
|
954
|
+
return result
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (result.kind === "value") {
|
|
958
|
+
self.lastValue = result.value
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return { kind: "none" } satisfies StatementResult
|
|
963
|
+
})
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private evaluateDoWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
967
|
+
const bodyNode = getNode(node, "body")
|
|
968
|
+
const testNode = getNode(node, "test")
|
|
969
|
+
|
|
970
|
+
const self = this
|
|
971
|
+
return Effect.gen(function* () {
|
|
972
|
+
do {
|
|
973
|
+
const result = yield* self.evaluateStatement(bodyNode)
|
|
974
|
+
|
|
975
|
+
if (result.kind === "continue") {
|
|
976
|
+
continue
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (result.kind === "break") {
|
|
980
|
+
return { kind: "none" } satisfies StatementResult
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (result.kind === "return") {
|
|
984
|
+
return result
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (result.kind === "value") {
|
|
988
|
+
self.lastValue = result.value
|
|
989
|
+
}
|
|
990
|
+
} while (yield* self.evaluateExpression(testNode))
|
|
991
|
+
|
|
992
|
+
return { kind: "none" } satisfies StatementResult
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private evaluateForStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
997
|
+
this.pushScope()
|
|
998
|
+
const self = this
|
|
999
|
+
return Effect.gen(function* () {
|
|
1000
|
+
const initNode = getOptionalNode(node, "init")
|
|
1001
|
+
const testNode = getOptionalNode(node, "test")
|
|
1002
|
+
const updateNode = getOptionalNode(node, "update")
|
|
1003
|
+
const bodyNode = getNode(node, "body")
|
|
1004
|
+
|
|
1005
|
+
if (initNode) {
|
|
1006
|
+
if (initNode.type === "VariableDeclaration") {
|
|
1007
|
+
yield* self.evaluateVariableDeclaration(initNode)
|
|
1008
|
+
} else {
|
|
1009
|
+
yield* self.evaluateExpression(initNode)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const perIterationBindings =
|
|
1014
|
+
initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var"
|
|
1015
|
+
? Array.from(self.currentScope().keys())
|
|
1016
|
+
: []
|
|
1017
|
+
|
|
1018
|
+
while (testNode ? yield* self.evaluateExpression(testNode) : true) {
|
|
1019
|
+
let iterationScope: Map<string, Binding> | undefined
|
|
1020
|
+
if (perIterationBindings.length > 0) {
|
|
1021
|
+
iterationScope = new Map(
|
|
1022
|
+
perIterationBindings.map((name) => {
|
|
1023
|
+
const binding = self.currentScope().get(name)!
|
|
1024
|
+
return [name, { ...binding }]
|
|
1025
|
+
}),
|
|
1026
|
+
)
|
|
1027
|
+
self.scopes.push(iterationScope)
|
|
1028
|
+
}
|
|
1029
|
+
const result = yield* self.evaluateStatement(bodyNode).pipe(
|
|
1030
|
+
Effect.ensuring(
|
|
1031
|
+
Effect.sync(() => {
|
|
1032
|
+
if (iterationScope) self.popScope()
|
|
1033
|
+
}),
|
|
1034
|
+
),
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
if (result.kind === "return") {
|
|
1038
|
+
return result
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (result.kind === "break") {
|
|
1042
|
+
return { kind: "none" } satisfies StatementResult
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (result.kind === "value") {
|
|
1046
|
+
self.lastValue = result.value
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (iterationScope) {
|
|
1050
|
+
const loopScope = self.currentScope()
|
|
1051
|
+
for (const name of perIterationBindings) {
|
|
1052
|
+
loopScope.set(name, { ...iterationScope.get(name)! })
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (updateNode) {
|
|
1057
|
+
yield* self.evaluateExpression(updateNode)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (result.kind === "continue") {
|
|
1061
|
+
continue
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return { kind: "none" } satisfies StatementResult
|
|
1066
|
+
}).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private evaluateForOfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
1070
|
+
if (getBoolean(node, "await")) {
|
|
1071
|
+
throw new InterpreterRuntimeError("for await...of is not supported.", node)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const self = this
|
|
1075
|
+
return Effect.gen(function* () {
|
|
1076
|
+
const left = getNode(node, "left")
|
|
1077
|
+
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
|
1078
|
+
const body = getNode(node, "body")
|
|
1079
|
+
|
|
1080
|
+
// Arrays iterate in place; strings iterate code points; Maps iterate [key, value]
|
|
1081
|
+
// pairs and Sets iterate values over a snapshot (mutation during iteration is safe).
|
|
1082
|
+
const iterable = Array.isArray(right) ? right : spreadItems(right)
|
|
1083
|
+
if (iterable === undefined) {
|
|
1084
|
+
throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
|
1088
|
+
let assignmentName: string | undefined
|
|
1089
|
+
|
|
1090
|
+
if (left.type === "VariableDeclaration") {
|
|
1091
|
+
const declarations = getArray(left, "declarations")
|
|
1092
|
+
if (declarations.length !== 1) {
|
|
1093
|
+
throw new InterpreterRuntimeError("for...of supports one declared binding.", left)
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const declarator = asNode(declarations[0], "declarations[0]")
|
|
1097
|
+
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
|
|
1098
|
+
} else if (left.type === "Identifier") {
|
|
1099
|
+
assignmentName = getString(left, "name")
|
|
1100
|
+
} else {
|
|
1101
|
+
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
for (const value of iterable) {
|
|
1105
|
+
if (declaration) {
|
|
1106
|
+
self.pushScope()
|
|
1107
|
+
yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
|
|
1108
|
+
} else if (assignmentName) {
|
|
1109
|
+
self.setIdentifierValue(assignmentName, value, left)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const result = yield* self.evaluateStatement(body).pipe(
|
|
1113
|
+
Effect.ensuring(
|
|
1114
|
+
Effect.sync(() => {
|
|
1115
|
+
if (declaration) self.popScope()
|
|
1116
|
+
}),
|
|
1117
|
+
),
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
if (result.kind === "return") {
|
|
1121
|
+
return result
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (result.kind === "break") {
|
|
1125
|
+
return { kind: "none" }
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (result.kind === "value") {
|
|
1129
|
+
self.lastValue = result.value
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (result.kind === "continue") {
|
|
1133
|
+
continue
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return { kind: "none" }
|
|
1138
|
+
})
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Own enumerable string keys of a value, shared by `for...in` and `Object.keys` over tool
|
|
1142
|
+
// references: plain data objects enumerate their own keys, arrays their index strings (plus
|
|
1143
|
+
// any own non-index properties, e.g. match results' index/groups - exactly Object.keys in
|
|
1144
|
+
// JS), and a tool reference the namespace/tool names at its path in the host tool tree.
|
|
1145
|
+
// Returns undefined for everything else so callers can raise a contextual error.
|
|
1146
|
+
private enumerableKeys(value: unknown): Array<string> | undefined {
|
|
1147
|
+
if (value instanceof ToolReference) {
|
|
1148
|
+
return [...this.toolKeys(value.path)]
|
|
1149
|
+
}
|
|
1150
|
+
if (Array.isArray(value)) {
|
|
1151
|
+
return Object.keys(value)
|
|
1152
|
+
}
|
|
1153
|
+
if (value !== null && typeof value === "object" && !isRuntimeReference(value)) {
|
|
1154
|
+
return Object.keys(value)
|
|
1155
|
+
}
|
|
1156
|
+
return undefined
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private evaluateForInStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
1160
|
+
const self = this
|
|
1161
|
+
return Effect.gen(function* () {
|
|
1162
|
+
const left = getNode(node, "left")
|
|
1163
|
+
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
|
1164
|
+
const body = getNode(node, "body")
|
|
1165
|
+
|
|
1166
|
+
// Keys are snapshotted up front (mutation during iteration is safe): plain objects
|
|
1167
|
+
// enumerate their own keys, arrays their index strings, and tool references the
|
|
1168
|
+
// namespace/tool names at that node - the same enumeration Object.keys performs.
|
|
1169
|
+
// Anything else (strings, Maps, Sets, numbers, null, ...) is a deliberate error rather
|
|
1170
|
+
// than real JS's surprising behavior (indices for strings, zero iterations for
|
|
1171
|
+
// Maps/Sets/null): the hint points at the constructs that do what the program means.
|
|
1172
|
+
const keys = self.enumerableKeys(right)
|
|
1173
|
+
if (keys === undefined) {
|
|
1174
|
+
throw new InterpreterRuntimeError(
|
|
1175
|
+
"for...in requires a plain object, array, or tools reference in CodeMode. Use for...of for arrays/strings/Maps/Sets, or Object.keys(value) for a key list.",
|
|
1176
|
+
node,
|
|
1177
|
+
)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
|
1181
|
+
let assignmentName: string | undefined
|
|
1182
|
+
|
|
1183
|
+
if (left.type === "VariableDeclaration") {
|
|
1184
|
+
const declarations = getArray(left, "declarations")
|
|
1185
|
+
if (declarations.length !== 1) {
|
|
1186
|
+
throw new InterpreterRuntimeError("for...in supports one declared binding.", left)
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const declarator = asNode(declarations[0], "declarations[0]")
|
|
1190
|
+
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
|
|
1191
|
+
} else if (left.type === "Identifier") {
|
|
1192
|
+
assignmentName = getString(left, "name")
|
|
1193
|
+
} else {
|
|
1194
|
+
throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
for (const key of keys) {
|
|
1198
|
+
if (declaration) {
|
|
1199
|
+
self.pushScope()
|
|
1200
|
+
yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left)
|
|
1201
|
+
} else if (assignmentName) {
|
|
1202
|
+
self.setIdentifierValue(assignmentName, key, left)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const result = yield* self.evaluateStatement(body).pipe(
|
|
1206
|
+
Effect.ensuring(
|
|
1207
|
+
Effect.sync(() => {
|
|
1208
|
+
if (declaration) self.popScope()
|
|
1209
|
+
}),
|
|
1210
|
+
),
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
if (result.kind === "return") {
|
|
1214
|
+
return result
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (result.kind === "break") {
|
|
1218
|
+
return { kind: "none" }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (result.kind === "value") {
|
|
1222
|
+
self.lastValue = result.value
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (result.kind === "continue") {
|
|
1226
|
+
continue
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return { kind: "none" }
|
|
1231
|
+
})
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
private evaluateBreakStatement(node: AstNode): StatementResult {
|
|
1235
|
+
const labelNode = getOptionalNode(node, "label")
|
|
1236
|
+
|
|
1237
|
+
if (labelNode) {
|
|
1238
|
+
throw new InterpreterRuntimeError("Labeled break is not supported in v1.", node)
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return { kind: "break" }
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private evaluateContinueStatement(node: AstNode): StatementResult {
|
|
1245
|
+
const labelNode = getOptionalNode(node, "label")
|
|
1246
|
+
|
|
1247
|
+
if (labelNode) {
|
|
1248
|
+
throw new InterpreterRuntimeError("Labeled continue is not supported in v1.", node)
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return { kind: "continue" }
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
private evaluateThrowStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
1255
|
+
const argument = getNode(node, "argument")
|
|
1256
|
+
return Effect.flatMap(this.evaluateExpression(argument), (value) => Effect.fail(new ProgramThrow(value)))
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
private evaluateTryStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
|
1260
|
+
const body = getNode(node, "block")
|
|
1261
|
+
const handler = getOptionalNode(node, "handler")
|
|
1262
|
+
const finalizer = getOptionalNode(node, "finalizer")
|
|
1263
|
+
const self = this
|
|
1264
|
+
|
|
1265
|
+
const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), {
|
|
1266
|
+
onFailure: (cause) => {
|
|
1267
|
+
if (cause.reasons.some(Cause.isInterruptReason) || !handler) {
|
|
1268
|
+
return Effect.failCause(cause)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// The program sees a plain { message } error (or the thrown value itself) - see
|
|
1272
|
+
// caughtErrorValue, shared with Promise.allSettled rejection reasons.
|
|
1273
|
+
const caught = caughtErrorValue(Cause.squash(cause))
|
|
1274
|
+
const parameter = getOptionalNode(handler, "param")
|
|
1275
|
+
self.pushScope()
|
|
1276
|
+
return Effect.gen(function* () {
|
|
1277
|
+
if (parameter) yield* self.declarePattern(parameter, caught, true, handler)
|
|
1278
|
+
return yield* self.evaluateStatement(getNode(handler, "body"))
|
|
1279
|
+
}).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
|
|
1280
|
+
},
|
|
1281
|
+
onSuccess: Effect.succeed,
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
if (!finalizer) return attempted
|
|
1285
|
+
|
|
1286
|
+
const isAbrupt = (result: StatementResult): boolean =>
|
|
1287
|
+
result.kind === "return" || result.kind === "break" || result.kind === "continue"
|
|
1288
|
+
|
|
1289
|
+
return Effect.matchCauseEffect(attempted, {
|
|
1290
|
+
onFailure: (cause) =>
|
|
1291
|
+
cause.reasons.some(Cause.isInterruptReason)
|
|
1292
|
+
? Effect.failCause(cause)
|
|
1293
|
+
: Effect.flatMap(this.evaluateStatement(finalizer), (final) =>
|
|
1294
|
+
isAbrupt(final) ? Effect.succeed(final) : Effect.failCause(cause),
|
|
1295
|
+
),
|
|
1296
|
+
onSuccess: (result) =>
|
|
1297
|
+
Effect.flatMap(this.evaluateStatement(finalizer), (final) =>
|
|
1298
|
+
isAbrupt(final) ? Effect.succeed(final) : Effect.succeed(result),
|
|
1299
|
+
),
|
|
1300
|
+
})
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
private evaluateVariableDeclaration(node: AstNode): Effect.Effect<void, unknown, R> {
|
|
1304
|
+
const kind = getString(node, "kind")
|
|
1305
|
+
const declarations = getArray(node, "declarations")
|
|
1306
|
+
const self = this
|
|
1307
|
+
return Effect.gen(function* () {
|
|
1308
|
+
for (const declarationValue of declarations) {
|
|
1309
|
+
const declaration = asNode(declarationValue, "declarations")
|
|
1310
|
+
|
|
1311
|
+
if (declaration.type !== "VariableDeclarator") {
|
|
1312
|
+
throw new InterpreterRuntimeError("Unsupported variable declaration shape.", declaration)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const init = getOptionalNode(declaration, "init")
|
|
1316
|
+
const value = init ? yield* self.evaluateExpression(init) : undefined
|
|
1317
|
+
yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration)
|
|
1318
|
+
}
|
|
1319
|
+
})
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private declarePattern(
|
|
1323
|
+
pattern: AstNode,
|
|
1324
|
+
value: unknown,
|
|
1325
|
+
mutable: boolean,
|
|
1326
|
+
node: AstNode,
|
|
1327
|
+
): Effect.Effect<void, unknown, R> {
|
|
1328
|
+
const self = this
|
|
1329
|
+
return Effect.gen(function* () {
|
|
1330
|
+
if (pattern.type === "Identifier") {
|
|
1331
|
+
self.declare(getString(pattern, "name"), value, mutable, node)
|
|
1332
|
+
return
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Default values: `x = expr` / `{ a = 1 }` - the default is evaluated only when the value is undefined.
|
|
1336
|
+
if (pattern.type === "AssignmentPattern") {
|
|
1337
|
+
const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
|
|
1338
|
+
yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node)
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (pattern.type === "ObjectPattern") {
|
|
1343
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
|
|
1344
|
+
throw new InterpreterRuntimeError(
|
|
1345
|
+
"Object destructuring requires a data object value.",
|
|
1346
|
+
pattern,
|
|
1347
|
+
"InvalidDataValue",
|
|
1348
|
+
)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const consumed = new Set<string>()
|
|
1352
|
+
for (const propertyValue of getArray(pattern, "properties")) {
|
|
1353
|
+
const property = asNode(propertyValue, "properties")
|
|
1354
|
+
|
|
1355
|
+
// Object rest: `{ a, ...others }` - gather the not-yet-consumed own keys.
|
|
1356
|
+
if (property.type === "RestElement") {
|
|
1357
|
+
const rest: SafeObject = Object.create(null) as SafeObject
|
|
1358
|
+
for (const [key, item] of Object.entries(value as SafeObject)) {
|
|
1359
|
+
if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
|
|
1360
|
+
}
|
|
1361
|
+
yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property)
|
|
1362
|
+
continue
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (
|
|
1366
|
+
property.type !== "Property" ||
|
|
1367
|
+
getBoolean(property, "computed") ||
|
|
1368
|
+
getString(property, "kind") !== "init"
|
|
1369
|
+
) {
|
|
1370
|
+
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const keyNode = getNode(property, "key")
|
|
1374
|
+
const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
|
|
1375
|
+
if (isBlockedMember(key)) {
|
|
1376
|
+
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode)
|
|
1377
|
+
}
|
|
1378
|
+
consumed.add(key)
|
|
1379
|
+
yield* self.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property)
|
|
1380
|
+
}
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (pattern.type === "ArrayPattern") {
|
|
1385
|
+
if (!Array.isArray(value)) {
|
|
1386
|
+
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
|
1390
|
+
if (item === null) continue
|
|
1391
|
+
const element = asNode(item, `elements[${index}]`)
|
|
1392
|
+
// Array rest: `[head, ...tail]` - binds the remaining elements (must be last).
|
|
1393
|
+
if (element.type === "RestElement") {
|
|
1394
|
+
yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element)
|
|
1395
|
+
break
|
|
1396
|
+
}
|
|
1397
|
+
yield* self.declarePattern(element, value[index], mutable, pattern)
|
|
1398
|
+
}
|
|
1399
|
+
return
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
|
|
1403
|
+
})
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1407
|
+
switch (node.type) {
|
|
1408
|
+
case "Literal": {
|
|
1409
|
+
// A regex literal parses as a Literal node carrying { pattern, flags }; construct the
|
|
1410
|
+
// sandbox regex from those (the host `value` instance is never exposed).
|
|
1411
|
+
const regex = node.regex
|
|
1412
|
+
if (isRecord(regex) && typeof regex.pattern === "string") {
|
|
1413
|
+
return Effect.sync(() =>
|
|
1414
|
+
this.constructRegExp([regex.pattern, typeof regex.flags === "string" ? regex.flags : ""], node),
|
|
1415
|
+
)
|
|
1416
|
+
}
|
|
1417
|
+
return Effect.sync(() => boundedData(node.value, "Literal"))
|
|
1418
|
+
}
|
|
1419
|
+
case "Identifier":
|
|
1420
|
+
return Effect.sync(() => this.getIdentifierValue(getString(node, "name"), node))
|
|
1421
|
+
case "BinaryExpression":
|
|
1422
|
+
return this.evaluateBinaryExpression(node)
|
|
1423
|
+
case "LogicalExpression":
|
|
1424
|
+
return this.evaluateLogicalExpression(node)
|
|
1425
|
+
case "UnaryExpression":
|
|
1426
|
+
return this.evaluateUnaryExpression(node)
|
|
1427
|
+
case "AssignmentExpression":
|
|
1428
|
+
return this.evaluateAssignmentExpression(node)
|
|
1429
|
+
case "CallExpression":
|
|
1430
|
+
return this.evaluateCallExpression(node)
|
|
1431
|
+
case "ArrowFunctionExpression":
|
|
1432
|
+
case "FunctionExpression":
|
|
1433
|
+
return Effect.sync(() => this.createFunction(node))
|
|
1434
|
+
case "MemberExpression":
|
|
1435
|
+
return this.readMember(node)
|
|
1436
|
+
case "ChainExpression":
|
|
1437
|
+
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) =>
|
|
1438
|
+
value === OptionalShortCircuit ? undefined : value,
|
|
1439
|
+
)
|
|
1440
|
+
case "ObjectExpression":
|
|
1441
|
+
return this.evaluateObjectExpression(node)
|
|
1442
|
+
case "ArrayExpression":
|
|
1443
|
+
return this.evaluateArrayExpression(node)
|
|
1444
|
+
case "TemplateLiteral":
|
|
1445
|
+
return this.evaluateTemplateLiteral(node)
|
|
1446
|
+
case "ConditionalExpression":
|
|
1447
|
+
return this.evaluateConditionalExpression(node)
|
|
1448
|
+
case "UpdateExpression":
|
|
1449
|
+
return this.evaluateUpdateExpression(node)
|
|
1450
|
+
case "AwaitExpression": {
|
|
1451
|
+
// `await` resolves a promise value; awaiting anything else is a passthrough no-op,
|
|
1452
|
+
// matching real JS semantics for non-thenables.
|
|
1453
|
+
const self = this
|
|
1454
|
+
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
|
|
1455
|
+
value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value),
|
|
1456
|
+
)
|
|
1457
|
+
}
|
|
1458
|
+
case "NewExpression":
|
|
1459
|
+
return this.evaluateNewExpression(node)
|
|
1460
|
+
default:
|
|
1461
|
+
throw unsupportedSyntax(node.type, node)
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private evaluateNewExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1466
|
+
const callee = getNode(node, "callee")
|
|
1467
|
+
if (callee.type !== "Identifier") {
|
|
1468
|
+
throw unsupportedSyntax("NewExpression", node)
|
|
1469
|
+
}
|
|
1470
|
+
const name = getString(callee, "name")
|
|
1471
|
+
const argNodes = getArray(node, "arguments")
|
|
1472
|
+
const self = this
|
|
1473
|
+
if (name === "Promise") {
|
|
1474
|
+
throw new InterpreterRuntimeError(
|
|
1475
|
+
"new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.",
|
|
1476
|
+
node,
|
|
1477
|
+
"UnsupportedSyntax",
|
|
1478
|
+
[supportedSyntaxMessage],
|
|
1479
|
+
)
|
|
1480
|
+
}
|
|
1481
|
+
if (errorConstructors.has(name)) {
|
|
1482
|
+
return Effect.gen(function* () {
|
|
1483
|
+
const arg =
|
|
1484
|
+
argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined
|
|
1485
|
+
return createErrorValue(name, arg === undefined ? "" : coerceToString(arg))
|
|
1486
|
+
})
|
|
1487
|
+
}
|
|
1488
|
+
if (valueConstructors.has(name)) {
|
|
1489
|
+
return Effect.gen(function* () {
|
|
1490
|
+
const args = yield* self.evaluateCallArguments(argNodes)
|
|
1491
|
+
switch (name) {
|
|
1492
|
+
case "Date":
|
|
1493
|
+
return self.constructDate(args)
|
|
1494
|
+
case "RegExp":
|
|
1495
|
+
return self.constructRegExp(args, node)
|
|
1496
|
+
case "Map":
|
|
1497
|
+
return self.constructMap(args[0], node)
|
|
1498
|
+
case "Set":
|
|
1499
|
+
return self.constructSet(args[0], node)
|
|
1500
|
+
case "URL":
|
|
1501
|
+
return self.constructURL(args, node)
|
|
1502
|
+
default:
|
|
1503
|
+
return self.constructURLSearchParams(args[0], node)
|
|
1504
|
+
}
|
|
1505
|
+
})
|
|
1506
|
+
}
|
|
1507
|
+
throw unsupportedSyntax("NewExpression", node)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
private constructDate(args: Array<unknown>): SandboxDate {
|
|
1511
|
+
if (args.length === 0) return new SandboxDate(Date.now())
|
|
1512
|
+
if (args.length === 1) {
|
|
1513
|
+
const arg = args[0]
|
|
1514
|
+
if (arg instanceof SandboxDate) return new SandboxDate(arg.time)
|
|
1515
|
+
if (typeof arg === "number") return new SandboxDate(new Date(arg).getTime())
|
|
1516
|
+
if (typeof arg === "string") return new SandboxDate(Date.parse(arg))
|
|
1517
|
+
return new SandboxDate(Number.NaN)
|
|
1518
|
+
}
|
|
1519
|
+
// new Date(year, month, day?, hours?, ...) - local-time component form.
|
|
1520
|
+
const parts = args.map((arg) => coerceToNumber(arg))
|
|
1521
|
+
return new SandboxDate(new Date(...(parts as [number, number])).getTime())
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private constructRegExp(args: Array<unknown>, node: AstNode): SandboxRegExp {
|
|
1525
|
+
const first = args[0]
|
|
1526
|
+
const pattern =
|
|
1527
|
+
first instanceof SandboxRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first)
|
|
1528
|
+
const flagsArg = args[1]
|
|
1529
|
+
if (flagsArg !== undefined && typeof flagsArg !== "string") {
|
|
1530
|
+
throw new InterpreterRuntimeError(
|
|
1531
|
+
`RegExp flags must be a string of flag characters (e.g. "g", "gi"), not ${flagsArg === null ? "null" : typeof flagsArg}.`,
|
|
1532
|
+
node,
|
|
1533
|
+
)
|
|
1534
|
+
}
|
|
1535
|
+
const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "")
|
|
1536
|
+
try {
|
|
1537
|
+
return new SandboxRegExp(pattern, flags)
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
// Say which part was rejected and how to fix it, instead of passing the engine
|
|
1540
|
+
// message through bare. A flags failure names the flags; a pattern failure gets the
|
|
1541
|
+
// escaping hint (the usual cause is an unescaped metacharacter in a built-up string).
|
|
1542
|
+
const reason = regexFailureReason(error)
|
|
1543
|
+
throw new InterpreterRuntimeError(
|
|
1544
|
+
/flag/i.test(reason)
|
|
1545
|
+
? `new RegExp(...) received invalid flags ${JSON.stringify(flags)} (${reason}). Valid flags are d, g, i, m, s, u, v, and y.`
|
|
1546
|
+
: `new RegExp(...) received ${JSON.stringify(pattern)}, which is not a valid regular expression pattern (${reason}). ${escapeRegexHint}`,
|
|
1547
|
+
node,
|
|
1548
|
+
).as("SyntaxError")
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
private constructMap(init: unknown, node: AstNode): SandboxMap {
|
|
1553
|
+
const target = new SandboxMap()
|
|
1554
|
+
if (init === undefined || init === null) return target
|
|
1555
|
+
const entries = Array.isArray(init)
|
|
1556
|
+
? init
|
|
1557
|
+
: init instanceof SandboxMap
|
|
1558
|
+
? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item])
|
|
1559
|
+
: undefined
|
|
1560
|
+
if (entries === undefined) {
|
|
1561
|
+
throw new InterpreterRuntimeError(
|
|
1562
|
+
"new Map(...) expects an array of [key, value] pairs, a Map, or no argument.",
|
|
1563
|
+
node,
|
|
1564
|
+
)
|
|
1565
|
+
}
|
|
1566
|
+
for (const pair of entries) {
|
|
1567
|
+
if (!Array.isArray(pair)) {
|
|
1568
|
+
throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node)
|
|
1569
|
+
}
|
|
1570
|
+
target.map.set(pair[0], pair[1])
|
|
1571
|
+
}
|
|
1572
|
+
return target
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private constructSet(init: unknown, node: AstNode): SandboxSet {
|
|
1576
|
+
const target = new SandboxSet()
|
|
1577
|
+
if (init === undefined || init === null) return target
|
|
1578
|
+
const items = Array.isArray(init)
|
|
1579
|
+
? init
|
|
1580
|
+
: init instanceof SandboxSet
|
|
1581
|
+
? Array.from(init.set.values())
|
|
1582
|
+
: typeof init === "string"
|
|
1583
|
+
? Array.from(init)
|
|
1584
|
+
: undefined
|
|
1585
|
+
if (items === undefined) {
|
|
1586
|
+
throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node)
|
|
1587
|
+
}
|
|
1588
|
+
for (const item of items) target.set.add(item)
|
|
1589
|
+
return target
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
private constructURL(args: Array<unknown>, node: AstNode): SandboxURL {
|
|
1593
|
+
if (args.length === 0) {
|
|
1594
|
+
throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as(
|
|
1595
|
+
"TypeError",
|
|
1596
|
+
)
|
|
1597
|
+
}
|
|
1598
|
+
const input = urlArgument(args[0], "new URL input")
|
|
1599
|
+
const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base")
|
|
1600
|
+
try {
|
|
1601
|
+
return new SandboxURL(new URL(input, base))
|
|
1602
|
+
} catch {
|
|
1603
|
+
throw new InterpreterRuntimeError(
|
|
1604
|
+
`new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`,
|
|
1605
|
+
node,
|
|
1606
|
+
).as("TypeError")
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
private constructURLSearchParams(init: unknown, node: AstNode): SandboxURLSearchParams {
|
|
1611
|
+
if (init === undefined) return new SandboxURLSearchParams(new URLSearchParams())
|
|
1612
|
+
if (init instanceof SandboxURLSearchParams) {
|
|
1613
|
+
return new SandboxURLSearchParams(new URLSearchParams(init.params))
|
|
1614
|
+
}
|
|
1615
|
+
if (typeof init === "string") return new SandboxURLSearchParams(new URLSearchParams(init))
|
|
1616
|
+
if (init === null || typeof init === "number" || typeof init === "boolean") {
|
|
1617
|
+
return new SandboxURLSearchParams(new URLSearchParams(coerceToString(init)))
|
|
1618
|
+
}
|
|
1619
|
+
if (init instanceof SandboxMap) {
|
|
1620
|
+
return this.constructURLSearchParams(
|
|
1621
|
+
Array.from(init.map.entries(), ([key, value]) => [key, value]),
|
|
1622
|
+
node,
|
|
1623
|
+
)
|
|
1624
|
+
}
|
|
1625
|
+
if (Array.isArray(init)) {
|
|
1626
|
+
const entries = init.map((pair) => {
|
|
1627
|
+
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
1628
|
+
throw new InterpreterRuntimeError(
|
|
1629
|
+
"new URLSearchParams(...) expects an array of [name, value] pairs.",
|
|
1630
|
+
node,
|
|
1631
|
+
).as("TypeError")
|
|
1632
|
+
}
|
|
1633
|
+
return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [
|
|
1634
|
+
string,
|
|
1635
|
+
string,
|
|
1636
|
+
]
|
|
1637
|
+
})
|
|
1638
|
+
return new SandboxURLSearchParams(new URLSearchParams(entries))
|
|
1639
|
+
}
|
|
1640
|
+
if (isSandboxValue(init)) return new SandboxURLSearchParams(new URLSearchParams())
|
|
1641
|
+
const data = boundedData(init, "new URLSearchParams input")
|
|
1642
|
+
if (data === null || typeof data !== "object") {
|
|
1643
|
+
throw new InterpreterRuntimeError(
|
|
1644
|
+
"new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.",
|
|
1645
|
+
node,
|
|
1646
|
+
).as("TypeError")
|
|
1647
|
+
}
|
|
1648
|
+
return new SandboxURLSearchParams(
|
|
1649
|
+
new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
|
|
1650
|
+
)
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
private evaluateBinaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1654
|
+
const operator = getString(node, "operator")
|
|
1655
|
+
const self = this
|
|
1656
|
+
return Effect.gen(function* () {
|
|
1657
|
+
const lhs = yield* self.evaluateExpression(getNode(node, "left"))
|
|
1658
|
+
const rhs = yield* self.evaluateExpression(getNode(node, "right"))
|
|
1659
|
+
// Like `typeof`, `instanceof` observes any value without coercing it (a promise or
|
|
1660
|
+
// function operand is a legitimate question, not an error), so it is handled before
|
|
1661
|
+
// the data-only operand check.
|
|
1662
|
+
if (operator === "instanceof") return instanceofValue(lhs, rhs, node)
|
|
1663
|
+
return boundedData(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result")
|
|
1664
|
+
})
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Applies a binary operator to two already-evaluated operands with CodeMode's coercion
|
|
1669
|
+
* semantics. Shared by binary expressions and compound assignment (`x op= y` must behave
|
|
1670
|
+
* exactly like `x = x op y`, coercion included).
|
|
1671
|
+
*/
|
|
1672
|
+
private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
|
|
1673
|
+
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
|
|
1674
|
+
throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue")
|
|
1675
|
+
}
|
|
1676
|
+
// Data objects/arrays are null-prototype, so JS's ToPrimitive throws an opaque host
|
|
1677
|
+
// "No default value" TypeError when an operator coerces them. Coerce to their JS string
|
|
1678
|
+
// form first (as String(x) / template literals do) so operators behave like JavaScript.
|
|
1679
|
+
// A Date follows its ToPrimitive hints: string for `+` (concatenation), its time value
|
|
1680
|
+
// for arithmetic and ordering - so `end - start` and `a < b` work as in JS.
|
|
1681
|
+
// Identity (=== / !==) and the right operand of `in` keep their raw object value.
|
|
1682
|
+
const coerceOperand = (operand: unknown): unknown => {
|
|
1683
|
+
if (operand instanceof SandboxDate) return operator === "+" ? coerceToString(operand) : operand.time
|
|
1684
|
+
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
|
|
1685
|
+
}
|
|
1686
|
+
const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"
|
|
1687
|
+
const l = coerceOperand(lhs)
|
|
1688
|
+
const r = coerceOperand(rhs)
|
|
1689
|
+
switch (operator) {
|
|
1690
|
+
case "+":
|
|
1691
|
+
return (l as string) + (r as string)
|
|
1692
|
+
case "-":
|
|
1693
|
+
return (l as number) - (r as number)
|
|
1694
|
+
case "*":
|
|
1695
|
+
return (l as number) * (r as number)
|
|
1696
|
+
case "/":
|
|
1697
|
+
return (l as number) / (r as number)
|
|
1698
|
+
case "%":
|
|
1699
|
+
return (l as number) % (r as number)
|
|
1700
|
+
case "**":
|
|
1701
|
+
return (l as number) ** (r as number)
|
|
1702
|
+
// Two objects compare by identity in JS (no ToPrimitive); only object-vs-primitive coerces.
|
|
1703
|
+
case "==":
|
|
1704
|
+
return bothObjects ? lhs === rhs : l == r
|
|
1705
|
+
case "===":
|
|
1706
|
+
return lhs === rhs
|
|
1707
|
+
case "!=":
|
|
1708
|
+
return bothObjects ? lhs !== rhs : l != r
|
|
1709
|
+
case "!==":
|
|
1710
|
+
return lhs !== rhs
|
|
1711
|
+
case "<":
|
|
1712
|
+
return (l as string) < (r as string)
|
|
1713
|
+
case "<=":
|
|
1714
|
+
return (l as string) <= (r as string)
|
|
1715
|
+
case ">":
|
|
1716
|
+
return (l as string) > (r as string)
|
|
1717
|
+
case ">=":
|
|
1718
|
+
return (l as string) >= (r as string)
|
|
1719
|
+
case "&":
|
|
1720
|
+
return (l as number) & (r as number)
|
|
1721
|
+
case "|":
|
|
1722
|
+
return (l as number) | (r as number)
|
|
1723
|
+
case "^":
|
|
1724
|
+
return (l as number) ^ (r as number)
|
|
1725
|
+
case "<<":
|
|
1726
|
+
return (l as number) << (r as number)
|
|
1727
|
+
case ">>":
|
|
1728
|
+
return (l as number) >> (r as number)
|
|
1729
|
+
case ">>>":
|
|
1730
|
+
return (l as number) >>> (r as number)
|
|
1731
|
+
case "in":
|
|
1732
|
+
if (rhs === null || typeof rhs !== "object") {
|
|
1733
|
+
throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node)
|
|
1734
|
+
}
|
|
1735
|
+
// Own properties only, so arrays don't leak the host Array.prototype (map/constructor/...).
|
|
1736
|
+
return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey)
|
|
1737
|
+
default:
|
|
1738
|
+
throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node)
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
private evaluateLogicalExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1743
|
+
const operator = getString(node, "operator")
|
|
1744
|
+
return Effect.flatMap(this.evaluateExpression(getNode(node, "left")), (left) => {
|
|
1745
|
+
if (operator === "&&") return left ? this.evaluateExpression(getNode(node, "right")) : Effect.succeed(left)
|
|
1746
|
+
if (operator === "||") return left ? Effect.succeed(left) : this.evaluateExpression(getNode(node, "right"))
|
|
1747
|
+
if (operator === "??")
|
|
1748
|
+
return left !== null && left !== undefined
|
|
1749
|
+
? Effect.succeed(left)
|
|
1750
|
+
: this.evaluateExpression(getNode(node, "right"))
|
|
1751
|
+
throw new InterpreterRuntimeError(`Unsupported logical operator '${operator}'.`, node)
|
|
1752
|
+
})
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
private evaluateUnaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1756
|
+
const operator = getString(node, "operator")
|
|
1757
|
+
const argument = getNode(node, "argument")
|
|
1758
|
+
// `typeof undeclaredIdentifier` is `"undefined"` in JS (never a ReferenceError), so
|
|
1759
|
+
// feature-detection guards like `typeof x !== "undefined"` don't crash. Short-circuit before
|
|
1760
|
+
// evaluating the argument; a declared-but-TDZ binding still falls through to the normal throw.
|
|
1761
|
+
if (operator === "typeof" && argument.type === "Identifier" && !this.resolveBinding(getString(argument, "name"))) {
|
|
1762
|
+
return Effect.succeed("undefined")
|
|
1763
|
+
}
|
|
1764
|
+
return Effect.map(this.evaluateExpression(argument), (value) => {
|
|
1765
|
+
// `typeof` and `!` never throw in JS - they observe any value (functions and runtime
|
|
1766
|
+
// references included) without coercing it, so feature detection and negation work.
|
|
1767
|
+
if (operator === "typeof") return typeofValue(value)
|
|
1768
|
+
if (operator === "!") return !value
|
|
1769
|
+
if (containsOpaqueReference(value)) {
|
|
1770
|
+
throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue")
|
|
1771
|
+
}
|
|
1772
|
+
// Numeric/bitwise unary operators ToPrimitive their operand; a Date yields its time value
|
|
1773
|
+
// (`+date` is the epoch-ms idiom), other null-prototype data objects/arrays coerce to
|
|
1774
|
+
// their JS string form first (see evaluateBinaryExpression).
|
|
1775
|
+
const operand =
|
|
1776
|
+
value instanceof SandboxDate
|
|
1777
|
+
? value.time
|
|
1778
|
+
: value !== null && typeof value === "object"
|
|
1779
|
+
? coerceToString(value)
|
|
1780
|
+
: value
|
|
1781
|
+
let result: unknown
|
|
1782
|
+
switch (operator) {
|
|
1783
|
+
case "+":
|
|
1784
|
+
result = +(operand as number)
|
|
1785
|
+
break
|
|
1786
|
+
case "-":
|
|
1787
|
+
result = -(operand as number)
|
|
1788
|
+
break
|
|
1789
|
+
case "~":
|
|
1790
|
+
result = ~(operand as number)
|
|
1791
|
+
break
|
|
1792
|
+
default:
|
|
1793
|
+
throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node)
|
|
1794
|
+
}
|
|
1795
|
+
return boundedData(result, "Unary expression result")
|
|
1796
|
+
})
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private evaluateAssignmentExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1800
|
+
const left = getNode(node, "left")
|
|
1801
|
+
const operator = getString(node, "operator")
|
|
1802
|
+
const self = this
|
|
1803
|
+
return Effect.gen(function* () {
|
|
1804
|
+
if (operator === "??=" || operator === "||=" || operator === "&&=") {
|
|
1805
|
+
return yield* self.evaluateLogicalAssignment(node, left, operator)
|
|
1806
|
+
}
|
|
1807
|
+
const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
|
|
1808
|
+
if (left.type === "Identifier") {
|
|
1809
|
+
const name = getString(left, "name")
|
|
1810
|
+
if (operator === "=") return self.setIdentifierValue(name, rightValue, left)
|
|
1811
|
+
const next = boundedData(
|
|
1812
|
+
self.applyCompoundAssignment(operator, self.getIdentifierValue(name, left), rightValue, node),
|
|
1813
|
+
"Assignment result",
|
|
1814
|
+
)
|
|
1815
|
+
return self.setIdentifierValue(name, next, left)
|
|
1816
|
+
}
|
|
1817
|
+
if (left.type === "MemberExpression") {
|
|
1818
|
+
if (operator === "=") return yield* self.writeMember(left, rightValue)
|
|
1819
|
+
return yield* self.modifyMember(left, (current) => {
|
|
1820
|
+
const next = boundedData(
|
|
1821
|
+
self.applyCompoundAssignment(operator, current, rightValue, node),
|
|
1822
|
+
"Assignment result",
|
|
1823
|
+
)
|
|
1824
|
+
return Effect.succeed({ write: true, next, result: next })
|
|
1825
|
+
})
|
|
1826
|
+
}
|
|
1827
|
+
throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
|
|
1828
|
+
})
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
private evaluateLogicalAssignment(
|
|
1832
|
+
node: AstNode,
|
|
1833
|
+
left: AstNode,
|
|
1834
|
+
operator: string,
|
|
1835
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
1836
|
+
const self = this
|
|
1837
|
+
const shouldAssign = (current: unknown): boolean =>
|
|
1838
|
+
operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current)
|
|
1839
|
+
if (left.type === "Identifier") {
|
|
1840
|
+
const name = getString(left, "name")
|
|
1841
|
+
return Effect.gen(function* () {
|
|
1842
|
+
const current = self.getIdentifierValue(name, left)
|
|
1843
|
+
if (!shouldAssign(current)) return current
|
|
1844
|
+
const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
|
|
1845
|
+
return self.setIdentifierValue(name, rightValue, left)
|
|
1846
|
+
})
|
|
1847
|
+
}
|
|
1848
|
+
if (left.type === "MemberExpression") {
|
|
1849
|
+
// Resolve the member exactly once; evaluate the RHS only if we actually assign.
|
|
1850
|
+
return self.modifyMember(left, (current) =>
|
|
1851
|
+
shouldAssign(current)
|
|
1852
|
+
? Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => ({
|
|
1853
|
+
write: true,
|
|
1854
|
+
next: rightValue,
|
|
1855
|
+
result: rightValue,
|
|
1856
|
+
}))
|
|
1857
|
+
: Effect.succeed({ write: false, next: current, result: current }),
|
|
1858
|
+
)
|
|
1859
|
+
}
|
|
1860
|
+
throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private evaluateUpdateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1864
|
+
const operator = getString(node, "operator")
|
|
1865
|
+
const argument = getNode(node, "argument")
|
|
1866
|
+
const prefix = getBoolean(node, "prefix")
|
|
1867
|
+
|
|
1868
|
+
const increment = operator === "++" ? 1 : operator === "--" ? -1 : undefined
|
|
1869
|
+
|
|
1870
|
+
if (increment === undefined) {
|
|
1871
|
+
throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (argument.type === "Identifier") {
|
|
1875
|
+
return Effect.sync(() => {
|
|
1876
|
+
const name = getString(argument, "name")
|
|
1877
|
+
const current = Number(this.getIdentifierValue(name, argument))
|
|
1878
|
+
const next = current + increment
|
|
1879
|
+
this.setIdentifierValue(name, next, argument)
|
|
1880
|
+
return prefix ? next : current
|
|
1881
|
+
})
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (argument.type === "MemberExpression") {
|
|
1885
|
+
return this.modifyMember(argument, (current) => {
|
|
1886
|
+
const value = Number(current)
|
|
1887
|
+
const next = value + increment
|
|
1888
|
+
return Effect.succeed({ write: true, next, result: prefix ? next : value })
|
|
1889
|
+
})
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument)
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
private evaluateCallExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
1896
|
+
const callee = getNode(node, "callee")
|
|
1897
|
+
const argNodes = getArray(node, "arguments")
|
|
1898
|
+
|
|
1899
|
+
const self = this
|
|
1900
|
+
return Effect.gen(function* () {
|
|
1901
|
+
const callable = yield* self.evaluateExpression(callee)
|
|
1902
|
+
if (callable === OptionalShortCircuit) return OptionalShortCircuit
|
|
1903
|
+
if ((callable === null || callable === undefined) && node.optional === true) return OptionalShortCircuit
|
|
1904
|
+
|
|
1905
|
+
const args = yield* self.evaluateCallArguments(argNodes)
|
|
1906
|
+
|
|
1907
|
+
if (callable instanceof ToolReference) {
|
|
1908
|
+
if (callable.path.length === 0) throw new InterpreterRuntimeError("The tools root is not callable.", callee)
|
|
1909
|
+
// An un-awaited tool call is a first-class promise value; the call itself starts now.
|
|
1910
|
+
return yield* self.createToolCallPromise(callable.path, args)
|
|
1911
|
+
}
|
|
1912
|
+
if (callable instanceof PromiseMethodReference) {
|
|
1913
|
+
return yield* self.invokePromiseMethod(callable, args, node)
|
|
1914
|
+
}
|
|
1915
|
+
if (callable instanceof CodeModeFunction) {
|
|
1916
|
+
return yield* self.invokeFunction(callable, args)
|
|
1917
|
+
}
|
|
1918
|
+
if (callable instanceof IntrinsicReference) {
|
|
1919
|
+
return yield* self.invokeIntrinsic(callable, args, node)
|
|
1920
|
+
}
|
|
1921
|
+
if (callable instanceof GlobalMethodReference) {
|
|
1922
|
+
if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node)
|
|
1923
|
+
if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
|
|
1924
|
+
return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node)
|
|
1925
|
+
}
|
|
1926
|
+
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
|
1927
|
+
}
|
|
1928
|
+
if (callable instanceof CoercionFunction) {
|
|
1929
|
+
return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`)
|
|
1930
|
+
}
|
|
1931
|
+
if (callable instanceof UriFunction) {
|
|
1932
|
+
return invokeUriFunction(callable, args, node)
|
|
1933
|
+
}
|
|
1934
|
+
// `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
|
|
1935
|
+
if (callable instanceof ErrorConstructorReference) {
|
|
1936
|
+
return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0]))
|
|
1937
|
+
}
|
|
1938
|
+
throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee)
|
|
1939
|
+
})
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate
|
|
1943
|
+
// namespace/tool names from the host tool tree - the discovery idiom a model reaches for
|
|
1944
|
+
// first. Every other Object helper cannot produce data from a tool reference, so it fails
|
|
1945
|
+
// with a pointer at the working idioms instead of the generic plain-objects-only message.
|
|
1946
|
+
private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown {
|
|
1947
|
+
if (name === "keys") {
|
|
1948
|
+
return boundedData(this.enumerableKeys(ref)!, "Object.keys result")
|
|
1949
|
+
}
|
|
1950
|
+
throw new InterpreterRuntimeError(
|
|
1951
|
+
`Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`,
|
|
1952
|
+
node,
|
|
1953
|
+
"InvalidDataValue",
|
|
1954
|
+
)
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
private invokeConsole(name: string, args: Array<unknown>, node: AstNode): undefined {
|
|
1958
|
+
if (!consoleMethods.has(name))
|
|
1959
|
+
throw new InterpreterRuntimeError(`console.${name} is not available in CodeMode.`, node)
|
|
1960
|
+
this.logs.push(publicErrorMessage(this.formatConsoleMessage(name, args, node)))
|
|
1961
|
+
return undefined
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
private formatConsoleMessage(name: string, args: Array<unknown>, node: AstNode): string {
|
|
1965
|
+
if (name === "dir") return args.length === 0 ? "undefined" : this.formatConsoleArgument(args[0])
|
|
1966
|
+
if (name === "table") return this.formatConsoleTable(args[0], args[1], node)
|
|
1967
|
+
const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : ""
|
|
1968
|
+
return `${prefix}${args.map((arg) => this.formatConsoleArgument(arg)).join(" ")}`
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Console arguments format deeply and totally: values render as a debugger would show them
|
|
1972
|
+
// rather than as boundary JSON - numbers keep NaN/Infinity (JSON would say null), sandbox
|
|
1973
|
+
// values keep their friendly forms at ANY depth (ISO date, /regex/flags, Map(n) [...],
|
|
1974
|
+
// Set(n) [...]), opaque runtime references become "[CodeMode reference]" markers in place,
|
|
1975
|
+
// and plain objects/arrays render JSON-style. Formatting never fails the program: cycles
|
|
1976
|
+
// render "[Circular]" and extreme depth degrades to "...".
|
|
1977
|
+
private formatConsoleArgument(value: unknown): string {
|
|
1978
|
+
if (value === undefined) return "undefined"
|
|
1979
|
+
// A top-level string prints bare; nested strings are JSON-quoted (see formatConsoleValue).
|
|
1980
|
+
if (typeof value === "string") return value
|
|
1981
|
+
return this.formatConsoleValue(value, new Set(), 0)
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
private formatConsoleValue(value: unknown, seen: Set<object>, depth: number): string {
|
|
1985
|
+
// Nested undefined renders as null, matching what JSON boundary output would show.
|
|
1986
|
+
if (value === null || value === undefined) return "null"
|
|
1987
|
+
if (typeof value === "string") return JSON.stringify(value)
|
|
1988
|
+
// String(value) keeps NaN/Infinity/-Infinity readable; finite numbers match their JSON form.
|
|
1989
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
|
1990
|
+
if (typeof value !== "object") return String(value)
|
|
1991
|
+
if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]"
|
|
1992
|
+
if (value instanceof SandboxDate) return coerceToString(value)
|
|
1993
|
+
if (value instanceof SandboxRegExp) return coerceToString(value)
|
|
1994
|
+
if (value instanceof SandboxURL) return coerceToString(value)
|
|
1995
|
+
if (value instanceof SandboxURLSearchParams) return coerceToString(value)
|
|
1996
|
+
if (depth > MAX_CONSOLE_DEPTH) return "..."
|
|
1997
|
+
if (seen.has(value)) return "[Circular]"
|
|
1998
|
+
if (value instanceof SandboxMap) {
|
|
1999
|
+
seen.add(value)
|
|
2000
|
+
try {
|
|
2001
|
+
const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item])
|
|
2002
|
+
return `Map(${value.map.size}) ${this.formatConsoleValue(entries, seen, depth + 1)}`
|
|
2003
|
+
} finally {
|
|
2004
|
+
seen.delete(value)
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (value instanceof SandboxSet) {
|
|
2008
|
+
seen.add(value)
|
|
2009
|
+
try {
|
|
2010
|
+
return `Set(${value.set.size}) ${this.formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}`
|
|
2011
|
+
} finally {
|
|
2012
|
+
seen.delete(value)
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (isRuntimeReference(value)) return "[CodeMode reference]"
|
|
2016
|
+
seen.add(value)
|
|
2017
|
+
try {
|
|
2018
|
+
if (Array.isArray(value)) {
|
|
2019
|
+
return `[${value.map((item) => this.formatConsoleValue(item, seen, depth + 1)).join(",")}]`
|
|
2020
|
+
}
|
|
2021
|
+
return `{${Object.entries(value)
|
|
2022
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${this.formatConsoleValue(item, seen, depth + 1)}`)
|
|
2023
|
+
.join(",")}}`
|
|
2024
|
+
} finally {
|
|
2025
|
+
seen.delete(value)
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
private formatConsoleTable(value: unknown, columnsArgument: unknown, node: AstNode): string {
|
|
2030
|
+
if (value === undefined) return "undefined"
|
|
2031
|
+
// Sandbox values are legitimate table data (cells render their friendly forms); only
|
|
2032
|
+
// truly opaque references (functions, tools, promises) collapse to the marker.
|
|
2033
|
+
if (containsOpaqueReference(value)) return "[CodeMode reference]"
|
|
2034
|
+
const data = boundedData(value, "console.table argument")
|
|
2035
|
+
const columns = this.consoleTableColumns(columnsArgument, node)
|
|
2036
|
+
const rows = this.consoleTableRows(data, columns)
|
|
2037
|
+
const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values))))
|
|
2038
|
+
const header = ["(index)", ...keys].join("\t")
|
|
2039
|
+
return [
|
|
2040
|
+
header,
|
|
2041
|
+
...rows.map((row) => [row.index, ...keys.map((key) => this.formatConsoleTableCell(row.values[key]))].join("\t")),
|
|
2042
|
+
].join("\n")
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
private consoleTableColumns(value: unknown, node: AstNode): ReadonlyArray<string> | undefined {
|
|
2046
|
+
if (value === undefined) return undefined
|
|
2047
|
+
if (containsRuntimeReference(value)) return undefined
|
|
2048
|
+
const columns = copyOut(copyIn(value, "console.table columns"), true)
|
|
2049
|
+
return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
private consoleTableRows(
|
|
2053
|
+
data: unknown,
|
|
2054
|
+
columns: ReadonlyArray<string> | undefined,
|
|
2055
|
+
): Array<{ readonly index: string; readonly values: Record<string, unknown> }> {
|
|
2056
|
+
if (Array.isArray(data)) {
|
|
2057
|
+
return data.map((item, index) => ({ index: String(index), values: this.consoleTableValues(item, columns) }))
|
|
2058
|
+
}
|
|
2059
|
+
if (data !== null && typeof data === "object" && !isSandboxValue(data)) {
|
|
2060
|
+
return Object.entries(data).map(([index, item]) => ({ index, values: this.consoleTableValues(item, columns) }))
|
|
2061
|
+
}
|
|
2062
|
+
return [{ index: "0", values: { Value: data } }]
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
private consoleTableValues(value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> {
|
|
2066
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) {
|
|
2067
|
+
const source = value as Record<string, unknown>
|
|
2068
|
+
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]]))
|
|
2069
|
+
return Object.fromEntries(Object.entries(source))
|
|
2070
|
+
}
|
|
2071
|
+
return { Value: value }
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
private formatConsoleTableCell(value: unknown): string {
|
|
2075
|
+
if (value === undefined) return ""
|
|
2076
|
+
if (typeof value === "string") return value
|
|
2077
|
+
return this.formatConsoleValue(value, new Set(), 0)
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
private evaluateCallArguments(argNodes: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> {
|
|
2081
|
+
const self = this
|
|
2082
|
+
return Effect.gen(function* () {
|
|
2083
|
+
const args: Array<unknown> = []
|
|
2084
|
+
for (const [index, arg] of argNodes.entries()) {
|
|
2085
|
+
const argNode = asNode(arg, `arguments[${index}]`)
|
|
2086
|
+
if (argNode.type === "SpreadElement") {
|
|
2087
|
+
const spread = yield* self.evaluateExpression(getNode(argNode, "argument"))
|
|
2088
|
+
const items = spreadItems(spread)
|
|
2089
|
+
if (items === undefined)
|
|
2090
|
+
throw new InterpreterRuntimeError(
|
|
2091
|
+
"Spread arguments require an array, string, Map, or Set in CodeMode.",
|
|
2092
|
+
argNode,
|
|
2093
|
+
)
|
|
2094
|
+
args.push(...items)
|
|
2095
|
+
} else {
|
|
2096
|
+
args.push(yield* self.evaluateExpression(argNode))
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return args
|
|
2100
|
+
})
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// Promise.* over ordinary runtime values. Combinators accept ANY array (or spreadable
|
|
2104
|
+
// collection) mixing promise values and plain data - built inline, beforehand, via spread,
|
|
2105
|
+
// whatever - because tool calls already run eagerly on their own fibers; the combinators
|
|
2106
|
+
// only observe settlements. Joining is therefore sequential (no extra fibers) without
|
|
2107
|
+
// costing parallelism, and the concurrency cap stays where the work is: the fork semaphore.
|
|
2108
|
+
private invokePromiseMethod(
|
|
2109
|
+
ref: PromiseMethodReference,
|
|
2110
|
+
args: Array<unknown>,
|
|
2111
|
+
node: AstNode,
|
|
2112
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2113
|
+
const self = this
|
|
2114
|
+
if (ref.name === "resolve") {
|
|
2115
|
+
// Promise.resolve of a promise is that promise (JS flattens); anything else is a
|
|
2116
|
+
// promise already fulfilled with the value.
|
|
2117
|
+
const value = args[0]
|
|
2118
|
+
return Effect.succeed(
|
|
2119
|
+
value instanceof SandboxPromise ? value : new SandboxPromise(undefined, Effect.succeed(value)),
|
|
2120
|
+
)
|
|
2121
|
+
}
|
|
2122
|
+
if (ref.name === "reject") {
|
|
2123
|
+
return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0]))))
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0])
|
|
2127
|
+
if (items === undefined) {
|
|
2128
|
+
throw new InterpreterRuntimeError(
|
|
2129
|
+
`Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
|
|
2130
|
+
node,
|
|
2131
|
+
)
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
switch (ref.name) {
|
|
2135
|
+
case "all": {
|
|
2136
|
+
// Mark every promise element observed up-front (Promise.all handles all of its
|
|
2137
|
+
// members' failures, as in JS), then join in index order; the first failure rejects
|
|
2138
|
+
// the whole call while unrelated in-flight members keep running.
|
|
2139
|
+
const settles = items.map((item) =>
|
|
2140
|
+
item instanceof SandboxPromise ? this.settlePromise(item, node) : Effect.succeed(item),
|
|
2141
|
+
)
|
|
2142
|
+
return Effect.gen(function* () {
|
|
2143
|
+
const values: Array<unknown> = []
|
|
2144
|
+
for (const settle of settles) values.push(yield* settle)
|
|
2145
|
+
return values
|
|
2146
|
+
})
|
|
2147
|
+
}
|
|
2148
|
+
case "allSettled": {
|
|
2149
|
+
const observations = items.map((item) =>
|
|
2150
|
+
item instanceof SandboxPromise
|
|
2151
|
+
? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit }))
|
|
2152
|
+
: Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }),
|
|
2153
|
+
)
|
|
2154
|
+
return Effect.gen(function* () {
|
|
2155
|
+
const outcomes: Array<unknown> = []
|
|
2156
|
+
for (const observation of observations) {
|
|
2157
|
+
const { exit, promise } = yield* observation
|
|
2158
|
+
if (Exit.isSuccess(exit)) {
|
|
2159
|
+
outcomes.push(
|
|
2160
|
+
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
|
|
2161
|
+
)
|
|
2162
|
+
continue
|
|
2163
|
+
}
|
|
2164
|
+
const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)
|
|
2165
|
+
if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) {
|
|
2166
|
+
// Execution teardown (timeout/host interruption), not a program-level rejection.
|
|
2167
|
+
return yield* Effect.failCause(exit.cause)
|
|
2168
|
+
}
|
|
2169
|
+
const thrown = raceInterrupted
|
|
2170
|
+
? new InterpreterRuntimeError(
|
|
2171
|
+
"This tool call was interrupted because another value settled a Promise.race first.",
|
|
2172
|
+
node,
|
|
2173
|
+
)
|
|
2174
|
+
: Cause.squash(exit.cause)
|
|
2175
|
+
outcomes.push(
|
|
2176
|
+
Object.assign(Object.create(null) as SafeObject, {
|
|
2177
|
+
status: "rejected",
|
|
2178
|
+
reason: caughtErrorValue(thrown),
|
|
2179
|
+
}),
|
|
2180
|
+
)
|
|
2181
|
+
}
|
|
2182
|
+
return outcomes
|
|
2183
|
+
})
|
|
2184
|
+
}
|
|
2185
|
+
case "race": {
|
|
2186
|
+
if (items.length === 0) {
|
|
2187
|
+
throw new InterpreterRuntimeError(
|
|
2188
|
+
"Promise.race([]) would never settle; provide at least one promise or value.",
|
|
2189
|
+
node,
|
|
2190
|
+
)
|
|
2191
|
+
}
|
|
2192
|
+
const observations = items.map((item, index) =>
|
|
2193
|
+
item instanceof SandboxPromise
|
|
2194
|
+
? Effect.map(this.observePromise(item), (exit) => ({ index, exit }))
|
|
2195
|
+
: Effect.succeed({ index, exit: Exit.succeed(item as unknown) }),
|
|
2196
|
+
)
|
|
2197
|
+
return Effect.gen(function* () {
|
|
2198
|
+
// First settlement (fulfilled OR rejected) wins; the observations never fail, so
|
|
2199
|
+
// racing them yields exactly that. Losing in-flight calls are then interrupted.
|
|
2200
|
+
const winner = yield* Effect.raceAll(observations)
|
|
2201
|
+
for (const [index, item] of items.entries()) {
|
|
2202
|
+
if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue
|
|
2203
|
+
item.interrupted = true
|
|
2204
|
+
yield* Fiber.interrupt(item.fiber)
|
|
2205
|
+
}
|
|
2206
|
+
const winningItem = items[winner.index]
|
|
2207
|
+
return yield* self.unwrapPromiseExit(
|
|
2208
|
+
winningItem instanceof SandboxPromise ? winningItem : undefined,
|
|
2209
|
+
winner.exit,
|
|
2210
|
+
node,
|
|
2211
|
+
)
|
|
2212
|
+
})
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
|
2218
|
+
const self = this
|
|
2219
|
+
return Effect.suspend(() => {
|
|
2220
|
+
const savedScopes = self.scopes
|
|
2221
|
+
self.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
|
|
2222
|
+
const run = Effect.gen(function* () {
|
|
2223
|
+
// Seed every parameter name into the scope as a TDZ slot first, so a default that
|
|
2224
|
+
// references another parameter resolves to that (uninitialized) param rather than
|
|
2225
|
+
// silently falling through to an outer binding of the same name - matching JS.
|
|
2226
|
+
const paramScope = self.currentScope()
|
|
2227
|
+
for (const parameter of fn.parameters) {
|
|
2228
|
+
for (const name of collectPatternNames(parameter)) {
|
|
2229
|
+
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
for (const [index, parameter] of fn.parameters.entries()) {
|
|
2233
|
+
if (parameter.type === "RestElement") {
|
|
2234
|
+
yield* self.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter)
|
|
2235
|
+
break
|
|
2236
|
+
}
|
|
2237
|
+
yield* self.declarePattern(parameter, args[index], true, parameter)
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (fn.body.type === "BlockStatement") {
|
|
2241
|
+
const result = yield* self.evaluateStatement(fn.body)
|
|
2242
|
+
return result.kind === "return" || result.kind === "value" ? result.value : undefined
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
return yield* self.evaluateExpression(fn.body)
|
|
2246
|
+
})
|
|
2247
|
+
return run.pipe(
|
|
2248
|
+
Effect.ensuring(
|
|
2249
|
+
Effect.sync(() => {
|
|
2250
|
+
self.scopes = savedScopes
|
|
2251
|
+
}),
|
|
2252
|
+
),
|
|
2253
|
+
)
|
|
2254
|
+
})
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
private invokeIntrinsic(
|
|
2258
|
+
ref: IntrinsicReference,
|
|
2259
|
+
args: Array<unknown>,
|
|
2260
|
+
node: AstNode,
|
|
2261
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2262
|
+
if (typeof ref.receiver === "string") {
|
|
2263
|
+
if (
|
|
2264
|
+
(ref.name === "replace" || ref.name === "replaceAll") &&
|
|
2265
|
+
(args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction)
|
|
2266
|
+
) {
|
|
2267
|
+
return this.invokeStringReplacer(ref.receiver, ref.name, args, node)
|
|
2268
|
+
}
|
|
2269
|
+
return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node))
|
|
2270
|
+
}
|
|
2271
|
+
if (typeof ref.receiver === "number") {
|
|
2272
|
+
return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node))
|
|
2273
|
+
}
|
|
2274
|
+
if (Array.isArray(ref.receiver)) {
|
|
2275
|
+
return this.invokeArrayMethod(ref.receiver, ref.name, args, node)
|
|
2276
|
+
}
|
|
2277
|
+
if (ref.receiver instanceof SandboxDate) {
|
|
2278
|
+
return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node))
|
|
2279
|
+
}
|
|
2280
|
+
if (ref.receiver instanceof SandboxRegExp) {
|
|
2281
|
+
return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node))
|
|
2282
|
+
}
|
|
2283
|
+
if (ref.receiver instanceof SandboxMap) {
|
|
2284
|
+
return this.invokeMapMethod(ref.receiver, ref.name, args, node)
|
|
2285
|
+
}
|
|
2286
|
+
if (ref.receiver instanceof SandboxSet) {
|
|
2287
|
+
return this.invokeSetMethod(ref.receiver, ref.name, args, node)
|
|
2288
|
+
}
|
|
2289
|
+
if (ref.receiver instanceof SandboxURL) {
|
|
2290
|
+
return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node))
|
|
2291
|
+
}
|
|
2292
|
+
if (ref.receiver instanceof SandboxURLSearchParams) {
|
|
2293
|
+
return this.invokeURLSearchParamsMethod(ref.receiver, ref.name, args, node)
|
|
2294
|
+
}
|
|
2295
|
+
throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
private invokeStringReplacer(
|
|
2299
|
+
value: string,
|
|
2300
|
+
name: "replace" | "replaceAll",
|
|
2301
|
+
args: Array<unknown>,
|
|
2302
|
+
node: AstNode,
|
|
2303
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2304
|
+
const apply = this.applyCollectionCallback(args[1], `String.${name}`, node)
|
|
2305
|
+
const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = []
|
|
2306
|
+
const collect = (...callbackArgs: Array<unknown>): string => {
|
|
2307
|
+
const match = callbackArgs[0]
|
|
2308
|
+
const groups = callbackArgs[callbackArgs.length - 1]
|
|
2309
|
+
const hasGroups = groups !== null && typeof groups === "object"
|
|
2310
|
+
const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)]
|
|
2311
|
+
if (typeof match !== "string" || typeof offset !== "number") {
|
|
2312
|
+
throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node)
|
|
2313
|
+
}
|
|
2314
|
+
if (hasGroups) {
|
|
2315
|
+
const safeGroups: SafeObject = Object.create(null) as SafeObject
|
|
2316
|
+
for (const [key, group] of Object.entries(groups)) {
|
|
2317
|
+
if (!isBlockedMember(key)) safeGroups[key] = group
|
|
2318
|
+
}
|
|
2319
|
+
callbackArgs[callbackArgs.length - 1] = safeGroups
|
|
2320
|
+
}
|
|
2321
|
+
matches.push({ match, offset, args: callbackArgs })
|
|
2322
|
+
return match
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
const pattern = args[0]
|
|
2326
|
+
if (pattern instanceof SandboxRegExp) {
|
|
2327
|
+
if (name === "replaceAll" && !pattern.regex.global) {
|
|
2328
|
+
throw new InterpreterRuntimeError(
|
|
2329
|
+
`String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`,
|
|
2330
|
+
node,
|
|
2331
|
+
)
|
|
2332
|
+
}
|
|
2333
|
+
if (name === "replace") value.replace(pattern.regex, collect)
|
|
2334
|
+
else value.replaceAll(pattern.regex, collect)
|
|
2335
|
+
} else {
|
|
2336
|
+
if (typeof pattern !== "string") {
|
|
2337
|
+
throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node)
|
|
2338
|
+
}
|
|
2339
|
+
if (name === "replace") value.replace(pattern, collect)
|
|
2340
|
+
else value.replaceAll(pattern, collect)
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
return Effect.gen(function* () {
|
|
2344
|
+
const output: Array<string> = []
|
|
2345
|
+
let end = 0
|
|
2346
|
+
for (const match of matches) {
|
|
2347
|
+
output.push(
|
|
2348
|
+
value.slice(end, match.offset),
|
|
2349
|
+
coerceToString(boundedData(yield* apply(match.args), `String.${name} replacer result`)),
|
|
2350
|
+
)
|
|
2351
|
+
end = match.offset + match.match.length
|
|
2352
|
+
}
|
|
2353
|
+
output.push(value.slice(end))
|
|
2354
|
+
return boundedData(output.join(""), `String.${name} result`)
|
|
2355
|
+
})
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// Runs a collection callback accepting a user function or supported builtin callable,
|
|
2359
|
+
// mirroring the array-method callback contract.
|
|
2360
|
+
private applyCollectionCallback(
|
|
2361
|
+
callback: unknown,
|
|
2362
|
+
name: string,
|
|
2363
|
+
node: AstNode,
|
|
2364
|
+
): (args: Array<unknown>) => Effect.Effect<unknown, unknown, R> {
|
|
2365
|
+
if (
|
|
2366
|
+
!(callback instanceof CodeModeFunction) &&
|
|
2367
|
+
!(callback instanceof CoercionFunction) &&
|
|
2368
|
+
!(callback instanceof UriFunction)
|
|
2369
|
+
) {
|
|
2370
|
+
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
|
|
2371
|
+
}
|
|
2372
|
+
return (callbackArgs) =>
|
|
2373
|
+
callback instanceof CoercionFunction
|
|
2374
|
+
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
|
|
2375
|
+
: callback instanceof UriFunction
|
|
2376
|
+
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
|
|
2377
|
+
: this.invokeFunction(callback, callbackArgs)
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
private invokeMapMethod(
|
|
2381
|
+
target: SandboxMap,
|
|
2382
|
+
name: string,
|
|
2383
|
+
args: Array<unknown>,
|
|
2384
|
+
node: AstNode,
|
|
2385
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2386
|
+
switch (name) {
|
|
2387
|
+
case "get":
|
|
2388
|
+
return Effect.succeed(target.map.get(args[0]))
|
|
2389
|
+
case "has":
|
|
2390
|
+
return Effect.succeed(target.map.has(args[0]))
|
|
2391
|
+
case "set":
|
|
2392
|
+
return Effect.sync(() => {
|
|
2393
|
+
target.map.set(args[0], args[1])
|
|
2394
|
+
return target
|
|
2395
|
+
})
|
|
2396
|
+
case "delete":
|
|
2397
|
+
return Effect.sync(() => target.map.delete(args[0]))
|
|
2398
|
+
case "clear":
|
|
2399
|
+
return Effect.sync(() => {
|
|
2400
|
+
target.map.clear()
|
|
2401
|
+
return undefined
|
|
2402
|
+
})
|
|
2403
|
+
case "keys":
|
|
2404
|
+
return Effect.sync(() => Array.from(target.map.keys()))
|
|
2405
|
+
case "values":
|
|
2406
|
+
return Effect.sync(() => Array.from(target.map.values()))
|
|
2407
|
+
case "entries":
|
|
2408
|
+
return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array<unknown> => [key, item]))
|
|
2409
|
+
case "forEach": {
|
|
2410
|
+
const apply = this.applyCollectionCallback(args[0], "Map.forEach", node)
|
|
2411
|
+
return Effect.gen(function* () {
|
|
2412
|
+
// Snapshot iteration, matching the array-method callback contract.
|
|
2413
|
+
for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target])
|
|
2414
|
+
return undefined
|
|
2415
|
+
})
|
|
2416
|
+
}
|
|
2417
|
+
default:
|
|
2418
|
+
throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node)
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
private invokeSetMethod(
|
|
2423
|
+
target: SandboxSet,
|
|
2424
|
+
name: string,
|
|
2425
|
+
args: Array<unknown>,
|
|
2426
|
+
node: AstNode,
|
|
2427
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2428
|
+
switch (name) {
|
|
2429
|
+
case "has":
|
|
2430
|
+
return Effect.succeed(target.set.has(args[0]))
|
|
2431
|
+
case "add":
|
|
2432
|
+
return Effect.sync(() => {
|
|
2433
|
+
target.set.add(args[0])
|
|
2434
|
+
return target
|
|
2435
|
+
})
|
|
2436
|
+
case "delete":
|
|
2437
|
+
return Effect.sync(() => target.set.delete(args[0]))
|
|
2438
|
+
case "clear":
|
|
2439
|
+
return Effect.sync(() => {
|
|
2440
|
+
target.set.clear()
|
|
2441
|
+
return undefined
|
|
2442
|
+
})
|
|
2443
|
+
case "keys":
|
|
2444
|
+
case "values":
|
|
2445
|
+
return Effect.sync(() => Array.from(target.set.values()))
|
|
2446
|
+
case "entries":
|
|
2447
|
+
return Effect.sync(() => Array.from(target.set.values(), (item): Array<unknown> => [item, item]))
|
|
2448
|
+
case "forEach": {
|
|
2449
|
+
const apply = this.applyCollectionCallback(args[0], "Set.forEach", node)
|
|
2450
|
+
return Effect.gen(function* () {
|
|
2451
|
+
for (const item of Array.from(target.set.values())) yield* apply([item, item, target])
|
|
2452
|
+
return undefined
|
|
2453
|
+
})
|
|
2454
|
+
}
|
|
2455
|
+
default:
|
|
2456
|
+
throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node)
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
private invokeURLSearchParamsMethod(
|
|
2461
|
+
target: SandboxURLSearchParams,
|
|
2462
|
+
name: string,
|
|
2463
|
+
args: Array<unknown>,
|
|
2464
|
+
node: AstNode,
|
|
2465
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2466
|
+
const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`)
|
|
2467
|
+
const requireArgs = (count: number): void => {
|
|
2468
|
+
if (args.length < count) {
|
|
2469
|
+
throw new InterpreterRuntimeError(
|
|
2470
|
+
`URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`,
|
|
2471
|
+
node,
|
|
2472
|
+
).as("TypeError")
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
switch (name) {
|
|
2476
|
+
case "append": {
|
|
2477
|
+
requireArgs(2)
|
|
2478
|
+
return Effect.sync(() => {
|
|
2479
|
+
target.params.append(arg(0), arg(1))
|
|
2480
|
+
return undefined
|
|
2481
|
+
})
|
|
2482
|
+
}
|
|
2483
|
+
case "delete": {
|
|
2484
|
+
requireArgs(1)
|
|
2485
|
+
return Effect.sync(() => {
|
|
2486
|
+
if (args[1] !== undefined) target.params.delete(arg(0), arg(1))
|
|
2487
|
+
else target.params.delete(arg(0))
|
|
2488
|
+
return undefined
|
|
2489
|
+
})
|
|
2490
|
+
}
|
|
2491
|
+
case "get":
|
|
2492
|
+
requireArgs(1)
|
|
2493
|
+
return Effect.sync(() => target.params.get(arg(0)))
|
|
2494
|
+
case "getAll":
|
|
2495
|
+
requireArgs(1)
|
|
2496
|
+
return Effect.sync(() => target.params.getAll(arg(0)))
|
|
2497
|
+
case "has":
|
|
2498
|
+
requireArgs(1)
|
|
2499
|
+
return Effect.sync(() =>
|
|
2500
|
+
args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)),
|
|
2501
|
+
)
|
|
2502
|
+
case "set": {
|
|
2503
|
+
requireArgs(2)
|
|
2504
|
+
return Effect.sync(() => {
|
|
2505
|
+
target.params.set(arg(0), arg(1))
|
|
2506
|
+
return undefined
|
|
2507
|
+
})
|
|
2508
|
+
}
|
|
2509
|
+
case "sort":
|
|
2510
|
+
return Effect.sync(() => {
|
|
2511
|
+
target.params.sort()
|
|
2512
|
+
return undefined
|
|
2513
|
+
})
|
|
2514
|
+
case "keys":
|
|
2515
|
+
return Effect.sync(() => Array.from(target.params.keys()))
|
|
2516
|
+
case "values":
|
|
2517
|
+
return Effect.sync(() => Array.from(target.params.values()))
|
|
2518
|
+
case "entries":
|
|
2519
|
+
return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array<unknown> => [key, value]))
|
|
2520
|
+
case "toString":
|
|
2521
|
+
return Effect.sync(() => target.params.toString())
|
|
2522
|
+
case "forEach": {
|
|
2523
|
+
requireArgs(1)
|
|
2524
|
+
const apply = this.applyCollectionCallback(args[0], "URLSearchParams.forEach", node)
|
|
2525
|
+
return Effect.gen(function* () {
|
|
2526
|
+
for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target])
|
|
2527
|
+
return undefined
|
|
2528
|
+
})
|
|
2529
|
+
}
|
|
2530
|
+
default:
|
|
2531
|
+
throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node)
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
private invokeArrayMethod(
|
|
2536
|
+
target: Array<unknown>,
|
|
2537
|
+
name: string,
|
|
2538
|
+
args: Array<unknown>,
|
|
2539
|
+
node: AstNode,
|
|
2540
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
2541
|
+
const optNumber = (value: unknown, label: string): number | undefined => {
|
|
2542
|
+
if (value === undefined) return undefined
|
|
2543
|
+
if (typeof value !== "number")
|
|
2544
|
+
throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node)
|
|
2545
|
+
return value
|
|
2546
|
+
}
|
|
2547
|
+
switch (name) {
|
|
2548
|
+
case "join": {
|
|
2549
|
+
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
|
|
2550
|
+
throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node)
|
|
2551
|
+
}
|
|
2552
|
+
const input = boundedData(target, "Array.join input") as Array<unknown>
|
|
2553
|
+
return Effect.succeed(
|
|
2554
|
+
input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
|
|
2555
|
+
)
|
|
2556
|
+
}
|
|
2557
|
+
case "includes":
|
|
2558
|
+
if (args.length === 0 || args.length > 2)
|
|
2559
|
+
throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node)
|
|
2560
|
+
return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index")))
|
|
2561
|
+
case "indexOf":
|
|
2562
|
+
return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index")))
|
|
2563
|
+
case "lastIndexOf":
|
|
2564
|
+
return Effect.succeed(
|
|
2565
|
+
args[1] === undefined
|
|
2566
|
+
? target.lastIndexOf(args[0])
|
|
2567
|
+
: target.lastIndexOf(args[0], optNumber(args[1], "start index")),
|
|
2568
|
+
)
|
|
2569
|
+
case "at":
|
|
2570
|
+
return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0))
|
|
2571
|
+
case "slice":
|
|
2572
|
+
return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end")))
|
|
2573
|
+
case "concat":
|
|
2574
|
+
return Effect.succeed(target.concat(...args))
|
|
2575
|
+
case "flat":
|
|
2576
|
+
return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1))
|
|
2577
|
+
case "reverse":
|
|
2578
|
+
return Effect.succeed([...target].reverse())
|
|
2579
|
+
case "sort":
|
|
2580
|
+
case "toSorted":
|
|
2581
|
+
return this.sortArray(target, args[0], node)
|
|
2582
|
+
case "toReversed":
|
|
2583
|
+
return Effect.succeed([...target].reverse())
|
|
2584
|
+
case "with": {
|
|
2585
|
+
const index = optNumber(args[0], "index") ?? 0
|
|
2586
|
+
const resolved = index < 0 ? target.length + index : index
|
|
2587
|
+
if (resolved < 0 || resolved >= target.length) {
|
|
2588
|
+
throw new InterpreterRuntimeError("Array.with index is out of range.", node)
|
|
2589
|
+
}
|
|
2590
|
+
const copied = [...target]
|
|
2591
|
+
copied[resolved] = args[1]
|
|
2592
|
+
return Effect.succeed(copied)
|
|
2593
|
+
}
|
|
2594
|
+
case "push": {
|
|
2595
|
+
// Validate before mutating (so no rollback is needed): inserting a container into
|
|
2596
|
+
// itself would create a cycle no later walk could survive.
|
|
2597
|
+
for (const item of args) this.rejectCircularInsertion(target, item, "Array.push result", node)
|
|
2598
|
+
target.push(...args)
|
|
2599
|
+
return Effect.succeed(target.length)
|
|
2600
|
+
}
|
|
2601
|
+
case "unshift": {
|
|
2602
|
+
for (const item of args) this.rejectCircularInsertion(target, item, "Array.unshift result", node)
|
|
2603
|
+
target.unshift(...args)
|
|
2604
|
+
return Effect.succeed(target.length)
|
|
2605
|
+
}
|
|
2606
|
+
case "pop":
|
|
2607
|
+
return Effect.succeed(target.pop())
|
|
2608
|
+
case "shift":
|
|
2609
|
+
return Effect.succeed(target.shift())
|
|
2610
|
+
case "splice": {
|
|
2611
|
+
// Mutates in place and returns the removed elements, exactly like JS: one argument
|
|
2612
|
+
// removes to the end, an undefined delete count removes nothing.
|
|
2613
|
+
if (args.length === 0) return Effect.succeed(target.splice(0, 0))
|
|
2614
|
+
const start = optNumber(args[0], "start") ?? 0
|
|
2615
|
+
if (args.length === 1) return Effect.succeed(target.splice(start))
|
|
2616
|
+
const deleteCount = optNumber(args[1], "delete count") ?? 0
|
|
2617
|
+
const inserted = args.slice(2)
|
|
2618
|
+
for (const item of inserted) this.rejectCircularInsertion(target, item, "Array.splice result", node)
|
|
2619
|
+
return Effect.succeed(target.splice(start, deleteCount, ...inserted))
|
|
2620
|
+
}
|
|
2621
|
+
case "fill": {
|
|
2622
|
+
this.rejectCircularInsertion(target, args[0], "Array.fill result", node)
|
|
2623
|
+
return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")))
|
|
2624
|
+
}
|
|
2625
|
+
case "copyWithin":
|
|
2626
|
+
return Effect.succeed(
|
|
2627
|
+
target.copyWithin(
|
|
2628
|
+
optNumber(args[0], "target index") ?? 0,
|
|
2629
|
+
optNumber(args[1], "start") ?? 0,
|
|
2630
|
+
optNumber(args[2], "end"),
|
|
2631
|
+
),
|
|
2632
|
+
)
|
|
2633
|
+
// keys/values/entries return arrays (not iterators), matching the Map/Set convention;
|
|
2634
|
+
// they work with for...of and spread either way.
|
|
2635
|
+
case "keys":
|
|
2636
|
+
return Effect.succeed(Array.from(target.keys()))
|
|
2637
|
+
case "values":
|
|
2638
|
+
return Effect.succeed([...target])
|
|
2639
|
+
case "entries":
|
|
2640
|
+
return Effect.succeed(Array.from(target.entries(), ([index, item]): Array<unknown> => [index, item]))
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const callback = args[0]
|
|
2644
|
+
if (
|
|
2645
|
+
!(callback instanceof CodeModeFunction) &&
|
|
2646
|
+
!(callback instanceof CoercionFunction) &&
|
|
2647
|
+
!(callback instanceof UriFunction)
|
|
2648
|
+
) {
|
|
2649
|
+
throw new InterpreterRuntimeError(`Array.${name} expects a function callback.`, node)
|
|
2650
|
+
}
|
|
2651
|
+
const self = this
|
|
2652
|
+
// Accept a user function or supported builtin callable, so idioms such as
|
|
2653
|
+
// `filter(Boolean)`, `map(String)`, and `map(encodeURIComponent)` work as in JS. Builtins
|
|
2654
|
+
// are synchronous; only CodeModeFunctions can await tool calls.
|
|
2655
|
+
const apply = (callbackArgs: Array<unknown>): Effect.Effect<unknown, unknown, R> =>
|
|
2656
|
+
callback instanceof CoercionFunction
|
|
2657
|
+
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
|
|
2658
|
+
: callback instanceof UriFunction
|
|
2659
|
+
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
|
|
2660
|
+
: self.invokeFunction(callback, callbackArgs)
|
|
2661
|
+
return Effect.gen(function* () {
|
|
2662
|
+
// Iterate a snapshot taken at call time so a callback that mutates the array can't
|
|
2663
|
+
// self-extend the loop - matching JS, where elements appended during iteration are not visited.
|
|
2664
|
+
const items = target.slice()
|
|
2665
|
+
switch (name) {
|
|
2666
|
+
case "map": {
|
|
2667
|
+
const values: Array<unknown> = []
|
|
2668
|
+
for (const [index, item] of items.entries()) values.push(yield* apply([item, index, items]))
|
|
2669
|
+
return values
|
|
2670
|
+
}
|
|
2671
|
+
case "flatMap": {
|
|
2672
|
+
const values: Array<unknown> = []
|
|
2673
|
+
for (const [index, item] of items.entries()) {
|
|
2674
|
+
const mapped = yield* apply([item, index, items])
|
|
2675
|
+
if (Array.isArray(mapped)) values.push(...mapped)
|
|
2676
|
+
else values.push(mapped)
|
|
2677
|
+
}
|
|
2678
|
+
return values
|
|
2679
|
+
}
|
|
2680
|
+
case "filter": {
|
|
2681
|
+
const values: Array<unknown> = []
|
|
2682
|
+
for (const [index, item] of items.entries()) {
|
|
2683
|
+
if (yield* apply([item, index, items])) values.push(item)
|
|
2684
|
+
}
|
|
2685
|
+
return values
|
|
2686
|
+
}
|
|
2687
|
+
case "find":
|
|
2688
|
+
for (const [index, item] of items.entries()) {
|
|
2689
|
+
if (yield* apply([item, index, items])) return item
|
|
2690
|
+
}
|
|
2691
|
+
return undefined
|
|
2692
|
+
case "findIndex":
|
|
2693
|
+
for (const [index, item] of items.entries()) {
|
|
2694
|
+
if (yield* apply([item, index, items])) return index
|
|
2695
|
+
}
|
|
2696
|
+
return -1
|
|
2697
|
+
case "some":
|
|
2698
|
+
for (const [index, item] of items.entries()) {
|
|
2699
|
+
if (yield* apply([item, index, items])) return true
|
|
2700
|
+
}
|
|
2701
|
+
return false
|
|
2702
|
+
case "every":
|
|
2703
|
+
for (const [index, item] of items.entries()) {
|
|
2704
|
+
if (!(yield* apply([item, index, items]))) return false
|
|
2705
|
+
}
|
|
2706
|
+
return true
|
|
2707
|
+
case "forEach":
|
|
2708
|
+
for (const [index, item] of items.entries()) yield* apply([item, index, items])
|
|
2709
|
+
return undefined
|
|
2710
|
+
case "reduce": {
|
|
2711
|
+
let accumulator: unknown
|
|
2712
|
+
let start: number
|
|
2713
|
+
if (args.length >= 2) {
|
|
2714
|
+
accumulator = args[1]
|
|
2715
|
+
start = 0
|
|
2716
|
+
} else {
|
|
2717
|
+
if (items.length === 0)
|
|
2718
|
+
throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
|
|
2719
|
+
accumulator = items[0]
|
|
2720
|
+
start = 1
|
|
2721
|
+
}
|
|
2722
|
+
for (let index = start; index < items.length; index += 1) {
|
|
2723
|
+
accumulator = yield* apply([accumulator, items[index], index, items])
|
|
2724
|
+
}
|
|
2725
|
+
return accumulator
|
|
2726
|
+
}
|
|
2727
|
+
case "reduceRight": {
|
|
2728
|
+
let accumulator: unknown
|
|
2729
|
+
let start: number
|
|
2730
|
+
if (args.length >= 2) {
|
|
2731
|
+
accumulator = args[1]
|
|
2732
|
+
start = items.length - 1
|
|
2733
|
+
} else {
|
|
2734
|
+
if (items.length === 0)
|
|
2735
|
+
throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
|
|
2736
|
+
accumulator = items[items.length - 1]
|
|
2737
|
+
start = items.length - 2
|
|
2738
|
+
}
|
|
2739
|
+
for (let index = start; index >= 0; index -= 1) {
|
|
2740
|
+
accumulator = yield* apply([accumulator, items[index], index, items])
|
|
2741
|
+
}
|
|
2742
|
+
return accumulator
|
|
2743
|
+
}
|
|
2744
|
+
case "findLast":
|
|
2745
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
2746
|
+
if (yield* apply([items[index], index, items])) return items[index]
|
|
2747
|
+
}
|
|
2748
|
+
return undefined
|
|
2749
|
+
case "findLastIndex":
|
|
2750
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
2751
|
+
if (yield* apply([items[index], index, items])) return index
|
|
2752
|
+
}
|
|
2753
|
+
return -1
|
|
2754
|
+
}
|
|
2755
|
+
throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node)
|
|
2756
|
+
})
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
private sortArray(
|
|
2760
|
+
target: Array<unknown>,
|
|
2761
|
+
comparator: unknown,
|
|
2762
|
+
node: AstNode,
|
|
2763
|
+
): Effect.Effect<Array<unknown>, unknown, R> {
|
|
2764
|
+
if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) {
|
|
2765
|
+
throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node)
|
|
2766
|
+
}
|
|
2767
|
+
if (!(comparator instanceof CodeModeFunction)) {
|
|
2768
|
+
return Effect.sync(() =>
|
|
2769
|
+
[...target].sort((a, b) => {
|
|
2770
|
+
const left = coerceToString(a)
|
|
2771
|
+
const right = coerceToString(b)
|
|
2772
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
2773
|
+
}),
|
|
2774
|
+
)
|
|
2775
|
+
}
|
|
2776
|
+
const self = this
|
|
2777
|
+
const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
|
|
2778
|
+
if (items.length <= 1) return Effect.succeed(items)
|
|
2779
|
+
const midpoint = Math.floor(items.length / 2)
|
|
2780
|
+
return Effect.gen(function* () {
|
|
2781
|
+
const left = yield* mergeSort(items.slice(0, midpoint))
|
|
2782
|
+
const right = yield* mergeSort(items.slice(midpoint))
|
|
2783
|
+
const merged: Array<unknown> = []
|
|
2784
|
+
let leftIndex = 0
|
|
2785
|
+
let rightIndex = 0
|
|
2786
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
2787
|
+
// Coerce the comparator's result like JS ToNumber (data objects -> NaN, never a host
|
|
2788
|
+
// crash) and treat NaN as 0 - the spec's "no consistent order" -> keep the left element.
|
|
2789
|
+
const order = coerceToNumber(yield* self.invokeFunction(comparator, [left[leftIndex], right[rightIndex]]))
|
|
2790
|
+
if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++])
|
|
2791
|
+
else merged.push(right[rightIndex++])
|
|
2792
|
+
}
|
|
2793
|
+
return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)]
|
|
2794
|
+
})
|
|
2795
|
+
}
|
|
2796
|
+
// Per spec, undefined elements sort to the end and the comparator is never called on them.
|
|
2797
|
+
const defined = target.filter((item) => item !== undefined)
|
|
2798
|
+
const undefinedCount = target.length - defined.length
|
|
2799
|
+
return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)])
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
private evaluateObjectExpression(node: AstNode): Effect.Effect<Record<string, unknown>, unknown, R> {
|
|
2803
|
+
const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown>
|
|
2804
|
+
const properties = getArray(node, "properties")
|
|
2805
|
+
const self = this
|
|
2806
|
+
return Effect.gen(function* () {
|
|
2807
|
+
for (const propertyValue of properties) {
|
|
2808
|
+
const property = asNode(propertyValue, "properties")
|
|
2809
|
+
|
|
2810
|
+
if (property.type === "SpreadElement") {
|
|
2811
|
+
const spread = yield* self.evaluateExpression(getNode(property, "argument"))
|
|
2812
|
+
// JS treats `{ ...null }` / `{ ...undefined }` as a no-op, so the common
|
|
2813
|
+
// `{ ...maybeOpts, override }` merge works when the operand is absent. Sandbox values
|
|
2814
|
+
// have no own enumerable properties in JS, so they are no-ops too.
|
|
2815
|
+
if (spread === null || spread === undefined || isSandboxValue(spread)) continue
|
|
2816
|
+
if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) {
|
|
2817
|
+
throw new InterpreterRuntimeError(
|
|
2818
|
+
"Object spread requires a data object in CodeMode.",
|
|
2819
|
+
property,
|
|
2820
|
+
"InvalidDataValue",
|
|
2821
|
+
)
|
|
2822
|
+
}
|
|
2823
|
+
for (const [key, value] of Object.entries(spread)) {
|
|
2824
|
+
if (isBlockedMember(key))
|
|
2825
|
+
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, property)
|
|
2826
|
+
objectValue[key] = value
|
|
2827
|
+
}
|
|
2828
|
+
continue
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
if (property.type !== "Property") {
|
|
2832
|
+
throw new InterpreterRuntimeError("Only standard object properties are supported.", property)
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
if (getString(property, "kind") !== "init") {
|
|
2836
|
+
throw new InterpreterRuntimeError("Only init object properties are supported.", property)
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
const keyNode = getNode(property, "key")
|
|
2840
|
+
const valueNode = getNode(property, "value")
|
|
2841
|
+
const computed = getBoolean(property, "computed")
|
|
2842
|
+
|
|
2843
|
+
let key: PropertyKey
|
|
2844
|
+
|
|
2845
|
+
if (computed) {
|
|
2846
|
+
key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode)
|
|
2847
|
+
} else if (keyNode.type === "Identifier") {
|
|
2848
|
+
key = getString(keyNode, "name")
|
|
2849
|
+
} else if (keyNode.type === "Literal") {
|
|
2850
|
+
key = self.toPropertyKey(keyNode.value, keyNode)
|
|
2851
|
+
} else {
|
|
2852
|
+
throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode)
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
if (isBlockedMember(String(key))) {
|
|
2856
|
+
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, keyNode)
|
|
2857
|
+
}
|
|
2858
|
+
objectValue[String(key)] = yield* self.evaluateExpression(valueNode)
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
return objectValue
|
|
2862
|
+
})
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
private evaluateArrayExpression(node: AstNode): Effect.Effect<Array<unknown>, unknown, R> {
|
|
2866
|
+
const elements = getArray(node, "elements")
|
|
2867
|
+
const values: Array<unknown> = []
|
|
2868
|
+
|
|
2869
|
+
const self = this
|
|
2870
|
+
return Effect.gen(function* () {
|
|
2871
|
+
for (const elementValue of elements) {
|
|
2872
|
+
if (elementValue === null) {
|
|
2873
|
+
values.push(undefined)
|
|
2874
|
+
continue
|
|
2875
|
+
}
|
|
2876
|
+
const element = asNode(elementValue, "elements")
|
|
2877
|
+
if (element.type === "SpreadElement") {
|
|
2878
|
+
const spread = yield* self.evaluateExpression(getNode(element, "argument"))
|
|
2879
|
+
const items = spreadItems(spread)
|
|
2880
|
+
if (items === undefined)
|
|
2881
|
+
throw new InterpreterRuntimeError(
|
|
2882
|
+
"Array spread requires an array, string, Map, or Set in CodeMode.",
|
|
2883
|
+
element,
|
|
2884
|
+
)
|
|
2885
|
+
values.push(...items)
|
|
2886
|
+
} else {
|
|
2887
|
+
values.push(yield* self.evaluateExpression(element))
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return values
|
|
2891
|
+
})
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
private evaluateTemplateLiteral(node: AstNode): Effect.Effect<string, unknown, R> {
|
|
2895
|
+
const quasis = getArray(node, "quasis")
|
|
2896
|
+
const expressions = getArray(node, "expressions")
|
|
2897
|
+
|
|
2898
|
+
let output = ""
|
|
2899
|
+
|
|
2900
|
+
const self = this
|
|
2901
|
+
return Effect.gen(function* () {
|
|
2902
|
+
for (let index = 0; index < quasis.length; index += 1) {
|
|
2903
|
+
const quasi = asNode(quasis[index], "quasis")
|
|
2904
|
+
const rawValue = quasi.value
|
|
2905
|
+
|
|
2906
|
+
if (!isRecord(rawValue) || typeof rawValue.cooked !== "string") {
|
|
2907
|
+
throw new InterpreterRuntimeError("Invalid template literal quasi.", quasi)
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
output += rawValue.cooked
|
|
2911
|
+
|
|
2912
|
+
if (index < expressions.length) {
|
|
2913
|
+
const raw = yield* self.evaluateExpression(asNode(expressions[index], "expressions"))
|
|
2914
|
+
// The preserving checkpoint keeps sandbox values intact, so coerceToString renders
|
|
2915
|
+
// them directly (ISO date, /regex/ literal form) instead of a JSON-serialized husk.
|
|
2916
|
+
output += coerceToString(boundedData(raw, "Template interpolation"))
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
return output
|
|
2921
|
+
})
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
private evaluateConditionalExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
2925
|
+
return Effect.flatMap(this.evaluateExpression(getNode(node, "test")), (test) =>
|
|
2926
|
+
this.evaluateExpression(getNode(node, test ? "consequent" : "alternate")),
|
|
2927
|
+
)
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown {
|
|
2931
|
+
// `x op= y` is `x = x op y`: dispatch through the shared binary operator implementation
|
|
2932
|
+
// so compound assignment inherits the same coercion semantics (Dates, data objects, ...).
|
|
2933
|
+
// Only the arithmetic/bitwise operators are compoundable; logical assignments (&&=/||=/??=)
|
|
2934
|
+
// short-circuit and are handled by evaluateLogicalAssignment before reaching here.
|
|
2935
|
+
if (!compoundOperators.has(operator)) {
|
|
2936
|
+
throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node)
|
|
2937
|
+
}
|
|
2938
|
+
return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node)
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
private getMemberReference(
|
|
2942
|
+
node: AstNode,
|
|
2943
|
+
): Effect.Effect<
|
|
2944
|
+
| MemberReference
|
|
2945
|
+
| ToolReference
|
|
2946
|
+
| PromiseMethodReference
|
|
2947
|
+
| IntrinsicReference
|
|
2948
|
+
| GlobalMethodReference
|
|
2949
|
+
| ComputedValue
|
|
2950
|
+
| typeof OptionalShortCircuit
|
|
2951
|
+
| undefined,
|
|
2952
|
+
unknown,
|
|
2953
|
+
R
|
|
2954
|
+
> {
|
|
2955
|
+
const objectNode = getNode(node, "object")
|
|
2956
|
+
const propertyNode = getNode(node, "property")
|
|
2957
|
+
const computed = getBoolean(node, "computed")
|
|
2958
|
+
const optional = node.optional === true
|
|
2959
|
+
const self = this
|
|
2960
|
+
return Effect.gen(function* () {
|
|
2961
|
+
const objectValue = yield* self.evaluateExpression(objectNode)
|
|
2962
|
+
if (objectValue === OptionalShortCircuit) return OptionalShortCircuit
|
|
2963
|
+
if ((objectValue === null || objectValue === undefined) && optional) return OptionalShortCircuit
|
|
2964
|
+
|
|
2965
|
+
const key = computed
|
|
2966
|
+
? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
|
|
2967
|
+
: propertyNode.type === "Identifier"
|
|
2968
|
+
? getString(propertyNode, "name")
|
|
2969
|
+
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
|
|
2970
|
+
|
|
2971
|
+
if (objectValue instanceof ToolReference) {
|
|
2972
|
+
if (typeof key !== "string" || isBlockedMember(key)) {
|
|
2973
|
+
throw new InterpreterRuntimeError("Tool paths must use safe string property names.", propertyNode)
|
|
2974
|
+
}
|
|
2975
|
+
return new ToolReference([...objectValue.path, key])
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (objectValue instanceof PromiseNamespace) {
|
|
2979
|
+
if (typeof key === "string" && promiseStatics.has(key as PromiseMethodName)) {
|
|
2980
|
+
return new PromiseMethodReference(key as PromiseMethodName)
|
|
2981
|
+
}
|
|
2982
|
+
throw new InterpreterRuntimeError(
|
|
2983
|
+
`Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`,
|
|
2984
|
+
propertyNode,
|
|
2985
|
+
)
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
if (objectValue instanceof GlobalNamespace) {
|
|
2989
|
+
if (typeof key !== "string" || isBlockedMember(key)) {
|
|
2990
|
+
throw new InterpreterRuntimeError(
|
|
2991
|
+
`${objectValue.name}.${String(key)} is not available in CodeMode.`,
|
|
2992
|
+
propertyNode,
|
|
2993
|
+
)
|
|
2994
|
+
}
|
|
2995
|
+
if (objectValue.name === "Math" && mathConstants.has(key)) {
|
|
2996
|
+
return new ComputedValue((Math as unknown as Record<string, number>)[key])
|
|
2997
|
+
}
|
|
2998
|
+
return new GlobalMethodReference(objectValue.name, key)
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
if (typeof objectValue === "string") {
|
|
3002
|
+
if (key === "length") return new ComputedValue(objectValue.length)
|
|
3003
|
+
if (typeof key === "number") return new ComputedValue(objectValue[key])
|
|
3004
|
+
if (typeof key === "string" && /^\d+$/.test(key)) return new ComputedValue(objectValue[Number(key)])
|
|
3005
|
+
if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3006
|
+
// Unknown property on a string reads as `undefined`, matching JS (`"x".foo === undefined`),
|
|
3007
|
+
// instead of throwing - so defensive access like `result?.login ?? result` on a JSON-string
|
|
3008
|
+
// tool result doesn't crash. (Optional chaining only guards null/undefined receivers, so a
|
|
3009
|
+
// real string still reaches here.) Only the method allowlist above yields callables.
|
|
3010
|
+
return new ComputedValue(undefined)
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
if (typeof objectValue === "number") {
|
|
3014
|
+
if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3015
|
+
// Unknown property on a number reads as `undefined`, matching JS, rather than throwing.
|
|
3016
|
+
return new ComputedValue(undefined)
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// Number / String expose a small allowlist of statics; everything else stays opaque.
|
|
3020
|
+
if (objectValue instanceof CoercionFunction && typeof key === "string" && !isBlockedMember(key)) {
|
|
3021
|
+
if (objectValue.name === "Number" && numberConstants.has(key)) {
|
|
3022
|
+
return new ComputedValue((Number as unknown as Record<string, number>)[key])
|
|
3023
|
+
}
|
|
3024
|
+
if (objectValue.name === "Number" && numberStatics.has(key)) return new GlobalMethodReference("Number", key)
|
|
3025
|
+
if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key)
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// Sandbox value types expose their method/property allowlists; any other key reads as
|
|
3029
|
+
// `undefined`, consistent with unknown-property reads on strings/numbers/arrays.
|
|
3030
|
+
if (objectValue instanceof SandboxDate) {
|
|
3031
|
+
if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3032
|
+
return new ComputedValue(undefined)
|
|
3033
|
+
}
|
|
3034
|
+
if (objectValue instanceof SandboxRegExp) {
|
|
3035
|
+
if (typeof key === "string" && regexpProperties.has(key)) {
|
|
3036
|
+
return new ComputedValue((objectValue.regex as unknown as Record<string, unknown>)[key])
|
|
3037
|
+
}
|
|
3038
|
+
if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3039
|
+
return new ComputedValue(undefined)
|
|
3040
|
+
}
|
|
3041
|
+
if (objectValue instanceof SandboxMap) {
|
|
3042
|
+
if (key === "size") return new ComputedValue(objectValue.map.size)
|
|
3043
|
+
if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3044
|
+
return new ComputedValue(undefined)
|
|
3045
|
+
}
|
|
3046
|
+
if (objectValue instanceof SandboxSet) {
|
|
3047
|
+
if (key === "size") return new ComputedValue(objectValue.set.size)
|
|
3048
|
+
if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3049
|
+
return new ComputedValue(undefined)
|
|
3050
|
+
}
|
|
3051
|
+
if (objectValue instanceof SandboxURL) {
|
|
3052
|
+
if (key === "searchParams") {
|
|
3053
|
+
return new ComputedValue(objectValue.searchParams)
|
|
3054
|
+
}
|
|
3055
|
+
if (typeof key === "string" && urlMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
|
3056
|
+
if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key }
|
|
3057
|
+
return new ComputedValue(undefined)
|
|
3058
|
+
}
|
|
3059
|
+
if (objectValue instanceof SandboxURLSearchParams) {
|
|
3060
|
+
if (key === "size") return new ComputedValue(objectValue.params.size)
|
|
3061
|
+
if (typeof key === "string" && urlSearchParamsMethods.has(key)) {
|
|
3062
|
+
return new IntrinsicReference(objectValue, key)
|
|
3063
|
+
}
|
|
3064
|
+
return new ComputedValue(undefined)
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Any property access on a promise is a confused program (`p.then(...)`, `p.value`);
|
|
3068
|
+
// reading `undefined` here would hide the missing await, so both paths get an explicit,
|
|
3069
|
+
// await-hinting error instead of the forgiving unknown-property fallthrough.
|
|
3070
|
+
if (objectValue instanceof SandboxPromise) {
|
|
3071
|
+
if (key === "then" || key === "catch" || key === "finally") {
|
|
3072
|
+
throw new InterpreterRuntimeError(
|
|
3073
|
+
`Promise.prototype.${String(key)} is not supported in CodeMode; use await instead (with try/catch to handle failures) - e.g. \`const result = await tools.ns.tool(...)\`.`,
|
|
3074
|
+
propertyNode,
|
|
3075
|
+
"UnsupportedSyntax",
|
|
3076
|
+
[supportedSyntaxMessage],
|
|
3077
|
+
)
|
|
3078
|
+
}
|
|
3079
|
+
throw new InterpreterRuntimeError(
|
|
3080
|
+
"This value is an un-awaited Promise and has no readable properties; await it first - e.g. `const result = await tools.ns.tool(...)`.",
|
|
3081
|
+
objectNode,
|
|
3082
|
+
"InvalidDataValue",
|
|
3083
|
+
)
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
if (isRuntimeReference(objectValue)) {
|
|
3087
|
+
throw new InterpreterRuntimeError(
|
|
3088
|
+
"CodeMode runtime references are opaque and do not expose properties.",
|
|
3089
|
+
objectNode,
|
|
3090
|
+
"InvalidDataValue",
|
|
3091
|
+
)
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
if (typeof objectValue !== "object" || objectValue === null) {
|
|
3095
|
+
throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode)
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
if (typeof key === "string" && isBlockedMember(key)) {
|
|
3099
|
+
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, propertyNode)
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
if (Array.isArray(objectValue)) {
|
|
3103
|
+
if (
|
|
3104
|
+
key !== "length" &&
|
|
3105
|
+
!(typeof key === "string" && arrayMethods.has(key)) &&
|
|
3106
|
+
typeof key !== "number" &&
|
|
3107
|
+
!/^\d+$/.test(key)
|
|
3108
|
+
) {
|
|
3109
|
+
// Own non-index properties read through (match results carry index/groups); like JS,
|
|
3110
|
+
// they are readable in place and dropped by JSON at data boundaries.
|
|
3111
|
+
if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
|
|
3112
|
+
return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
|
|
3113
|
+
}
|
|
3114
|
+
// Unknown property on an array reads as `undefined`, matching JS (`[1,2].foo === undefined`),
|
|
3115
|
+
// instead of throwing - so defensive access under optional chaining behaves as expected.
|
|
3116
|
+
return new ComputedValue(undefined)
|
|
3117
|
+
}
|
|
3118
|
+
return { target: objectValue, key }
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
return { target: objectValue as SafeObject, key }
|
|
3122
|
+
})
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
private readMember(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
|
3126
|
+
return Effect.map(this.getMemberReference(node), (reference) => {
|
|
3127
|
+
if (reference === OptionalShortCircuit) return OptionalShortCircuit
|
|
3128
|
+
if (reference instanceof ComputedValue) return reference.value
|
|
3129
|
+
if (
|
|
3130
|
+
reference === undefined ||
|
|
3131
|
+
reference instanceof ToolReference ||
|
|
3132
|
+
reference instanceof PromiseMethodReference ||
|
|
3133
|
+
reference instanceof IntrinsicReference ||
|
|
3134
|
+
reference instanceof GlobalMethodReference
|
|
3135
|
+
)
|
|
3136
|
+
return reference
|
|
3137
|
+
if (Array.isArray(reference.target)) {
|
|
3138
|
+
if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
|
|
3139
|
+
return new IntrinsicReference(reference.target, reference.key)
|
|
3140
|
+
}
|
|
3141
|
+
return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)]
|
|
3142
|
+
}
|
|
3143
|
+
if (reference.target instanceof SandboxURL) {
|
|
3144
|
+
return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
|
|
3145
|
+
}
|
|
3146
|
+
return reference.target[String(reference.key)]
|
|
3147
|
+
})
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
private writeMember(node: AstNode, value: unknown): Effect.Effect<unknown, unknown, R> {
|
|
3151
|
+
return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// Resolves the member reference EXACTLY ONCE (so a side-effecting object/key expression
|
|
3155
|
+
// runs once), then lets `compute` decide whether to write - enabling compound assignment,
|
|
3156
|
+
// updates, plain writes, and short-circuiting logical assignment to share one safe path.
|
|
3157
|
+
private modifyMember(
|
|
3158
|
+
node: AstNode,
|
|
3159
|
+
compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>,
|
|
3160
|
+
): Effect.Effect<unknown, unknown, R> {
|
|
3161
|
+
const self = this
|
|
3162
|
+
return Effect.gen(function* () {
|
|
3163
|
+
const reference = yield* self.getMemberReference(node)
|
|
3164
|
+
if (
|
|
3165
|
+
reference === OptionalShortCircuit ||
|
|
3166
|
+
reference instanceof ComputedValue ||
|
|
3167
|
+
reference === undefined ||
|
|
3168
|
+
reference instanceof ToolReference ||
|
|
3169
|
+
reference instanceof PromiseMethodReference ||
|
|
3170
|
+
reference instanceof IntrinsicReference ||
|
|
3171
|
+
reference instanceof GlobalMethodReference
|
|
3172
|
+
) {
|
|
3173
|
+
throw new InterpreterRuntimeError("Only data fields may be assigned in CodeMode.", node)
|
|
3174
|
+
}
|
|
3175
|
+
if (Array.isArray(reference.target)) {
|
|
3176
|
+
if (reference.key === "length")
|
|
3177
|
+
throw new InterpreterRuntimeError("Array length cannot be assigned in CodeMode.", node)
|
|
3178
|
+
if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
|
|
3179
|
+
throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node)
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key)
|
|
3183
|
+
const current =
|
|
3184
|
+
reference.target instanceof SandboxURL
|
|
3185
|
+
? (reference.target.url as unknown as Record<string, unknown>)[key]
|
|
3186
|
+
: (reference.target as Record<PropertyKey, unknown>)[key]
|
|
3187
|
+
const { write, next, result } = yield* compute(current)
|
|
3188
|
+
if (write) self.assignToReference(reference, key, next, node)
|
|
3189
|
+
return result
|
|
3190
|
+
})
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// Rejects inserting a value that (transitively) contains the container it is being inserted
|
|
3194
|
+
// into - the mutation that would create a circular structure no later walk could survive.
|
|
3195
|
+
private rejectCircularInsertion(
|
|
3196
|
+
container: object,
|
|
3197
|
+
value: unknown,
|
|
3198
|
+
label: string,
|
|
3199
|
+
node: AstNode,
|
|
3200
|
+
seen = new Set<object>(),
|
|
3201
|
+
): void {
|
|
3202
|
+
if (value === container)
|
|
3203
|
+
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
|
|
3204
|
+
if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return
|
|
3205
|
+
seen.add(value)
|
|
3206
|
+
const items = Array.isArray(value) ? value : Object.values(value)
|
|
3207
|
+
for (const item of items) this.rejectCircularInsertion(container, item, label, node, seen)
|
|
3208
|
+
seen.delete(value)
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void {
|
|
3212
|
+
if (Array.isArray(reference.target)) {
|
|
3213
|
+
const target = reference.target
|
|
3214
|
+
const index = key as number
|
|
3215
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
3216
|
+
throw new InterpreterRuntimeError(
|
|
3217
|
+
"Array assignment index must be a non-negative integer.",
|
|
3218
|
+
node,
|
|
3219
|
+
"InvalidDataValue",
|
|
3220
|
+
)
|
|
3221
|
+
}
|
|
3222
|
+
this.rejectCircularInsertion(target, next, "Array assignment result", node)
|
|
3223
|
+
target[index] = next
|
|
3224
|
+
return
|
|
3225
|
+
}
|
|
3226
|
+
if (reference.target instanceof SandboxURL) {
|
|
3227
|
+
const property = key as string
|
|
3228
|
+
if (!urlWritableProperties.has(property)) {
|
|
3229
|
+
throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError")
|
|
3230
|
+
}
|
|
3231
|
+
try {
|
|
3232
|
+
const url = reference.target.url as unknown as Record<string, string>
|
|
3233
|
+
url[property] = uriArgument(next, `URL.${property} value`)
|
|
3234
|
+
return
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error
|
|
3237
|
+
throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node).as("TypeError")
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
const target = reference.target as SafeObject
|
|
3241
|
+
const objectKey = key as string
|
|
3242
|
+
this.rejectCircularInsertion(target, next, "Object assignment result", node)
|
|
3243
|
+
target[objectKey] = next
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
private toPropertyKey(value: unknown, node: AstNode): string | number {
|
|
3247
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
3248
|
+
return value
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
throw new InterpreterRuntimeError("Property key must be a string or number.", node)
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
private declare(name: string, value: unknown, mutable: boolean, node: AstNode): void {
|
|
3255
|
+
const scope = this.currentScope()
|
|
3256
|
+
|
|
3257
|
+
// A pre-seeded parameter slot (initialized === false) is being bound for the first time;
|
|
3258
|
+
// anything else already present is a genuine duplicate declaration.
|
|
3259
|
+
const existing = scope.get(name)
|
|
3260
|
+
if (existing && existing.initialized !== false) {
|
|
3261
|
+
throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node)
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
scope.set(name, { mutable, value, initialized: true })
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
private getIdentifierValue(name: string, node: AstNode): unknown {
|
|
3268
|
+
const binding = this.resolveBinding(name)
|
|
3269
|
+
|
|
3270
|
+
if (!binding) {
|
|
3271
|
+
throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// A parameter default that forward-references a later (not-yet-bound) parameter - JS TDZ.
|
|
3275
|
+
if (binding.initialized === false) {
|
|
3276
|
+
throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError")
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
return binding.value
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
private setIdentifierValue(name: string, value: unknown, node: AstNode): unknown {
|
|
3283
|
+
const binding = this.resolveBinding(name)
|
|
3284
|
+
|
|
3285
|
+
if (!binding) {
|
|
3286
|
+
throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
if (!binding.mutable) {
|
|
3290
|
+
throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError")
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
binding.value = value
|
|
3294
|
+
return value
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
private resolveBinding(name: string): Binding | undefined {
|
|
3298
|
+
for (let index = this.scopes.length - 1; index >= 0; index -= 1) {
|
|
3299
|
+
const scope = this.scopes[index]
|
|
3300
|
+
const binding = scope?.get(name)
|
|
3301
|
+
|
|
3302
|
+
if (binding) {
|
|
3303
|
+
return binding
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
return undefined
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
private currentScope(): Map<string, Binding> {
|
|
3311
|
+
const scope = this.scopes[this.scopes.length - 1]
|
|
3312
|
+
|
|
3313
|
+
if (!scope) {
|
|
3314
|
+
throw new InterpreterRuntimeError("Interpreter scope stack is empty.")
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
return scope
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
private pushScope(): void {
|
|
3321
|
+
this.scopes.push(new Map())
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
private popScope(): void {
|
|
3325
|
+
this.scopes.pop()
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* Executes one Effect-native CodeMode program without constructing a reusable runtime.
|
|
3331
|
+
*
|
|
3332
|
+
* @example
|
|
3333
|
+
* ```ts
|
|
3334
|
+
* const result = yield* CodeMode.execute({
|
|
3335
|
+
* tools: { lookup },
|
|
3336
|
+
* code: `return await tools.lookup({ id: "order_42" })`,
|
|
3337
|
+
* })
|
|
3338
|
+
* ```
|
|
3339
|
+
*/
|
|
3340
|
+
export const executeWithLimits = <const Tools extends Record<string, unknown>>(
|
|
3341
|
+
options: ExecuteOptions<Tools>,
|
|
3342
|
+
limits: ResolvedExecutionLimits,
|
|
3343
|
+
searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"],
|
|
3344
|
+
): Effect.Effect<Result, never, Services<Tools>> => {
|
|
3345
|
+
const hooks = {
|
|
3346
|
+
...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }),
|
|
3347
|
+
...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }),
|
|
3348
|
+
}
|
|
3349
|
+
const tools = ToolRuntime.make(
|
|
3350
|
+
(options.tools ?? {}) as HostTools<Services<Tools>>,
|
|
3351
|
+
limits.maxToolCalls,
|
|
3352
|
+
searchIndex,
|
|
3353
|
+
hooks,
|
|
3354
|
+
)
|
|
3355
|
+
const logs: Array<string> = []
|
|
3356
|
+
const logged = () => (logs.length > 0 ? { logs: [...logs] } : {})
|
|
3357
|
+
|
|
3358
|
+
if (options.code.trim().length === 0) {
|
|
3359
|
+
return Effect.succeed({
|
|
3360
|
+
ok: false,
|
|
3361
|
+
error: { kind: "ParseError", message: "Code cannot be empty." },
|
|
3362
|
+
toolCalls: tools.calls,
|
|
3363
|
+
})
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
const operation = Effect.gen(function* () {
|
|
3367
|
+
const program = parseProgram(options.code)
|
|
3368
|
+
const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.keys, logs)
|
|
3369
|
+
const value = yield* interpreter.run(program)
|
|
3370
|
+
const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
|
|
3371
|
+
return {
|
|
3372
|
+
ok: true,
|
|
3373
|
+
value: result,
|
|
3374
|
+
...logged(),
|
|
3375
|
+
toolCalls: tools.calls,
|
|
3376
|
+
} satisfies Result
|
|
3377
|
+
}).pipe((program) => {
|
|
3378
|
+
const timeoutMs = limits.timeoutMs
|
|
3379
|
+
if (timeoutMs === undefined) return program
|
|
3380
|
+
return program.pipe(
|
|
3381
|
+
Effect.timeoutOrElse({
|
|
3382
|
+
duration: timeoutMs,
|
|
3383
|
+
orElse: () =>
|
|
3384
|
+
Effect.succeed({
|
|
3385
|
+
ok: false,
|
|
3386
|
+
error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` },
|
|
3387
|
+
...logged(),
|
|
3388
|
+
toolCalls: tools.calls,
|
|
3389
|
+
} satisfies Result),
|
|
3390
|
+
}),
|
|
3391
|
+
)
|
|
3392
|
+
})
|
|
3393
|
+
|
|
3394
|
+
return operation.pipe(
|
|
3395
|
+
Effect.catchCause((cause) =>
|
|
3396
|
+
Cause.hasInterruptsOnly(cause)
|
|
3397
|
+
? Effect.interrupt
|
|
3398
|
+
: Effect.succeed({
|
|
3399
|
+
ok: false,
|
|
3400
|
+
error: normalizeError(Cause.squash(cause)),
|
|
3401
|
+
...logged(),
|
|
3402
|
+
toolCalls: tools.calls,
|
|
3403
|
+
} satisfies Result),
|
|
3404
|
+
),
|
|
3405
|
+
Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))),
|
|
3406
|
+
)
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength
|
|
3410
|
+
|
|
3411
|
+
// Truncates to a UTF-8 byte budget without splitting a code point (a split multi-byte
|
|
3412
|
+
// sequence decodes to a replacement character, which is dropped).
|
|
3413
|
+
const utf8Truncate = (value: string, maxBytes: number): string => {
|
|
3414
|
+
const bytes = new TextEncoder().encode(value)
|
|
3415
|
+
if (bytes.byteLength <= maxBytes) return value
|
|
3416
|
+
const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes)))
|
|
3417
|
+
return text.endsWith("\uFFFD") ? text.slice(0, -1) : text
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
/**
|
|
3421
|
+
* Bounds the model-facing output (serialized result value plus logs) to `maxOutputBytes`.
|
|
3422
|
+
* Oversized values are replaced by their truncated serialized text with an explanatory marker,
|
|
3423
|
+
* and logs are kept from the start until the remaining budget is exhausted. Truncation never
|
|
3424
|
+
* fails the execution; `truncated: true` marks affected results. Only runs when the host set
|
|
3425
|
+
* `maxOutputBytes` - with the limit absent, output passes through unbounded.
|
|
3426
|
+
*/
|
|
3427
|
+
const boundOutput = (result: Result, maxOutputBytes: number): Result => {
|
|
3428
|
+
let truncated = false
|
|
3429
|
+
|
|
3430
|
+
let value: DataValue = null
|
|
3431
|
+
let valueBytes = 0
|
|
3432
|
+
if (result.ok) {
|
|
3433
|
+
const serialized = JSON.stringify(result.value) ?? "null"
|
|
3434
|
+
const bytes = utf8ByteLength(serialized)
|
|
3435
|
+
if (bytes > maxOutputBytes) {
|
|
3436
|
+
truncated = true
|
|
3437
|
+
value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]`
|
|
3438
|
+
valueBytes = maxOutputBytes
|
|
3439
|
+
} else {
|
|
3440
|
+
value = result.value
|
|
3441
|
+
valueBytes = bytes
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
const logs = result.logs ?? []
|
|
3446
|
+
const kept: Array<string> = []
|
|
3447
|
+
const logBudget = Math.max(0, maxOutputBytes - valueBytes)
|
|
3448
|
+
let logBytes = 0
|
|
3449
|
+
for (const line of logs) {
|
|
3450
|
+
const lineBytes = utf8ByteLength(line) + 1
|
|
3451
|
+
if (logBytes + lineBytes > logBudget) break
|
|
3452
|
+
logBytes += lineBytes
|
|
3453
|
+
kept.push(line)
|
|
3454
|
+
}
|
|
3455
|
+
if (kept.length < logs.length) {
|
|
3456
|
+
truncated = true
|
|
3457
|
+
kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`)
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
if (!truncated) return result
|
|
3461
|
+
const logsPart = kept.length > 0 ? { logs: kept } : {}
|
|
3462
|
+
return result.ok
|
|
3463
|
+
? { ok: true, value, ...logsPart, truncated: true, toolCalls: result.toolCalls }
|
|
3464
|
+
: { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls }
|
|
3465
|
+
}
|