mustardscript 0.1.0

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.
Files changed (99) hide show
  1. package/Cargo.lock +1579 -0
  2. package/Cargo.toml +40 -0
  3. package/LICENSE +201 -0
  4. package/README.md +828 -0
  5. package/SECURITY.md +34 -0
  6. package/crates/mustard/Cargo.toml +31 -0
  7. package/crates/mustard/src/cancellation.rs +28 -0
  8. package/crates/mustard/src/diagnostic.rs +145 -0
  9. package/crates/mustard/src/ir.rs +435 -0
  10. package/crates/mustard/src/lib.rs +21 -0
  11. package/crates/mustard/src/limits.rs +22 -0
  12. package/crates/mustard/src/parser/expressions.rs +723 -0
  13. package/crates/mustard/src/parser/mod.rs +115 -0
  14. package/crates/mustard/src/parser/operators.rs +105 -0
  15. package/crates/mustard/src/parser/patterns.rs +123 -0
  16. package/crates/mustard/src/parser/scope.rs +107 -0
  17. package/crates/mustard/src/parser/statements.rs +298 -0
  18. package/crates/mustard/src/parser/tests/acceptance.rs +339 -0
  19. package/crates/mustard/src/parser/tests/mod.rs +2 -0
  20. package/crates/mustard/src/parser/tests/rejections.rs +107 -0
  21. package/crates/mustard/src/runtime/accounting.rs +613 -0
  22. package/crates/mustard/src/runtime/api.rs +192 -0
  23. package/crates/mustard/src/runtime/async_runtime/mod.rs +5 -0
  24. package/crates/mustard/src/runtime/async_runtime/promises.rs +246 -0
  25. package/crates/mustard/src/runtime/async_runtime/reactions.rs +400 -0
  26. package/crates/mustard/src/runtime/async_runtime/scheduler.rs +224 -0
  27. package/crates/mustard/src/runtime/builtins/arrays.rs +1205 -0
  28. package/crates/mustard/src/runtime/builtins/collections.rs +573 -0
  29. package/crates/mustard/src/runtime/builtins/install.rs +501 -0
  30. package/crates/mustard/src/runtime/builtins/intl.rs +553 -0
  31. package/crates/mustard/src/runtime/builtins/mod.rs +25 -0
  32. package/crates/mustard/src/runtime/builtins/objects.rs +405 -0
  33. package/crates/mustard/src/runtime/builtins/primitives.rs +859 -0
  34. package/crates/mustard/src/runtime/builtins/promises.rs +335 -0
  35. package/crates/mustard/src/runtime/builtins/regexp.rs +356 -0
  36. package/crates/mustard/src/runtime/builtins/strings.rs +803 -0
  37. package/crates/mustard/src/runtime/builtins/support.rs +561 -0
  38. package/crates/mustard/src/runtime/bytecode.rs +123 -0
  39. package/crates/mustard/src/runtime/compiler/assignments.rs +690 -0
  40. package/crates/mustard/src/runtime/compiler/bindings.rs +92 -0
  41. package/crates/mustard/src/runtime/compiler/context.rs +46 -0
  42. package/crates/mustard/src/runtime/compiler/control.rs +342 -0
  43. package/crates/mustard/src/runtime/compiler/expressions.rs +372 -0
  44. package/crates/mustard/src/runtime/compiler/mod.rs +173 -0
  45. package/crates/mustard/src/runtime/compiler/statements.rs +459 -0
  46. package/crates/mustard/src/runtime/conversions/boundary.rs +293 -0
  47. package/crates/mustard/src/runtime/conversions/coercions.rs +217 -0
  48. package/crates/mustard/src/runtime/conversions/errors.rs +118 -0
  49. package/crates/mustard/src/runtime/conversions/mod.rs +14 -0
  50. package/crates/mustard/src/runtime/conversions/operators.rs +334 -0
  51. package/crates/mustard/src/runtime/env.rs +355 -0
  52. package/crates/mustard/src/runtime/exceptions.rs +377 -0
  53. package/crates/mustard/src/runtime/gc.rs +595 -0
  54. package/crates/mustard/src/runtime/mod.rs +318 -0
  55. package/crates/mustard/src/runtime/properties.rs +1762 -0
  56. package/crates/mustard/src/runtime/serialization.rs +127 -0
  57. package/crates/mustard/src/runtime/shared.rs +108 -0
  58. package/crates/mustard/src/runtime/snapshot_validation_tests.rs +93 -0
  59. package/crates/mustard/src/runtime/state.rs +652 -0
  60. package/crates/mustard/src/runtime/tests/async_host.rs +104 -0
  61. package/crates/mustard/src/runtime/tests/collections.rs +50 -0
  62. package/crates/mustard/src/runtime/tests/diagnostics.rs +36 -0
  63. package/crates/mustard/src/runtime/tests/exceptions.rs +122 -0
  64. package/crates/mustard/src/runtime/tests/execution.rs +553 -0
  65. package/crates/mustard/src/runtime/tests/gc.rs +533 -0
  66. package/crates/mustard/src/runtime/tests/mod.rs +56 -0
  67. package/crates/mustard/src/runtime/tests/serialization.rs +170 -0
  68. package/crates/mustard/src/runtime/validation/bytecode.rs +484 -0
  69. package/crates/mustard/src/runtime/validation/mod.rs +14 -0
  70. package/crates/mustard/src/runtime/validation/policy.rs +94 -0
  71. package/crates/mustard/src/runtime/validation/snapshot.rs +406 -0
  72. package/crates/mustard/src/runtime/validation/walk.rs +206 -0
  73. package/crates/mustard/src/runtime/vm.rs +1016 -0
  74. package/crates/mustard/src/span.rs +22 -0
  75. package/crates/mustard/src/structured.rs +107 -0
  76. package/crates/mustard-bridge/Cargo.toml +17 -0
  77. package/crates/mustard-bridge/src/codec.rs +46 -0
  78. package/crates/mustard-bridge/src/dto.rs +99 -0
  79. package/crates/mustard-bridge/src/lib.rs +12 -0
  80. package/crates/mustard-bridge/src/operations.rs +142 -0
  81. package/crates/mustard-node/Cargo.toml +24 -0
  82. package/crates/mustard-node/build.rs +3 -0
  83. package/crates/mustard-node/src/lib.rs +236 -0
  84. package/crates/mustard-sidecar/Cargo.toml +21 -0
  85. package/crates/mustard-sidecar/src/lib.rs +134 -0
  86. package/crates/mustard-sidecar/src/main.rs +36 -0
  87. package/dist/index.js +20 -0
  88. package/dist/install.js +117 -0
  89. package/dist/lib/cancellation.js +124 -0
  90. package/dist/lib/errors.js +46 -0
  91. package/dist/lib/executor.js +555 -0
  92. package/dist/lib/policy.js +292 -0
  93. package/dist/lib/progress.js +356 -0
  94. package/dist/lib/runtime.js +109 -0
  95. package/dist/lib/structured.js +286 -0
  96. package/dist/native-loader.js +227 -0
  97. package/index.d.ts +23 -0
  98. package/mustard.d.ts +220 -0
  99. package/package.json +97 -0
