@shapeshift-labs/frontier-lang-parser 0.3.47 → 0.3.48

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.
@@ -53,6 +53,7 @@ function parseActionIfBlock(header, body, index, state) {
53
53
  id: details.id,
54
54
  name: details.name,
55
55
  comparisonType: details.comparisonType,
56
+ callType: details.callType,
56
57
  condition: details.condition,
57
58
  body: parseActionBodyRecords(body, state)
58
59
  });
@@ -65,11 +66,13 @@ function parseActionIfHeader(header, index) {
65
66
  const name = firstIdentifier(nameText) ?? `if_${index}`;
66
67
  const conditionText = explicitCondition?.[1]?.trim() || withoutId;
67
68
  const comparisonType = readInlineComparisonType(header);
69
+ const callType = readInlineCallType(header);
68
70
  return {
69
71
  id: idFrom(header, `action_body_if_${name}`),
70
72
  name,
71
73
  comparisonType,
72
- condition: conditionText ? readActionValue(conditionText, { comparisonType }) : undefined
74
+ callType,
75
+ condition: conditionText ? readActionValue(conditionText, { comparisonType, callType }) : undefined
73
76
  };
74
77
  }
75
78
 
@@ -83,9 +86,10 @@ function parseActionBodyLine(line, index) {
83
86
  const path = readInlineWord('path', rest);
84
87
  const valueType = readInlineType(rest);
85
88
  const comparisonType = readInlineComparisonType(rest);
86
- const value = readInlineActionValue('value', rest, { valueType, comparisonType });
89
+ const callType = readInlineCallType(rest);
90
+ const value = readInlineActionValue('value', rest, { valueType, comparisonType, callType });
87
91
  if (!path || !value) return undefined;
88
- return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, comparisonType, value });
92
+ return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, comparisonType, callType, value });
89
93
  }
90
94
  if (rowKind === 'remove') {
91
95
  const path = readInlineWord('path', rest);
@@ -101,9 +105,10 @@ function parseActionBodyLine(line, index) {
101
105
  if (rowKind === 'let') {
102
106
  const valueType = readInlineType(rest);
103
107
  const comparisonType = readInlineComparisonType(rest);
104
- const value = readInlineActionBindingValue('value', rest, { valueType, comparisonType });
108
+ const callType = readInlineCallType(rest);
109
+ const value = readInlineActionBindingValue('value', rest, { valueType, comparisonType, callType });
105
110
  if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
106
- return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, comparisonType, value });
111
+ return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, comparisonType, callType, value });
107
112
  }
