runtime.ts 138 KB

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