@opencode/codemode 2.0.15 → 2.0.17

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
- import { containsOpaqueReference, describeValue, rejectCircularInsertion, typeofValue } from "./references.js";
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);
@@ -388,9 +429,6 @@ class Frame {
388
429
  const self = this;
389
430
  return Effect.gen(function* () {
390
431
  const discriminant = yield* self.evaluateExpression(node.discriminant);
391
- if (containsOpaqueReference(discriminant)) {
392
- throw invalidData("Switch discriminants must be data values.", node);
393
- }
394
432
  self.scopes.push();
395
433
  return yield* Effect.gen(function* () {
396
434
  const cases = node.cases;
@@ -405,11 +443,7 @@ class Frame {
405
443
  defaultIndex = index;
406
444
  continue;
407
445
  }
408
- const candidate = yield* self.evaluateExpression(test);
409
- if (containsOpaqueReference(candidate)) {
410
- throw invalidData("Switch case values must be data values.", test);
411
- }
412
- if (candidate === discriminant) {
446
+ if ((yield* self.evaluateExpression(test)) === discriminant) {
413
447
  selected = index;
414
448
  break;
415
449
  }
@@ -852,20 +886,20 @@ class Frame {
852
886
  return;
853
887
  }
854
888
  if (pattern.type === "ObjectPattern") {
855
- if (!(value instanceof Obj)) {
856
- 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);
857
891
  }
858
892
  const consumed = new Set();
859
893
  for (const property of pattern.properties) {
860
894
  if (property.type === "RestElement") {
861
895
  const rest = new Obj(self.ctx.builtins.Object);
862
- assign(rest, value, consumed);
896
+ assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed);
863
897
  yield* self.declarePattern(property.argument, rest, mutable, property, initialize);
864
898
  continue;
865
899
  }
866
900
  const key = yield* self.destructuringPropertyKey(property);
867
901
  consumed.add(typeof key === "symbol" ? key : String(key));
868
- 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);
869
903
  }
870
904
  return;
871
905
  }
@@ -892,20 +926,20 @@ class Frame {
892
926
  return;
893
927
  }
894
928
  if (pattern.type === "ObjectPattern") {
895
- if (!(value instanceof Obj)) {
896
- 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);
897
931
  }
898
932
  const consumed = new Set();
899
933
  for (const property of pattern.properties) {
900
934
  if (property.type === "RestElement") {
901
935
  const rest = new Obj(self.ctx.builtins.Object);
902
- assign(rest, value, consumed);
936
+ assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed);
903
937
  yield* self.assignPattern(property.argument, rest, property);
904
938
  continue;
905
939
  }
906
940
  const key = yield* self.destructuringPropertyKey(property);
907
941
  consumed.add(typeof key === "symbol" ? key : String(key));
908
- yield* self.assignPattern(property.value, self.readProperty(value, key, property), property);
942
+ yield* self.assignPattern(property.value, self.destructuredProperty(value, key, property), property);
909
943
  }
910
944
  return;
911
945
  }
@@ -967,7 +1001,7 @@ class Frame {
967
1001
  }
968
1002
  const keyNode = property.key;
969
1003
  if (property.computed) {
970
- return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value));
1004
+ return Effect.flatMap(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode));
971
1005
  }
972
1006
  if (keyNode.type === "Identifier")
973
1007
  return Effect.succeed(keyNode.name);
@@ -985,6 +1019,8 @@ class Frame {
985
1019
  }
986
1020
  case "Identifier":
987
1021
  return Effect.sync(() => this.scopes.get(node.name, node));
1022
+ case "ThisExpression":
1023
+ return Effect.sync(() => this.scopes.get("this", node));
988
1024
  case "BinaryExpression":
989
1025
  return this.evaluateBinaryExpression(node);
990
1026
  case "LogicalExpression":
