@opencode/codemode 2.0.16 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,35 @@ import { checkStringLength } from "./limits.js";
6
6
  import { locate, materialize } from "./errors.js";
7
7
  import { primitivePrototype } from "./intrinsics.js";
8
8
  import { globals } from "./globals.js";
9
- import { assign, Callable, define, frozen, get, hostCursor, IteratorObj, has, hidden, hasPrototype, keys, Native, parseArrayIndex, Arr, Fn, GeneratorObj, Obj, PromiseObj, record, remove, set, coerceToNumber, coerceToString, } from "./objects.js";
10
- import { preserveConsumerError } from "./callback.js";
9
+ import { assign, Callable, define, frozen, get, hostCursor, IteratorObj, has, hidden, hasPrototype, keys, Native, parseArrayIndex, Arguments, Arr, Fn, GeneratorObj, Obj, PromiseObj, record, remove, set, coerceToNumber, coerceToString, } from "./objects.js";
10
+ import { preserveConsumerError, toPrimitive } from "./callback.js";
11
11
  import { Pending, resolvePromise, resolvePromiseValue } from "./promises.js";
12
12
  import { describeValue, isOpaque, rejectCircularInsertion, typeofValue } from "./references.js";
13
13
  import { ScopeStack } from "./scope.js";
14
14
  import { constructRegExp } from "../stdlib/regexp.js";
15
15
  import { enumerableSource } from "../stdlib/object.js";
16
16
  import { compoundOperators } from "../stdlib/value.js";
17
+ /** The binary operators that convert object operands through ToPrimitive before acting on primitives. */
18
+ const primitiveOperators = new Set([
19
+ "+",
20
+ "-",
21
+ "*",
22
+ "/",
23
+ "%",
24
+ "**",
25
+ "<",
26
+ "<=",
27
+ ">",
28
+ ">=",
29
+ "&",
30
+ "|",
31
+ "^",
32
+ "<<",
33
+ ">>",
34
+ ">>>",
35
+ ]);
36
+ /** ToPropertyKey on a primitive (or an opaque value, which keeps its built-in string form). */
37
+ const propertyKey = (value) => typeof value === "string" || typeof value === "number" || typeof value === "symbol" ? value : coerceToString(value);
17
38
  // What a loop does with its body's result: exit with a StatementResult, or undefined to keep iterating.
18
39
  // Unlabelled break ends this loop; a label the loop does not carry propagates outward.
19
40
  const loopExit = (result, labels) => {
@@ -94,6 +115,25 @@ const collectPatternNames = (pattern, out = []) => {
94
115
  }
95
116
  return out;
96
117
  };
118
+ // Whether a function body (or a parameter default) reads `arguments`, looking through arrows but not nested
119
+ // functions, which own theirs. Memoized so the object is only built for calls that can observe it.
120
+ const argumentsUse = new WeakMap();
121
+ const usesArguments = (fn) => {
122
+ const cached = argumentsUse.get(fn.body);
123
+ if (cached !== undefined)
124
+ return cached;
125
+ const found = [...fn.parameters, fn.body].some(function visit(node) {
126
+ if (node === null || typeof node !== "object")
127
+ return false;
128
+ if (node.type === "Identifier")
129
+ return node.name === "arguments";
130
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression")
131
+ return false;
132
+ return Object.values(node).some((child) => Array.isArray(child) ? child.some((item) => visit(item)) : visit(child));
133
+ });
134
+ argumentsUse.set(fn.body, found);
135
+ return found;
136
+ };
97
137
  // `var` names declared anywhere in a function body except inside nested functions, which own theirs.
98
138
  // Memoized per body: a function's var names never change, and hoisting runs on every call.
99
139
  const varNames = new WeakMap();
