@weborigami/language 0.1.0 → 0.2.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.
- package/index.ts +1 -0
- package/package.json +6 -4
- package/src/compiler/compile.js +33 -15
- package/src/compiler/origami.pegjs +222 -193
- package/src/compiler/parse.js +1380 -1040
- package/src/compiler/parserHelpers.js +171 -45
- package/src/runtime/ops.js +137 -55
- package/test/cases/ReadMe.md +1 -0
- package/test/cases/conditionalExpression.yaml +101 -0
- package/test/cases/logicalAndExpression.yaml +146 -0
- package/test/cases/logicalOrExpression.yaml +145 -0
- package/test/cases/nullishCoalescingExpression.yaml +105 -0
- package/test/compiler/compile.test.js +3 -3
- package/test/compiler/parse.test.js +447 -363
- package/test/generated/conditionalExpression.test.js +58 -0
- package/test/generated/logicalAndExpression.test.js +80 -0
- package/test/generated/logicalOrExpression.test.js +78 -0
- package/test/generated/nullishCoalescingExpression.test.js +64 -0
- package/test/generator/generateTests.js +80 -0
- package/test/generator/oriEval.js +15 -0
- package/test/runtime/ops.test.js +127 -23
|
@@ -4,16 +4,26 @@ import * as ops from "../runtime/ops.js";
|
|
|
4
4
|
|
|
5
5
|
// Parser helpers
|
|
6
6
|
|
|
7
|
+
/** @typedef {import("../../index.ts").Code} Code */
|
|
8
|
+
|
|
9
|
+
// Marker for a reference that may be a builtin or a scope reference
|
|
10
|
+
export const undetermined = Symbol("undetermined");
|
|
11
|
+
|
|
12
|
+
const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* If a parse result is an object that will be evaluated at runtime, attach the
|
|
9
16
|
* location of the source code that produced it for debugging and error messages.
|
|
17
|
+
*
|
|
18
|
+
* @param {Code} code
|
|
19
|
+
* @param {any} location
|
|
10
20
|
*/
|
|
11
|
-
export function annotate(
|
|
12
|
-
if (typeof
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
export function annotate(code, location) {
|
|
22
|
+
if (typeof code === "object" && code !== null && location) {
|
|
23
|
+
code.location = location;
|
|
24
|
+
code.source = codeFragment(location);
|
|
15
25
|
}
|
|
16
|
-
return
|
|
26
|
+
return code;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
/**
|
|
@@ -21,31 +31,51 @@ export function annotate(parseResult, location) {
|
|
|
21
31
|
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
22
32
|
* infinite recursion.
|
|
23
33
|
*
|
|
24
|
-
* @param {
|
|
34
|
+
* @param {Code} code
|
|
25
35
|
* @param {string} key
|
|
26
36
|
*/
|
|
27
37
|
function avoidRecursivePropertyCalls(code, key) {
|
|
28
38
|
if (!(code instanceof Array)) {
|
|
29
39
|
return code;
|
|
30
40
|
}
|
|
41
|
+
/** @type {Code} */
|
|
31
42
|
let modified;
|
|
32
43
|
if (
|
|
33
44
|
code[0] === ops.scope &&
|
|
34
45
|
trailingSlash.remove(code[1]) === trailingSlash.remove(key)
|
|
35
46
|
) {
|
|
36
47
|
// Rewrite to avoid recursion
|
|
48
|
+
// @ts-ignore
|
|
37
49
|
modified = [ops.inherited, code[1]];
|
|
38
50
|
} else if (code[0] === ops.lambda && code[1].includes(key)) {
|
|
39
51
|
// Lambda that defines the key; don't rewrite
|
|
40
52
|
return code;
|
|
41
53
|
} else {
|
|
42
54
|
// Process any nested code
|
|
55
|
+
// @ts-ignore
|
|
43
56
|
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
|
44
57
|
}
|
|
45
58
|
annotate(modified, code.location);
|
|
46
59
|
return modified;
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Downgrade a potential builtin reference to a scope reference.
|
|
64
|
+
*
|
|
65
|
+
* @param {Code} code
|
|
66
|
+
*/
|
|
67
|
+
export function downgradeReference(code) {
|
|
68
|
+
if (code && code.length === 2 && code[0] === undetermined) {
|
|
69
|
+
/** @type {Code} */
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
const result = [ops.scope, code[1]];
|
|
72
|
+
annotate(result, code.location);
|
|
73
|
+
return result;
|
|
74
|
+
} else {
|
|
75
|
+
return code;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
// Return true if the code will generate an async object.
|
|
50
80
|
function isCodeForAsyncObject(code) {
|
|
51
81
|
if (!(code instanceof Array)) {
|
|
@@ -95,56 +125,111 @@ export function makeArray(entries) {
|
|
|
95
125
|
}
|
|
96
126
|
|
|
97
127
|
/**
|
|
98
|
-
*
|
|
128
|
+
* Create a chain of binary operators. The head is the first value, and the tail
|
|
129
|
+
* is an array of [operator, value] pairs.
|
|
99
130
|
*
|
|
131
|
+
* @param {Code} head
|
|
132
|
+
* @param {[any, Code][]} tail
|
|
133
|
+
*/
|
|
134
|
+
export function makeBinaryOperatorChain(head, tail) {
|
|
135
|
+
/** @type {Code} */
|
|
136
|
+
let value = head;
|
|
137
|
+
for (const [operatorToken, right] of tail) {
|
|
138
|
+
const left = value;
|
|
139
|
+
const operators = {
|
|
140
|
+
"===": ops.strictEqual,
|
|
141
|
+
"!==": ops.notStrictEqual,
|
|
142
|
+
"==": ops.equal,
|
|
143
|
+
"!=": ops.notEqual,
|
|
144
|
+
};
|
|
145
|
+
const op = operators[operatorToken];
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
value = [op, left, right];
|
|
148
|
+
value.location = {
|
|
149
|
+
source: left.location.source,
|
|
150
|
+
start: left.location.start,
|
|
151
|
+
end: right.location.end,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
100
158
|
* @param {Code} target
|
|
101
|
-
* @param {
|
|
102
|
-
* @returns
|
|
159
|
+
* @param {any[]} args
|
|
103
160
|
*/
|
|
104
|
-
export function
|
|
161
|
+
export function makeCall(target, args) {
|
|
105
162
|
if (!(target instanceof Array)) {
|
|
106
163
|
const error = new SyntaxError(`Can't call this like a function: ${target}`);
|
|
107
|
-
/** @type {any} */ (error).location = location;
|
|
164
|
+
/** @type {any} */ (error).location = /** @type {any} */ (target).location;
|
|
108
165
|
throw error;
|
|
109
166
|
}
|
|
110
167
|
|
|
111
|
-
let value = target;
|
|
112
168
|
const source = target.location.source;
|
|
113
|
-
// The chain is an array of arguments (which are themselves arrays). We
|
|
114
|
-
// successively apply the top-level elements of that chain to build up the
|
|
115
|
-
// function composition.
|
|
116
169
|
let start = target.location.start;
|
|
117
170
|
let end = target.location.end;
|
|
118
|
-
for (const args of chain) {
|
|
119
|
-
/** @type {Code} */
|
|
120
|
-
let fnCall;
|
|
121
171
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Create a location spanning the newly-constructed function call.
|
|
134
|
-
if (args instanceof Array) {
|
|
135
|
-
if (args.location) {
|
|
136
|
-
end = args.location.end;
|
|
137
|
-
} else {
|
|
138
|
-
throw "Internal parser error: no location for function call argument";
|
|
172
|
+
let fnCall;
|
|
173
|
+
if (args[0] === ops.traverse) {
|
|
174
|
+
let tree = target;
|
|
175
|
+
|
|
176
|
+
if (tree[0] === undetermined) {
|
|
177
|
+
// In a traversal, downgrade ops.builtin references to ops.scope
|
|
178
|
+
tree = downgradeReference(tree);
|
|
179
|
+
if (tree[0] === ops.scope && !trailingSlash.has(tree[1])) {
|
|
180
|
+
// Target didn't parse with a trailing slash; add one
|
|
181
|
+
tree[1] = trailingSlash.add(tree[1]);
|
|
139
182
|
}
|
|
140
183
|
}
|
|
141
184
|
|
|
142
|
-
|
|
185
|
+
if (args.length > 1) {
|
|
186
|
+
// Regular traverse
|
|
187
|
+
const keys = args.slice(1);
|
|
188
|
+
fnCall = [ops.traverse, tree, ...keys];
|
|
189
|
+
} else {
|
|
190
|
+
// Traverse without arguments equates to unpack
|
|
191
|
+
fnCall = [ops.unpack, tree];
|
|
192
|
+
}
|
|
193
|
+
} else if (args[0] === ops.template) {
|
|
194
|
+
// Tagged template
|
|
195
|
+
fnCall = [upgradeReference(target), ...args.slice(1)];
|
|
196
|
+
} else {
|
|
197
|
+
// Function call with explicit or implicit parentheses
|
|
198
|
+
fnCall = [upgradeReference(target), ...args];
|
|
199
|
+
}
|
|
143
200
|
|
|
144
|
-
|
|
201
|
+
// Create a location spanning the newly-constructed function call.
|
|
202
|
+
if (args instanceof Array) {
|
|
203
|
+
// @ts-ignore
|
|
204
|
+
end = args.location?.end ?? args.at(-1)?.location?.end;
|
|
205
|
+
if (end === undefined) {
|
|
206
|
+
throw "Internal parser error: no location for function call argument";
|
|
207
|
+
}
|
|
145
208
|
}
|
|
146
209
|
|
|
147
|
-
|
|
210
|
+
// @ts-ignore
|
|
211
|
+
annotate(fnCall, { start, source, end });
|
|
212
|
+
|
|
213
|
+
return fnCall;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* For functions that short-circuit arguments, we need to defer evaluation of
|
|
218
|
+
* the arguments until the function is called. Exception: if the argument is a
|
|
219
|
+
* literal, we leave it alone.
|
|
220
|
+
*
|
|
221
|
+
* @param {any[]} args
|
|
222
|
+
*/
|
|
223
|
+
export function makeDeferredArguments(args) {
|
|
224
|
+
return args.map((arg) => {
|
|
225
|
+
if (arg instanceof Array && arg[0] === ops.literal) {
|
|
226
|
+
return arg;
|
|
227
|
+
}
|
|
228
|
+
const fn = [ops.lambda, [], arg];
|
|
229
|
+
// @ts-ignore
|
|
230
|
+
annotate(fn, arg.location);
|
|
231
|
+
return fn;
|
|
232
|
+
});
|
|
148
233
|
}
|
|
149
234
|
|
|
150
235
|
export function makeObject(entries, op) {
|
|
@@ -195,13 +280,15 @@ export function makeObject(entries, op) {
|
|
|
195
280
|
}
|
|
196
281
|
|
|
197
282
|
// Similar to a function call, but the order is reversed.
|
|
198
|
-
export function makePipeline(
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
283
|
+
export function makePipeline(arg, fn) {
|
|
284
|
+
const upgraded = upgradeReference(fn);
|
|
285
|
+
const result = makeCall(upgraded, [arg]);
|
|
286
|
+
const source = fn.location.source;
|
|
287
|
+
let start = arg.location.start;
|
|
288
|
+
let end = fn.location.end;
|
|
289
|
+
// @ts-ignore
|
|
290
|
+
annotate(result, { start, source, end });
|
|
291
|
+
return result;
|
|
205
292
|
}
|
|
206
293
|
|
|
207
294
|
// Define a property on an object.
|
|
@@ -210,6 +297,21 @@ export function makeProperty(key, value) {
|
|
|
210
297
|
return [key, modified];
|
|
211
298
|
}
|
|
212
299
|
|
|
300
|
+
export function makeReference(identifier) {
|
|
301
|
+
// We can't know for sure that an identifier is a builtin reference until we
|
|
302
|
+
// see whether it's being called as a function.
|
|
303
|
+
let op;
|
|
304
|
+
if (builtinRegex.test(identifier)) {
|
|
305
|
+
op = identifier.endsWith(":")
|
|
306
|
+
? // Namespace is always a builtin reference
|
|
307
|
+
ops.builtin
|
|
308
|
+
: undetermined;
|
|
309
|
+
} else {
|
|
310
|
+
op = ops.scope;
|
|
311
|
+
}
|
|
312
|
+
return [op, identifier];
|
|
313
|
+
}
|
|
314
|
+
|
|
213
315
|
export function makeTemplate(op, head, tail) {
|
|
214
316
|
const strings = [head];
|
|
215
317
|
const values = [];
|
|
@@ -219,3 +321,27 @@ export function makeTemplate(op, head, tail) {
|
|
|
219
321
|
}
|
|
220
322
|
return [op, [ops.literal, strings], ...values];
|
|
221
323
|
}
|
|
324
|
+
|
|
325
|
+
export function makeUnaryOperatorCall(operator, value) {
|
|
326
|
+
const operators = {
|
|
327
|
+
"!": ops.logicalNot,
|
|
328
|
+
};
|
|
329
|
+
return [operators[operator], value];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Upgrade a potential builtin reference to an actual builtin reference.
|
|
334
|
+
*
|
|
335
|
+
* @param {Code} code
|
|
336
|
+
*/
|
|
337
|
+
export function upgradeReference(code) {
|
|
338
|
+
if (code.length === 2 && code[0] === undetermined) {
|
|
339
|
+
/** @type {Code} */
|
|
340
|
+
// @ts-ignore
|
|
341
|
+
const result = [ops.builtin, code[1]];
|
|
342
|
+
annotate(result, code.location);
|
|
343
|
+
return result;
|
|
344
|
+
} else {
|
|
345
|
+
return code;
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -48,26 +48,42 @@ export async function builtin(key) {
|
|
|
48
48
|
if (!this) {
|
|
49
49
|
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
50
50
|
}
|
|
51
|
-
let current = this;
|
|
52
|
-
while (current.parent) {
|
|
53
|
-
current = current.parent;
|
|
54
|
-
}
|
|
55
51
|
|
|
56
|
-
const
|
|
52
|
+
const builtins = Tree.root(this);
|
|
53
|
+
const value = await builtins.get(key);
|
|
57
54
|
if (value === undefined) {
|
|
58
|
-
throw await builtinReferenceError(this,
|
|
55
|
+
throw await builtinReferenceError(this, builtins, key);
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
return value;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* Concatenate the given arguments.
|
|
63
|
+
*
|
|
64
|
+
* @this {AsyncTree|null}
|
|
65
|
+
* @param {any[]} args
|
|
66
|
+
*/
|
|
67
|
+
export async function concat(...args) {
|
|
68
|
+
return treeConcat.call(this, args);
|
|
69
|
+
}
|
|
70
|
+
addOpLabel(concat, "«ops.concat»");
|
|
71
|
+
|
|
72
|
+
export async function conditional(condition, truthy, falsy) {
|
|
73
|
+
return condition ? truthy() : falsy();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function equal(a, b) {
|
|
77
|
+
return a == b;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Look up the given key as an external reference and cache the value for future
|
|
82
|
+
* requests.
|
|
67
83
|
*
|
|
68
84
|
* @this {AsyncTree|null}
|
|
69
85
|
*/
|
|
70
|
-
export async function
|
|
86
|
+
export async function external(key, cache) {
|
|
71
87
|
if (key in cache) {
|
|
72
88
|
return cache[key];
|
|
73
89
|
}
|
|
@@ -80,42 +96,20 @@ export async function cache(key, cache) {
|
|
|
80
96
|
return value;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
/**
|
|
84
|
-
* Concatenate the given arguments.
|
|
85
|
-
*
|
|
86
|
-
* @this {AsyncTree|null}
|
|
87
|
-
* @param {any[]} args
|
|
88
|
-
*/
|
|
89
|
-
export async function concat(...args) {
|
|
90
|
-
return treeConcat.call(this, args);
|
|
91
|
-
}
|
|
92
|
-
addOpLabel(concat, "«ops.concat»");
|
|
93
|
-
|
|
94
99
|
/**
|
|
95
100
|
* This op is only used during parsing. It signals to ops.object that the
|
|
96
101
|
* "arguments" of the expression should be used to define a property getter.
|
|
97
102
|
*/
|
|
98
103
|
export const getter = new String("«ops.getter»");
|
|
99
104
|
|
|
100
|
-
/**
|
|
101
|
-
* Files tree for the filesystem root.
|
|
102
|
-
*
|
|
103
|
-
* @this {AsyncTree|null}
|
|
104
|
-
*/
|
|
105
|
-
export async function filesRoot() {
|
|
106
|
-
let tree = new OrigamiFiles("/");
|
|
107
|
-
tree.parent = root(this);
|
|
108
|
-
return tree;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
105
|
/**
|
|
112
106
|
* Files tree for the user's home directory.
|
|
113
107
|
*
|
|
114
108
|
* @this {AsyncTree|null}
|
|
115
109
|
*/
|
|
116
|
-
export async function
|
|
110
|
+
export async function homeDirectory() {
|
|
117
111
|
const tree = new OrigamiFiles(os.homedir());
|
|
118
|
-
tree.parent = root(this);
|
|
112
|
+
tree.parent = this ? Tree.root(this) : null;
|
|
119
113
|
return tree;
|
|
120
114
|
}
|
|
121
115
|
|
|
@@ -143,31 +137,37 @@ addOpLabel(inherited, "«ops.inherited»");
|
|
|
143
137
|
* @param {string[]} parameters
|
|
144
138
|
* @param {Code} code
|
|
145
139
|
*/
|
|
146
|
-
|
|
147
140
|
export function lambda(parameters, code) {
|
|
148
141
|
const context = this;
|
|
149
142
|
|
|
150
143
|
/** @this {Treelike|null} */
|
|
151
144
|
async function invoke(...args) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
let target;
|
|
146
|
+
if (parameters.length === 0) {
|
|
147
|
+
// No parameters
|
|
148
|
+
target = context;
|
|
149
|
+
} else {
|
|
150
|
+
// Add arguments to scope.
|
|
151
|
+
const ambients = {};
|
|
152
|
+
for (const parameter of parameters) {
|
|
153
|
+
ambients[parameter] = args.shift();
|
|
154
|
+
}
|
|
155
|
+
Object.defineProperty(ambients, codeSymbol, {
|
|
156
|
+
value: code,
|
|
157
|
+
enumerable: false,
|
|
158
|
+
});
|
|
159
|
+
const ambientTree = new ObjectTree(ambients);
|
|
160
|
+
ambientTree.parent = context;
|
|
161
|
+
target = ambientTree;
|
|
156
162
|
}
|
|
157
|
-
Object.defineProperty(ambients, codeSymbol, {
|
|
158
|
-
value: code,
|
|
159
|
-
enumerable: false,
|
|
160
|
-
});
|
|
161
|
-
const ambientTree = new ObjectTree(ambients);
|
|
162
|
-
ambientTree.parent = context;
|
|
163
163
|
|
|
164
|
-
let result = await evaluate.call(
|
|
164
|
+
let result = await evaluate.call(target, code);
|
|
165
165
|
|
|
166
166
|
// Bind a function result to the ambients so that it has access to the
|
|
167
167
|
// parameter values -- i.e., like a closure.
|
|
168
168
|
if (result instanceof Function) {
|
|
169
169
|
const resultCode = result.code;
|
|
170
|
-
result = result.bind(
|
|
170
|
+
result = result.bind(target);
|
|
171
171
|
if (code) {
|
|
172
172
|
// Copy over Origami code
|
|
173
173
|
result.code = resultCode;
|
|
@@ -198,6 +198,53 @@ export async function literal(value) {
|
|
|
198
198
|
}
|
|
199
199
|
addOpLabel(literal, "«ops.literal»");
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Logical AND operator
|
|
203
|
+
*/
|
|
204
|
+
export async function logicalAnd(head, ...tail) {
|
|
205
|
+
if (!head) {
|
|
206
|
+
return head;
|
|
207
|
+
}
|
|
208
|
+
// Evaluate the tail arguments in order, short-circuiting if any are falsy.
|
|
209
|
+
let lastValue;
|
|
210
|
+
for (const arg of tail) {
|
|
211
|
+
lastValue = arg instanceof Function ? await arg() : arg;
|
|
212
|
+
if (!lastValue) {
|
|
213
|
+
return lastValue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Return the last value (not `true`)
|
|
218
|
+
return lastValue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Logical NOT operator
|
|
223
|
+
*/
|
|
224
|
+
export async function logicalNot(value) {
|
|
225
|
+
return !value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Logical OR operator
|
|
230
|
+
*/
|
|
231
|
+
export async function logicalOr(head, ...tail) {
|
|
232
|
+
if (head) {
|
|
233
|
+
return head;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Evaluate the tail arguments in order, short-circuiting if any are truthy.
|
|
237
|
+
let lastValue;
|
|
238
|
+
for (const arg of tail) {
|
|
239
|
+
lastValue = arg instanceof Function ? await arg() : arg;
|
|
240
|
+
if (lastValue) {
|
|
241
|
+
return lastValue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return lastValue;
|
|
246
|
+
}
|
|
247
|
+
|
|
201
248
|
/**
|
|
202
249
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
203
250
|
*
|
|
@@ -222,14 +269,45 @@ export async function object(...entries) {
|
|
|
222
269
|
}
|
|
223
270
|
addOpLabel(object, "«ops.object»");
|
|
224
271
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
272
|
+
export async function notEqual(a, b) {
|
|
273
|
+
return a != b;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function notStrictEqual(a, b) {
|
|
277
|
+
return a !== b;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Nullish coalescing operator
|
|
282
|
+
*/
|
|
283
|
+
export async function nullishCoalescing(head, ...tail) {
|
|
284
|
+
if (head != null) {
|
|
285
|
+
return head;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let lastValue;
|
|
289
|
+
for (const arg of tail) {
|
|
290
|
+
lastValue = arg instanceof Function ? await arg() : arg;
|
|
291
|
+
if (lastValue != null) {
|
|
292
|
+
return lastValue;
|
|
293
|
+
}
|
|
231
294
|
}
|
|
232
|
-
|
|
295
|
+
|
|
296
|
+
return lastValue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Files tree for the filesystem root.
|
|
301
|
+
*
|
|
302
|
+
* @this {AsyncTree|null}
|
|
303
|
+
*/
|
|
304
|
+
export async function rootDirectory(key) {
|
|
305
|
+
let tree = new OrigamiFiles("/");
|
|
306
|
+
// We set the builtins as the parent because logically the filesystem root is
|
|
307
|
+
// outside the project. This ignores the edge case where the project itself is
|
|
308
|
+
// the root of the filesystem and has a config file.
|
|
309
|
+
tree.parent = this ? Tree.root(this) : null;
|
|
310
|
+
return key ? tree.get(key) : tree;
|
|
233
311
|
}
|
|
234
312
|
|
|
235
313
|
/**
|
|
@@ -243,7 +321,7 @@ export async function scope(key) {
|
|
|
243
321
|
}
|
|
244
322
|
const scope = scopeFn(this);
|
|
245
323
|
const value = await scope.get(key);
|
|
246
|
-
if (value === undefined) {
|
|
324
|
+
if (value === undefined && key !== "undefined") {
|
|
247
325
|
throw await scopeReferenceError(scope, key);
|
|
248
326
|
}
|
|
249
327
|
return value;
|
|
@@ -256,11 +334,15 @@ addOpLabel(scope, "«ops.scope»");
|
|
|
256
334
|
*/
|
|
257
335
|
export function spread(...args) {
|
|
258
336
|
throw new Error(
|
|
259
|
-
"
|
|
337
|
+
"Internal error: a spread operation wasn't compiled correctly."
|
|
260
338
|
);
|
|
261
339
|
}
|
|
262
340
|
addOpLabel(spread, "«ops.spread»");
|
|
263
341
|
|
|
342
|
+
export async function strictEqual(a, b) {
|
|
343
|
+
return a === b;
|
|
344
|
+
}
|
|
345
|
+
|
|
264
346
|
/**
|
|
265
347
|
* Apply the default tagged template function.
|
|
266
348
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder defines expression tests in YAML files so that we can programmatically test the evaluation of the expressions in both JavaScript and Origami.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Conditional (ternary) expression tests
|
|
2
|
+
|
|
3
|
+
- source: "true ? 42 : 0"
|
|
4
|
+
expected: 42
|
|
5
|
+
description: "Condition is true, evaluates and returns the first operand"
|
|
6
|
+
|
|
7
|
+
- source: "false ? 42 : 0"
|
|
8
|
+
expected: 0
|
|
9
|
+
description: "Condition is false, evaluates and returns the second operand"
|
|
10
|
+
|
|
11
|
+
- source: "1 ? 'yes' : 'no'"
|
|
12
|
+
expected: "yes"
|
|
13
|
+
description: "Truthy condition with string operands"
|
|
14
|
+
|
|
15
|
+
- source: "0 ? 'yes' : 'no'"
|
|
16
|
+
expected: "no"
|
|
17
|
+
description: "Falsy condition with string operands"
|
|
18
|
+
|
|
19
|
+
- source: "'non-empty' ? 1 : 2"
|
|
20
|
+
expected: 1
|
|
21
|
+
description: "Truthy string condition with numeric operands"
|
|
22
|
+
|
|
23
|
+
- source: "'' ? 1 : 2"
|
|
24
|
+
expected: 2
|
|
25
|
+
description: "Falsy string condition with numeric operands"
|
|
26
|
+
|
|
27
|
+
- source: "null ? 'a' : 'b'"
|
|
28
|
+
expected: "b"
|
|
29
|
+
description: "Falsy null condition"
|
|
30
|
+
|
|
31
|
+
- source: "undefined ? 'a' : 'b'"
|
|
32
|
+
expected: "b"
|
|
33
|
+
description: "Falsy undefined condition"
|
|
34
|
+
|
|
35
|
+
- source: "NaN ? 'a' : 'b'"
|
|
36
|
+
expected: "b"
|
|
37
|
+
description: "Falsy NaN condition"
|
|
38
|
+
|
|
39
|
+
- source: "42 ? true : false"
|
|
40
|
+
expected: true
|
|
41
|
+
description: "Truthy numeric condition with boolean operands"
|
|
42
|
+
|
|
43
|
+
- source: "0 ? true : false"
|
|
44
|
+
expected: false
|
|
45
|
+
description: "Falsy numeric condition with boolean operands"
|
|
46
|
+
|
|
47
|
+
- source: "[] ? 'array' : 'no array'"
|
|
48
|
+
expected: "array"
|
|
49
|
+
description: "Truthy array condition"
|
|
50
|
+
|
|
51
|
+
- source: "{} ? 'object' : 'no object'"
|
|
52
|
+
expected: "object"
|
|
53
|
+
description: "Truthy object condition"
|
|
54
|
+
|
|
55
|
+
- source: "false ? null : undefined"
|
|
56
|
+
expected: __undefined__
|
|
57
|
+
description: "Condition is false, returns undefined"
|
|
58
|
+
|
|
59
|
+
- source: "null ? null : null"
|
|
60
|
+
expected: __null__
|
|
61
|
+
description: "Condition is falsy, returns null"
|
|
62
|
+
|
|
63
|
+
- source: "true ? NaN : 42"
|
|
64
|
+
expected: __NaN__
|
|
65
|
+
description: "Condition is true, evaluates and returns NaN"
|
|
66
|
+
|
|
67
|
+
- source: "(true ? 1 : 2) ? 3 : 4"
|
|
68
|
+
expected: 3
|
|
69
|
+
description: "Nested ternary where first expression evaluates to 1, which is truthy"
|
|
70
|
+
|
|
71
|
+
- source: "(false ? 1 : 2) ? 3 : 4"
|
|
72
|
+
expected: 3
|
|
73
|
+
description: "Nested ternary where first expression evaluates to 2, which is truthy"
|
|
74
|
+
|
|
75
|
+
- source: "(false ? 1 : 0) ? 3 : 4"
|
|
76
|
+
expected: 4
|
|
77
|
+
description: "Nested ternary where first expression evaluates to 0, which is falsy"
|
|
78
|
+
|
|
79
|
+
- source: "true ? (false ? 10 : 20) : 30"
|
|
80
|
+
expected: 20
|
|
81
|
+
description: "Nested ternary in the true branch of outer ternary"
|
|
82
|
+
|
|
83
|
+
- source: "false ? (false ? 10 : 20) : 30"
|
|
84
|
+
expected: 30
|
|
85
|
+
description: "Nested ternary in the false branch of outer ternary"
|
|
86
|
+
|
|
87
|
+
# - source: "'truthy' ? 1 + 2 : 3 + 4"
|
|
88
|
+
# expected: 3
|
|
89
|
+
# description: "Evaluates and returns the true branch with an arithmetic expression"
|
|
90
|
+
|
|
91
|
+
# - source: "'' ? 1 + 2 : 3 + 4"
|
|
92
|
+
# expected: 7
|
|
93
|
+
# description: "Evaluates and returns the false branch with an arithmetic expression"
|
|
94
|
+
|
|
95
|
+
- source: "undefined ? undefined : null"
|
|
96
|
+
expected: __null__
|
|
97
|
+
description: "Condition is falsy, returns null"
|
|
98
|
+
|
|
99
|
+
- source: "null ? undefined : undefined"
|
|
100
|
+
expected: __undefined__
|
|
101
|
+
description: "Condition is falsy, returns undefined"
|