@@ -1069,76 +1105,111 @@ class Frame {
1069
1105
  const rhs = yield* self.evaluateExpression(node.right);
1070
1106
  if (operator === "instanceof")
1071
1107
  return instanceofValue(lhs, rhs, node);
1108
+ if (lhs instanceof Obj || rhs instanceof Obj)
1109
+ return yield* self.applyOperator(operator, lhs, rhs, node);
1072
1110
  return self.applyBinaryOperator(operator, lhs, rhs, node);
1073
1111
  });
1074
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
+ }
1075
1142
  applyBinaryOperator(operator, lhs, rhs, node) {
1076
1143
  if (operator === "===")
1077
1144
  return lhs === rhs;
1078
1145
  if (operator === "!==")
1079
1146
  return lhs !== rhs;
1080
- if (operator === "in" && rhs instanceof Obj && !containsOpaqueReference(lhs)) {
1081
- return has(rhs, lhs !== null && typeof lhs === "object" ? coerceToString(lhs) : lhs);
1082
- }
1083
- if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
1147
+ if (operator === "==")
1148
+ return this.looselyEqual(lhs, rhs, node);
1149
+ if (operator === "!=")
1150
+ return !this.looselyEqual(lhs, rhs, node);
1151
+ if (operator === "in" && rhs instanceof Obj && !isOpaque(lhs))
1152
+ return has(rhs, propertyKey(lhs));
1153
+ if (isOpaque(lhs) || isOpaque(rhs)) {
1084
1154
  throw invalidData("Binary operators require data values.", node);
1085
1155
  }
1086
- // Addition and loose equality use the default hint; every other operator asks for a number.
1087
- const hint = operator === "+" || operator === "==" || operator === "!=" ? "default" : "number";
1088
- const coerceOperand = (operand) => (operand instanceof Obj ? operand.toPrimitive(hint) : operand);
1089
- const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object";
1090
- const l = coerceOperand(lhs);
1091
- const r = coerceOperand(rhs);
1156
+ // Object operands were already converted by applyOperator; only primitives reach the arithmetic below.
1092
1157
  switch (operator) {
1093
1158
  case "+": {
1094
- const sum = l + r;
1159
+ const sum = lhs + rhs;
1095
1160
  if (typeof sum === "string")
1096
1161
  checkStringLength(sum.length);
1097
1162
  return sum;
1098
1163
  }
1099
1164
  case "-":
1100
- return l - r;
1165
+ return lhs - rhs;
1101
1166
  case "*":
1102
- return l * r;
1167
+ return lhs * rhs;
1103
1168
  case "/":
1104
- return l / r;
1169
+ return lhs / rhs;
1105
1170
  case "%":
1106
- return l % r;
1171
+ return lhs % rhs;
1107
1172
  case "**":
1108
- return l ** r;
1109
- case "==":
1110
- return bothObjects ? lhs === rhs : l == r;
1111
- case "!=":
1112
- return bothObjects ? lhs !== rhs : l != r;
1173
+ return lhs ** rhs;
1113
1174
  case "<":
1114
- return l < r;
1175
+ return lhs < rhs;
1115
1176
  case "<=":
1116
- return l <= r;
1177
+ return lhs <= rhs;
1117
1178
  case ">":
1118
- return l > r;
1179
+ return lhs > rhs;
1119
1180
  case ">=":
1120
- return l >= r;
1181
+ return lhs >= rhs;
1121
1182
  case "&":
1122
- return l & r;
1183
+ return lhs & rhs;
1123
1184
  case "|":
1124
- return l | r;
1185
+ return lhs | rhs;
1125
1186
  case "^":
1126
- return l ^ r;
1187
+ return lhs ^ rhs;
1127
1188
  case "<<":
1128
- return l << r;
1189
+ return lhs << rhs;
1129
1190
  case ">>":
1130
- return l >> r;
1191
+ return lhs >> rhs;
1131
1192
  case ">>>":
1132
- return l >>> r;
1193
+ return lhs >>> rhs;
1133
1194
  case "in":
1134
- if (!(rhs instanceof Obj)) {
1135
- throw typeError("The 'in' operator requires a data object on the right-hand side.", node);
1136
- }
1137
- return has(rhs, coerceOperand(lhs));
1195
+ throw typeError("The 'in' operator requires a data object on the right-hand side.", node);
1138
1196
  default:
1139
1197
  throw typeError(`Unsupported binary operator '${operator}'.`, node);
1140
1198
  }
1141
1199
  }