108
113
  if (rowKind === 'return') {
109
114
  const valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
@@ -195,6 +200,7 @@ function isActionBindingName(value) {
195
200
  function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
196
201
  function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
197
202
  function readInlineComparisonType(text) { return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text); }
203
+ function readInlineCallType(text) { return readInlineWord('call', text) ?? readInlineWord('callType', text); }
198
204
  function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
199
205
  function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
200
206
  function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
@@ -2,6 +2,7 @@ export const NUMERIC_OPERATORS = new Set(['+', '-', '*', '/', '%']);
2
2
 
3
3
  const ORDERED_COMPARISON_OPERATORS = new Set(['>', '>=', '<', '<=']);
4
4
  const NUMERIC_TYPES = new Set(['number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal']);
5
+ const CALL_TYPES = new Set(['text', 'string', 'bool', 'boolean', 'number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal', 'json']);
5
6
 
6
7
  export function hasNumericOperator(node) {
7
8
  if (!node || typeof node !== 'object') return false;
@@ -22,10 +23,22 @@ export function hasNonLiteralOrderedComparison(node) {
22
23
  return false;
23
24
  }
24
25
 
26
+ export function hasCallExpression(node) {
27
+ if (!node || typeof node !== 'object') return false;
28
+ if (node.kind === 'call') return true;
29
+ if (node.kind === 'binary' || node.kind === 'logical') return hasCallExpression(node.left) || hasCallExpression(node.right);
30
+ if (node.kind === 'unary') return hasCallExpression(node.argument);
31
+ return false;
32
+ }
33
+
25
34
  export function isNumericType(value) {
26
35
  return NUMERIC_TYPES.has(String(value ?? '').trim().toLowerCase());
27
36
  }
28
37
 
38
+ export function isSupportedCallType(value) {
39
+ return CALL_TYPES.has(String(value ?? '').trim().toLowerCase());
40
+ }
41
+
29
42
  function isLiteralNumericComparison(node) {
30
43
  return node.left?.kind === 'literal' && typeof node.left.value === 'number' && node.right?.kind === 'literal' && typeof node.right.value === 'number';
31
44
  }
@@ -0,0 +1,92 @@
1
+ import { NUMERIC_OPERATORS } from './action-expression-semantics.js';
2
+
3
+ export function tokenizeActionExpression(text) {
4
+ const tokens = [];
5
+ let index = 0;
6
+ while (index < text.length) {
7
+ const char = text[index];
8
+ if (/\s/.test(char)) {
9
+ index++;
10
+ continue;
11
+ }
12
+ if (char === '"' || char === "'") {
13
+ const read = readStringToken(text, index);
14
+ if (!read.ok) return read;
15
+ tokens.push(read.token);
16
+ index = read.next;
17
+ continue;
18
+ }
19
+ const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
20
+ const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
21
+ if (number) {
22
+ tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
23
+ index += number[0].length;
24
+ continue;
25
+ }
26
+ const identifier = /^[A-Za-z_$][\w$-]*/.exec(text.slice(index));
27
+ if (identifier) {
28
+ tokens.push({ type: 'identifier', text: identifier[0], start: index, end: index + identifier[0].length });
29
+ index += identifier[0].length;
30
+ continue;
31
+ }
32
+ const triple = text.slice(index, index + 3);
33
+ if (triple === '===' || triple === '!==') return fail('unsupported-action-expression-operator');
34
+ const double = text.slice(index, index + 2);
35
+ if (double === '&&' || double === '||' || double === '==' || double === '!=' || double === '>=' || double === '<=') {
36
+ tokens.push({ type: 'operator', text: double, start: index, end: index + 2 });
37
+ index += 2;
38
+ continue;
39
+ }
40
+ if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
41
+ tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
42
+ index++;
43
+ continue;
44
+ }
45
+ if (char === '(' || char === ')' || char === '.' || char === ',') {
46
+ tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
47
+ index++;
48
+ continue;
49
+ }
50
+ if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
51
+ return fail('malformed-action-expression');
52
+ }
53
+ tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
54
+ return { ok: true, tokens };
55
+ }
56
+
57
+ function readStringToken(text, start) {
58
+ const quote = text[start];
59
+ let value = '';
60
+ let index = start + 1;
61
+ while (index < text.length) {
62
+ const char = text[index];
63
+ if (char === quote) {
64
+ return { ok: true, token: { type: 'string', value, text: text.slice(start, index + 1), start, end: index + 1 }, next: index + 1 };
65
+ }
66
+ if (char === '\\') {
67
+ const next = text[index + 1];
68
+ if (next === undefined) return fail('malformed-action-expression');
69
+ if (next === 'n') value += '\n';
70
+ else if (next === 'r') value += '\r';
71
+ else if (next === 't') value += '\t';
72
+ else if (next === '\\' || next === '"' || next === "'") value += next;
73
+ else return fail('malformed-action-expression');
74
+ index += 2;
75
+ continue;
76
+ }
77
+ value += char;
78
+ index++;
79
+ }
80
+ return fail('malformed-action-expression');
81
+ }
82
+
83
+ function canStartSignedNumber(tokens) {
84
+ const previous = tokens[tokens.length - 1];
85
+ if (!previous) return true;
86
+ if (previous.type === 'operator') return true;
87
+ return previous.type === 'punctuation' && (previous.text === '(' || previous.text === ',');
88
+ }
89
+
90
+ function fail(reason) {
91
+ return { ok: false, reason };
92
+ }
@@ -1,7 +1,9 @@
1
- import { NUMERIC_OPERATORS, hasNonLiteralOrderedComparison, hasNumericOperator, isNumericType } from './action-expression-semantics.js';
1
+ import { hasCallExpression, hasNonLiteralOrderedComparison, hasNumericOperator, isNumericType, isSupportedCallType } from './action-expression-semantics.js';
2
+ import { tokenizeActionExpression } from './action-expression-tokenizer.js';
2
3
 
3
4
  const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
4
5
  const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
6
+ const BLOCKED_CALL_ROOTS = new Set([...BLOCKED_REF_ROOTS, 'input', 'state', 'patches']);
5
7
 
6
8
  export function readActionValue(value, options = {}) {
7
9
  const parsed = parseActionValue(value, options);
@@ -19,10 +21,15 @@ export function parseActionValue(value, options = {}) {
19
21
  if (hasNonLiteralOrderedComparison(expression.ast) && !isNumericType(options.comparisonType ?? options.compareType)) {
20
22
  return fail((options.comparisonType ?? options.compareType) ? 'unsupported-action-comparison-type' : 'missing-action-comparison-type');
21
23
  }
24
+ if (hasCallExpression(expression.ast) && !isSupportedCallType(options.callType ?? options.call ?? options.returns)) {
25
+ return fail((options.callType ?? options.call ?? options.returns) ? 'unsupported-action-call-type' : 'missing-action-call-type');
26
+ }
22
27
  const valueType = options.valueType ?? options.type;
23
28
  const comparisonType = options.comparisonType ?? options.compareType;
24
- if (expression.literal) return { ok: true, value: compactRecord({ value: expression.ast.value, valueType, comparisonType }) };
25
- return { ok: true, value: compactRecord({ expression: text, expressionAst: expression.ast, valueType, comparisonType }) };
29
+ const callType = options.callType ?? options.call ?? options.returns;
30
+ const ast = callType ? attachCallType(expression.ast, callType) : expression.ast;
31
+ if (expression.literal) return { ok: true, value: compactRecord({ value: ast.value, valueType, comparisonType, callType }) };
32
+ return { ok: true, value: compactRecord({ expression: text, expressionAst: ast, valueType, comparisonType, callType }) };
26
33
  }
27
34
 
28
35
  export function parseActionExpression(value, options = {}) {
@@ -41,86 +48,6 @@ export function isSupportedActionValue(value, options = {}) {
41
48
  return parseActionValue(value, options).ok;
42
49
  }
43
50
 
44
- function tokenizeActionExpression(text) {
45
- const tokens = [];
46
- let index = 0;
47
- while (index < text.length) {
48
- const char = text[index];
49
- if (/\s/.test(char)) {
50
- index++;
51
- continue;
52
- }
53
- if (char === '"' || char === "'") {
54
- const read = readStringToken(text, index);
55
- if (!read.ok) return read;
56
- tokens.push(read.token);
57
- index = read.next;
58
- continue;
59
- }
60
- const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
61
- const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
62
- if (number) {
63
- tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
64
- index += number[0].length;
65
- continue;
66
- }
67
- const identifier = /^[A-Za-z_$][\w$-]*/.exec(text.slice(index));
68
- if (identifier) {
69
- tokens.push({ type: 'identifier', text: identifier[0], start: index, end: index + identifier[0].length });
70
- index += identifier[0].length;
71
- continue;
72
- }
73
- const triple = text.slice(index, index + 3);
74
- if (triple === '===' || triple === '!==') return fail('unsupported-action-expression-operator');
75
- const double = text.slice(index, index + 2);
76
- if (double === '&&' || double === '||' || double === '==' || double === '!=' || double === '>=' || double === '<=') {
77
- tokens.push({ type: 'operator', text: double, start: index, end: index + 2 });
78
- index += 2;
79
- continue;
80
- }
81
- if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
82
- tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
83
- index++;
84
- continue;
85
- }
86
- if (char === '(' || char === ')' || char === '.') {
87
- tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
88
- index++;
89
- continue;
90
- }
91
- if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
92
- return fail('malformed-action-expression');
93
- }
94
- tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
95
- return { ok: true, tokens };
96
- }
97
-
98
- function readStringToken(text, start) {
99
- const quote = text[start];
100
- let value = '';
101
- let index = start + 1;
102
- while (index < text.length) {
103
- const char = text[index];
104
- if (char === quote) {
105
- return { ok: true, token: { type: 'string', value, text: text.slice(start, index + 1), start, end: index + 1 }, next: index + 1 };
106
- }
107
- if (char === '\\') {
108
- const next = text[index + 1];
109
- if (next === undefined) return fail('malformed-action-expression');
110
- if (next === 'n') value += '\n';
111
- else if (next === 'r') value += '\r';
112
- else if (next === 't') value += '\t';
113
- else if (next === '\\' || next === '"' || next === "'") value += next;
114
- else return fail('malformed-action-expression');
115
- index += 2;
116
- continue;
117
- }
118
- value += char;
119
- index++;
120
- }
121
- return fail('malformed-action-expression');
122
- }
123
-
124
51
  class ExpressionParser {
125
52
  constructor(tokens, options) {
126
53
  this.tokens = tokens;
@@ -220,6 +147,7 @@ class ExpressionParser {
220
147
  const first = this.consumeIdentifier();
221
148
  if (!first) return { kind: 'literal', value: null };
222
149
  parts.push(first);
150
+ if (this.peek().type === 'punctuation' && this.peek().text === '(') return this.parseCall(first);
223
151
  while (this.matchPunctuation('.')) {
224
152
  const part = this.consumeIdentifier();
225
153
  if (!part) {
@@ -229,7 +157,7 @@ class ExpressionParser {
229
157
  parts.push(part);
230
158
  }
231
159
  if (this.peek().type === 'punctuation' && this.peek().text === '(') {
232
- this.reject('unsupported-action-expression-ref');
160
+ this.reject('unsupported-action-call-callee');
233
161
  return refNode(parts);
234
162
  }
235
163
  if (BLOCKED_REF_ROOTS.has(parts[0])) {
@@ -239,6 +167,31 @@ class ExpressionParser {
239
167
  return refNode(parts);
240
168
  }
241
169
 
170
+ parseCall(callee) {
171
+ if (!IDENTIFIER.test(callee) || BLOCKED_CALL_ROOTS.has(callee)) {
172
+ this.reject('unsupported-action-call-callee');
173
+ return { kind: 'call', callee, args: [] };
174
+ }
175
+ this.matchPunctuation('(');
176
+ const args = [];
177
+ if (this.matchPunctuation(')')) return compactRecord({ kind: 'call', callee, args, callType: this.options.callType ?? this.options.call ?? this.options.returns });
178
+ while (this.ok) {
179
+ const argument = this.parseExpression();
180
+ if (hasCallExpression(argument)) this.reject('unsupported-action-call-argument');
181
+ args.push(argument);
182
+ if (this.matchPunctuation(')')) break;
183
+ if (!this.matchPunctuation(',')) {
184
+ this.reject('malformed-action-expression');
185
+ break;
186
+ }
187
+ if (this.peek().type === 'punctuation' && this.peek().text === ')') {
188
+ this.reject('malformed-action-expression');
189
+ break;
190
+ }
191
+ }
192
+ return compactRecord({ kind: 'call', callee, args, callType: this.options.callType ?? this.options.call ?? this.options.returns });
193
+ }
194
+
242
195
  consumeIdentifier() {
243
196
  const token = this.peek();
244
197
  if (token.type !== 'identifier' || !IDENTIFIER.test(token.text)) {
@@ -285,13 +238,6 @@ class ExpressionParser {
285
238
  }
286
239
  }
287
240
 
288
- function canStartSignedNumber(tokens) {
289
- const previous = tokens[tokens.length - 1];
290
- if (!previous) return true;
291
- if (previous.type === 'operator') return true;
292
- return previous.type === 'punctuation' && previous.text === '(';
293
- }
294
-
295
241
  function refNode(parts) {
296
242
  const [root, ...rest] = parts;
297
243
  const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
@@ -303,6 +249,14 @@ function fail(reason) {
303
249
  return { ok: false, reason };
304
250
  }
305
251
 
252
+ function attachCallType(node, callType) {
253
+ if (!node || typeof node !== 'object') return node;
254
+ if (node.kind === 'call') return compactRecord({ ...node, callType });
255
+ if (node.kind === 'binary' || node.kind === 'logical') return { ...node, left: attachCallType(node.left, callType), right: attachCallType(node.right, callType) };
256
+ if (node.kind === 'unary') return { ...node, argument: attachCallType(node.argument, callType) };
257
+ return node;
258
+ }
259
+
306
260
  function compactRecord(record) {
307
261
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
308
262
  }
@@ -91,10 +91,10 @@ function actionRowName(rowKind, rawName, rowIndex) {
91
91
 
92
92
  function validateActionRow(rowKind, rawName, rest, header) {
93
93
  if (!ACTION_BODY_ROWS.has(rowKind)) return { ok: false, reason: 'unsupported-action-body-row' };
94
- if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(header) });
94
+ if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header), { comparisonType: readInlineComparisonType(header), callType: readInlineCallType(header) });
95
95
  if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
96
96
  if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
97
- return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest) });
97
+ return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) });
98
98
  }
99
99
  if (rowKind === 'remove') {
100
100
  return readInlineWord('path', rest) ? { ok: true } : { ok: false, reason: 'missing-action-path' };
@@ -112,9 +112,9 @@ function validateActionRow(rowKind, rawName, rest, header) {
112
112
  return { ok: false, reason: 'unsupported-action-binding-name' };
113
113
  }
114
114
  const value = readInlineValue('value', rest);
115
- const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest) }) : undefined;
115
+ const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest), comparisonType: readInlineComparisonType(rest), callType: readInlineCallType(rest) }) : undefined;
116
116
  if (parsed?.ok) return { ok: true };
117
- 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') {
117
+ 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') {
118
118
  return { ok: false, reason: parsed.reason };
119
119
  }
120
120
  return { ok: false, reason: 'unsupported-action-binding-value' };
@@ -138,6 +138,10 @@ function readInlineComparisonType(text) {
138
138
  return readInlineWord('compare', text) ?? readInlineWord('comparisonType', text) ?? readInlineWord('compareType', text);
139
139
  }
140
140
 
141
+ function readInlineCallType(text) {
142
+ return readInlineWord('call', text) ?? readInlineWord('callType', text);
143
+ }
144
+
141
145
  function readIfCondition(header) {
142
146
  const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
143
147
  const explicit = /\bcondition\s+(.+)$/.exec(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.47",
3
+ "version": "0.3.48",
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/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-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",