@shapeshift-labs/frontier-lang-parser 0.3.52 → 0.3.53
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/dist/action-body.js +22 -0
- package/dist/action-for-in-block.js +77 -0
- package/dist/action-syntax-children.js +11 -1
- package/package.json +2 -2
package/dist/action-body.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFrontierNestedBlocks } from './source-syntax-report.js';
|
|
2
2
|
import { readActionValue } from './action-expression.js';
|
|
3
3
|
import { readElseHeaderBlock } from './action-else-block.js';
|
|
4
|
+
import { readActionForInHeader, readForInHeaderBlock, validateActionForInHeader } from './action-for-in-block.js';
|
|
4
5
|
import { findActionMatchingBrace, skipActionWhitespaceAndComments } from './action-source-blocks.js';
|
|
5
6
|
import {
|
|
6
7
|
readActionMatchCaseHeader,
|
|
@@ -33,6 +34,13 @@ function parseActionBodyRecords(source, state) {
|
|
|
33
34
|
while (offset < source.length) {
|
|
34
35
|
offset = skipActionWhitespaceAndComments(source, offset);
|
|
35
36
|
if (offset >= source.length) break;
|
|
37
|
+
const forBlock = readForInHeaderBlock(source, offset, source.length);
|
|
38
|
+
if (forBlock) {
|
|
39
|
+
const record = parseActionForInBlock(forBlock.header, forBlock.body, state.index++, state);
|
|
40
|
+
if (record) records.push(record);
|
|
41
|
+
offset = forBlock.end;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
36
44
|
const matchBlock = readMatchHeaderBlock(source, offset, source.length);
|
|
37
45
|
if (matchBlock) {
|
|
38
46
|
const record = parseActionMatchBlock(matchBlock.header, matchBlock.body, state.index++, state);
|
|
@@ -70,6 +78,20 @@ function parseActionBodyRecords(source, state) {
|
|
|
70
78
|
return records;
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
function parseActionForInBlock(header, body, index, state) {
|
|
82
|
+
const validation = validateActionForInHeader(header);
|
|
83
|
+
if (!validation.ok) return undefined;
|
|
84
|
+
const details = readActionForInHeader(header, index);
|
|
85
|
+
return compactRecord({
|
|
86
|
+
kind: 'forIn',
|
|
87
|
+
id: details.id,
|
|
88
|
+
name: details.name,
|
|
89
|
+
itemName: details.itemName,
|
|
90
|
+
collection: details.collection,
|
|
91
|
+
body: parseActionBodyRecords(body, state)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
function parseActionMatchBlock(header, body, index, state) {
|
|
74
96
|
const details = readActionMatchHeader(header, index);
|
|
75
97
|
if (!details.value) return undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseActionValue, readActionValue } from './action-expression.js';
|
|
2
|
+
import { findActionMatchingBrace } from './action-source-blocks.js';
|
|
3
|
+
|
|
4
|
+
export function readForInHeaderBlock(source, offset, endOffset, helpers = {}) {
|
|
5
|
+
const match = /^for\b([^{\n]*)\{/.exec(source.slice(offset, endOffset));
|
|
6
|
+
if (!match) return undefined;
|
|
7
|
+
const open = offset + match[0].length - 1;
|
|
8
|
+
const findBrace = helpers.findMatchingBrace ?? findActionMatchingBrace;
|
|
9
|
+
const close = findBrace(source, open);
|
|
10
|
+
if (close < 0 || close > endOffset) return undefined;
|
|
11
|
+
return {
|
|
12
|
+
start: offset,
|
|
13
|
+
header: match[1].trim(),
|
|
14
|
+
open,
|
|
15
|
+
close,
|
|
16
|
+
body: source.slice(open + 1, close),
|
|
17
|
+
end: close + 1
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readActionForInHeader(header, index) {
|
|
22
|
+
const rawText = rawHeader(header, 'for');
|
|
23
|
+
const hasExplicitId = hasId(rawText);
|
|
24
|
+
const text = stripIds(rawText);
|
|
25
|
+
const shape = /^([A-Za-z_$][\w$-]*)\s+in\s+(.+)$/.exec(text);
|
|
26
|
+
const itemName = shape?.[1];
|
|
27
|
+
const collectionText = shape?.[2]?.trim();
|
|
28
|
+
const collection = collectionText ? readActionValue(collectionText) : undefined;
|
|
29
|
+
return cleanRecord({
|
|
30
|
+
id: idFrom(header, `action_body_for_${itemName ?? index}`),
|
|
31
|
+
name: itemName ?? `for_${index}`,
|
|
32
|
+
itemName,
|
|
33
|
+
collection,
|
|
34
|
+
collectionText,
|
|
35
|
+
hasExplicitId,
|
|
36
|
+
malformed: !shape
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateActionForInHeader(header) {
|
|
41
|
+
const details = readActionForInHeader(header, 0);
|
|
42
|
+
const stripped = stripIds(rawHeader(header, 'for'));
|
|
43
|
+
if (details.malformed && /^in\b/.test(stripped)) return { ok: false, reason: 'missing-action-for-item' };
|
|
44
|
+
if (details.malformed && !/\bin\b/.test(stripped)) return { ok: false, reason: 'missing-action-for-collection' };
|
|
45
|
+
if (details.malformed) return { ok: false, reason: 'malformed-action-for-header' };
|
|
46
|
+
if (!details.itemName) return { ok: false, reason: 'missing-action-for-item' };
|
|
47
|
+
if (!isActionBindingName(details.itemName) || details.itemName === 'in') return { ok: false, reason: 'unsupported-action-for-item' };
|
|
48
|
+
if (!details.hasExplicitId) return { ok: false, reason: 'missing-action-for-id' };
|
|
49
|
+
if (!details.collectionText) return { ok: false, reason: 'missing-action-for-collection' };
|
|
50
|
+
const parsed = parseActionValue(details.collectionText);
|
|
51
|
+
if (!parsed.ok) return { ok: false, reason: 'unsupported-action-for-collection' };
|
|
52
|
+
if (!details.collection) return { ok: false, reason: 'unsupported-action-for-collection' };
|
|
53
|
+
if (!isSupportedCollectionExpression(details.collection)) {
|
|
54
|
+
return { ok: false, reason: 'unsupported-action-for-collection' };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isSupportedCollectionExpression(value) {
|
|
60
|
+
const ast = value?.expressionAst;
|
|
61
|
+
return ast?.kind === 'ref'
|
|
62
|
+
&& (ast.scope === 'input' || ast.scope === 'state' || ast.scope === 'local')
|
|
63
|
+
&& Array.isArray(ast.path)
|
|
64
|
+
&& ast.path.length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rawHeader(header, keyword) {
|
|
68
|
+
return String(header ?? '').replace(new RegExp('^' + keyword + '\\b'), '').replace(/\{\s*$/, '').trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
72
|
+
function hasId(header) { return /@id\(\s*["'][^"']+["']\s*\)/.test(header); }
|
|
73
|
+
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
74
|
+
function isActionBindingName(value) { return /^[A-Za-z_$][\w$-]*$/.test(String(value ?? '')); }
|
|
75
|
+
function cleanRecord(record) {
|
|
76
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
77
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { parseActionValue } from './action-expression.js';
|
|
2
2
|
import { readElseHeaderBlock } from './action-else-block.js';
|
|
3
|
+
import { readForInHeaderBlock, validateActionForInHeader } from './action-for-in-block.js';
|
|
3
4
|
import { findActionMatchingBrace, readActionNestedBlocks, skipActionWhitespaceAndComments } from './action-source-blocks.js';
|
|
4
5
|
import { readMatchBranchBlock, readMatchHeaderBlock, validateActionMatchBranchHeader, validateActionMatchHeader } from './action-match-block.js';
|
|
5
6
|
|
|
6
|
-
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match']);
|
|
7
|
+
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match', 'for']);
|
|
7
8
|
|
|
8
9
|
export function readActionSyntaxChildren(source, block, options) {
|
|
9
10
|
const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
|
|
@@ -24,6 +25,14 @@ function readActionSyntaxRows(source, block, options, state, startOffset, endOff
|
|
|
24
25
|
while (offset < endOffset) {
|
|
25
26
|
offset = skipActionWhitespaceAndComments(source, offset, endOffset);
|
|
26
27
|
if (offset >= endOffset) break;
|
|
28
|
+
const forBlock = readForInHeaderBlock(source, offset, endOffset);
|
|
29
|
+
if (forBlock) {
|
|
30
|
+
const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, forBlock.open + 1).trim(), startOffset: offset, endOffset: forBlock.open + 1 }, state, parentActionBodyId);
|
|
31
|
+
children.push(child);
|
|
32
|
+
if (child.recognized) children.push(...readActionSyntaxRows(source, block, options, state, forBlock.open + 1, forBlock.close, child.id));
|
|
33
|
+
offset = forBlock.end;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
27
36
|
const matchBlock = readMatchHeaderBlock(source, offset, endOffset);
|
|
28
37
|
if (matchBlock) {
|
|
29
38
|
const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, matchBlock.open + 1).trim(), startOffset: offset, endOffset: matchBlock.open + 1 }, state, parentActionBodyId);
|
|
@@ -141,6 +150,7 @@ function validateActionRow(rowKind, rawName, rest, header) {
|
|
|
141
150
|
if (!ACTION_BODY_ROWS.has(rowKind)) return { ok: false, reason: 'unsupported-action-body-row' };
|
|
142
151
|
if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(header), callType: readInlineCallType(header) });
|
|
143
152
|
if (rowKind === 'match') return validateActionMatchHeader(header);
|
|
153
|
+
if (rowKind === 'for') return validateActionForInHeader(header);
|
|
144
154
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
145
155
|
if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
|
|
146
156
|
return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shapeshift-labs/frontier-lang-parser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.53",
|
|
4
4
|
"description": "Parser for the first Frontier Lang .frontier syntax slice.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "node scripts/build.mjs",
|
|
25
|
-
"test": "npm run build && node test/smoke.mjs && node test/action-body-smoke.mjs && node test/action-return-smoke.mjs && node test/action-else-smoke.mjs && node test/action-match-smoke.mjs && node test/action-call-smoke.mjs && node test/target-projection-aggregate-smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/conversion-evidence-smoke.mjs && node test/package-canvas-surface-smoke.mjs && node test/view-render-graph-smoke.mjs && node test/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs && node test/dialect-registry-smoke.mjs",
|
|
25
|
+
"test": "npm run build && node test/smoke.mjs && node test/action-body-smoke.mjs && node test/action-return-smoke.mjs && node test/action-else-smoke.mjs && node test/action-match-smoke.mjs && node test/action-for-in-smoke.mjs && node test/action-call-smoke.mjs && node test/target-projection-aggregate-smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/conversion-evidence-smoke.mjs && node test/package-canvas-surface-smoke.mjs && node test/view-render-graph-smoke.mjs && node test/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs && node test/dialect-registry-smoke.mjs",
|
|
26
26
|
"typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p test/tsconfig.json",
|
|
27
27
|
"fuzz": "npm run build && node fuzz/smoke.mjs",
|
|
28
28
|
"bench": "npm run build && node bench/smoke.mjs",
|