@weborigami/language 0.2.9 → 0.2.11

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.
@@ -1,7 +1,13 @@
1
1
  import { trailingSlash } from "@weborigami/async-tree";
2
+ import * as YAMLModule from "yaml";
2
3
  import codeFragment from "../runtime/codeFragment.js";
3
4
  import * as ops from "../runtime/ops.js";
4
5
 
6
+ // The "yaml" package doesn't seem to provide a default export that the browser can
7
+ // recognize, so we have to handle two ways to accommodate Node and the browser.
8
+ // @ts-ignore
9
+ const YAML = YAMLModule.default ?? YAMLModule.YAML;
10
+
5
11
  // Parser helpers
6
12
 
7
13
  /** @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode */
@@ -30,6 +36,28 @@ export function annotate(code, location) {
30
36
  return annotated;
31
37
  }
32
38
 
39
+ /**
40
+ * In the given code, replace all scope refernces to the given name with the
41
+ * given macro code.
42
+ *
43
+ * @param {AnnotatedCode} code
44
+ * @param {string} name
45
+ * @param {AnnotatedCode} macro
46
+ */
47
+ export function applyMacro(code, name, macro) {
48
+ if (!(code instanceof Array)) {
49
+ return code;
50
+ }
51
+
52
+ const [fn, ...args] = code;
53
+ if (fn === ops.scope && args[0] === name) {
54
+ return macro;
55
+ }
56
+
57
+ const applied = code.map((child) => applyMacro(child, name, macro));
58
+ return annotate(applied, code.location);
59
+ }
60
+
33
61
  /**
34
62
  * The indicated code is being used to define a property named by the given key.
35
63
  * Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
@@ -50,7 +78,10 @@ function avoidRecursivePropertyCalls(code, key) {
50
78
  ) {
51
79
  // Rewrite to avoid recursion
52
80
  modified = [ops.inherited, code[1]];
53
- } else if (code[0] === ops.lambda && code[1].includes(key)) {
81
+ } else if (
82
+ code[0] === ops.lambda &&
83
+ code[1].some((param) => param[1] === key)
84
+ ) {
54
85
  // Lambda that defines the key; don't rewrite
55
86
  return code;
56
87
  } else {
@@ -382,6 +413,59 @@ export function makeUnaryOperation(operator, value, location) {
382
413
  return annotate([operators[operator], value], location);
383
414
  }
384
415
 
416
+ /**
417
+ * Make an object from YAML front matter
418
+ *
419
+ * @param {string} text
420
+ * @param {CodeLocation} location
421
+ */
422
+ export function makeYamlObject(text, location) {
423
+ // Account for the "---" delimiter at the beginning of the YAML front matter
424
+ const yamlLineDelta = 1;
425
+ const yamlOffsetDelta = 4; // 3 dashes + 1 newline
426
+
427
+ let parsed;
428
+ try {
429
+ parsed = YAML.parse(text);
430
+ } catch (/** @type {any} */ yamlError) {
431
+ // Convert YAML error to a SyntaxError
432
+
433
+ let { message } = yamlError;
434
+ // Remove the line number and column if present
435
+ const lineNumberRegex = /( at line )(\d+)(,)/;
436
+ const lineNumberMatch = message.match(lineNumberRegex);
437
+ if (lineNumberMatch) {
438
+ message = message.slice(0, lineNumberMatch.index);
439
+ }
440
+
441
+ /** @type {any} */
442
+ const error = new SyntaxError(message);
443
+ error.location = {
444
+ end: {
445
+ column: yamlError.linePos[1].col,
446
+ line: yamlError.linePos[1].line + yamlLineDelta,
447
+ offset: yamlError.pos[1] + yamlOffsetDelta,
448
+ },
449
+ source: location.source,
450
+ start: {
451
+ column: yamlError.linePos[0].col,
452
+ line: yamlError.linePos[0].line + yamlLineDelta,
453
+ offset: yamlError.pos[0] + yamlOffsetDelta,
454
+ },
455
+ };
456
+ throw error;
457
+ }
458
+
459
+ if (!(parsed instanceof Object)) {
460
+ /** @type {any} */
461
+ const error = new SyntaxError("YAML front matter must be an object.");
462
+ error.location = location;
463
+ throw error;
464
+ }
465
+
466
+ return annotate([ops.literal, parsed], location);
467
+ }
468
+
385
469
  /**
386
470
  * Upgrade a potential builtin reference to an actual builtin reference.
387
471
  *
@@ -1,9 +1,5 @@
1
1
  import { Mixin } from "../../index.ts";
2
2
 
3
- declare const EventTargetMixin: Mixin<{
4
- addEventListener(type: string, listener: EventListener): void;
5
- dispatchEvent(event: Event): boolean;
6
- removeEventListener(type: string, listener: EventListener): void;
7
- }>;
3
+ declare const EventTargetMixin: Mixin<EventTarget>;
8
4
 
9
5
  export default EventTargetMixin;
@@ -5,6 +5,8 @@ import {
5
5
  trailingSlash,
6
6
  TraverseError,
7
7
  } from "@weborigami/async-tree";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
8
10
  import codeFragment from "./codeFragment.js";
9
11
  import { typos } from "./typos.js";
10
12
 
@@ -85,9 +87,24 @@ export function formatError(error) {
85
87
  if (!fragmentInMessage) {
86
88
  message += `\nevaluating: ${fragment}`;
87
89
  }
90
+
88
91
  if (typeof source === "object" && source.url) {
89
- message += `\n at ${source.url.href}:${line}:${start.column}`;
92
+ const { url } = source;
93
+ let fileRef;
94
+ // If URL is a file: URL, change to a relative path
95
+ if (url.protocol === "file:") {
96
+ fileRef = fileURLToPath(url);
97
+ const relativePath = path.relative(process.cwd(), fileRef);
98
+ if (!relativePath.startsWith("..")) {
99
+ fileRef = relativePath;
100
+ }
101
+ } else {
102
+ // Not a file: URL, use as is
103
+ fileRef = url.href;
104
+ }
105
+ message += `\n at ${fileRef}:${line}:${start.column}`;
90
106
  } else if (source.text.includes("\n")) {
107
+ // Don't know the URL, but has multiple lines so add line number
91
108
  message += `\n at line ${line}, column ${start.column}`;
92
109
  }
93
110
  }
@@ -113,7 +130,7 @@ export function maybeOrigamiSourceCode(text) {
113
130
 
114
131
  export async function scopeReferenceError(scope, key) {
115
132
  const messages = [
116
- `"${key}" is not in scope.`,
133
+ `"${key}" is not in scope or is undefined.`,
117
134
  await formatScopeTypos(scope, key),
118
135
  ];
119
136
  const message = messages.join(" ");
@@ -1,6 +1,5 @@
1
1
  import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
2
2
  import codeFragment from "./codeFragment.js";
3
- import { ops } from "./internal.js";
4
3
  import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
5
4
 
6
5
  /**
@@ -20,14 +19,7 @@ export default async function evaluate(code) {
20
19
  }
21
20
 
22
21
  let evaluated;
23
- const unevaluatedFns = [
24
- ops.external,
25
- ops.lambda,
26
- ops.merge,
27
- ops.object,
28
- ops.literal,
29
- ];
30
- if (unevaluatedFns.includes(code[0])) {
22
+ if (code[0]?.unevaluatedArgs) {
31
23
  // Don't evaluate instructions, use as is.
32
24
  evaluated = code;
33
25
  } else {
@@ -10,6 +10,7 @@ import {
10
10
  Tree,
11
11
  isUnpackable,
12
12
  scope as scopeFn,
13
+ symbols,
13
14
  concat as treeConcat,
14
15
  } from "@weborigami/async-tree";
15
16
  import os from "node:os";
@@ -113,6 +114,29 @@ export async function conditional(condition, truthy, falsy) {
113
114
  return value instanceof Function ? await value() : value;
114
115
  }
115
116
 
117
+ /**
118
+ * Construct a document object by invoking the body code (a lambda) and adding
119
+ * the resulting text to the front data.
120
+ *
121
+ * @this {AsyncTree|null}
122
+ * @param {any} frontData
123
+ * @param {AnnotatedCode} bodyCode
124
+ */
125
+ export async function document(frontData, bodyCode) {
126
+ const context = new ObjectTree(frontData);
127
+ context.parent = this;
128
+ const bodyFn = await evaluate.call(context, bodyCode);
129
+ const body = await bodyFn();
130
+ const object = {
131
+ ...frontData,
132
+ "@text": body,
133
+ };
134
+ object[symbols.parent] = this;
135
+ return object;
136
+ }
137
+ addOpLabel(document, "«ops.document");
138
+ document.unevaluatedArgs = true;
139
+
116
140
  export function division(a, b) {
117
141
  return a / b;
118
142
  }