@@ -171,7 +211,8 @@ export class Interpreter {
171
211
  this.pending = options.pending;
172
212
  this.builtins = options.builtins;
173
213
  this.logs = options.logs ?? [];
174
- const globalScope = new Map();
214
+ // Program code has no receiver: top-level `this` is undefined, as in a module.
215
+ const globalScope = new Map([["this", { mutable: false, value: undefined }]]);
175
216
  // Calling back into the program never reads frame state, so any frame serves; the root is always alive.
176
217
  this.root = new Frame(this, new ScopeStack([globalScope]));
177
218
  for (const [name, value] of [...globals(this), ...(options.globals?.(this) ?? [])]) {
@@ -321,7 +362,7 @@ class Frame {
321
362
  }
322
363
  createFunction(node, name = node.type === "ArrowFunctionExpression" ? "" : (node.id?.name ?? "")) {
323
364
  const builtins = this.ctx.builtins;
324
- const fn = new Fn(builtins.Function, name, node.params, node.body, this.scopes.capture(), node.async, node.generator);
365
+ const fn = new Fn(builtins.Function, name, node.params, node.body, this.scopes.capture(), node.async, node.generator, node.type === "ArrowFunctionExpression");
325
366
  // Each generator function gets its own prototype, so `g() instanceof g` holds as in JS.
326
367
  if (node.generator)
327
368
  define(fn, "prototype", new Obj(node.async ? builtins.AsyncGenerator : builtins.Generator), hidden);
@@ -845,20 +886,20 @@ class Frame {
845
886
  return;
846
887
  }
847
888
  if (pattern.type === "ObjectPattern") {
848
- if (!(value instanceof Obj)) {
849
- throw typeError(`Object destructuring requires a data object or array value, received ${describeValue(value)}.`, pattern);
889
+ if (value === null || value === undefined) {
890
+ throw typeError(`Cannot destructure ${describeValue(value)} as it is ${value}.`, pattern);
850
891
  }
851
892
  const consumed = new Set();
852
893
  for (const property of pattern.properties) {
853
894
  if (property.type === "RestElement") {
854
895
  const rest = new Obj(self.ctx.builtins.Object);
855
- assign(rest, value, consumed);
896
+ assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed);
856
897
  yield* self.declarePattern(property.argument, rest, mutable, property, initialize);
857
898
  continue;
858
899
  }
859
900
  const key = yield* self.destructuringPropertyKey(property);
860
901
  consumed.add(typeof key === "symbol" ? key : String(key));
861
- yield* self.declarePattern(property.value, self.readProperty(value, key, property), mutable, property, initialize);
902
+ yield* self.declarePattern(property.value, self.destructuredProperty(value, key, property), mutable, property, initialize);
862
903
  }
863
904
  return;
864
905
  }
@@ -885,20 +926,20 @@ class Frame {
885
926
  return;
886
927
  }
887
928
  if (pattern.type === "ObjectPattern") {
888
- if (!(value instanceof Obj)) {
889
- throw invalidData(`Object destructuring requires a data object or array value, received ${describeValue(value)}.`, pattern);
929
+ if (value === null || value === undefined) {
930
+ throw typeError(`Cannot destructure ${describeValue(value)} as it is ${value}.`, pattern);
890
931
  }
891
932
  const consumed = new Set();
892
933
  for (const property of pattern.properties) {
893
934
  if (property.type === "RestElement") {
894
935
  const rest = new Obj(self.ctx.builtins.Object);
895
- assign(rest, value, consumed);
936
+ assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed);
896
937
  yield* self.assignPattern(property.argument, rest, property);
897
938
  continue;
898
939
  }
899
940
  const key = yield* self.destructuringPropertyKey(property);
900
941
  consumed.add(typeof key === "symbol" ? key : String(key));
901
- yield* self.assignPattern(property.value, self.readProperty(value, key, property), property);
942
+ yield* self.assignPattern(property.value, self.destructuredProperty(value, key, property), property);
902
943
  }
903
944
  return;
904
945
  }
@@ -960,7 +1001,7 @@ class Frame {
960
1001
  }
961
1002
  const keyNode = property.key;
962
1003
  if (property.computed) {
963
- return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value));
1004
+ return Effect.flatMap(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode));
964
1005
  }
965
1006
  if (keyNode.type === "Identifier")
966
1007
  return Effect.succeed(keyNode.name);
@@ -978,6 +1019,8 @@ class Frame {
978
1019
  }
979
1020
  case "Identifier":
980
1021
  return Effect.sync(() => this.scopes.get(node.name, node));
1022
+ case "ThisExpression":
1023
+ return Effect.sync(() => this.scopes.get("this", node));
981
1024
  case "BinaryExpression":
982
1025
  return this.evaluateBinaryExpression(node);
983
1026
  case "LogicalExpression":
@@ -1062,9 +1105,40 @@ class Frame {
1062
1105
  const rhs = yield* self.evaluateExpression(node.right);
1063
1106
  if (operator === "instanceof")
1064
1107
  return instanceofValue(lhs, rhs, node);
1108
+ if (lhs instanceof Obj || rhs instanceof Obj)
1109
+ return yield* self.applyOperator(operator, lhs, rhs, node);
1065
1110
  return self.applyBinaryOperator(operator, lhs, rhs, node);
1066
1111
  });
1067
1112
  }
