@shapeshift-labs/frontier-lang-parser 0.3.52 → 0.3.54

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,8 @@
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 { readActionRepeatHeader, readRepeatHeaderBlock, validateActionRepeatHeader } from './action-repeat-block.js';
4
6
  import { findActionMatchingBrace, skipActionWhitespaceAndComments } from './action-source-blocks.js';
5
7
  import {
6
8
  readActionMatchCaseHeader,
@@ -33,6 +35,20 @@ function parseActionBodyRecords(source, state) {
33
35
  while (offset < source.length) {
34
36
  offset = skipActionWhitespaceAndComments(source, offset);
35
37
  if (offset >= source.length) break;
38
+ const repeatBlock = readRepeatHeaderBlock(source, offset, source.length);
39
+ if (repeatBlock) {
40
+ const record = parseActionRepeatBlock(repeatBlock.header, repeatBlock.body, state.index++, state);
41
+ if (record) records.push(record);
42
+ offset = repeatBlock.end;
43
+ continue;
44
+ }
45
+ const forBlock = readForInHeaderBlock(source, offset, source.length);
46
+ if (forBlock) {
47
+ const record = parseActionForInBlock(forBlock.header, forBlock.body, state.index++, state);
48
+ if (record) records.push(record);
49
+ offset = forBlock.end;
50
+ continue;
51
+ }
36
52
  const matchBlock = readMatchHeaderBlock(source, offset, source.length);
37
53
  if (matchBlock) {
38
54
  const record = parseActionMatchBlock(matchBlock.header, matchBlock.body, state.index++, state);
@@ -70,6 +86,34 @@ function parseActionBodyRecords(source, state) {
70
86
  return records;
71
87
  }
72
88
 
89
+ function parseActionRepeatBlock(header, body, index, state) {
90
+ const validation = validateActionRepeatHeader(header);
91
+ if (!validation.ok) return undefined;
92
+ const details = readActionRepeatHeader(header, index);
93
+ return compactRecord({
94
+ kind: 'repeat',
95
+ id: details.id,
96
+ name: details.name,
97
+ indexName: details.indexName,
98
+ count: details.count,
99
+ body: parseActionBodyRecords(body, state)
100
+ });
101
+ }
102
+
103
+ function parseActionForInBlock(header, body, index, state) {
104
+ const validation = validateActionForInHeader(header);
105
+ if (!validation.ok) return undefined;
106
+ const details = readActionForInHeader(header, index);
107
+ return compactRecord({
108
+ kind: 'forIn',
109
+ id: details.id,
110
+ name: details.name,
111
+ itemName: details.itemName,
112
+ collection: details.collection,
113
+ body: parseActionBodyRecords(body, state)
114
+ });
115
+ }
116
+
73
117
  function parseActionMatchBlock(header, body, index, state) {
74
118
  const details = readActionMatchHeader(header, index);
75
119
  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
+ }
@@ -0,0 +1,78 @@
1
+ import { parseActionValue, readActionValue } from './action-expression.js';
2
+ import { findActionMatchingBrace } from './action-source-blocks.js';
3
+
4
+ export function readRepeatHeaderBlock(source, offset, endOffset, helpers = {}) {
5
+ const match = /^repeat\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 readActionRepeatHeader(header, index) {
22
+ const rawText = rawHeader(header, 'repeat');
23
+ const hasExplicitId = hasId(rawText);
24
+ const text = stripIds(rawText);
25
+ const shape = /^([A-Za-z_$][\w$-]*)\s+times\s+(.+)$/.exec(text);
26
+ const indexName = shape?.[1];
27
+ const countText = shape?.[2]?.trim();
28
+ const count = countText ? readActionValue(countText) : undefined;
29
+ return cleanRecord({
30
+ id: idFrom(header, `action_body_repeat_${indexName ?? index}`),
31
+ name: indexName ?? `repeat_${index}`,
32
+ indexName,
33
+ count,
34
+ countText,
35
+ hasExplicitId,
36
+ malformed: !shape
37
+ });
38
+ }
39
+
40
+ export function validateActionRepeatHeader(header) {
41
+ const details = readActionRepeatHeader(header, 0);
42
+ const stripped = stripIds(rawHeader(header, 'repeat'));
43
+ if (details.malformed && /^times\b/.test(stripped)) return { ok: false, reason: 'missing-action-repeat-index' };
44
+ if (details.malformed && !/\btimes\b/.test(stripped)) return { ok: false, reason: 'missing-action-repeat-count' };
45
+ if (details.malformed) return { ok: false, reason: 'malformed-action-repeat-header' };
46
+ if (!details.indexName) return { ok: false, reason: 'missing-action-repeat-index' };
47
+ if (!isActionBindingName(details.indexName) || details.indexName === 'times') return { ok: false, reason: 'unsupported-action-repeat-index' };
48
+ if (!details.hasExplicitId) return { ok: false, reason: 'missing-action-repeat-id' };
49
+ if (!details.countText) return { ok: false, reason: 'missing-action-repeat-count' };
50
+ const parsed = parseActionValue(details.countText);
51
+ if (!parsed.ok) return { ok: false, reason: 'unsupported-action-repeat-count' };
52
+ if (!details.count) return { ok: false, reason: 'unsupported-action-repeat-count' };
53
+ if (!isSupportedCountExpression(details.count)) return { ok: false, reason: 'unsupported-action-repeat-count' };
54
+ return { ok: true };
55
+ }
56
+
57
+ function isSupportedCountExpression(value) {
58
+ if (Object.hasOwn(value ?? {}, 'value')) {
59
+ return typeof value.value === 'number' && Number.isFinite(value.value) && value.value >= 0;
60
+ }
61
+ const ast = value?.expressionAst;
62
+ return ast?.kind === 'ref'
63
+ && (ast.scope === 'input' || ast.scope === 'state' || ast.scope === 'local')
64
+ && Array.isArray(ast.path)
65
+ && ast.path.length > 0;
66
+ }
67
+
68
+ function rawHeader(header, keyword) {
69
+ return String(header ?? '').replace(new RegExp('^' + keyword + '\\b'), '').replace(/\{\s*$/, '').trim();
70
+ }
71
+
72
+ function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
73
+ function hasId(header) { return /@id\(\s*["'][^"']+["']\s*\)/.test(header); }
74
+ function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
75
+ function isActionBindingName(value) { return /^[A-Za-z_$][\w$-]*$/.test(String(value ?? '')); }
76
+ function cleanRecord(record) {
77
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
78
+ }
@@ -1,9 +1,11 @@
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 { readRepeatHeaderBlock, validateActionRepeatHeader } from './action-repeat-block.js';
3
5
  import { findActionMatchingBrace, readActionNestedBlocks, skipActionWhitespaceAndComments } from './action-source-blocks.js';
4
6
  import { readMatchBranchBlock, readMatchHeaderBlock, validateActionMatchBranchHeader, validateActionMatchHeader } from './action-match-block.js';
5
7
 
6
- const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match']);
8
+ const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match', 'for', 'repeat']);
7
9
 
8
10
  export function readActionSyntaxChildren(source, block, options) {
9
11
  const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
@@ -24,6 +26,22 @@ function readActionSyntaxRows(source, block, options, state, startOffset, endOff
24
26
  while (offset < endOffset) {
25
27
  offset = skipActionWhitespaceAndComments(source, offset, endOffset);
26
28
  if (offset >= endOffset) break;
29
+ const repeatBlock = readRepeatHeaderBlock(source, offset, endOffset);
30
+ if (repeatBlock) {
31
+ const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, repeatBlock.open + 1).trim(), startOffset: offset, endOffset: repeatBlock.open + 1 }, state, parentActionBodyId);
32
+ children.push(child);
33
+ if (child.recognized) children.push(...readActionSyntaxRows(source, block, options, state, repeatBlock.open + 1, repeatBlock.close, child.id));
34
+ offset = repeatBlock.end;
35
+ continue;
36
+ }
37
+ const forBlock = readForInHeaderBlock(source, offset, endOffset);
38
+ if (forBlock) {
39
+ const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, forBlock.open + 1).trim(), startOffset: offset, endOffset: forBlock.open + 1 }, state, parentActionBodyId);
40
+ children.push(child);
41
+ if (child.recognized) children.push(...readActionSyntaxRows(source, block, options, state, forBlock.open + 1, forBlock.close, child.id));
42
+ offset = forBlock.end;
43
+ continue;
44
+ }
27
45
  const matchBlock = readMatchHeaderBlock(source, offset, endOffset);
28
46
  if (matchBlock) {
29
47
  const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, matchBlock.open + 1).trim(), startOffset: offset, endOffset: matchBlock.open + 1 }, state, parentActionBodyId);
@@ -141,6 +159,8 @@ function validateActionRow(rowKind, rawName, rest, header) {
141
159
  if (!ACTION_BODY_ROWS.has(rowKind)) return { ok: false, reason: 'unsupported-action-body-row' };
142
160
  if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(header), callType: readInlineCallType(header) });
143
161
  if (rowKind === 'match') return validateActionMatchHeader(header);
162
+ if (rowKind === 'for') return validateActionForInHeader(header);
163
+ if (rowKind === 'repeat') return validateActionRepeatHeader(header);
144
164
  if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
145
165
  if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
146
166
  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.52",
3
+ "version": "0.3.54",
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-repeat-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",