@@ -158,6 +182,7 @@ export async function external(path, code, cache) {
158
182
  return value;
159
183
  }
160
184
  addOpLabel(external, "«ops.external»");
185
+ external.unevaluatedArgs = true;
161
186
 
162
187
  /**
163
188
  * This op is only used during parsing. It signals to ops.object that the
@@ -223,7 +248,8 @@ export function lambda(parameters, code) {
223
248
  // Add arguments to scope.
224
249
  const ambients = {};
225
250
  for (const parameter of parameters) {
226
- ambients[parameter] = args.shift();
251
+ const parameterName = parameter[1];
252
+ ambients[parameterName] = args.shift();
227
253
  }
228
254
  Object.defineProperty(ambients, codeSymbol, {
229
255
  value: code,
@@ -262,6 +288,7 @@ export function lambda(parameters, code) {
262
288
  return invoke;
263
289
  }
264
290
  addOpLabel(lambda, "«ops.lambda");
291
+ lambda.unevaluatedArgs = true;
265
292
 
266
293
  export function lessThan(a, b) {
267
294
  return a < b;
@@ -283,6 +310,7 @@ export async function literal(value) {
283
310
  return value;
284
311
  }
285
312
  addOpLabel(literal, "«ops.literal»");
313
+ literal.unevaluatedArgs = true;
286
314
 
287
315
  /**
288
316
  * Logical AND operator
@@ -376,6 +404,7 @@ export async function merge(...codes) {
376
404
  return mergeTrees.call(this, ...trees);
377
405
  }
378
406
  addOpLabel(merge, "«ops.merge»");
407
+ merge.unevaluatedArgs = true;
379
408
 
380
409
  export function multiplication(a, b) {
381
410
  return a * b;
@@ -424,6 +453,7 @@ export async function object(...entries) {
424
453
  return expressionObject(entries, this);
425
454
  }
426
455
  addOpLabel(object, "«ops.object»");
456
+ object.unevaluatedArgs = true;
427
457
 
428
458
  export function remainder(a, b) {
429
459
  return a % b;
@@ -4,7 +4,7 @@ import assert from "node:assert";
4
4
  export function assertCodeEqual(actual, expected) {
5
5
  const actualStripped = stripCodeLocations(actual);
6
6
  const expectedStripped = stripCodeLocations(expected);
7
- assert.deepEqual(actualStripped, expectedStripped);
7
+ assert.deepStrictEqual(actualStripped, expectedStripped);
8
8
  }
9
9
 
10
10
  /**
@@ -18,7 +18,7 @@ describe("optimize", () => {
18
18
  const code = fn.code;
19
19
  assertCodeEqual(code, [
20
20
  ops.lambda,
21
- ["name"],
21
+ [[ops.literal, "name"]],
22
22
  [
23
23
  ops.object,
24
24
  ["a", 1],
@@ -64,12 +64,16 @@ describe("Origami parser", () => {
64
64
  ]);
65
65
  assertParse("arrowFunction", "x => y", [
66
66
  ops.lambda,
67
- ["x"],
67
+ [[ops.literal, "x"]],
68
68
  [ops.scope, "y"],
69
69
  ]);
70
70
  assertParse("arrowFunction", "(a, b, c) ⇒ fn(a, b, c)", [
71
71
  ops.lambda,
72
- ["a", "b", "c"],
72
+ [
73
+ [ops.literal, "a"],
74
+ [ops.literal, "b"],
75
+ [ops.literal, "c"],
76
+ ],
73
77
  [
74
78
  [ops.builtin, "fn"],
75
79
  [ops.scope, "a"],
@@ -79,10 +83,10 @@ describe("Origami parser", () => {
79
83
  ]);
80
84
  assertParse("arrowFunction", "a => b => fn(a, b)", [
81
85
  ops.lambda,
82
- ["a"],
86
+ [[ops.literal, "a"]],
83
87
  [
84
88
  ops.lambda,
85
- ["b"],
89
+ [[ops.literal, "b"]],
86
90
  [
87
91
  [ops.builtin, "fn"],
88
92
  [ops.scope, "a"],
@@ -247,20 +251,20 @@ describe("Origami parser", () => {
247
251
  assertParse("conditionalExpression", "true ? 1 : 0", [
248
252
  ops.conditional,
249
253
  [ops.scope, "true"],
250
- [ops.literal, "1"],
251
- [ops.literal, "0"],
254
+ [ops.literal, 1],
255
+ [ops.literal, 0],
252
256
  ]);
253
257
  assertParse("conditionalExpression", "false ? () => 1 : 0", [
254
258
  ops.conditional,
255
259
  [ops.scope, "false"],
256
- [ops.lambda, [], [ops.lambda, [], [ops.literal, "1"]]],
257
- [ops.literal, "0"],
260
+ [ops.lambda, [], [ops.lambda, [], [ops.literal, 1]]],
261
+ [ops.literal, 0],
258
262
  ]);
259
263
  assertParse("conditionalExpression", "false ? =1 : 0", [
260
264
  ops.conditional,
261
265
  [ops.scope, "false"],
262
- [ops.lambda, [], [ops.lambda, ["_"], [ops.literal, "1"]]],
263
- [ops.literal, "0"],
266
+ [ops.lambda, [], [ops.lambda, [[ops.literal, "_"]], [ops.literal, 1]]],
267
+ [ops.literal, 0],
264
268
  ]);
265
269
  });
266
270
 
@@ -282,6 +286,43 @@ describe("Origami parser", () => {
282
286
  ]);
283
287
  });
284
288
 
289
+ test("error thrown for missing token", () => {
290
+ assertThrows("arrowFunction", "(a) => ", "Expected an expression");
291
+ assertThrows("arrowFunction", "a ⇒ ", "Expected an expression");
292
+ assertThrows("callExpression", "fn(a", "Expected right parenthesis");
293
+ assertThrows("doubleQuoteString", '"foo', "Expected closing quote");
294
+ assertThrows("guillemetString", "«foo", "Expected closing guillemet");
295
+ assertThrows("objectGetter", "a =", "Expected an expression");
296
+ assertThrows("objectProperty", "a:", "Expected an expression");
297
+ assertThrows("singleQuoteString", "'foo", "Expected closing quote");
298
+ assertThrows("templateLiteral", "`foo", "Expected closing backtick");
299
+ });
300
+
301
+ test.only("error thrown for invalid Origami front matter expression", () => {
302
+ assertThrows(
303
+ "templateDocument",
304
+ `---
305
+ (name) => foo)
306
+ ---
307
+ Body`,
308
+ 'Expected "---"',
309
+ { line: 2, column: 14 }
310
+ );
311
+ });
312
+
313
+ test("error thrown for invalid YAML front matter", () => {
314
+ assertThrows(
315
+ "templateDocument",
316
+ `---
317
+ a : 1
318
+ }
319
+ ---
320
+ Body`,
321
+ "Unexpected flow-map-end token",
322
+ { line: 3, column: 1 }
323
+ );
324
+ });
325
+
285
326
  test("exponentiationExpression", () => {
286
327
  assertParse("exponentiationExpression", "2 ** 2 ** 3", [
287
328
  ops.exponentiation,
@@ -293,12 +334,10 @@ describe("Origami parser", () => {
293
334
  test("expression", () => {
294
335
  assertParse(
295
336
  "expression",
296
- `
297
- {
298
- index.html = index.ori(teamData.yaml)
299
- thumbnails = map(images, { value: thumbnail.js })
300
- }
301
- `,
337
+ `{
338
+ index.html = index.ori(teamData.yaml)
339
+ thumbnails = map(images, { value: thumbnail.js })
340
+ }`,
302
341
  [
303
342
  ops.object,
304
343
  [
@@ -389,7 +428,7 @@ describe("Origami parser", () => {
389
428
  ]);
390
429
  assertParse("expression", "fn =`x`", [
391
430
  [ops.builtin, "fn"],
392
- [ops.lambda, ["_"], [ops.template, [ops.literal, ["x"]]]],
431
+ [ops.lambda, [[ops.literal, "_"]], [ops.template, [ops.literal, ["x"]]]],
393
432
  ]);
394
433
  assertParse("expression", "copy app.js(formulas), files:snapshot", [
395
434
  [ops.builtin, "copy"],
@@ -406,7 +445,7 @@ describe("Origami parser", () => {
406
445
  [ops.builtin, "map"],
407
446
  [
408
447
  ops.lambda,
409
- ["_"],
448
+ [[ops.literal, "_"]],
410
449
  [
411
450
  ops.template,
412
451
  [ops.literal, ["<li>", "</li>"]],
@@ -426,7 +465,10 @@ describe("Origami parser", () => {
426
465
  ]);
427
466
  assertParse("expression", "(post, slug) => fn.js(post, slug)", [
428
467
  ops.lambda,
429
- ["post", "slug"],
468
+ [
469
+ [ops.literal, "post"],
470
+ [ops.literal, "slug"],
471
+ ],
430
472
  [
431
473
  [ops.scope, "fn.js"],
432
474
  [ops.scope, "post"],
@@ -718,7 +760,7 @@ describe("Origami parser", () => {
718
760
  assertParse("objectEntry", "a: a", ["a", [ops.inherited, "a"]]);
719
761
  assertParse("objectEntry", "a: (a) => a", [
720
762
  "a",
721
- [ops.lambda, ["a"], [ops.scope, "a"]],
763
+ [ops.lambda, [[ops.literal, "a"]], [ops.scope, "a"]],
722
764
  ]);
723
765
  assertParse("objectEntry", "posts/: map(posts, post.ori)", [
724
766
  "posts/",
@@ -943,12 +985,12 @@ describe("Origami parser", () => {
943
985
  test("shorthandFunction", () => {
944
986
  assertParse("shorthandFunction", "=message", [
945
987
  ops.lambda,
946
- ["_"],
988
+ [[ops.literal, "_"]],
947
989
  [undetermined, "message"],
948
990
  ]);
949
991
  assertParse("shorthandFunction", "=`Hello, ${name}.`", [
950
992
  ops.lambda,
951
- ["_"],
993
+ [[ops.literal, "_"]],
952
994
  [
953
995
  ops.template,
954
996
  [ops.literal, ["Hello, ", "."]],
@@ -957,7 +999,7 @@ describe("Origami parser", () => {
957
999
  ]);
958
1000
  assertParse("shorthandFunction", "=indent`hello`", [
959
1001
  ops.lambda,
960
- ["_"],
1002
+ [[ops.literal, "_"]],
961
1003
  [
962
1004
  [ops.builtin, "indent"],
963
1005
  [ops.literal, ["hello"]],
@@ -988,19 +1030,19 @@ describe("Origami parser", () => {
988
1030
  ]);
989
1031
  });
990
1032
 
991
- test("templateDocument", () => {
992
- assertParse("templateDocument", "hello${foo}world", [
1033
+ test("templateBody", () => {
1034
+ assertParse("templateBody", "hello${foo}world", [
993
1035
  ops.lambda,
994
- ["_"],
1036
+ [[ops.literal, "_"]],
995
1037
  [
996
1038
  ops.templateIndent,
997
1039
  [ops.literal, ["hello", "world"]],
998
1040
  [ops.concat, [ops.scope, "foo"]],
999
1041
  ],
1000
1042
  ]);
1001
- assertParse("templateDocument", "Documents can contain ` backticks", [
1043
+ assertParse("templateBody", "Documents can contain ` backticks", [
1002
1044
  ops.lambda,
1003
- ["_"],
1045
+ [[ops.literal, "_"]],
1004
1046
  [
1005
1047
  ops.templateIndent,
1006
1048
  [ops.literal, ["Documents can contain ` backticks"]],
@@ -1008,6 +1050,66 @@ describe("Origami parser", () => {
1008
1050
  ]);
1009
1051
  });
1010
1052
 
1053
+ test("templateDocument with no front matter", () => {
1054
+ assertParse("templateDocument", "Hello, world!", [
1055
+ ops.lambda,
1056
+ [[ops.literal, "_"]],
1057
+ [ops.templateIndent, [ops.literal, ["Hello, world!"]]],
1058
+ ]);
1059
+ });
1060
+
1061
+ test("templateDocument with YAML front matter", () => {
1062
+ assertParse(
1063
+ "templateDocument",
1064
+ `---
1065
+ title: Title goes here
1066
+ ---
1067
+ Body text`,
1068
+ [
1069
+ ops.document,
1070
+ [ops.literal, { title: "Title goes here" }],
1071
+ [
1072
+ ops.lambda,
1073
+ [[ops.literal, "_"]],
1074
+ [ops.templateIndent, [ops.literal, ["Body text"]]],
1075
+ ],
1076
+ ]
1077
+ );
1078
+ });
1079
+
1080
+ test("templateDocument with Origami front matter", () => {
1081
+ assertParse(
1082
+ "templateDocument",
1083
+ `---
1084
+ {
1085
+ title: "Title"
1086
+ @text: @template()
1087
+ }
1088
+ ---
1089
+ <h1>\${ title }</h1>
1090
+ `,
1091
+ [
1092
+ ops.object,
1093
+ ["title", [ops.literal, "Title"]],
1094
+ [
1095
+ "@text",
1096
+ [
1097
+ [
1098
+ ops.lambda,
1099
+ [[ops.literal, "_"]],
1100
+ [
1101
+ ops.templateIndent,
1102
+ [ops.literal, ["<h1>", "</h1>\n"]],
1103
+ [ops.concat, [ops.scope, "title"]],
1104
+ ],
1105
+ ],
1106
+ undefined,
1107
+ ],
1108
+ ],
1109
+ ]
1110
+ );
1111
+ });
1112
+
1011
1113
  test("templateLiteral", () => {
1012
1114
  assertParse("templateLiteral", "`Hello, world.`", [
1013
1115
  ops.template,
@@ -1033,7 +1135,7 @@ describe("Origami parser", () => {
1033
1135
  [ops.scope, "people"],
1034
1136
  [
1035
1137
  ops.lambda,
1036
- ["_"],
1138
+ [[ops.literal, "_"]],
1037
1139
  [
1038
1140
  ops.template,
1039
1141
  [ops.literal, ["", ""]],
@@ -1087,7 +1189,7 @@ function assertParse(startRule, source, expected, checkLocation = true) {
1087
1189
  code.location.start.offset,
1088
1190
  code.location.end.offset
1089
1191
  );
1090
- assert.equal(resultSource, source.trim());
1192
+ assert.equal(resultSource, source);
1091
1193
  }
1092
1194
 
1093
1195
  assertCodeEqual(code, expected);
@@ -1101,3 +1203,23 @@ function assertCodeLocations(code) {
1101
1203
  }
1102
1204
  }
1103
1205
  }
1206
+
1207
+ function assertThrows(startRule, source, message, position) {
1208
+ try {
1209
+ parse(source, {
1210
+ grammarSource: { text: source },
1211
+ startRule,
1212
+ });
1213
+ } catch (/** @type {any} */ error) {
1214
+ assert(
1215
+ error.message.includes(message),
1216
+ `Error message incorrect:\n expected: "${message}"\n actual: "${error.message}"`
1217
+ );
1218
+ if (position) {
1219
+ assert.equal(error.location.start.line, position.line);
1220
+ assert.equal(error.location.start.column, position.column);
1221
+ }
1222
+ return;
1223
+ }
1224
+ assert.fail(`Expected error: ${message}`);
1225
+ }
@@ -80,6 +80,29 @@ describe("ops", () => {
80
80
  assert.strictEqual(await ops.conditional(false, errorFn, trueFn), true);
81
81
  });