1200
+ // IsLooselyEqual: objects (including functions and tool references) compare by identity, and a nullish
1201
+ // primitive never equals an object.
1202
+ looselyEqual(lhs, rhs, node) {
1203
+ const lhsObject = lhs !== null && typeof lhs === "object";
1204
+ const rhsObject = rhs !== null && typeof rhs === "object";
1205
+ if (lhsObject === rhsObject)
1206
+ return lhsObject ? lhs === rhs : lhs == rhs;
1207
+ const primitive = lhsObject ? rhs : lhs;
1208
+ if (primitive === null || primitive === undefined)
1209
+ return false;
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);
1212
+ }
1142
1213
  evaluateLogicalExpression(node) {
1143
1214
  const operator = node.operator;
1144
1215
  return Effect.flatMap(this.evaluateExpression(node.left), (left) => {
@@ -1160,17 +1231,19 @@ class Frame {
1160
1231
  if (operator === "typeof" && argument.type === "Identifier" && !this.scopes.resolve(argument.name)) {
1161
1232
  return Effect.succeed("undefined");
1162
1233
  }
1163
- return Effect.map(this.evaluateExpression(argument), (value) => {
1234
+ const self = this;
1235
+ return Effect.gen(function* () {
1236
+ const value = yield* self.evaluateExpression(argument);
1164
1237
  if (operator === "typeof")
1165
1238
  return typeofValue(value);
1166
1239
  if (operator === "!")
1167
1240
  return !value;
1168
1241
  if (operator === "void")
1169
1242
  return undefined;
1170
- if (containsOpaqueReference(value)) {
1243
+ const operand = yield* self.toPrimitive(value, "number", node);
1244
+ if (isOpaque(operand)) {
1171
1245
  throw invalidData("Unary operators require data values.", node);
1172
1246
  }
1173
- const operand = value instanceof Obj ? value.toPrimitive("number") : value;
1174
1247
  let result;
1175
1248
  switch (operator) {
1176
1249
  case "+":
@@ -1191,11 +1264,16 @@ class Frame {
1191
1264
  evaluateAssignmentExpression(node) {
1192
1265
  const left = node.left;
1193
1266
  const operator = node.operator;
1267
+ // The binary operator a compound assignment applies: `+=` is `+`.
1268
+ const binary = operator.slice(0, -1);
1194
1269
  const self = this;
1195
1270
  return Effect.gen(function* () {
1196
1271
  if (operator === "??=" || operator === "||=" || operator === "&&=") {
1197
1272
  return yield* self.evaluateLogicalAssignment(node, left, operator);
1198
1273
  }
1274
+ if (operator !== "=" && !compoundOperators.has(operator)) {
1275
+ throw typeError(`Unsupported assignment operator '${operator}'.`, node);
1276
+ }
1199
1277
  if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) {
1200
1278
  const rightValue = yield* self.evaluateExpression(node.right);
1201
1279
  yield* self.assignPattern(left, rightValue, node);
@@ -1206,17 +1284,23 @@ class Frame {
1206
1284
  if (operator !== "=") {
1207
1285
  const current = self.scopes.get(name, left);
1208
1286
  const rightValue = yield* self.evaluateExpression(node.right);
1209
- 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);
1210
1291
  }
1211
1292
  const rightValue = yield* self.evaluateNamed(node.right, name);
1212
1293
  return self.scopes.set(name, rightValue, left);
1213
1294
  }
1214
1295
  if (left.type === "MemberExpression") {
1215
- 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) => {
1216
1297
  if (operator === "=")
1217
- return { write: true, next: rightValue, result: rightValue };
1218
- const next = self.applyCompoundAssignment(operator, current, rightValue, node);
1219
- 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
+ }));
1220
1304
  }));
1221
1305
  }
1222
1306
  throw typeError("Assignment target must be an Identifier or MemberExpression.", left);
@@ -1254,29 +1338,32 @@ class Frame {
1254
1338
  if (increment === undefined) {
1255
1339
  throw typeError(`Unsupported update operator '${operator}'.`, node);
1256
1340
  }
1257
- // CodeMode numeric coercion, not host Number(): null-prototype data objects would make
1258
- // 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.
1259
1342
  const operand = (current) => {
1260
- if (containsOpaqueReference(current)) {
1343
+ if (isOpaque(current)) {
1261
1344
  throw invalidData(`'${operator}' requires a data value.`, argument);
1262
1345
  }
1263
1346
  return coerceToNumber(current);
1264
1347
  };
1265
1348
  if (argument.type === "Identifier") {
1266
- return Effect.sync(() => {
1267
- const name = argument.name;
1268
- const current = operand(this.scopes.get(name, argument));
1269
- 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;
1270
1354
  this.scopes.set(name, next, argument);
1271
- return prefix ? next : current;
1272
- });
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);
1273
1360
  }
1274
1361
  if (argument.type === "MemberExpression") {
1275
- return this.modifyMember(argument, (current) => {
1276
- const value = operand(current);
1362
+ return this.modifyMember(argument, (current) => Effect.map(this.toPrimitive(current, "number", argument), (primitive) => {
1363
+ const value = operand(primitive);
1277
1364
  const next = value + increment;
1278
- return Effect.succeed({ write: true, next, result: prefix ? next : value });
1279
- });
1365
+ return { write: true, next, result: prefix ? next : value };
1366
+ }));
1280
1367
  }
1281
1368
  throw typeError("Update target must be an Identifier or MemberExpression.", argument);
1282
1369
  }
@@ -1320,7 +1407,7 @@ class Frame {
1320
1407
  return yield* self.createToolCallPromise(callable.path, args);
1321
1408
  }
1322
1409
  if (callable instanceof Fn)
1323
- return yield* self.invokeFunction(callable, args, node);
1410
+ return yield* self.invokeFunction(callable, thisValue, args, node);
1324
1411
  if (callable instanceof Native) {
1325
1412
  return yield* self.native(() => callable.call(thisValue, args), node);
1326
1413
  }
@@ -1356,15 +1443,26 @@ class Frame {
1356
1443
  });
1357
1444
  }
1358
1445
  // A callback invoked by a built-in runs below the call that invoked the built-in, so the deeper of the two counts.
1359
- invokeFunction(fn, args, node) {
1446
+ invokeFunction(fn, thisValue, args, node) {
1360
1447
  const self = this;
1361
1448
  return Effect.flatMap(CallSite, (site) => {
1362
1449
  const depth = Math.max(self.depth, site.depth) + 1;
1363
1450
  if (depth > MAX_CALL_DEPTH)
1364
1451
  throw rangeError("Maximum call stack size exceeded", node);
1365
1452
  const invocation = new Frame(this.ctx, new ScopeStack([...fn.capturedScopes, new Map()]), depth);
1366
- // Seed all parameters first so defaults cannot fall through to same-named outer bindings.
1367
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.
1368
1466
  for (const parameter of fn.parameters) {
1369
1467
  for (const name of collectPatternNames(parameter)) {
1370
1468
  paramScope.set(name, { mutable: true, value: undefined, initialized: false });
@@ -1616,13 +1714,13 @@ class Frame {
1616
1714
  const keyNode = property.key;
1617
1715
  let key;
1618
1716
  if (property.computed) {
1619
- key = self.toPropertyKey(yield* self.evaluateExpression(keyNode));
1717
+ key = yield* self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode);
1620
1718
  }
1621
1719
  else if (keyNode.type === "Identifier") {
1622
1720
  key = keyNode.name;
1623
1721
  }
1624
1722
  else if (keyNode.type === "Literal") {
1625
- key = self.toPropertyKey(literal(keyNode));
1723
+ key = propertyKey(literal(keyNode));
1626
1724
  }
1627
1725
  else {
1628
1726
  throw typeError("Unsupported object property key shape.", keyNode);
@@ -1682,7 +1780,7 @@ class Frame {
1682
1780
  checkStringLength(output.length);
1683
1781
  if (index < expressions.length) {
1684
1782
  const raw = yield* self.evaluateExpression(expressions[index]);
1685
- output += coerceToString(raw);
1783
+ output += coerceToString(yield* self.toPrimitive(raw, "string", expressions[index]));
1686
1784
  checkStringLength(output.length);
1687
1785
  }
1688
1786
  }
@@ -1713,12 +1811,6 @@ class Frame {
1713
1811
  evaluateConditionalExpression(node) {
1714
1812
  return Effect.flatMap(this.evaluateExpression(node.test), (test) => this.evaluateExpression(test ? node.consequent : node.alternate));
1715
1813
  }
1716
- applyCompoundAssignment(operator, current, incoming, node) {
1717
- if (!compoundOperators.has(operator)) {
1718
- throw typeError(`Unsupported assignment operator '${operator}'.`, node);
1719
- }
1720
- return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node);
1721
- }
1722
1814
  getMemberReference(node) {
1723
1815
  const objectNode = node.object;
1724
1816
  const propertyNode = node.property;
@@ -1733,36 +1825,52 @@ class Frame {
1733
1825
  return OptionalShortCircuit;
1734
1826
  if ((objectValue === null || objectValue === undefined) && node.optional)
1735
1827
  return OptionalShortCircuit;
1736
- const key = node.computed
1737
- ? self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
1738
- : propertyNode.type === "Identifier"
1739
- ? propertyNode.name
1740
- : self.toPropertyKey(yield* self.evaluateExpression(propertyNode));
1741
- if (objectValue instanceof ToolReference) {
1742
- if (typeof key !== "string") {
1743
- throw typeError("Tool paths must use string property names.", propertyNode);
1744
- }
1745
- return new ToolReference([...objectValue.path, key]);
1746
- }
1747
- if (objectValue instanceof Obj)
1748
- return { target: objectValue, key, receiver: objectValue };
1749
- // Strings own length and indexes; every other primitive property reads through the wrapper prototype.
1750
- if (typeof objectValue === "string") {
1751
- if (key === "length")
1752
- return { value: objectValue.length };
1753
- const index = typeof key === "symbol" ? undefined : parseArrayIndex(key);
1754
- if (index !== undefined)
1755
- return { value: objectValue[index] };
1756
- }
1757
- const proto = primitivePrototype(self.ctx.builtins, objectValue);
1758
- if (proto !== undefined)
1759
- 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.
1760
1833
  if (objectValue === null || objectValue === undefined) {
1761
- throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode);
1834
+ throw typeError(`Cannot read properties of ${objectValue} (reading '${coerceToString(keyValue)}').`, objectNode);
1762
1835
  }
1763
- 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);
1764
1838
  });
1765
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
+ }
1766
1874
  readReference(reference, node) {
1767
1875
  // Reject unknown promise properties so a missing await cannot hide.
1768
1876
  if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) {
@@ -1841,14 +1949,13 @@ class Frame {
1841
1949
  })();
1842
1950
  if (written)
1843
1951
  return;
1844
- if (target instanceof Arr && key === "length")
1845
- throw rangeError("Invalid array length", node);
1846
1952
  throw typeError(`Cannot assign to read only property '${String(key)}'.`, node);
1847
1953
  }
1848
- // ToPropertyKey: anything else becomes its string form, so `counts[row.category]` works when the field is null.
1849
- toPropertyKey(value) {
1850
- if (typeof value === "string" || typeof value === "number" || typeof value === "symbol")
1851
- return value;
1852
- 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);
1853
1960
  }
1854
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;