@weborigami/language 0.0.69 → 0.0.71-beta.1
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/main.js +2 -0
- package/package.json +3 -3
- package/src/compiler/compile.js +64 -41
- package/src/compiler/origami.pegjs +40 -28
- package/src/compiler/parse.js +286 -193
- package/src/compiler/parserHelpers.js +9 -13
- package/src/runtime/WatchFilesMixin.js +1 -0
- package/src/runtime/evaluate.js +15 -15
- package/src/runtime/extensions.js +40 -14
- package/src/runtime/ops.js +64 -32
- package/src/runtime/symbols.js +3 -0
- package/src/runtime/taggedTemplate.js +9 -0
- package/test/compiler/compile.test.js +47 -0
- package/test/compiler/parse.test.js +166 -195
- package/test/compiler/stripCodeLocations.js +18 -0
- package/test/runtime/ops.test.js +16 -9
- package/test/runtime/taggedTemplate.test.js +10 -0
package/main.js
CHANGED
|
@@ -11,5 +11,7 @@ export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtens
|
|
|
11
11
|
export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
|
|
12
12
|
export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
|
|
13
13
|
export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
|
|
14
|
+
export * as symbols from "./src/runtime/symbols.js";
|
|
15
|
+
export { default as taggedTemplate } from "./src/runtime/taggedTemplate.js";
|
|
14
16
|
export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
|
|
15
17
|
export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.71-beta.1",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"typescript": "5.6.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/async-tree": "0.0.
|
|
15
|
-
"@weborigami/types": "0.0.
|
|
14
|
+
"@weborigami/async-tree": "0.0.71-beta.1",
|
|
15
|
+
"@weborigami/types": "0.0.71-beta.1",
|
|
16
16
|
"watcher": "2.3.1"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
package/src/compiler/compile.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { trailingSlash } from "@weborigami/async-tree";
|
|
1
2
|
import { createExpressionFunction } from "../runtime/expressionFunction.js";
|
|
3
|
+
import { ops } from "../runtime/internal.js";
|
|
2
4
|
import { parse } from "./parse.js";
|
|
3
5
|
|
|
4
6
|
function compile(source, startRule) {
|
|
5
7
|
if (typeof source === "string") {
|
|
6
8
|
source = { text: source };
|
|
7
9
|
}
|
|
8
|
-
|
|
9
|
-
// heuristics are non-local and hard to implement in our parser.
|
|
10
|
-
const preprocessed = trimTemplateWhitespace(source.text);
|
|
11
|
-
const parseResult = parse(preprocessed, {
|
|
10
|
+
const code = parse(source.text, {
|
|
12
11
|
grammarSource: source,
|
|
13
12
|
startRule,
|
|
14
13
|
});
|
|
15
|
-
const
|
|
14
|
+
const cache = {};
|
|
15
|
+
const modified = cacheNonLocalScopeReferences(code, cache);
|
|
16
|
+
// const modified = code;
|
|
17
|
+
const fn = createExpressionFunction(modified);
|
|
16
18
|
return fn;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -20,42 +22,63 @@ export function expression(source) {
|
|
|
20
22
|
return compile(source, "expression");
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// Given code containing ops.scope calls, upgrade them to ops.cache calls unless
|
|
26
|
+
// they refer to local variables: variables defined by object literals or lambda
|
|
27
|
+
// parameters.
|
|
28
|
+
export function cacheNonLocalScopeReferences(code, cache, locals = {}) {
|
|
29
|
+
const [fn, ...args] = code;
|
|
30
|
+
|
|
31
|
+
let additionalLocalNames;
|
|
32
|
+
switch (fn) {
|
|
33
|
+
case ops.scope:
|
|
34
|
+
const key = args[0];
|
|
35
|
+
const normalizedKey = trailingSlash.remove(key);
|
|
36
|
+
if (locals[normalizedKey]) {
|
|
37
|
+
return code;
|
|
38
|
+
} else {
|
|
39
|
+
// Upgrade to cached scope lookup
|
|
40
|
+
const modified = [ops.cache, key, cache];
|
|
41
|
+
/** @type {any} */ (modified).location = code.location;
|
|
42
|
+
return modified;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case ops.lambda:
|
|
46
|
+
const parameters = args[0];
|
|
47
|
+
additionalLocalNames = parameters;
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case ops.object:
|
|
51
|
+
const entries = args;
|
|
52
|
+
additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let updatedLocals = { ...locals };
|
|
57
|
+
if (additionalLocalNames) {
|
|
58
|
+
for (const key of additionalLocalNames) {
|
|
59
|
+
updatedLocals[key] = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const modified = code.map((child) => {
|
|
64
|
+
if (Array.isArray(child)) {
|
|
65
|
+
// Review: This currently descends into arrays that are not instructions,
|
|
66
|
+
// such as the parameters of a lambda. This should be harmless, but it'd
|
|
67
|
+
// be preferable to only descend into instructions. This would require
|
|
68
|
+
// surrounding ops.lambda parameters with ops.literal, and ops.object
|
|
69
|
+
// entries with ops.array.
|
|
70
|
+
return cacheNonLocalScopeReferences(child, cache, updatedLocals);
|
|
71
|
+
} else {
|
|
72
|
+
return child;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (code.location) {
|
|
77
|
+
modified.location = code.location;
|
|
78
|
+
}
|
|
79
|
+
return modified;
|
|
25
80
|
}
|
|
26
81
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// substitution itself contains a multi-line template literal.
|
|
30
|
-
//
|
|
31
|
-
// Example:
|
|
32
|
-
//
|
|
33
|
-
// ${ if `
|
|
34
|
-
// true text
|
|
35
|
-
// `, `
|
|
36
|
-
// false text
|
|
37
|
-
// ` }
|
|
38
|
-
//
|
|
39
|
-
// Case 1: a substitution that starts the text or starts a line (there's only
|
|
40
|
-
// whitespace before the `${`), and has the line end with the start of a
|
|
41
|
-
// template literal (there's only whitespace after the backtick) marks the start
|
|
42
|
-
// of a block.
|
|
43
|
-
//
|
|
44
|
-
// Case 2: a line in the middle that ends one template literal and starts
|
|
45
|
-
// another is an internal break in the block. Edge case: three backticks in a
|
|
46
|
-
// row, like ```, are common in markdown and are not treated as a break.
|
|
47
|
-
//
|
|
48
|
-
// Case 3: a line that ends a template literal and ends with `}` or ends the
|
|
49
|
-
// text marks the end of the block.
|
|
50
|
-
//
|
|
51
|
-
// In all three cases, we trim spaces and tabs from the start and end of the
|
|
52
|
-
// line. In case 1, we also remove the preceding newline.
|
|
53
|
-
function trimTemplateWhitespace(text) {
|
|
54
|
-
const regex1 = /(^|\n)[ \t]*((?:{{|\${).*?`)[ \t]*\n/g;
|
|
55
|
-
const regex2 = /\n[ \t]*(`(?!`).*?`)[ \t]*\n/g;
|
|
56
|
-
const regex3js = /\n[ \t]*(`(?!`).*?(?:}}|[^\\]}))[ \t]*(?:\n|$)/g;
|
|
57
|
-
const trimBlockStarts = text.replace(regex1, "$1$2");
|
|
58
|
-
const trimBlockBreaks = trimBlockStarts.replace(regex2, "\n$1");
|
|
59
|
-
const trimBlockEnds = trimBlockBreaks.replace(regex3js, "\n$1");
|
|
60
|
-
return trimBlockEnds;
|
|
82
|
+
export function templateDocument(source) {
|
|
83
|
+
return compile(source, "templateDocument");
|
|
61
84
|
}
|
|
@@ -103,7 +103,7 @@ doubleArrow = "⇒" / "=>"
|
|
|
103
103
|
|
|
104
104
|
doubleQuoteString "double quote string"
|
|
105
105
|
= '"' chars:doubleQuoteStringChar* '"' {
|
|
106
|
-
return annotate([ops.
|
|
106
|
+
return annotate([ops.literal, chars.join("")], location());
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
doubleQuoteStringChar
|
|
@@ -132,13 +132,21 @@ expression "Origami expression"
|
|
|
132
132
|
|
|
133
133
|
float "floating-point number"
|
|
134
134
|
= sign? digits? "." digits {
|
|
135
|
-
return annotate([ops.
|
|
135
|
+
return annotate([ops.literal, parseFloat(text())], location());
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
|
|
139
139
|
// of function calls, like `fn(arg1)(arg2)(arg3)`.
|
|
140
140
|
functionComposition "function composition"
|
|
141
|
-
|
|
141
|
+
// Function with at least one argument and maybe implicit parens arguments
|
|
142
|
+
= target:callTarget chain:args+ end:implicitParensArgs? {
|
|
143
|
+
if (end) {
|
|
144
|
+
chain.push(end);
|
|
145
|
+
}
|
|
146
|
+
return annotate(makeFunctionCall(target, chain, location()), location());
|
|
147
|
+
}
|
|
148
|
+
// Function with implicit parens arguments after maybe other arguments
|
|
149
|
+
/ target:callTarget chain:args* end:implicitParensArgs {
|
|
142
150
|
if (end) {
|
|
143
151
|
chain.push(end);
|
|
144
152
|
}
|
|
@@ -153,7 +161,7 @@ group "parenthetical group"
|
|
|
153
161
|
|
|
154
162
|
guillemetString "guillemet string"
|
|
155
163
|
= '«' chars:guillemetStringChar* '»' {
|
|
156
|
-
return annotate([ops.
|
|
164
|
+
return annotate([ops.literal, chars.join("")], location());
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
guillemetStringChar
|
|
@@ -166,7 +174,7 @@ host "HTTP/HTTPS host"
|
|
|
166
174
|
= identifier:identifier port:(":" @number)? {
|
|
167
175
|
const portText = port ? `:${port[1]}` : "";
|
|
168
176
|
const hostText = identifier + portText;
|
|
169
|
-
return annotate([ops.
|
|
177
|
+
return annotate([ops.literal, hostText], location());
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
identifier "identifier"
|
|
@@ -195,13 +203,13 @@ inlineSpace
|
|
|
195
203
|
|
|
196
204
|
integer "integer"
|
|
197
205
|
= sign? digits {
|
|
198
|
-
return annotate([ops.
|
|
206
|
+
return annotate([ops.literal, parseInt(text())], location());
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
// A lambda expression: `=foo()`
|
|
202
210
|
lambda "lambda function"
|
|
203
211
|
= "=" __ expr:expr {
|
|
204
|
-
return annotate([ops.lambda,
|
|
212
|
+
return annotate([ops.lambda, ["_"], expr], location());
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
// A path that begins with a slash: `/foo/bar`
|
|
@@ -277,7 +285,7 @@ objectPublicKey
|
|
|
277
285
|
return identifier + (slash ?? "");
|
|
278
286
|
}
|
|
279
287
|
/ string:string {
|
|
280
|
-
// Remove `ops.
|
|
288
|
+
// Remove `ops.literal` from the string code
|
|
281
289
|
return string[1];
|
|
282
290
|
}
|
|
283
291
|
|
|
@@ -309,7 +317,7 @@ path "slash-separated path"
|
|
|
309
317
|
// A path key followed by a slash
|
|
310
318
|
pathElement
|
|
311
319
|
= chars:pathKeyChar* "/" {
|
|
312
|
-
return annotate([ops.
|
|
320
|
+
return annotate([ops.literal, chars.join("") + "/"], location());
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
// A single character in a slash-separated path.
|
|
@@ -322,7 +330,7 @@ pathKeyChar
|
|
|
322
330
|
// A path key without a slash
|
|
323
331
|
pathTail
|
|
324
332
|
= chars:pathKeyChar+ {
|
|
325
|
-
return annotate([ops.
|
|
333
|
+
return annotate([ops.literal, chars.join("")], location());
|
|
326
334
|
}
|
|
327
335
|
|
|
328
336
|
// Parse a protocol call like `fn://foo/bar`.
|
|
@@ -354,6 +362,7 @@ scopeReference "scope reference"
|
|
|
354
362
|
scopeTraverse
|
|
355
363
|
= ref:scopeReference "/" path:path {
|
|
356
364
|
const head = [ops.scope, `${ ref[1] }/`];
|
|
365
|
+
head.location = ref.location;
|
|
357
366
|
return annotate([ops.traverse, head, ...path], location());
|
|
358
367
|
}
|
|
359
368
|
|
|
@@ -374,7 +383,7 @@ singleLineComment
|
|
|
374
383
|
|
|
375
384
|
singleQuoteString "single quote string"
|
|
376
385
|
= "'" chars:singleQuoteStringChar* "'" {
|
|
377
|
-
return annotate([ops.
|
|
386
|
+
return annotate([ops.literal, chars.join("")], location());
|
|
378
387
|
}
|
|
379
388
|
|
|
380
389
|
singleQuoteStringChar
|
|
@@ -402,8 +411,10 @@ step
|
|
|
402
411
|
/ templateLiteral
|
|
403
412
|
/ string
|
|
404
413
|
/ group
|
|
405
|
-
//
|
|
414
|
+
// Things that have a distinctive character, but not at the start
|
|
406
415
|
/ protocolCall
|
|
416
|
+
/ taggedTemplate
|
|
417
|
+
/ scopeTraverse
|
|
407
418
|
// Least distinctive option is a simple scope reference, so it comes last.
|
|
408
419
|
/ scopeReference
|
|
409
420
|
|
|
@@ -415,11 +426,16 @@ string "string"
|
|
|
415
426
|
/ singleQuoteString
|
|
416
427
|
/ guillemetString
|
|
417
428
|
|
|
429
|
+
taggedTemplate
|
|
430
|
+
= tag:callTarget "`" contents:templateLiteralContents "`" {
|
|
431
|
+
return annotate(makeTemplate(tag, contents[0], contents[1]), location());
|
|
432
|
+
}
|
|
433
|
+
|
|
418
434
|
// A top-level document defining a template. This is the same as a template
|
|
419
435
|
// literal, but can contain backticks at the top level.
|
|
420
436
|
templateDocument "template"
|
|
421
437
|
= contents:templateDocumentContents {
|
|
422
|
-
return annotate([ops.lambda,
|
|
438
|
+
return annotate([ops.lambda, ["_"], contents], location());
|
|
423
439
|
}
|
|
424
440
|
|
|
425
441
|
// Template documents can contain backticks at the top level.
|
|
@@ -428,19 +444,19 @@ templateDocumentChar
|
|
|
428
444
|
|
|
429
445
|
// The contents of a template document containing plain text and substitutions
|
|
430
446
|
templateDocumentContents
|
|
431
|
-
=
|
|
432
|
-
return annotate(makeTemplate(
|
|
447
|
+
= head:templateDocumentText tail:(templateSubstitution templateDocumentText)* {
|
|
448
|
+
return annotate(makeTemplate(ops.template, head, tail), location());
|
|
433
449
|
}
|
|
434
450
|
|
|
435
451
|
templateDocumentText "template text"
|
|
436
|
-
= chars:templateDocumentChar
|
|
437
|
-
|
|
438
|
-
|
|
452
|
+
= chars:templateDocumentChar* {
|
|
453
|
+
return chars.join("");
|
|
454
|
+
}
|
|
439
455
|
|
|
440
456
|
// A backtick-quoted template literal
|
|
441
457
|
templateLiteral "template literal"
|
|
442
458
|
= "`" contents:templateLiteralContents "`" {
|
|
443
|
-
return annotate(contents, location());
|
|
459
|
+
return annotate(makeTemplate(ops.template, contents[0], contents[1]), location());
|
|
444
460
|
}
|
|
445
461
|
|
|
446
462
|
templateLiteralChar
|
|
@@ -448,21 +464,17 @@ templateLiteralChar
|
|
|
448
464
|
|
|
449
465
|
// The contents of a template literal containing plain text and substitutions
|
|
450
466
|
templateLiteralContents
|
|
451
|
-
=
|
|
452
|
-
return annotate(makeTemplate(parts), location());
|
|
453
|
-
}
|
|
467
|
+
= head:templateLiteralText tail:(templateSubstitution templateLiteralText)*
|
|
454
468
|
|
|
455
469
|
// Plain text in a template literal
|
|
456
470
|
templateLiteralText
|
|
457
|
-
= chars:templateLiteralChar
|
|
458
|
-
|
|
459
|
-
|
|
471
|
+
= chars:templateLiteralChar* {
|
|
472
|
+
return chars.join("");
|
|
473
|
+
}
|
|
460
474
|
|
|
461
475
|
// A substitution in a template literal: `${x}`
|
|
462
476
|
templateSubstitution "template substitution"
|
|
463
|
-
= "${" __ expr
|
|
464
|
-
return annotate(expr, location());
|
|
465
|
-
}
|
|
477
|
+
= "${" __ @expr __ "}"
|
|
466
478
|
|
|
467
479
|
textChar
|
|
468
480
|
= escapedChar
|