82
82
 
83
+ test("ops.documentFunction", async () => {
84
+ const code = createCode([
85
+ ops.document,
86
+ {
87
+ a: 1,
88
+ },
89
+ [
90
+ ops.lambda,
91
+ [["_"]],
92
+ [
93
+ ops.template,
94
+ [ops.literal, ["a = ", ""]],
95
+ [ops.concat, [ops.scope, "a"]],
96
+ ],
97
+ ],
98
+ ]);
99
+ const result = await evaluate.call(null, code);
100
+ assert.deepEqual(result, {
101
+ a: 1,
102
+ "@text": "a = 1",
103
+ });
104
+ });
105
+
83
106
  test("ops.division divides two numbers", async () => {
84
107
  assert.strictEqual(ops.division(12, 2), 6);
85
108
  assert.strictEqual(ops.division(3, 2), 1.5);
@@ -169,7 +192,10 @@ describe("ops", () => {
169
192
  test("ops.lambda adds input parameters to scope", async () => {
170
193
  const code = createCode([
171
194
  ops.lambda,
172
- ["a", "b"],
195
+ [
196
+ [ops.literal, "a"],
197
+ [ops.literal, "b"],
198
+ ],
173
199
  [ops.concat, [ops.scope, "b"], [ops.scope, "a"]],
174
200
  ]);
175
201
  const fn = await evaluate.call(null, code);