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