1113
+ /** ToPrimitive for an operand: data objects run their own methods; opaque values stay for the data gates below. */
1114
+ toPrimitive(value, hint, node) {
1115
+ return this.native(() => toPrimitive(this.ctx, value, hint), node);
1116
+ }
1117
+ // Arithmetic, relational, and bitwise operators convert both operands first, left then right, so a `valueOf`
1118
+ // runs (and throws) in spec order; `+` asks for the default hint and the rest for a number.
1119
+ applyOperator(operator, lhs, rhs, node) {
1120
+ if (!(lhs instanceof Obj || rhs instanceof Obj))
1121
+ return Effect.succeed(this.applyBinaryOperator(operator, lhs, rhs, node));
1122
+ // IsLooselyEqual converts only an object facing a non-nullish primitive; two objects (including tool
1123
+ // references, which are not Obj) compare by identity.
1124
+ const equality = operator === "==" || operator === "!=";
1125
+ // `in` checks the right operand before ToPropertyKey on the left, so a bad right side wins over a bad key.
1126
+ if (operator === "in" && lhs instanceof Obj && !isOpaque(lhs) && rhs instanceof Obj) {
1127
+ return Effect.map(this.toPropertyKey(lhs, node), (key) => has(rhs, key));
1128
+ }
1129
+ const other = lhs instanceof Obj ? rhs : lhs;
1130
+ const converts = primitiveOperators.has(operator) ||
1131
+ (equality && other !== null && other !== undefined && typeof other !== "object");
1132
+ if (!converts)
1133
+ return Effect.succeed(this.applyBinaryOperator(operator, lhs, rhs, node));
1134
+ const hint = operator === "+" || equality ? "default" : "number";
1135
+ const self = this;
1136
+ return Effect.gen(function* () {
1137
+ const l = yield* self.toPrimitive(lhs, hint, node);
1138
+ const r = yield* self.toPrimitive(rhs, hint, node);
1139
+ return self.applyBinaryOperator(operator, l, r, node);
1140
+ });
1141
+ }
1068
1142
  applyBinaryOperator(operator, lhs, rhs, node) {
1069
1143
  if (operator === "===")
1070
1144
  return lhs === rhs;
@@ -1074,78 +1148,67 @@ class Frame {
1074
1148
  return this.looselyEqual(lhs, rhs, node);
1075
1149
  if (operator === "!=")
1076
1150
  return !this.looselyEqual(lhs, rhs, node);
1077
- if (operator === "in" && rhs instanceof Obj && !isOpaque(lhs)) {
1078
- return has(rhs, lhs !== null && typeof lhs === "object" ? coerceToString(lhs) : lhs);
1079
- }
1151
+ if (operator === "in" && rhs instanceof Obj && !isOpaque(lhs))
1152
+ return has(rhs, propertyKey(lhs));
1080
1153
  if (isOpaque(lhs) || isOpaque(rhs)) {
1081
1154
  throw invalidData("Binary operators require data values.", node);
1082
1155
  }
1083
- // Addition uses the default hint; every other operator asks for a number.
1084
- const hint = operator === "+" ? "default" : "number";
1085
- const coerceOperand = (operand) => (operand instanceof Obj ? operand.toPrimitive(hint) : operand);
1086
- const l = coerceOperand(lhs);
1087
- const r = coerceOperand(rhs);
1156
+ // Object operands were already converted by applyOperator; only primitives reach the arithmetic below.
1088
1157
  switch (operator) {
1089
1158
  case "+": {
1090
- const sum = l + r;
1159
+ const sum = lhs + rhs;
1091
1160
  if (typeof sum === "string")
1092
1161
  checkStringLength(sum.length);
1093
1162
  return sum;
1094
1163
  }
1095
1164
  case "-":
1096
- return l - r;
1165
+ return lhs - rhs;
1097
1166
  case "*":
1098
- return l * r;
1167
+ return lhs * rhs;
1099
1168
  case "/":
1100
- return l / r;
1169
+ return lhs / rhs;
1101
1170
  case "%":
1102
- return l % r;
1171
+ return lhs % rhs;
1103
1172
  case "**":
1104
- return l ** r;
1173
+ return lhs ** rhs;
1105
1174
  case "<":
1106
- return l < r;
1175
+ return lhs < rhs;
1107
1176
  case "<=":
1108
- return l <= r;
1177
+ return lhs <= rhs;
1109
1178
  case ">":
1110
- return l > r;
1179
+ return lhs > rhs;
1111
1180
  case ">=":
1112
- return l >= r;
1181
+ return lhs >= rhs;
1113
1182
  case "&":
1114
- return l & r;
1183
+ return lhs & rhs;
1115
1184
  case "|":
1116
- return l | r;
1185
+ return lhs | rhs;
1117
1186
  case "^":
1118
- return l ^ r;
1187
+ return lhs ^ rhs;
1119
1188
  case "<<":
1120
- return l << r;
1189
+ return lhs << rhs;
1121
1190
  case ">>":
1122
- return l >> r;
1191
+ return lhs >> rhs;
1123
1192
  case ">>>":
1124
- return l >>> r;
1193
+ return lhs >>> rhs;
1125
1194
  case "in":
1126
- if (!(rhs instanceof Obj)) {
1127
- throw typeError("The 'in' operator requires a data object on the right-hand side.", node);
1128
- }
1129
- return has(rhs, coerceOperand(lhs));
1195
+ throw typeError("The 'in' operator requires a data object on the right-hand side.", node);
1130
1196
  default:
1131
1197
  throw typeError(`Unsupported binary operator '${operator}'.`, node);
1132
1198
  }
1133
1199
  }
1134
- // IsLooselyEqual: objects (including functions and tool references) compare by identity, and only a
1135
- // data object facing a non-nullish primitive needs to coerce, so an opaque value is rejected only there.
1200
+ // IsLooselyEqual: objects (including functions and tool references) compare by identity, and a nullish
1201
+ // primitive never equals an object.
1136
1202
  looselyEqual(lhs, rhs, node) {
1137
1203
  const lhsObject = lhs !== null && typeof lhs === "object";
1138
1204
  const rhsObject = rhs !== null && typeof rhs === "object";
1139
1205
  if (lhsObject === rhsObject)
1140
1206
  return lhsObject ? lhs === rhs : lhs == rhs;
1141
- const object = lhsObject ? lhs : rhs;
1142
1207
  const primitive = lhsObject ? rhs : lhs;
1143
1208
  if (primitive === null || primitive === undefined)
1144
1209
  return false;
1145
- if (!(object instanceof Obj) || isOpaque(object)) {
1146
- throw invalidData("Binary operators require data values.", node);
1147
- }
1148
- return object.toPrimitive("default") == primitive;
1210
+ // Data objects were converted by applyOperator, so only an opaque reference facing a primitive gets here.
1211
+ throw invalidData("Binary operators require data values.", node);
1149
1212
  }
1150
1213
  evaluateLogicalExpression(node) {
1151
1214
  const operator = node.operator;
@@ -1168,17 +1231,19 @@ class Frame {
1168
1231
  if (operator === "typeof" && argument.type === "Identifier" && !this.scopes.resolve(argument.name)) {
1169
1232
  return Effect.succeed("undefined");
1170
1233
  }
1171
- return Effect.map(this.evaluateExpression(argument), (value) => {
1234
+ const self = this;
1235
+ return Effect.gen(function* () {
1236
+ const value = yield* self.evaluateExpression(argument);
1172
1237
  if (operator === "typeof")
1173
1238
  return typeofValue(value);
1174
1239
  if (operator === "!")
1175
1240
  return !value;
1176
1241
  if (operator === "void")
1177
1242
  return undefined;
1178
- if (isOpaque(value)) {
1243
+ const operand = yield* self.toPrimitive(value, "number", node);
1244
+ if (isOpaque(operand)) {
1179
1245
  throw invalidData("Unary operators require data values.", node);
1180
1246
  }
1181
- const operand = value instanceof Obj ? value.toPrimitive("number") : value;
1182
1247
  let result;
1183
1248
  switch (operator) {
1184
1249
  case "+":
@@ -1199,11 +1264,16 @@ class Frame {
1199
1264
  evaluateAssignmentExpression(node) {
1200
1265
  const left = node.left;
1201
1266
  const operator = node.operator;
1267
+ // The binary operator a compound assignment applies: `+=` is `+`.
1268
+ const binary = operator.slice(0, -1);
1202
1269
  const self = this;
1203
1270
  return Effect.gen(function* () {
1204
1271
  if (operator === "??=" || operator === "||=" || operator === "&&=") {
1205
1272
  return yield* self.evaluateLogicalAssignment(node, left, operator);
1206
1273
  }
1274
+ if (operator !== "=" && !compoundOperators.has(operator)) {
1275
+ throw typeError(`Unsupported assignment operator '${operator}'.`, node);
1276
+ }
1207
1277
  if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) {
1208
1278
  const rightValue = yield* self.evaluateExpression(node.right);
1209
1279
  yield* self.assignPattern(left, rightValue, node);
@@ -1214,17 +1284,23 @@ class Frame {
1214
1284
  if (operator !== "=") {
1215
1285
  const current = self.scopes.get(name, left);
1216
1286
  const rightValue = yield* self.evaluateExpression(node.right);
1217
- return self.scopes.set(name, self.applyCompoundAssignment(operator, current, rightValue, node), left);
1287
+ const next = current instanceof Obj || rightValue instanceof Obj
1288
+ ? yield* self.applyOperator(binary, current, rightValue, node)
1289
+ : self.applyBinaryOperator(binary, current, rightValue, node);
1290
+ return self.scopes.set(name, next, left);
1218
1291
  }
1219
1292
  const rightValue = yield* self.evaluateNamed(node.right, name);
1220
1293
  return self.scopes.set(name, rightValue, left);
1221
1294
  }
1222
1295
  if (left.type === "MemberExpression") {
1223
- return yield* self.modifyMember(left, (current) => Effect.map(self.evaluateExpression(node.right), (rightValue) => {
1296
+ return yield* self.modifyMember(left, (current) => Effect.flatMap(self.evaluateExpression(node.right), (rightValue) => {
1224
1297
  if (operator === "=")
1225
- return { write: true, next: rightValue, result: rightValue };
1226
- const next = self.applyCompoundAssignment(operator, current, rightValue, node);
1227
- return { write: true, next, result: next };
1298
+ return Effect.succeed({ write: true, next: rightValue, result: rightValue });
1299
+ return Effect.map(self.applyOperator(binary, current, rightValue, node), (next) => ({
1300
+ write: true,
1301
+ next,
1302
+ result: next,
1303
+ }));
1228
1304
  }));
1229
1305
  }
1230
1306
  throw typeError("Assignment target must be an Identifier or MemberExpression.", left);
@@ -1262,8 +1338,7 @@ class Frame {
1262
1338
  if (increment === undefined) {
1263
1339
  throw typeError(`Unsupported update operator '${operator}'.`, node);
1264
1340
  }
1265
- // CodeMode numeric coercion, not host Number(): null-prototype data objects would make
1266
- // the host throw during ToPrimitive, and opaque runtime references must reject clearly.
1341
+ // CodeMode numeric coercion, not host Number(), so opaque runtime references reject clearly.
1267
1342
  const operand = (current) => {
1268
1343
  if (isOpaque(current)) {
1269
1344
  throw invalidData(`'${operator}' requires a data value.`, argument);
@@ -1271,20 +1346,24 @@ class Frame {
1271
1346
  return coerceToNumber(current);
1272
1347
  };
1273
1348
  if (argument.type === "Identifier") {
1274
- return Effect.sync(() => {
1275
- const name = argument.name;
1276
- const current = operand(this.scopes.get(name, argument));
1277
- const next = current + increment;
1349
+ const name = argument.name;
1350
+ const current = this.scopes.get(name, argument);
1351
+ const update = (value) => {
1352
+ const before = operand(value);
1353
+ const next = before + increment;
1278
1354
  this.scopes.set(name, next, argument);
1279
- return prefix ? next : current;
1280
- });
1355
+ return prefix ? next : before;
1356
+ };
1357
+ if (!(current instanceof Obj))
1358
+ return Effect.sync(() => update(current));
1359
+ return Effect.map(this.toPrimitive(current, "number", argument), update);
1281
1360
  }
1282
1361
  if (argument.type === "MemberExpression") {
1283
- return this.modifyMember(argument, (current) => {
1284
- const value = operand(current);
1362
+ return this.modifyMember(argument, (current) => Effect.map(this.toPrimitive(current, "number", argument), (primitive) => {
1363
+ const value = operand(primitive);
1285
1364
  const next = value + increment;
1286
- return Effect.succeed({ write: true, next, result: prefix ? next : value });
1287
- });
1365
+ return { write: true, next, result: prefix ? next : value };
1366
+ }));
1288
1367
  }
1289
1368
  throw typeError("Update target must be an Identifier or MemberExpression.", argument);
1290
1369
  }
@@ -1328,7 +1407,7 @@ class Frame {
1328
1407
  return yield* self.createToolCallPromise(callable.path, args);
1329
1408
  }
1330
1409
  if (callable instanceof Fn)
1331
- return yield* self.invokeFunction(callable, args, node);
1410
+ return yield* self.invokeFunction(callable, thisValue, args, node);
1332
1411
  if (callable instanceof Native) {
1333
1412
  return yield* self.native(() => callable.call(thisValue, args), node);
1334
1413
  }
@@ -1364,15 +1443,26 @@ class Frame {
1364
1443
  });
1365
1444
  }
1366
1445
  // A callback invoked by a built-in runs below the call that invoked the built-in, so the deeper of the two counts.
1367
- invokeFunction(fn, args, node) {
1446
+ invokeFunction(fn, thisValue, args, node) {
1368
1447
  const self = this;
1369
1448
  return Effect.flatMap(CallSite, (site) => {
1370
1449
  const depth = Math.max(self.depth, site.depth) + 1;
1371
1450
  if (depth > MAX_CALL_DEPTH)
1372
1451
  throw rangeError("Maximum call stack size exceeded", node);
1373
1452
  const invocation = new Frame(this.ctx, new ScopeStack([...fn.capturedScopes, new Map()]), depth);
1374
- // Seed all parameters first so defaults cannot fall through to same-named outer bindings.
1375
1453
  const paramScope = invocation.scopes.current();
1454
+ // `this` and `arguments` are scope bindings so arrows resolve them lexically; a parameter named
1455
+ // `arguments` shadows the object, as in JS.
1456
+ if (!fn.arrow)
1457
+ paramScope.set("this", { mutable: false, value: thisValue, initialized: true });
1458
+ if (!fn.arrow && usesArguments(fn)) {
1459
+ paramScope.set("arguments", {
1460
+ mutable: true,
1461
+ value: new Arguments(self.ctx.builtins.Object, args),
1462
+ initialized: true,
1463
+ });
1464
+ }
1465
+ // Seed all parameters first so defaults cannot fall through to same-named outer bindings.
1376
1466
  for (const parameter of fn.parameters) {
1377
1467
  for (const name of collectPatternNames(parameter)) {
1378
1468
  paramScope.set(name, { mutable: true, value: undefined, initialized: false });
@@ -1624,13 +1714,13 @@ class Frame {
1624
1714
  const keyNode = property.key;
1625
1715
  let key;
1626
1716
  if (property.computed) {
1627
- key = self.toPropertyKey(yield* self.evaluateExpression(keyNode));
1717
+ key = yield* self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode);
1628
1718
  }
1629
1719
  else if (keyNode.type === "Identifier") {
1630
1720
  key = keyNode.name;
1631
1721
  }
1632
1722
  else if (keyNode.type === "Literal") {
1633
- key = self.toPropertyKey(literal(keyNode));
1723
+ key = propertyKey(literal(keyNode));
1634
1724
  }
1635
1725
  else {
1636
1726
  throw typeError("Unsupported object property key shape.", keyNode);
@@ -1690,7 +1780,7 @@ class Frame {
1690
1780
  checkStringLength(output.length);
1691
1781
  if (index < expressions.length) {
1692
1782
  const raw = yield* self.evaluateExpression(expressions[index]);
1693
- output += coerceToString(raw);
1783
+ output += coerceToString(yield* self.toPrimitive(raw, "string", expressions[index]));
1694
1784
  checkStringLength(output.length);
1695
1785
  }
1696
1786
  }
@@ -1721,12 +1811,6 @@ class Frame {
1721
1811
  evaluateConditionalExpression(node) {
1722
1812
  return Effect.flatMap(this.evaluateExpression(node.test), (test) => this.evaluateExpression(test ? node.consequent : node.alternate));
1723
1813
  }
1724
- applyCompoundAssignment(operator, current, incoming, node) {
1725
- if (!compoundOperators.has(operator)) {
1726
- throw typeError(`Unsupported assignment operator '${operator}'.`, node);
1727
- }
1728
- return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node);
1729
- }
1730
1814
  getMemberReference(node) {
1731
1815
  const objectNode = node.object;
1732
1816
  const propertyNode = node.property;
@@ -1741,36 +1825,52 @@ class Frame {
1741
1825
  return OptionalShortCircuit;
1742
1826
  if ((objectValue === null || objectValue === undefined) && node.optional)
1743
1827
  return OptionalShortCircuit;
1744
- const key = node.computed
1745
- ? self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
1746
- : propertyNode.type === "Identifier"
1747
- ? propertyNode.name
1748
- : self.toPropertyKey(yield* self.evaluateExpression(propertyNode));
1749
- if (objectValue instanceof ToolReference) {
1750
- if (typeof key !== "string") {
1751
- throw typeError("Tool paths must use string property names.", propertyNode);
1752
- }
1753
- return new ToolReference([...objectValue.path, key]);
1754
- }
1755
- if (objectValue instanceof Obj)
1756
- return { target: objectValue, key, receiver: objectValue };
1757
- // Strings own length and indexes; every other primitive property reads through the wrapper prototype.
1758
- if (typeof objectValue === "string") {
1759
- if (key === "length")
1760
- return { value: objectValue.length };
1761
- const index = typeof key === "symbol" ? undefined : parseArrayIndex(key);
1762
- if (index !== undefined)
1763
- return { value: objectValue[index] };
1764
- }
1765
- const proto = primitivePrototype(self.ctx.builtins, objectValue);
1766
- if (proto !== undefined)
1767
- return { target: proto, key, receiver: objectValue };
1828
+ const keyValue = !node.computed && propertyNode.type === "Identifier"
1829
+ ? propertyNode.name
1830
+ : yield* self.evaluateExpression(propertyNode);
1831
+ // GetValue applies ToObject to the base before ToPropertyKey, so a nullish base throws before the key's own
1832
+ // toString runs.
1768
1833
  if (objectValue === null || objectValue === undefined) {
1769
- throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode);
1834
+ throw typeError(`Cannot read properties of ${objectValue} (reading '${coerceToString(keyValue)}').`, objectNode);
1770
1835
  }
1771
- throw typeError("Cannot access a property on a non-object value.", objectNode);
1836
+ const key = yield* self.toPropertyKey(keyValue, propertyNode);
1837
+ return self.resolveProperty(objectValue, key, objectNode, propertyNode);
1772
1838
  });
1773
1839
  }
1840
+ resolveProperty(objectValue, key, objectNode, propertyNode) {
1841
+ if (objectValue instanceof ToolReference) {
1842
+ if (typeof key !== "string") {
1843
+ throw typeError("Tool paths must use string property names.", propertyNode);
1844
+ }
1845
+ return new ToolReference([...objectValue.path, key]);
1846
+ }
1847
+ if (objectValue instanceof Obj)
1848
+ return { target: objectValue, key, receiver: objectValue };
1849
+ // Strings own length and indexes; every other primitive property reads through the wrapper prototype.
1850
+ if (typeof objectValue === "string") {
1851
+ if (key === "length")
1852
+ return { value: objectValue.length };
1853
+ const index = typeof key === "symbol" ? undefined : parseArrayIndex(key);
1854
+ if (index !== undefined)
1855
+ return { value: objectValue[index] };
1856
+ }
1857
+ const proto = primitivePrototype(this.ctx.builtins, objectValue);
1858
+ if (proto !== undefined)
1859
+ return { target: proto, key, receiver: objectValue };
1860
+ if (objectValue === null || objectValue === undefined) {
1861
+ throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode);
1862
+ }
1863
+ throw typeError("Cannot access a property on a non-object value.", objectNode);
1864
+ }
1865
+ // One destructured property, read the way a member expression would read it (primitives use their prototype).
1866
+ destructuredProperty(source, key, node) {
1867
+ const reference = this.resolveProperty(source, key, node, node);
1868
+ if (reference instanceof ToolReference)
1869
+ return reference;
1870
+ if ("value" in reference)
1871
+ return reference.value;
1872
+ return this.readProperty(reference.target, reference.key, node, reference.receiver);
1873
+ }
1774
1874
  readReference(reference, node) {
1775
1875
  // Reject unknown promise properties so a missing await cannot hide.
1776
1876
  if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) {
@@ -1851,10 +1951,11 @@ class Frame {
1851
1951
  return;
1852
1952
  throw typeError(`Cannot assign to read only property '${String(key)}'.`, node);
1853
1953
  }
1854
- // ToPropertyKey: anything else becomes its string form, so `counts[row.category]` works when the field is null.
1855
- toPropertyKey(value) {
1856
- if (typeof value === "string" || typeof value === "number" || typeof value === "symbol")
1857
- return value;
1858
- return coerceToString(value);
1954
+ // ToPropertyKey: a data object converts through its own `toString`/`valueOf` first; anything else becomes its
1955
+ // string form synchronously, so `counts[row.category]` works when the field is null.
1956
+ toPropertyKey(value, node) {
1957
+ if (!(value instanceof Obj))
1958
+ return Effect.succeed(propertyKey(value));
1959
+ return Effect.map(this.toPrimitive(value, "string", node), propertyKey);
1859
1960
  }
1860
1961
  }
@@ -2,7 +2,7 @@ import { ErrorObj, Obj, type Value } from "./objects.js";
2
2
  export declare const errorTypes: readonly ["Error", "TypeError", "RangeError", "SyntaxError", "ReferenceError", "EvalError", "URIError", "AggregateError"];
3
3
  export type ErrorType = (typeof errorTypes)[number];
4
4
  export declare const isErrorType: (name: string) => name is ErrorType;
5
- declare const builtins: readonly ["Object", "Function", "Array", "String", "Number", "Boolean", "Date", "RegExp", "Map", "Set", "URL", "URLSearchParams", "Headers", "Uint8Array", "TextEncoder", "TextDecoder", "Promise", "Iterator", "IteratorHelper", "AsyncIterator", "Generator", "AsyncGenerator"];
5
+ declare const builtins: readonly ["Object", "Function", "Array", "String", "Number", "Boolean", "Date", "RegExp", "Map", "Set", "WeakMap", "WeakSet", "URL", "URLSearchParams", "Headers", "Uint8Array", "TextEncoder", "TextDecoder", "Promise", "Iterator", "IteratorHelper", "AsyncIterator", "Generator", "AsyncGenerator"];
6
6
  /**
7
7
  * The built-in prototype objects of one runtime, allocated empty in dependency order. The globals populate them
8
8
  * and attach their constructors when the runtime is built.
@@ -22,6 +22,8 @@ const builtins = [
22
22
  "RegExp",
23
23
  "Map",
24
24
  "Set",
25
+ "WeakMap",
26
+ "WeakSet",
25
27
  "URL",
26
28
  "URLSearchParams",
27
29
  "Headers",
@@ -78,6 +80,8 @@ export const createBuiltins = () => {
78
80
  RegExp: plain(),
79
81
  Map: plain(),
80
82
  Set: plain(),
83
+ WeakMap: plain(),
84
+ WeakSet: plain(),
81
85
  URL: plain(),
82
86
  URLSearchParams: plain(),
83
87
  Headers: plain(),
@@ -62,7 +62,7 @@ export declare const rangeError: (message: string, node?: AstNode) => PendingThr
62
62
  export declare const referenceError: (message: string, node?: AstNode) => PendingThrow;
63
63
  export declare const syntaxError: (message: string, node?: AstNode) => PendingThrow;
64
64
  export declare const uriError: (message: string, node?: AstNode) => PendingThrow;
65
- export declare const supportedSyntaxMessage = "This is a restricted JavaScript-like language. Supported: plain and async functions, data literals, destructuring, standard control flow, await and Promise, and built-ins such as Array, Object, Math, JSON, Date, RegExp, Map, Set, and URL. Unsupported: classes, this, getters/setters, BigInt, and custom Symbols. Use plain functions and data objects instead.";
65
+ export declare const supportedSyntaxMessage = "This is a restricted JavaScript-like language. Supported: plain and async functions, data literals, destructuring, standard control flow, await and Promise, and built-ins such as Array, Object, Math, JSON, Date, RegExp, Map, Set, and URL. Unsupported: classes, getters/setters, BigInt, and custom Symbols. Use plain functions and data objects instead.";
66
66
  export declare const unsupportedSyntax: (kind: string, node: AstNode) => PendingThrow;
67
67
  export declare const sourceLocation: (node: AstNode) => {
68
68
  readonly line: number;
@@ -49,7 +49,7 @@ export const referenceError = failure("ReferenceError");
49
49
  export const syntaxError = failure("SyntaxError");
50
50
  export const uriError = failure("URIError");
51
51
  // Orient the agent rather than enumerate JavaScript; interpreter-support.md is the full matrix.
52
- export const supportedSyntaxMessage = "This is a restricted JavaScript-like language. Supported: plain and async functions, data literals, destructuring, standard control flow, await and Promise, and built-ins such as Array, Object, Math, JSON, Date, RegExp, Map, Set, and URL. Unsupported: classes, this, getters/setters, BigInt, and custom Symbols. Use plain functions and data objects instead.";
52
+ export const supportedSyntaxMessage = "This is a restricted JavaScript-like language. Supported: plain and async functions, data literals, destructuring, standard control flow, await and Promise, and built-ins such as Array, Object, Math, JSON, Date, RegExp, Map, Set, and URL. Unsupported: classes, getters/setters, BigInt, and custom Symbols. Use plain functions and data objects instead.";
53
53
  export const unsupportedSyntax = (kind, node) => new PendingThrow("SyntaxError", `Syntax '${kind}' is not supported. ${supportedSyntaxMessage}`, node, "UnsupportedSyntax", [supportedSyntaxMessage]);
54
54
  // Acorn lines are 1-based and its columns are 0-based. Diagnostics use 1-based columns of the submitted source.
55
55
  export const sourceLocation = (node) => ({