@shapeshift-labs/frontier-lang-parser 0.3.53 → 0.3.55

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.
@@ -2,6 +2,7 @@ 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
4
  import { readActionForInHeader, readForInHeaderBlock, validateActionForInHeader } from './action-for-in-block.js';
5
+ import { readActionRepeatHeader, readRepeatHeaderBlock, validateActionRepeatHeader } from './action-repeat-block.js';
5
6
  import { findActionMatchingBrace, skipActionWhitespaceAndComments } from './action-source-blocks.js';
6
7
  import {
7
8
  readActionMatchCaseHeader,
@@ -34,6 +35,13 @@ function parseActionBodyRecords(source, state) {
34
35
  while (offset < source.length) {
35
36
  offset = skipActionWhitespaceAndComments(source, offset);
36
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
+ }
37
45
  const forBlock = readForInHeaderBlock(source, offset, source.length);
38
46
  if (forBlock) {
39
47
  const record = parseActionForInBlock(forBlock.header, forBlock.body, state.index++, state);
@@ -78,6 +86,21 @@ function parseActionBodyRecords(source, state) {
78
86
  return records;
79
87
  }
80
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
+ repeatKind: 'times',
96
+ id: details.id,
97
+ name: details.name,
98
+ indexName: details.indexName,
99
+ count: details.count,
100
+ body: parseActionBodyRecords(body, state)
101
+ });
102
+ }
103
+
81
104
  function parseActionForInBlock(header, body, index, state) {
82
105
  const validation = validateActionForInHeader(header);
83
106
  if (!validation.ok) return undefined;
@@ -0,0 +1,81 @@
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 shape = /^([A-Za-z_$][\w$-]*)\s+@id\(\s*["'][^"']+["']\s*\)\s+times\s+(.+)$/.exec(rawText);
25
+ const indexName = shape?.[1];
26
+ const countText = shape?.[2]?.trim();
27
+ const count = countText ? readActionValue(countText) : undefined;
28
+ return cleanRecord({
29
+ id: idFrom(header, `action_body_repeat_${indexName ?? index}`),
30
+ name: indexName ?? `repeat_${index}`,
31
+ indexName,
32
+ count,
33
+ countText,
34
+ hasExplicitId,
35
+ malformed: !shape
36
+ });
37
+ }
38
+
39
+ export function validateActionRepeatHeader(header) {
40
+ const details = readActionRepeatHeader(header, 0);
41
+ const stripped = stripIds(rawHeader(header, 'repeat'));
42
+ if (details.malformed && /^times\b/.test(stripped)) return { ok: false, reason: 'missing-action-repeat-index' };
43
+ if (details.malformed && /\btimes\s*$/.test(stripped)) return { ok: false, reason: 'missing-action-repeat-count' };
44
+ if (details.malformed && !/\btimes\b/.test(stripped)) return { ok: false, reason: 'missing-action-repeat-count' };
45
+ if (details.malformed && !details.hasExplicitId && /^[A-Za-z_$][\w$-]*\s+times\s+.+$/.test(stripped)) {
46
+ return { ok: false, reason: 'missing-action-repeat-id' };
47
+ }
48
+ if (details.malformed) return { ok: false, reason: 'malformed-action-repeat-header' };
49
+ if (!details.indexName) return { ok: false, reason: 'missing-action-repeat-index' };
50
+ if (!isActionBindingName(details.indexName) || details.indexName === 'times') return { ok: false, reason: 'unsupported-action-repeat-index' };
51
+ if (!details.hasExplicitId) return { ok: false, reason: 'missing-action-repeat-id' };
52
+ if (!details.countText) return { ok: false, reason: 'missing-action-repeat-count' };
53
+ const parsed = parseActionValue(details.countText);
54
+ if (!parsed.ok) return { ok: false, reason: 'unsupported-action-repeat-count' };
55
+ if (!details.count) return { ok: false, reason: 'unsupported-action-repeat-count' };
56
+ if (!isSupportedCountExpression(details.count)) return { ok: false, reason: 'unsupported-action-repeat-count' };
57
+ return { ok: true };
58
+ }
59
+
60
+ function isSupportedCountExpression(value) {
61
+ if (Object.hasOwn(value ?? {}, 'value')) {
62
+ return typeof value.value === 'number' && Number.isInteger(value.value) && value.value >= 0;
63
+ }
64
+ const ast = value?.expressionAst;
65
+ return ast?.kind === 'ref'
66
+ && (ast.scope === 'input' || ast.scope === 'state' || ast.scope === 'local')
67
+ && Array.isArray(ast.path)
68
+ && ast.path.length > 0;
69
+ }
70
+
71
+ function rawHeader(header, keyword) {
72
+ return String(header ?? '').replace(new RegExp('^' + keyword + '\\b'), '').replace(/\{\s*$/, '').trim();
73
+ }
74
+
75
+ function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
76
+ function hasId(header) { return /@id\(\s*["'][^"']+["']\s*\)/.test(header); }
77
+ function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
78
+ function isActionBindingName(value) { return /^[A-Za-z_$][\w$-]*$/.test(String(value ?? '')); }
79
+ function cleanRecord(record) {
80
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
81
+ }
@@ -1,10 +1,11 @@
1
1
  import { parseActionValue } from './action-expression.js';
2
2
  import { readElseHeaderBlock } from './action-else-block.js';
3
3
  import { readForInHeaderBlock, validateActionForInHeader } from './action-for-in-block.js';
4
+ import { readRepeatHeaderBlock, validateActionRepeatHeader } from './action-repeat-block.js';
4
5
  import { findActionMatchingBrace, readActionNestedBlocks, skipActionWhitespaceAndComments } from './action-source-blocks.js';
5
6
  import { readMatchBranchBlock, readMatchHeaderBlock, validateActionMatchBranchHeader, validateActionMatchHeader } from './action-match-block.js';
6
7
 
7
- const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match', 'for']);
8
+ const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let', 'match', 'for', 'repeat']);
8
9
 
9
10
  export function readActionSyntaxChildren(source, block, options) {
10
11
  const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
@@ -25,6 +26,14 @@ function readActionSyntaxRows(source, block, options, state, startOffset, endOff
25
26
  while (offset < endOffset) {
26
27
  offset = skipActionWhitespaceAndComments(source, offset, endOffset);
27
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
+ }
28
37
  const forBlock = readForInHeaderBlock(source, offset, endOffset);
29
38
  if (forBlock) {
30
39
  const child = actionSyntaxChild(source, block, options, { text: source.slice(offset, forBlock.open + 1).trim(), startOffset: offset, endOffset: forBlock.open + 1 }, state, parentActionBodyId);
@@ -151,6 +160,7 @@ function validateActionRow(rowKind, rawName, rest, header) {
151
160
  if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(header), callType: readInlineCallType(header) });
152
161
  if (rowKind === 'match') return validateActionMatchHeader(header);
153
162
  if (rowKind === 'for') return validateActionForInHeader(header);
163
+ if (rowKind === 'repeat') return validateActionRepeatHeader(header);
154
164
  if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
155
165
  if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
156
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.53",
3
+ "version": "0.3.55",
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-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",
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",