@shapeshift-labs/frontier-lang-parser 0.3.50 → 0.3.52
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 +73 -75
- package/dist/action-else-block.js +12 -1
- package/dist/action-match-block.js +128 -0
- package/dist/action-source-blocks.js +87 -0
- package/dist/action-syntax-children.js +44 -102
- package/package.json +2 -2
package/dist/action-body.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
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 { findActionMatchingBrace, skipActionWhitespaceAndComments } from './action-source-blocks.js';
|
|
5
|
+
import {
|
|
6
|
+
readActionMatchCaseHeader,
|
|
7
|
+
readActionMatchDefaultHeader,
|
|
8
|
+
readActionMatchHeader,
|
|
9
|
+
readMatchBranchBlock,
|
|
10
|
+
readMatchBranchBlocks,
|
|
11
|
+
readMatchHeaderBlock,
|
|
12
|
+
validateActionMatchBranchHeader
|
|
13
|
+
} from './action-match-block.js';
|
|
4
14
|
|
|
5
15
|
export function readActionBodyRecords(body) {
|
|
6
16
|
const records = [];
|
|
@@ -21,25 +31,31 @@ function parseActionBodyRecords(source, state) {
|
|
|
21
31
|
const records = [];
|
|
22
32
|
let offset = 0;
|
|
23
33
|
while (offset < source.length) {
|
|
24
|
-
offset =
|
|
34
|
+
offset = skipActionWhitespaceAndComments(source, offset);
|
|
25
35
|
if (offset >= source.length) break;
|
|
36
|
+
const matchBlock = readMatchHeaderBlock(source, offset, source.length);
|
|
37
|
+
if (matchBlock) {
|
|
38
|
+
const record = parseActionMatchBlock(matchBlock.header, matchBlock.body, state.index++, state);
|
|
39
|
+
if (record) records.push(record);
|
|
40
|
+
offset = matchBlock.end;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
26
43
|
const ifHeader = /^if\b([^{\n]*)\{/.exec(source.slice(offset));
|
|
27
44
|
if (ifHeader) {
|
|
28
45
|
const open = offset + ifHeader[0].length - 1;
|
|
29
|
-
const close =
|
|
46
|
+
const close = findActionMatchingBrace(source, open);
|
|
30
47
|
if (close < 0) break;
|
|
31
|
-
const elseBlock = readElseHeaderBlock(source, close + 1, source.length, { skipWhitespace:
|
|
48
|
+
const elseBlock = readElseHeaderBlock(source, close + 1, source.length, { skipWhitespace: skipActionWhitespaceAndComments, findMatchingBrace: findActionMatchingBrace });
|
|
32
49
|
const index = state.index++;
|
|
33
50
|
const record = parseActionIfBlock(ifHeader[1].trim(), source.slice(open + 1, close), index, state, elseBlock);
|
|
34
51
|
if (record) records.push(record);
|
|
35
|
-
offset = elseBlock ? elseBlock.
|
|
52
|
+
offset = elseBlock ? elseBlock.end : close + 1;
|
|
36
53
|
continue;
|
|
37
54
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
offset = close < 0 ? source.length : close + 1;
|
|
55
|
+
const branchBlock = readMatchBranchBlock(source, offset, source.length);
|
|
56
|
+
const elseBlock = readElseHeaderBlock(source, offset, source.length, { skipWhitespace: skipActionWhitespaceAndComments, findMatchingBrace: findActionMatchingBrace });
|
|
57
|
+
if (branchBlock || elseBlock) {
|
|
58
|
+
offset = branchBlock?.end ?? elseBlock.end;
|
|
43
59
|
continue;
|
|
44
60
|
}
|
|
45
61
|
const lineEnd = source.indexOf('\n', offset);
|
|
@@ -54,10 +70,46 @@ function parseActionBodyRecords(source, state) {
|
|
|
54
70
|
return records;
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
function parseActionMatchBlock(header, body, index, state) {
|
|
74
|
+
const details = readActionMatchHeader(header, index);
|
|
75
|
+
if (!details.value) return undefined;
|
|
76
|
+
const branches = readMatchBranchBlocks(body, 0, body.length);
|
|
77
|
+
const cases = [];
|
|
78
|
+
let defaultRecord;
|
|
79
|
+
for (const branch of branches) {
|
|
80
|
+
const branchIndex = state.index++;
|
|
81
|
+
if (branch.kind === 'case') {
|
|
82
|
+
const validation = validateActionMatchBranchHeader('case', branch.header);
|
|
83
|
+
const caseDetails = readActionMatchCaseHeader(branch.header, branchIndex);
|
|
84
|
+
const { valueText: _valueText, ...caseRecord } = caseDetails;
|
|
85
|
+
if (validation.ok) cases.push({ ...caseRecord, body: parseActionBodyRecords(branch.body, state) });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!defaultRecord) {
|
|
89
|
+
const defaultDetails = readActionMatchDefaultHeader(branch.header, branchIndex);
|
|
90
|
+
defaultRecord = { ...defaultDetails, body: parseActionBodyRecords(branch.body, state) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!cases.length) return undefined;
|
|
94
|
+
return compactRecord({
|
|
95
|
+
kind: 'match',
|
|
96
|
+
id: details.id,
|
|
97
|
+
name: details.name,
|
|
98
|
+
valueType: details.valueType,
|
|
99
|
+
comparisonType: details.comparisonType,
|
|
100
|
+
callType: details.callType,
|
|
101
|
+
value: details.value,
|
|
102
|
+
cases,
|
|
103
|
+
defaultId: defaultRecord?.id,
|
|
104
|
+
defaultName: defaultRecord?.name,
|
|
105
|
+
defaultBody: defaultRecord?.body
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
57
109
|
function parseActionIfBlock(header, body, index, state, elseBlock) {
|
|
58
110
|
const details = parseActionIfHeader(header, index);
|
|
59
111
|
if (!details.condition) return undefined;
|
|
60
|
-
const elseDetails =
|
|
112
|
+
const elseDetails = parseActionElseBlock(elseBlock, state, index);
|
|
61
113
|
return compactRecord({
|
|
62
114
|
kind: 'if',
|
|
63
115
|
id: details.id,
|
|
@@ -68,10 +120,20 @@ function parseActionIfBlock(header, body, index, state, elseBlock) {
|
|
|
68
120
|
body: parseActionBodyRecords(body, state),
|
|
69
121
|
elseId: elseDetails?.id,
|
|
70
122
|
elseName: elseDetails?.name,
|
|
71
|
-
elseBody:
|
|
123
|
+
elseBody: elseDetails?.body
|
|
72
124
|
});
|
|
73
125
|
}
|
|
74
126
|
|
|
127
|
+
function parseActionElseBlock(elseBlock, state, index) {
|
|
128
|
+
if (!elseBlock) return undefined;
|
|
129
|
+
if (elseBlock.isElseIf) {
|
|
130
|
+
const nested = parseActionIfBlock(elseBlock.header, elseBlock.body, state.index++, state, elseBlock.tail);
|
|
131
|
+
return nested ? { id: nested.id, name: nested.name, body: [nested] } : undefined;
|
|
132
|
+
}
|
|
133
|
+
const details = parseActionElseHeader(elseBlock.header, index);
|
|
134
|
+
return { ...details, body: parseActionBodyRecords(elseBlock.body, state) };
|
|
135
|
+
}
|
|
136
|
+
|
|
75
137
|
function parseActionIfHeader(header, index) {
|
|
76
138
|
const withoutId = header.replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
77
139
|
const explicitCondition = /\bcondition\s+(.+)$/.exec(withoutId);
|
|
@@ -163,70 +225,6 @@ function readReturnDetails(rawName, rest) {
|
|
|
163
225
|
};
|
|
164
226
|
}
|
|
165
227
|
|
|
166
|
-
function skipWhitespaceAndComments(source, offset) {
|
|
167
|
-
let index = offset;
|
|
168
|
-
while (index < source.length) {
|
|
169
|
-
while (index < source.length && /\s/.test(source[index])) index++;
|
|
170
|
-
if (source[index] !== '#') return index;
|
|
171
|
-
const lineEnd = source.indexOf('\n', index);
|
|
172
|
-
index = lineEnd < 0 ? source.length : lineEnd + 1;
|
|
173
|
-
}
|
|
174
|
-
return index;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function findMatchingBrace(source, open) {
|
|
178
|
-
let depth = 0;
|
|
179
|
-
let state = 'code';
|
|
180
|
-
let quote = '';
|
|
181
|
-
for (let index = open; index < source.length; index++) {
|
|
182
|
-
const char = source[index];
|
|
183
|
-
const next = source[index + 1];
|
|
184
|
-
if (state === 'line-comment') {
|
|
185
|
-
if (char === '\n') state = 'code';
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (state === 'block-comment') {
|
|
189
|
-
if (char === '*' && next === '/') {
|
|
190
|
-
index++;
|
|
191
|
-
state = 'code';
|
|
192
|
-
}
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (state === 'string') {
|
|
196
|
-
if (char === '\\') {
|
|
197
|
-
index++;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (char === quote) {
|
|
201
|
-
state = 'code';
|
|
202
|
-
quote = '';
|
|
203
|
-
}
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (char === '/' && next === '/') {
|
|
207
|
-
index++;
|
|
208
|
-
state = 'line-comment';
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (char === '/' && next === '*') {
|
|
212
|
-
index++;
|
|
213
|
-
state = 'block-comment';
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
217
|
-
state = 'string';
|
|
218
|
-
quote = char;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (char === '{') depth++;
|
|
222
|
-
if (char === '}') {
|
|
223
|
-
depth--;
|
|
224
|
-
if (depth === 0) return index;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return -1;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
228
|
function readInlineActionValue(label, text, options = {}) {
|
|
231
229
|
const value = readInlineValue(label, text);
|
|
232
230
|
return value ? readActionValue(value, options) : undefined;
|
|
@@ -3,16 +3,27 @@ export function readElseHeaderBlock(source, offset, endOffset = source.length, h
|
|
|
3
3
|
const findMatchingBrace = helpers.findMatchingBrace;
|
|
4
4
|
if (typeof skipWhitespace !== 'function' || typeof findMatchingBrace !== 'function') return undefined;
|
|
5
5
|
const elseOffset = skipWhitespace(source, offset, endOffset);
|
|
6
|
+
const elseIfHeader = /^else\s+if\b([^{\n]*)\{/.exec(source.slice(elseOffset, endOffset));
|
|
7
|
+
if (elseIfHeader) return readBlock(source, elseOffset, elseIfHeader, endOffset, findMatchingBrace, true, helpers);
|
|
6
8
|
const elseHeader = /^else\b([^{\n]*)\{/.exec(source.slice(elseOffset, endOffset));
|
|
7
9
|
if (!elseHeader) return undefined;
|
|
10
|
+
return readBlock(source, elseOffset, elseHeader, endOffset, findMatchingBrace, false, helpers);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readBlock(source, elseOffset, elseHeader, endOffset, findMatchingBrace, isElseIf, helpers) {
|
|
8
14
|
const header = elseHeader[1].trim();
|
|
9
15
|
const open = elseOffset + elseHeader[0].length - 1;
|
|
10
16
|
const close = findMatchingBrace(source, open);
|
|
11
17
|
if (close < 0 || close > endOffset) return undefined;
|
|
12
|
-
|
|
18
|
+
const tail = isElseIf ? readElseHeaderBlock(source, close + 1, endOffset, helpers) : undefined;
|
|
19
|
+
return { header, start: elseOffset, open, close, end: tail?.end ?? close + 1, body: source.slice(open + 1, close), isElseIf, tail, supported: isElseIf ? isSupportedElseIfHeader(header) : isSupportedElseHeader(header) };
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
export function isSupportedElseHeader(header) {
|
|
16
23
|
const withoutId = String(header ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
17
24
|
return !withoutId || /^[A-Za-z_$][\w$-]*$/.test(withoutId);
|
|
18
25
|
}
|
|
26
|
+
|
|
27
|
+
function isSupportedElseIfHeader(header) {
|
|
28
|
+
return String(header ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim().length > 0;
|
|
29
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readActionValue } from './action-expression.js';
|
|
2
|
+
import { findActionMatchingBrace, skipActionWhitespaceAndComments } from './action-source-blocks.js';
|
|
3
|
+
|
|
4
|
+
export function readMatchHeaderBlock(source, offset, endOffset, helpers = {}) {
|
|
5
|
+
const match = /^match\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 readMatchBranchBlock(source, offset, endOffset, helpers = {}) {
|
|
22
|
+
const start = (helpers.skipWhitespace ?? skipActionWhitespaceAndComments)(source, offset, endOffset);
|
|
23
|
+
const match = /^(case|default)\b([^{\n]*)\{/.exec(source.slice(start, endOffset));
|
|
24
|
+
if (!match) return undefined;
|
|
25
|
+
const open = start + match[0].length - 1;
|
|
26
|
+
const close = (helpers.findMatchingBrace ?? findActionMatchingBrace)(source, open);
|
|
27
|
+
if (close < 0 || close > endOffset) return undefined;
|
|
28
|
+
return {
|
|
29
|
+
kind: match[1],
|
|
30
|
+
start,
|
|
31
|
+
header: match[2].trim(),
|
|
32
|
+
open,
|
|
33
|
+
close,
|
|
34
|
+
body: source.slice(open + 1, close),
|
|
35
|
+
end: close + 1
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readMatchBranchBlocks(source, startOffset, endOffset, helpers = {}) {
|
|
40
|
+
const branches = [];
|
|
41
|
+
let offset = startOffset;
|
|
42
|
+
while (offset < endOffset) {
|
|
43
|
+
const branch = readMatchBranchBlock(source, offset, endOffset, helpers);
|
|
44
|
+
if (branch) {
|
|
45
|
+
branches.push(branch);
|
|
46
|
+
offset = branch.end;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const lineEnd = source.indexOf('\n', offset);
|
|
50
|
+
offset = lineEnd < 0 || lineEnd > endOffset ? endOffset : lineEnd + 1;
|
|
51
|
+
}
|
|
52
|
+
return branches;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readActionMatchHeader(header, index) {
|
|
56
|
+
const text = cleanHeader(header, 'match');
|
|
57
|
+
const valueText = readInlineValue('value', text);
|
|
58
|
+
const valueType = readInlineType(text);
|
|
59
|
+
const comparisonType = readInlineComparisonType(text);
|
|
60
|
+
const callType = readInlineCallType(text);
|
|
61
|
+
const value = valueText ? readActionValue(valueText, { valueType, comparisonType, callType }) : undefined;
|
|
62
|
+
return cleanRecord({
|
|
63
|
+
id: idFrom(header, `action_body_match_${firstIdentifier(beforeLabel(text, 'value')) ?? index}`),
|
|
64
|
+
name: firstIdentifier(beforeLabel(text, 'value')) ?? `match_${index}`,
|
|
65
|
+
valueType,
|
|
66
|
+
comparisonType,
|
|
67
|
+
callType,
|
|
68
|
+
value,
|
|
69
|
+
valueText
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readActionMatchCaseHeader(header, index) {
|
|
74
|
+
const text = cleanHeader(header, 'case');
|
|
75
|
+
const valueText = readInlineValue('value', text);
|
|
76
|
+
const value = valueText ? readActionValue(valueText) : undefined;
|
|
77
|
+
return cleanRecord({
|
|
78
|
+
id: idFrom(header, `action_body_case_${firstIdentifier(beforeLabel(text, 'value')) ?? index}`),
|
|
79
|
+
name: firstIdentifier(beforeLabel(text, 'value')) ?? `case_${index}`,
|
|
80
|
+
value,
|
|
81
|
+
valueText
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readActionMatchDefaultHeader(header, index) {
|
|
86
|
+
const text = cleanHeader(header, 'default');
|
|
87
|
+
return {
|
|
88
|
+
id: idFrom(header, `action_body_default_${firstIdentifier(text) ?? index}`),
|
|
89
|
+
name: firstIdentifier(text) ?? `default_${index}`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateActionMatchHeader(header) {
|
|
94
|
+
const details = readActionMatchHeader(header, 0);
|
|
95
|
+
return details.value ? { ok: true } : { ok: false, reason: 'missing-action-match-value' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function validateActionMatchBranchHeader(rowKind, header) {
|
|
99
|
+
if (rowKind === 'default') return { ok: true };
|
|
100
|
+
if (rowKind !== 'case') return { ok: false, reason: 'unsupported-action-match-branch' };
|
|
101
|
+
const details = readActionMatchCaseHeader(header, 0);
|
|
102
|
+
if (!details.value) return { ok: false, reason: 'missing-action-match-case-value' };
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(details.value, 'value')) {
|
|
104
|
+
return { ok: false, reason: 'unsupported-action-match-case-value' };
|
|
105
|
+
}
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cleanHeader(header, keyword) {
|
|
110
|
+
return stripIds(String(header ?? '').replace(new RegExp('^' + keyword + '\\b'), '').replace(/\{\s*$/, '')).trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function beforeLabel(text, label) {
|
|
114
|
+
const index = text.search(new RegExp('(?:^|\\s)' + label + '\\s+'));
|
|
115
|
+
return index < 0 ? text : text.slice(0, index).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
119
|
+
function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
|
|
120
|
+
function readInlineComparisonType(text) { return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text); }
|
|
121
|
+
function readInlineCallType(text) { return readInlineWord('call', text) ?? readInlineWord('callType', text); }
|
|
122
|
+
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
123
|
+
function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
|
|
124
|
+
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
125
|
+
function firstIdentifier(text) { return /^([A-Za-z_$][\w$-]*)\b/.exec(text)?.[1]; }
|
|
126
|
+
function cleanRecord(record) {
|
|
127
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
128
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function readActionNestedBlocks(kind, source) {
|
|
2
|
+
const blocks = [];
|
|
3
|
+
const header = new RegExp('\\b' + escapeRegExp(kind) + '(?:\\s+([^{}\\n]+?))?\\s*\\{', 'g');
|
|
4
|
+
let match;
|
|
5
|
+
while ((match = header.exec(source))) {
|
|
6
|
+
const open = header.lastIndex - 1;
|
|
7
|
+
const close = findActionMatchingBrace(source, open);
|
|
8
|
+
if (close < 0) continue;
|
|
9
|
+
blocks.push({
|
|
10
|
+
header: (match[1] ?? '').trim(),
|
|
11
|
+
start: match.index,
|
|
12
|
+
bodyStart: open + 1,
|
|
13
|
+
bodyEnd: close,
|
|
14
|
+
end: close + 1
|
|
15
|
+
});
|
|
16
|
+
header.lastIndex = close + 1;
|
|
17
|
+
}
|
|
18
|
+
return blocks;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function skipActionWhitespaceAndComments(source, offset, endOffset = source.length) {
|
|
22
|
+
let index = offset;
|
|
23
|
+
while (index < endOffset) {
|
|
24
|
+
while (index < endOffset && /\s/.test(source[index])) index++;
|
|
25
|
+
if (source[index] !== '#') return index;
|
|
26
|
+
const lineEnd = source.indexOf('\n', index);
|
|
27
|
+
index = lineEnd < 0 || lineEnd > endOffset ? endOffset : lineEnd + 1;
|
|
28
|
+
}
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findActionMatchingBrace(source, open) {
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let state = 'code';
|
|
35
|
+
let quote = '';
|
|
36
|
+
for (let index = open; index < source.length; index++) {
|
|
37
|
+
const char = source[index];
|
|
38
|
+
const next = source[index + 1];
|
|
39
|
+
if (state === 'line-comment') {
|
|
40
|
+
if (char === '\n') state = 'code';
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (state === 'block-comment') {
|
|
44
|
+
if (char === '*' && next === '/') {
|
|
45
|
+
index++;
|
|
46
|
+
state = 'code';
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (state === 'string') {
|
|
51
|
+
if (char === '\\') {
|
|
52
|
+
index++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (char === quote) {
|
|
56
|
+
state = 'code';
|
|
57
|
+
quote = '';
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (char === '/' && next === '/') {
|
|
62
|
+
index++;
|
|
63
|
+
state = 'line-comment';
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (char === '/' && next === '*') {
|
|
67
|
+
index++;
|
|
68
|
+
state = 'block-comment';
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
72
|
+
state = 'string';
|
|
73
|
+
quote = char;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === '{') depth++;
|
|
77
|
+
if (char === '}') {
|
|
78
|
+
depth--;
|
|
79
|
+
if (depth === 0) return index;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function escapeRegExp(value) {
|
|
86
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { parseActionValue } from './action-expression.js';
|
|
2
2
|
import { readElseHeaderBlock } from './action-else-block.js';
|
|
3
|
+
import { findActionMatchingBrace, readActionNestedBlocks, skipActionWhitespaceAndComments } from './action-source-blocks.js';
|
|
4
|
+
import { readMatchBranchBlock, readMatchHeaderBlock, validateActionMatchBranchHeader, validateActionMatchHeader } from './action-match-block.js';
|
|
3
5
|
|
|
4
|
-
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let']);
|
|
6
|
+
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match']);
|
|
5
7
|
|
|
6
8
|
export function readActionSyntaxChildren(source, block, options) {
|
|
7
9
|
const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
|
|
8
|
-
const bodyBlocks =
|
|
9
|
-
const state = { rowIndex: 0 };
|
|
10
|
+
const bodyBlocks = readActionNestedBlocks('body', body);
|
|
11
|
+
const state = { rowIndex: 0, rowKinds: new Map() };
|
|
10
12
|
const children = [];
|
|
11
13
|
for (const bodyBlock of bodyBlocks) {
|
|
12
14
|
const bodyStartOffset = block.bodyStartOffset + bodyBlock.bodyStart;
|
|
@@ -20,12 +22,20 @@ function readActionSyntaxRows(source, block, options, state, startOffset, endOff
|
|
|
20
22
|
const children = [];
|
|
21
23
|
let offset = startOffset;
|
|
22
24
|
while (offset < endOffset) {
|
|
23
|
-
offset =
|
|
25
|
+
offset = skipActionWhitespaceAndComments(source, offset, endOffset);
|
|
24
26
|
if (offset >= endOffset) break;
|
|
27
|
+
const matchBlock = readMatchHeaderBlock(source, offset, endOffset);
|
|
28
|
+
if (matchBlock) {
|
|
29
|
+
const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, matchBlock.open + 1).trim(), startOffset: offset, endOffset: matchBlock.open + 1 }, state, parentActionBodyId);
|
|
30
|
+
children.push(child);
|
|
31
|
+
if (child.recognized) children.push(...readActionSyntaxRows(source, block, options, state, matchBlock.open + 1, matchBlock.close, child.id));
|
|
32
|
+
offset = matchBlock.end;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
25
35
|
const ifHeader = /^if\b([^{\n]*)\{/.exec(source.slice(offset, endOffset));
|
|
26
36
|
if (ifHeader) {
|
|
27
37
|
const open = offset + ifHeader[0].length - 1;
|
|
28
|
-
const close =
|
|
38
|
+
const close = findActionMatchingBrace(source, open);
|
|
29
39
|
if (close < 0 || close > endOffset) break;
|
|
30
40
|
const child = actionSyntaxChild(source, block, options, {
|
|
31
41
|
text: source.slice(offset, open + 1).trim(),
|
|
@@ -34,29 +44,42 @@ function readActionSyntaxRows(source, block, options, state, startOffset, endOff
|
|
|
34
44
|
}, state, parentActionBodyId);
|
|
35
45
|
children.push(child);
|
|
36
46
|
children.push(...readActionSyntaxRows(source, block, options, state, open + 1, close, child.id));
|
|
37
|
-
const elseBlock = readElseHeaderBlock(source, close + 1, endOffset, { skipWhitespace:
|
|
47
|
+
const elseBlock = readElseHeaderBlock(source, close + 1, endOffset, { skipWhitespace: skipActionWhitespaceAndComments, findMatchingBrace: findActionMatchingBrace });
|
|
38
48
|
if (elseBlock) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
startOffset:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
let branch = elseBlock, parentId = child.id;
|
|
50
|
+
while (branch) {
|
|
51
|
+
const elseChild = actionSyntaxChild(source, block, options, { text: source.slice(branch.start, branch.open + 1).trim(), startOffset: branch.start, endOffset: branch.open + 1 }, state, parentId, { recognized: branch.supported });
|
|
52
|
+
children.push(elseChild);
|
|
53
|
+
if (branch.supported) children.push(...readActionSyntaxRows(source, block, options, state, branch.open + 1, branch.close, elseChild.id));
|
|
54
|
+
parentId = elseChild.id;
|
|
55
|
+
branch = branch.tail;
|
|
56
|
+
}
|
|
57
|
+
offset = elseBlock.end;
|
|
47
58
|
continue;
|
|
48
59
|
}
|
|
49
60
|
offset = close + 1;
|
|
50
61
|
continue;
|
|
51
62
|
}
|
|
52
|
-
const
|
|
63
|
+
const branchBlock = readMatchBranchBlock(source, offset, endOffset);
|
|
64
|
+
if (branchBlock) {
|
|
65
|
+
const parentKind = state.rowKinds.get(parentActionBodyId);
|
|
66
|
+
const validation = parentKind === 'match'
|
|
67
|
+
? validateActionMatchBranchHeader(branchBlock.kind, source.slice(branchBlock.start, branchBlock.open + 1).trim())
|
|
68
|
+
: { ok: false, reason: 'unsupported-action-body-row' };
|
|
69
|
+
const child = actionSyntaxChild(source, block, options, { text: source.slice(branchBlock.start, branchBlock.open + 1).trim(), startOffset: branchBlock.start, endOffset: branchBlock.open + 1 }, state, parentActionBodyId, { recognized: validation.ok, reason: validation.reason });
|
|
70
|
+
children.push(child);
|
|
71
|
+
if (child.recognized) children.push(...readActionSyntaxRows(source, block, options, state, branchBlock.open + 1, branchBlock.close, child.id));
|
|
72
|
+
offset = branchBlock.end;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const elseBlock = readElseHeaderBlock(source, offset, endOffset, { skipWhitespace: skipActionWhitespaceAndComments, findMatchingBrace: findActionMatchingBrace });
|
|
53
76
|
if (elseBlock) {
|
|
54
77
|
children.push(actionSyntaxChild(source, block, options, {
|
|
55
78
|
text: source.slice(elseBlock.start, elseBlock.open + 1).trim(),
|
|
56
79
|
startOffset: elseBlock.start,
|
|
57
80
|
endOffset: elseBlock.open + 1
|
|
58
81
|
}, state, parentActionBodyId));
|
|
59
|
-
offset = elseBlock.
|
|
82
|
+
offset = elseBlock.end;
|
|
60
83
|
continue;
|
|
61
84
|
}
|
|
62
85
|
const lineEnd = source.indexOf('\n', offset);
|
|
@@ -83,7 +106,7 @@ function actionSyntaxChild(source, block, options, line, state, parentActionBody
|
|
|
83
106
|
const rest = row?.[2]?.startsWith('@') ? ` ${row[2]}${row[3] ?? ''}` : (row?.[3] ?? '');
|
|
84
107
|
const validation = validateActionRow(rowKind, row?.[2], rest, line.text);
|
|
85
108
|
const recognized = overrides.recognized ?? (ACTION_BODY_ROWS.has(rowKind) && validation.ok);
|
|
86
|
-
|
|
109
|
+
const child = cleanRecord({
|
|
87
110
|
kind: recognized ? 'actionBodyRow' : 'actionUnknownRow',
|
|
88
111
|
rowKind,
|
|
89
112
|
normalizedRowKind: recognized ? rowKind : 'unknown',
|
|
@@ -101,8 +124,10 @@ function actionSyntaxChild(source, block, options, line, state, parentActionBody
|
|
|
101
124
|
parentActionBodyId,
|
|
102
125
|
sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
|
|
103
126
|
recognized,
|
|
104
|
-
reason: recognized ? undefined : validation.reason
|
|
127
|
+
reason: recognized ? undefined : overrides.reason ?? validation.reason
|
|
105
128
|
});
|
|
129
|
+
state.rowKinds?.set(child.id, child.rowKind);
|
|
130
|
+
return child;
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
function actionRowName(rowKind, rawName, rowIndex) {
|
|
@@ -115,6 +140,7 @@ function actionRowName(rowKind, rawName, rowIndex) {
|
|
|
115
140
|
function validateActionRow(rowKind, rawName, rest, header) {
|
|
116
141
|
if (!ACTION_BODY_ROWS.has(rowKind)) return { ok: false, reason: 'unsupported-action-body-row' };
|
|
117
142
|
if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(header), callType: readInlineCallType(header) });
|
|
143
|
+
if (rowKind === 'match') return validateActionMatchHeader(header);
|
|
118
144
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
119
145
|
if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
|
|
120
146
|
return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) });
|
|
@@ -209,90 +235,6 @@ function isActionExpressionAdmissionReason(reason) {
|
|
|
209
235
|
|| reason === 'missing-action-expression';
|
|
210
236
|
}
|
|
211
237
|
|
|
212
|
-
function readNestedBodyBlocks(kind, source) {
|
|
213
|
-
const blocks = [];
|
|
214
|
-
const header = new RegExp('\\b' + kind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:\\s+([^{}\\n]+?))?\\s*\\{', 'g');
|
|
215
|
-
let match;
|
|
216
|
-
while ((match = header.exec(source))) {
|
|
217
|
-
const open = header.lastIndex - 1;
|
|
218
|
-
const close = findMatchingBrace(source, open);
|
|
219
|
-
if (close < 0) continue;
|
|
220
|
-
blocks.push({
|
|
221
|
-
header: (match[1] ?? '').trim(),
|
|
222
|
-
start: match.index,
|
|
223
|
-
bodyStart: open + 1,
|
|
224
|
-
bodyEnd: close,
|
|
225
|
-
end: close + 1
|
|
226
|
-
});
|
|
227
|
-
header.lastIndex = close + 1;
|
|
228
|
-
}
|
|
229
|
-
return blocks;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function skipWhitespaceAndLineComments(source, offset, endOffset) {
|
|
233
|
-
let index = offset;
|
|
234
|
-
while (index < endOffset) {
|
|
235
|
-
while (index < endOffset && /\s/.test(source[index])) index++;
|
|
236
|
-
if (source[index] !== '#') return index;
|
|
237
|
-
const lineEnd = source.indexOf('\n', index);
|
|
238
|
-
index = lineEnd < 0 || lineEnd > endOffset ? endOffset : lineEnd + 1;
|
|
239
|
-
}
|
|
240
|
-
return index;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function findMatchingBrace(source, open) {
|
|
244
|
-
let depth = 0;
|
|
245
|
-
let state = 'code';
|
|
246
|
-
let quote = '';
|
|
247
|
-
for (let index = open; index < source.length; index++) {
|
|
248
|
-
const char = source[index];
|
|
249
|
-
const next = source[index + 1];
|
|
250
|
-
if (state === 'line-comment') {
|
|
251
|
-
if (char === '\n') state = 'code';
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (state === 'block-comment') {
|
|
255
|
-
if (char === '*' && next === '/') {
|
|
256
|
-
index++;
|
|
257
|
-
state = 'code';
|
|
258
|
-
}
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (state === 'string') {
|
|
262
|
-
if (char === '\\') {
|
|
263
|
-
index++;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (char === quote) {
|
|
267
|
-
state = 'code';
|
|
268
|
-
quote = '';
|
|
269
|
-
}
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (char === '/' && next === '/') {
|
|
273
|
-
index++;
|
|
274
|
-
state = 'line-comment';
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
if (char === '/' && next === '*') {
|
|
278
|
-
index++;
|
|
279
|
-
state = 'block-comment';
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
283
|
-
state = 'string';
|
|
284
|
-
quote = char;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (char === '{') depth++;
|
|
288
|
-
if (char === '}') {
|
|
289
|
-
depth--;
|
|
290
|
-
if (depth === 0) return index;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return -1;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
238
|
function sourcePosition(source, offset) {
|
|
297
239
|
const lines = source.slice(0, offset).split('\n');
|
|
298
240
|
return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
|
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.52",
|
|
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-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-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",
|