@@ -0,0 +1,553 @@
1
+ use super::*;
2
+
3
+ #[test]
4
+ fn runs_arithmetic_and_locals() {
5
+ let value = run(r#"
6
+ const a = 4;
7
+ const b = 3;
8
+ a * b + 2;
9
+ "#);
10
+ assert_eq!(
11
+ value,
12
+ StructuredValue::Number(StructuredNumber::Finite(14.0))
13
+ );
14
+ }
15
+
16
+ #[test]
17
+ fn runs_string_relational_comparisons_with_lexicographic_ordering() {
18
+ let value = run(
19
+ r#"
20
+ const left = "az";
21
+ const right = "ba";
22
+ [left < right, left <= left, right > left, right >= right];
23
+ "#,
24
+ );
25
+ assert_eq!(
26
+ value,
27
+ StructuredValue::Array(vec![
28
+ StructuredValue::Bool(true),
29
+ StructuredValue::Bool(true),
30
+ StructuredValue::Bool(true),
31
+ StructuredValue::Bool(true),
32
+ ])
33
+ );
34
+ }
35
+
36
+ #[test]
37
+ fn runs_logical_assignment_with_short_circuit_semantics() {
38
+ let value = run(
39
+ r#"
40
+ let orAssign = 0;
41
+ const orAssignResult = (orAssign ||= 4);
42
+ let orKeep = 7;
43
+ const orKeepResult = (orKeep ||= 9);
44
+ let andAssign = 3;
45
+ const andAssignResult = (andAssign &&= 5);
46
+ let andKeep = 0;
47
+ const andKeepResult = (andKeep &&= 9);
48
+
49
+ const counts = { object: 0, key: 0, rhs: 0 };
50
+ const record = { slot: 0, gate: 2 };
51
+ function object() {
52
+ counts.object += 1;
53
+ return record;
54
+ }
55
+ function key() {
56
+ counts.key += 1;
57
+ return "slot";
58
+ }
59
+ function rhs(value) {
60
+ counts.rhs += 1;
61
+ return value;
62
+ }
63
+
64
+ const computedOrAssign = (object()[key()] ||= rhs(6));
65
+ const computedOrKeep = (object()[key()] ||= rhs(7));
66
+ const staticAndAssign = (object().gate &&= rhs(8));
67
+ record.gate = 0;
68
+ const staticAndKeep = (object().gate &&= rhs(9));
69
+
70
+ [
71
+ orAssign,
72
+ orAssignResult,
73
+ orKeep,
74
+ orKeepResult,
75
+ andAssign,
76
+ andAssignResult,
77
+ andKeep,
78
+ andKeepResult,
79
+ computedOrAssign,
80
+ computedOrKeep,
81
+ staticAndAssign,
82
+ staticAndKeep,
83
+ counts.object,
84
+ counts.key,
85
+ counts.rhs,
86
+ record.slot,
87
+ record.gate,
88
+ ];
89
+ "#,
90
+ );
91
+ assert_eq!(
92
+ value,
93
+ StructuredValue::Array(vec![
94
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
95
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
96
+ StructuredValue::Number(StructuredNumber::Finite(7.0)),
97
+ StructuredValue::Number(StructuredNumber::Finite(7.0)),
98
+ StructuredValue::Number(StructuredNumber::Finite(5.0)),
99
+ StructuredValue::Number(StructuredNumber::Finite(5.0)),
100
+ StructuredValue::Number(StructuredNumber::Finite(0.0)),
101
+ StructuredValue::Number(StructuredNumber::Finite(0.0)),
102
+ StructuredValue::Number(StructuredNumber::Finite(6.0)),
103
+ StructuredValue::Number(StructuredNumber::Finite(6.0)),
104
+ StructuredValue::Number(StructuredNumber::Finite(8.0)),
105
+ StructuredValue::Number(StructuredNumber::Finite(0.0)),
106
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
107
+ StructuredValue::Number(StructuredNumber::Finite(2.0)),
108
+ StructuredValue::Number(StructuredNumber::Finite(3.0)),
109
+ StructuredValue::Number(StructuredNumber::Finite(6.0)),
110
+ StructuredValue::Number(StructuredNumber::Finite(0.0)),
111
+ ])
112
+ );
113
+ }
114
+
115
+ #[test]
116
+ fn runs_functions_and_closures() {
117
+ let value = run(r#"
118
+ function makeAdder(x) {
119
+ return (y) => x + y;
120
+ }
121
+ const add2 = makeAdder(2);
122
+ add2(5);
123
+ "#);
124
+ assert_eq!(
125
+ value,
126
+ StructuredValue::Number(StructuredNumber::Finite(7.0))
127
+ );
128
+ }
129
+
130
+ #[test]
131
+ fn runs_arrays_objects_and_member_access() {
132
+ let value = run(r#"
133
+ const values = [1, 2];
134
+ const record = { total: values[0] + values[1] };
135
+ record.total;
136
+ "#);
137
+ assert_eq!(
138
+ value,
139
+ StructuredValue::Number(StructuredNumber::Finite(3.0))
140
+ );
141
+ }
142
+
143
+ #[test]
144
+ fn runs_for_in_over_plain_objects_and_arrays() {
145
+ let value = run(r#"
146
+ const object = { beta: 2, alpha: 1 };
147
+ const array = [10, 20];
148
+ array.extra = 30;
149
+ const objectKeys = [];
150
+ for (const key in object) {
151
+ objectKeys[objectKeys.length] = key;
152
+ }
153
+ const arrayKeys = [];
154
+ for (const key in array) {
155
+ arrayKeys[arrayKeys.length] = key;
156
+ }
157
+ [objectKeys, arrayKeys];
158
+ "#);
159
+ assert_eq!(
160
+ value,
161
+ StructuredValue::Array(vec![
162
+ StructuredValue::Array(vec![
163
+ StructuredValue::String("alpha".to_string()),
164
+ StructuredValue::String("beta".to_string()),
165
+ ]),
166
+ StructuredValue::Array(vec![
167
+ StructuredValue::String("0".to_string()),
168
+ StructuredValue::String("1".to_string()),
169
+ StructuredValue::String("extra".to_string()),
170
+ ]),
171
+ ])
172
+ );
173
+ }
174
+
175
+ #[test]
176
+ fn for_in_supports_assignment_target_headers() {
177
+ let value = run(r#"
178
+ const record = { current: "" };
179
+ for (record.current in { beta: 2, alpha: 1 }) {
180
+ }
181
+ record.current;
182
+ "#);
183
+ assert_eq!(value, StructuredValue::String("beta".to_string()));
184
+ }
185
+
186
+ #[test]
187
+ fn runs_branching_loops_and_switch() {
188
+ let value = run(r#"
189
+ let total = 0;
190
+ let i = 0;
191
+ while (i < 4) {
192
+ total += i;
193
+ i += 1;
194
+ }
195
+ do {
196
+ total += 1;
197
+ } while (false);
198
+ for (let j = 0; j < 2; j += 1) {
199
+ if (j === 0) {
200
+ continue;
201
+ }
202
+ total += j;
203
+ }
204
+ switch (total) {
205
+ case 8:
206
+ total += 1;
207
+ break;
208
+ default:
209
+ total = 0;
210
+ }
211
+ total;
212
+ "#);
213
+ assert_eq!(
214
+ value,
215
+ StructuredValue::Number(StructuredNumber::Finite(9.0))
216
+ );
217
+ }
218
+
219
+ #[test]
220
+ fn for_in_snapshots_keys_and_survives_resumption() {
221
+ let program = compile(r#"
222
+ let total = 0;
223
+ for (const key in { beta: 2, alpha: 1 }) {
224
+ total += fetch_data(key);
225
+ }
226
+ total;
227
+ "#)
228
+ .expect("source should compile");
229
+
230
+ let first = match start(
231
+ &program,
232
+ ExecutionOptions {
233
+ inputs: IndexMap::new(),
234
+ capabilities: vec!["fetch_data".to_string()],
235
+ limits: RuntimeLimits::default(),
236
+ cancellation_token: None,
237
+ },
238
+ )
239
+ .expect("start should suspend")
240
+ {
241
+ ExecutionStep::Suspended(suspension) => suspension,
242
+ ExecutionStep::Completed(value) => panic!("expected suspension, got {value:?}"),
243
+ };
244
+ assert_eq!(first.capability, "fetch_data");
245
+ assert_eq!(
246
+ first.args,
247
+ vec![StructuredValue::String("alpha".to_string())]
248
+ );
249
+
250
+ let encoded = dump_snapshot(&first.snapshot).expect("snapshot should serialize");
251
+ let loaded = load_snapshot(&encoded).expect("snapshot should deserialize");
252
+
253
+ let second = match resume_with_options(
254
+ loaded,
255
+ ResumePayload::Value(StructuredValue::Number(StructuredNumber::Finite(10.0))),
256
+ ResumeOptions {
257
+ cancellation_token: None,
258
+ snapshot_policy: Some(snapshot_policy(
259
+ &["fetch_data"],
260
+ RuntimeLimits::default(),
261
+ )),
262
+ },
263
+ )
264
+ .expect("resume should work")
265
+ {
266
+ ExecutionStep::Suspended(suspension) => suspension,
267
+ ExecutionStep::Completed(value) => panic!("expected second suspension, got {value:?}"),
268
+ };
269
+ assert_eq!(second.capability, "fetch_data");
270
+ assert_eq!(
271
+ second.args,
272
+ vec![StructuredValue::String("beta".to_string())]
273
+ );
274
+
275
+ match resume(second.snapshot, ResumePayload::Value(StructuredValue::Number(
276
+ StructuredNumber::Finite(20.0),
277
+ )))
278
+ .expect("final resume should work")
279
+ {
280
+ ExecutionStep::Completed(value) => {
281
+ assert_eq!(value, StructuredValue::Number(StructuredNumber::Finite(30.0)));
282
+ }
283
+ other => panic!("expected completion, got {other:?}"),
284
+ }
285
+ }
286
+
287
+ #[test]
288
+ fn for_in_rejects_unsupported_rhs_values() {
289
+ let program = compile(
290
+ r#"
291
+ for (const key in new Map()) {
292
+ key;
293
+ }
294
+ "#,
295
+ )
296
+ .expect("source should compile");
297
+
298
+ let error = execute(&program, ExecutionOptions::default()).expect_err("execution should fail");
299
+ assert!(
300
+ error
301
+ .to_string()
302
+ .contains("Object helpers currently only support plain objects and arrays")
303
+ );
304
+ }
305
+
306
+ #[test]
307
+ fn runs_math_and_json_builtins() {
308
+ let value = run(r#"
309
+ const encoded = JSON.stringify({ value: Math.max(1, 9, 4) });
310
+ JSON.parse(encoded).value;
311
+ "#);
312
+ assert_eq!(
313
+ value,
314
+ StructuredValue::Number(StructuredNumber::Finite(9.0))
315
+ );
316
+ }
317
+
318
+ #[test]
319
+ fn preserves_supported_enumeration_order_for_json_stringify() {
320
+ let value = run(r#"
321
+ const record = {};
322
+ record.beta = "b";
323
+ record[10] = "ten";
324
+ record.alpha = "a";
325
+ record[2] = "two";
326
+ record["01"] = "leading";
327
+ const values = ["c", "d"];
328
+ values.extra = "ignored";
329
+ JSON.stringify({ record, values });
330
+ "#);
331
+ assert_eq!(
332
+ value,
333
+ StructuredValue::String(
334
+ r#"{"record":{"2":"two","10":"ten","beta":"b","alpha":"a","01":"leading"},"values":["c","d"]}"#
335
+ .to_string()
336
+ )
337
+ );
338
+ }
339
+
340
+ #[test]
341
+ fn runs_object_literals_with_computed_keys_methods_and_spread() {
342
+ let value = run(
343
+ r#"
344
+ const key = "value";
345
+ const extra = [3];
346
+ extra.label = "ok";
347
+ const record = {
348
+ alpha: 1,
349
+ [key]: 2,
350
+ total(step) {
351
+ return this.alpha + this[key] + step;
352
+ },
353
+ ...null,
354
+ ...extra,
355
+ ...{ beta: 4 },
356
+ };
357
+ [record.value, record[0], record.label, record.total(5), record.beta];
358
+ "#,
359
+ );
360
+ assert_eq!(
361
+ value,
362
+ StructuredValue::Array(vec![
363
+ StructuredValue::Number(StructuredNumber::Finite(2.0)),
364
+ StructuredValue::Number(StructuredNumber::Finite(3.0)),
365
+ StructuredValue::String("ok".to_string()),
366
+ StructuredValue::Number(StructuredNumber::Finite(8.0)),
367
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
368
+ ])
369
+ );
370
+ }
371
+
372
+ #[test]
373
+ fn runs_array_spread_and_spread_arguments() {
374
+ let value = run(
375
+ r#"
376
+ const values = [2, 4];
377
+ const sparse = [2, , 4];
378
+ const extra = new Set("ab");
379
+ const box = {
380
+ base: 10,
381
+ total(...args) {
382
+ return this.base + args[0] + args[1] + args[2] + args[3];
383
+ },
384
+ };
385
+ const array = [1, ...sparse, ...extra, 5];
386
+ const built = new Array(...values, 9);
387
+ [
388
+ array,
389
+ box.total(...values, 6),
390
+ [1, ...sparse][2],
391
+ Math.max(...values, 3),
392
+ built,
393
+ ({ missing: null }).missing?.(...values),
394
+ ];
395
+ "#,
396
+ );
397
+ assert_eq!(
398
+ value,
399
+ StructuredValue::Array(vec![
400
+ StructuredValue::Array(vec![
401
+ StructuredValue::Number(StructuredNumber::Finite(1.0)),
402
+ StructuredValue::Number(StructuredNumber::Finite(2.0)),
403
+ StructuredValue::Null,
404
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
405
+ StructuredValue::String("a".to_string()),
406
+ StructuredValue::String("b".to_string()),
407
+ StructuredValue::Number(StructuredNumber::Finite(5.0)),
408
+ ]),
409
+ StructuredValue::Number(StructuredNumber::Finite(22.0)),
410
+ StructuredValue::Null,
411
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
412
+ StructuredValue::Array(vec![
413
+ StructuredValue::Number(StructuredNumber::Finite(2.0)),
414
+ StructuredValue::Number(StructuredNumber::Finite(4.0)),
415
+ StructuredValue::Number(StructuredNumber::Finite(9.0)),
416
+ ]),
417
+ StructuredValue::Undefined,
418
+ ])
419
+ );
420
+ }
421
+
422
+ #[test]
423
+ fn spread_fails_closed_for_unsupported_iterables() {
424
+ let program = compile(
425
+ r#"
426
+ const object = { alpha: 1 };
427
+ [ [...object], Math.max(...object) ];
428
+ "#,
429
+ )
430
+ .expect("spread should lower");
431
+ let error = execute(&program, ExecutionOptions::default())
432
+ .expect_err("unsupported spread sources should fail closed at runtime");
433
+ assert!(
434
+ error
435
+ .to_string()
436
+ .contains("value is not iterable in the supported surface")
437
+ );
438
+ }
439
+
440
+ #[test]
441
+ fn runs_sparse_arrays_across_helpers_and_json() {
442
+ let value = run(
443
+ r#"
444
+ const values = [1, , undefined, 4];
445
+ const callbackIndexes = [];
446
+ values.forEach((value, index) => {
447
+ callbackIndexes[callbackIndexes.length] = index;
448
+ });
449
+ const sliced = values.slice(0, 4);
450
+ const mapped = values.map((value, index) => value ?? (index + 10));
451
+ JSON.stringify({
452
+ length: values.length,
453
+ holeIsUndefined: values[1] === undefined,
454
+ hasHole: 1 in values,
455
+ hasUndefined: 2 in values,
456
+ keys: Object.keys(values),
457
+ entries: Object.entries(values),
458
+ iterated: Array.from(values.values()),
459
+ includesUndefined: values.includes(undefined),
460
+ indexOfUndefined: values.indexOf(undefined),
461
+ joined: values.join("-"),
462
+ json: JSON.stringify(values),
463
+ callbackIndexes,
464
+ slicedHasHole: 1 in sliced,
465
+ mappedHasHole: 1 in mapped,
466
+ mappedKeys: Object.keys(mapped),
467
+ });
468
+ "#,
469
+ );
470
+ assert_eq!(
471
+ value,
472
+ StructuredValue::String(
473
+ r#"{"length":4,"holeIsUndefined":true,"hasHole":false,"hasUndefined":true,"keys":["0","2","3"],"entries":[["0",1],["2",null],["3",4]],"iterated":[1,null,null,4],"includesUndefined":true,"indexOfUndefined":2,"joined":"1---4","json":"[1,null,null,4]","callbackIndexes":[0,2,3],"slicedHasHole":false,"mappedHasHole":false,"mappedKeys":["0","2","3"]}"#
474
+ .to_string()
475
+ )
476
+ );
477
+ }
478
+
479
+ #[test]
480
+ fn structured_inputs_preserve_sparse_array_holes() {
481
+ let program = compile(
482
+ r#"
483
+ [
484
+ Object.keys(value),
485
+ value[0] === undefined,
486
+ 0 in value,
487
+ 1 in value,
488
+ value[1],
489
+ JSON.stringify(value),
490
+ ];
491
+ "#,
492
+ )
493
+ .expect("source should compile");
494
+
495
+ let value = execute(
496
+ &program,
497
+ ExecutionOptions {
498
+ inputs: IndexMap::from([(
499
+ "value".to_string(),
500
+ StructuredValue::Array(vec![
501
+ StructuredValue::Hole,
502
+ StructuredValue::Number(StructuredNumber::Finite(2.0)),
503
+ StructuredValue::Hole,
504
+ ]),
505
+ )]),
506
+ ..ExecutionOptions::default()
507
+ },
508
+ )
509
+ .expect("program should run");
510
+
511
+ assert_eq!(
512
+ value,
513
+ StructuredValue::Array(vec![
514
+ StructuredValue::Array(vec![StructuredValue::String("1".to_string())]),
515
+ StructuredValue::Bool(true),
516
+ StructuredValue::Bool(false),
517
+ StructuredValue::Bool(true),
518
+ StructuredValue::Number(StructuredNumber::Finite(2.0)),
519
+ StructuredValue::String("[null,2,null]".to_string()),
520
+ ])
521
+ );
522
+ }
523
+
524
+ #[test]
525
+ fn object_spread_fails_closed_for_unsupported_sources() {
526
+ let program = compile("({ ...1 });").expect("object spread should lower");
527
+ let error = execute(&program, ExecutionOptions::default())
528
+ .expect_err("unsupported object spread sources should fail closed at runtime");
529
+ assert!(
530
+ error
531
+ .to_string()
532
+ .contains("object spread currently only supports plain objects and arrays")
533
+ );
534
+ }
535
+
536
+ #[test]
537
+ fn enforces_instruction_budget() {
538
+ let program = compile("while (true) {}").expect("source should compile");
539
+ let error = execute(
540
+ &program,
541
+ ExecutionOptions {
542
+ inputs: IndexMap::new(),
543
+ capabilities: Vec::new(),
544
+ limits: RuntimeLimits {
545
+ instruction_budget: 100,
546
+ ..RuntimeLimits::default()
547
+ },
548
+ cancellation_token: None,
549
+ },
550
+ )
551
+ .expect_err("infinite loop should exhaust budget");
552
+ assert!(error.to_string().contains("instruction budget exhausted"));
553
+ }