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

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.
@@ -92,6 +92,7 @@ function parseActionRepeatBlock(header, body, index, state) {
92
92
  const details = readActionRepeatHeader(header, index);
93
93
  return compactRecord({
94
94
  kind: 'repeat',
95
+ repeatKind: 'times',
95
96
  id: details.id,
96
97
  name: details.name,
97
98
  indexName: details.indexName,
@@ -9,6 +9,8 @@ export function hasNumericOperator(node) {
9
9
  if (node.kind === 'binary') return NUMERIC_OPERATORS.has(node.op) || hasNumericOperator(node.left) || hasNumericOperator(node.right);
10
10
  if (node.kind === 'logical') return hasNumericOperator(node.left) || hasNumericOperator(node.right);
11
11
  if (node.kind === 'unary') return hasNumericOperator(node.argument);
12
+ if (node.kind === 'array') return (node.elements ?? []).some(hasNumericOperator);
13
+ if (node.kind === 'object') return (node.entries ?? []).some((entry) => hasNumericOperator(entry.value));
12
14
  return false;
13
15
  }
14
16
 
@@ -20,6 +22,8 @@ export function hasNonLiteralOrderedComparison(node) {
20
22
  }
21
23
  if (node.kind === 'logical') return hasNonLiteralOrderedComparison(node.left) || hasNonLiteralOrderedComparison(node.right);
22
24
  if (node.kind === 'unary') return hasNonLiteralOrderedComparison(node.argument);
25
+ if (node.kind === 'array') return (node.elements ?? []).some(hasNonLiteralOrderedComparison);
26
+ if (node.kind === 'object') return (node.entries ?? []).some((entry) => hasNonLiteralOrderedComparison(entry.value));
23
27
  return false;
24
28
  }
25
29
 
@@ -28,6 +32,8 @@ export function hasCallExpression(node) {
28
32
  if (node.kind === 'call') return true;
29
33
  if (node.kind === 'binary' || node.kind === 'logical') return hasCallExpression(node.left) || hasCallExpression(node.right);
30
34
  if (node.kind === 'unary') return hasCallExpression(node.argument);
35
+ if (node.kind === 'array') return (node.elements ?? []).some(hasCallExpression);
36
+ if (node.kind === 'object') return (node.entries ?? []).some((entry) => hasCallExpression(entry.value));
31
37
  return false;
32
38
  }
33
39
 
@@ -42,12 +42,12 @@ export function tokenizeActionExpression(text) {
42
42
  index++;
43
43
  continue;
44
44
  }
45
- if (char === '(' || char === ')' || char === '.' || char === ',') {
45
+ if (char === '(' || char === ')' || char === '.' || char === ',' || char === '[' || char === ']' || char === '{' || char === '}' || char === ':') {
46
46
  tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
47
47
  index++;
48
48
  continue;
49
49
  }
50
- if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
50
+ if ('=&|?'.includes(char)) return fail('unsupported-action-expression-operator');
51
51
  return fail('malformed-action-expression');
52
52
  }
53
53
  tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
@@ -84,7 +84,7 @@ function canStartSignedNumber(tokens) {
84
84
  const previous = tokens[tokens.length - 1];
85
85
  if (!previous) return true;
86
86
  if (previous.type === 'operator') return true;
87
- return previous.type === 'punctuation' && (previous.text === '(' || previous.text === ',');
87
+ return previous.type === 'punctuation' && (previous.text === '(' || previous.text === ',' || previous.text === '[' || previous.text === ':');
88
88
  }
89
89
 
90
90
  function fail(reason) {
@@ -138,10 +138,66 @@ class ExpressionParser {
138
138
  if (!this.matchPunctuation(')')) this.reject('malformed-action-expression');
139
139
  return expression;
140
140
  }
141
+ if (token.type === 'punctuation' && token.text === '[') return this.parseArray();
142
+ if (token.type === 'punctuation' && token.text === '{') return this.parseObject();
141
143
  this.reject('malformed-action-expression');
142
144
  return { kind: 'literal', value: null };
143
145
  }
144
146
 
147
+ parseArray() {
148
+ this.matchPunctuation('[');
149
+ const elements = [];
150
+ if (this.matchPunctuation(']')) return { kind: 'array', elements };
151
+ while (this.ok) {
152
+ elements.push(this.parseExpression());
153
+ if (this.matchPunctuation(']')) break;
154
+ if (!this.matchPunctuation(',')) {
155
+ this.reject('malformed-action-expression');
156
+ break;
157
+ }
158
+ if (this.peek().type === 'punctuation' && this.peek().text === ']') {
159
+ this.reject('malformed-action-expression');
160
+ break;
161
+ }
162
+ }
163
+ return { kind: 'array', elements };
164
+ }
165
+
166
+ parseObject() {
167
+ this.matchPunctuation('{');
168
+ const entries = [];
169
+ if (this.matchPunctuation('}')) return { kind: 'object', entries };
170
+ while (this.ok) {
171
+ const key = this.readObjectKey();
172
+ if (key === undefined) {
173
+ this.reject('malformed-action-expression');
174
+ break;
175
+ }
176
+ if (!this.matchPunctuation(':')) {
177
+ this.reject('malformed-action-expression');
178
+ break;
179
+ }
180
+ entries.push({ key, value: this.parseExpression() });
181
+ if (this.matchPunctuation('}')) break;
182
+ if (!this.matchPunctuation(',')) {
183
+ this.reject('malformed-action-expression');
184
+ break;
185
+ }
186
+ if (this.peek().type === 'punctuation' && this.peek().text === '}') {
187
+ this.reject('malformed-action-expression');
188
+ break;
189
+ }
190
+ }
191
+ return { kind: 'object', entries };
192
+ }
193
+
194
+ readObjectKey() {
195
+ const token = this.peek();
196
+ if (token.type !== 'identifier' && token.type !== 'string') return undefined;
197
+ this.index++;
198
+ return token.text === '__proto__' ? '__proto__' : String(token.value ?? token.text);
199
+ }
200
+
145
201
  parseRef() {
146
202
  const parts = [];
147
203
  const first = this.consumeIdentifier();
@@ -254,6 +310,8 @@ function attachCallType(node, callType) {
254
310
  if (node.kind === 'call') return compactRecord({ ...node, callType });
255
311
  if (node.kind === 'binary' || node.kind === 'logical') return { ...node, left: attachCallType(node.left, callType), right: attachCallType(node.right, callType) };
256
312
  if (node.kind === 'unary') return { ...node, argument: attachCallType(node.argument, callType) };
313
+ if (node.kind === 'array') return { ...node, elements: (node.elements ?? []).map((element) => attachCallType(element, callType)) };
314
+ if (node.kind === 'object') return { ...node, entries: (node.entries ?? []).map((entry) => ({ ...entry, value: attachCallType(entry.value, callType) })) };
257
315
  return node;
258
316
  }
259
317
 
@@ -21,8 +21,7 @@ export function readRepeatHeaderBlock(source, offset, endOffset, helpers = {}) {
21
21
  export function readActionRepeatHeader(header, index) {
22
22
  const rawText = rawHeader(header, 'repeat');
23
23
  const hasExplicitId = hasId(rawText);
24
- const text = stripIds(rawText);
25
- const shape = /^([A-Za-z_$][\w$-]*)\s+times\s+(.+)$/.exec(text);
24
+ const shape = /^([A-Za-z_$][\w$-]*)\s+@id\(\s*["'][^"']+["']\s*\)\s+times\s+(.+)$/.exec(rawText);
26
25
  const indexName = shape?.[1];
27
26
  const countText = shape?.[2]?.trim();
28
27
  const count = countText ? readActionValue(countText) : undefined;
@@ -41,7 +40,11 @@ export function validateActionRepeatHeader(header) {
41
40
  const details = readActionRepeatHeader(header, 0);
42
41
  const stripped = stripIds(rawHeader(header, 'repeat'));
43
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
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
+ }
45
48
  if (details.malformed) return { ok: false, reason: 'malformed-action-repeat-header' };
46
49
  if (!details.indexName) return { ok: false, reason: 'missing-action-repeat-index' };
47
50
  if (!isActionBindingName(details.indexName) || details.indexName === 'times') return { ok: false, reason: 'unsupported-action-repeat-index' };
@@ -56,7 +59,7 @@ export function validateActionRepeatHeader(header) {
56
59
 
57
60
  function isSupportedCountExpression(value) {
58
61
  if (Object.hasOwn(value ?? {}, 'value')) {
59
- return typeof value.value === 'number' && Number.isFinite(value.value) && value.value >= 0;
62
+ return typeof value.value === 'number' && Number.isInteger(value.value) && value.value >= 0;
60
63
  }
61
64
  const ast = value?.expressionAst;
62
65
  return ast?.kind === 'ref'
@@ -187,7 +187,7 @@ function validateActionRow(rowKind, rawName, rest, header) {
187
187
  const value = readInlineValue('value', rest);
188
188
  const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) }) : undefined;
189
189
  if (parsed?.ok) return { ok: true };
190
- if (parsed?.reason === 'missing-action-expression-type' || parsed?.reason === 'unsupported-action-expression-type' || parsed?.reason === 'missing-action-comparison-type' || parsed?.reason === 'unsupported-action-comparison-type' || parsed?.reason === 'missing-action-call-type' || parsed?.reason === 'unsupported-action-call-type' || parsed?.reason === 'unsupported-action-call-callee' || parsed?.reason === 'unsupported-action-call-argument') {
190
+ if (isActionExpressionAdmissionReason(parsed?.reason)) {
191
191
  return { ok: false, reason: parsed.reason };
192
192
  }
193
193
  return { ok: false, reason: 'unsupported-action-binding-value' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.54",
3
+ "version": "0.3.56",
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-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",
25
+ "test": "npm run build && node test/smoke.mjs && node test/action-body-smoke.mjs && node test/action-structured-literals-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",