@shapeshift-labs/frontier-lang-parser 0.3.60 → 0.3.62

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.
package/dist/index.js CHANGED
@@ -303,12 +303,17 @@ function readTypeFields(body) {
303
303
  }
304
304
  function readVariants(body) {
305
305
  const variants = [];
306
+ const seenNames = new Set();
307
+ const seenIds = new Set();
306
308
  const re = /^\s*variant\s+([A-Za-z_$][\w$]*)(.*)$/gm;
307
309
  let match;
308
310
  while ((match = re.exec(body))) {
309
311
  const fields = readVariantPayloadFields(match[2] ?? '', match[1], parseTypeExpression);
310
312
  if (fields === null) continue;
311
313
  const id = /@id\(\s*["']([^"']+)["']\s*\)/.exec(match[2] ?? '')?.[1];
314
+ const variantId = id ?? `type_variant_${safeId(match[1])}`;
315
+ if (seenNames.has(match[1]) || seenIds.has(variantId)) continue;
316
+ seenNames.add(match[1]); seenIds.add(variantId);
312
317
  variants.push({ ...(id ? { id } : {}), name: match[1], ...(fields?.length ? { fields } : {}) });
313
318
  }
314
319
  return variants.length ? variants : undefined;
@@ -1,39 +1,58 @@
1
1
  import { readActionSyntaxChildren } from './action-syntax-children.js';
2
2
  import { ROW_SYNTAX_CONFIG } from './source-syntax-row-config.js';
3
+ import { readTypeExpressionSyntaxChildren } from './source-syntax-type-expressions.js';
4
+ import { inspectTypeExpressionSyntax } from './type-expressions.js';
3
5
  import { inspectVariantPayload } from './type-variants.js';
4
6
 
5
7
  const ROW_NAME_PATTERN = '([A-Za-z_$@./:*+-][\\w$./@:*+-]*)';
6
8
 
7
9
  export function readSourceSyntaxChildren(source, block, options = {}) {
10
+ let children = [];
8
11
  if (block.malformed) return [];
9
12
  if (block.kind === 'action') {
10
- return readActionSyntaxChildren(source, block, options);
13
+ children = readActionSyntaxChildren(source, block, options);
14
+ } else if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') {
15
+ children = readConversionSyntaxChildren(source, block, options);
16
+ } else if (block.kind === 'type') {
17
+ children = readTypeSyntaxChildren(source, block, options);
18
+ } else {
19
+ const rowConfig = ROW_SYNTAX_CONFIG[block.kind];
20
+ if (rowConfig) children = readGenericRowSyntaxChildren(source, block, options, rowConfig);
11
21
  }
12
- if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') {
13
- return readConversionSyntaxChildren(source, block, options);
14
- }
15
- if (block.kind === 'type') {
16
- return readTypeSyntaxChildren(source, block, options);
17
- }
18
- const rowConfig = ROW_SYNTAX_CONFIG[block.kind];
19
- if (rowConfig) return readGenericRowSyntaxChildren(source, block, options, rowConfig);
20
- return [];
22
+ return [...children, ...readTypeExpressionSyntaxChildren(source, block, options)];
21
23
  }
22
24
 
23
25
  function readTypeSyntaxChildren(source, block, options) {
24
26
  const children = [];
27
+ const seenVariantNames = new Set();
28
+ const seenVariantIds = new Set();
25
29
  for (const line of readBodyLines(source, block)) {
26
30
  if (!line.text || line.text.startsWith('#')) continue;
27
31
  const variant = /^variant\s+([A-Za-z_$][\w$]*)(.*)$/.exec(line.text);
28
32
  if (!variant) continue;
29
33
  const [, name, rest] = variant;
30
- const payload = inspectVariantPayload(rest ?? '');
34
+ const payload = inspectVariantPayload(rest ?? '', inspectTypeExpressionSyntax);
35
+ const id = idFrom(rest, `type_variant_${safeId(name)}`);
36
+ let recognized = payload.ok;
37
+ let reason = payload.ok ? undefined : payload.reason;
38
+ if (recognized && seenVariantNames.has(name)) {
39
+ recognized = false;
40
+ reason = 'duplicate-type-variant-name';
41
+ }
42
+ if (recognized && seenVariantIds.has(id)) {
43
+ recognized = false;
44
+ reason = 'duplicate-type-variant-id';
45
+ }
46
+ if (recognized) {
47
+ seenVariantNames.add(name);
48
+ seenVariantIds.add(id);
49
+ }
31
50
  children.push(cleanRecord({
32
51
  kind: 'typeVariant',
33
52
  rowKind: 'variant',
34
53
  normalizedRowKind: 'variant',
35
54
  name,
36
- id: idFrom(rest, `type_variant_${safeId(name)}`),
55
+ id,
37
56
  header: line.text,
38
57
  startOffset: line.startOffset,
39
58
  endOffset: line.endOffset,
@@ -44,8 +63,8 @@ function readTypeSyntaxChildren(source, block, options) {
44
63
  moduleId: block.moduleId,
45
64
  moduleName: block.moduleName,
46
65
  sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
47
- recognized: payload.ok,
48
- reason: payload.ok ? undefined : payload.reason,
66
+ recognized,
67
+ reason,
49
68
  fieldCount: payload.fieldCount,
50
69
  fieldIds: payload.fieldIds
51
70
  }));
@@ -0,0 +1,172 @@
1
+ import { inspectTypeExpressionSyntax } from './type-expressions.js';
2
+
3
+ export function readTypeExpressionSyntaxChildren(source, block, options = {}) {
4
+ const children = [];
5
+ for (const line of readBodyLines(source, block)) {
6
+ if (!line.text || line.text.startsWith('#')) continue;
7
+ for (const candidate of readTypeExpressionCandidates(block, line.text)) {
8
+ const inspected = inspectTypeExpressionSyntax(candidate.typeSource);
9
+ if (inspected.ok) continue;
10
+ children.push(cleanRecord({
11
+ kind: 'typeExpressionSyntax',
12
+ rowKind: candidate.rowKind,
13
+ normalizedRowKind: candidate.rowKind,
14
+ name: candidate.name,
15
+ id: `${block.id ?? block.kind}_${candidate.rowKind}_${safeId(candidate.name)}_${line.startOffset}`,
16
+ header: line.text,
17
+ startOffset: line.startOffset,
18
+ endOffset: line.endOffset,
19
+ location: sourcePosition(source, line.startOffset),
20
+ parentKind: block.kind,
21
+ parentId: block.id,
22
+ parentName: block.name,
23
+ moduleId: block.moduleId,
24
+ moduleName: block.moduleName,
25
+ sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
26
+ recognized: false,
27
+ reason: inspected.reason,
28
+ typeSource: candidate.typeSource
29
+ }));
30
+ }
31
+ }
32
+ return children;
33
+ }
34
+
35
+ function readTypeExpressionCandidates(block, text) {
36
+ if (block.kind === 'type') return readTypeBlockTypeExpressionCandidates(block, text);
37
+ if (block.kind === 'entity') return readNamedTypeFieldCandidate(text, 'field', { stopAtMetadata: true });
38
+ if (block.kind === 'state') return readNamedTypeFieldCandidate(text, 'collection', { stopAtMetadata: true });
39
+ if (block.kind === 'view') return readViewTypeExpressionCandidates(text);
40
+ if (block.kind === 'action' || block.kind === 'effect' || block.kind === 'capability' || block.kind === 'extern') {
41
+ return readSignatureTypeExpressionCandidates(text);
42
+ }
43
+ if (block.kind === 'lattice') return readCarrierTypeExpressionCandidate(text);
44
+ return [];
45
+ }
46
+
47
+ function readTypeBlockTypeExpressionCandidates(block, text) {
48
+ const alias = /^=\s*(.+)$/.exec(text);
49
+ if (alias) return [{ rowKind: 'typeAlias', name: block.name ?? 'alias', typeSource: alias[1].trim() }];
50
+ if (text.startsWith('variant ')) return [];
51
+ return readNamedTypeFieldCandidate(text, 'typeField');
52
+ }
53
+
54
+ function readViewTypeExpressionCandidates(text) {
55
+ const prop = /^prop\s+([A-Za-z_$][\w$.-]*)(?:\s+@id\(\s*["'][^"']+["']\s*\))?\s*:\s*(.+)$/.exec(text);
56
+ if (prop) {
57
+ const typeSource = prop[2].replace(/\s+optional\s*$/, '').trim();
58
+ return typeSource ? [{ rowKind: 'viewProp', name: prop[1], typeSource }] : [];
59
+ }
60
+ const event = /^event\s+([A-Za-z_$][\w$.-]*)(?:\s+@id\(\s*["'][^"']+["']\s*\))?(.*)$/.exec(text);
61
+ if (!event) return [];
62
+ const input = readInlineType('input', event[2]);
63
+ return input ? [{ rowKind: 'viewEventInput', name: event[1], typeSource: input }] : [];
64
+ }
65
+
66
+ function readSignatureTypeExpressionCandidates(text) {
67
+ const input = /^input\s*:?\s*(.+)$/.exec(text);
68
+ if (input) return [{ rowKind: 'input', name: 'input', typeSource: input[1].trim() }];
69
+ const returns = /^returns\s+(.+)$/.exec(text);
70
+ if (returns) return [{ rowKind: 'returns', name: 'returns', typeSource: returns[1].trim() }];
71
+ return [];
72
+ }
73
+
74
+ function readCarrierTypeExpressionCandidate(text) {
75
+ const carrier = /^carrier\s+(.+)$/.exec(text);
76
+ return carrier ? [{ rowKind: 'carrier', name: 'carrier', typeSource: carrier[1].trim() }] : [];
77
+ }
78
+
79
+ function readNamedTypeFieldCandidate(text, rowKind, options = {}) {
80
+ const field = /^([A-Za-z_$][\w$]*)(?:\s+@id\(\s*["'][^"']+["']\s*\))?\s*:\s*(.+)$/.exec(text);
81
+ if (!field) return [];
82
+ const typeSource = options.stopAtMetadata ? readTypeSourceBeforeMetadata(field[2]) : field[2].trim();
83
+ return typeSource ? [{ rowKind, name: field[1], typeSource }] : [];
84
+ }
85
+
86
+ function readTypeSourceBeforeMetadata(source) {
87
+ let stack = [];
88
+ let quote = '';
89
+ let escaped = false;
90
+ for (let index = 0; index < source.length; index++) {
91
+ const char = source[index];
92
+ if (quote) {
93
+ if (escaped) {
94
+ escaped = false;
95
+ } else if (char === '\\') {
96
+ escaped = true;
97
+ } else if (char === quote) {
98
+ quote = '';
99
+ }
100
+ continue;
101
+ }
102
+ if (char === '"' || char === "'") {
103
+ quote = char;
104
+ continue;
105
+ }
106
+ if (stack.length === 0 && char === '{') return source.slice(0, index).trim();
107
+ if (char === '<' || char === '(' || char === '[' || char === '{') {
108
+ stack.push(closingDelimiterFor(char));
109
+ continue;
110
+ }
111
+ if (char === '>' || char === ')' || char === ']' || char === '}') {
112
+ if (stack[stack.length - 1] === char) stack = stack.slice(0, -1);
113
+ continue;
114
+ }
115
+ if (stack.length === 0 && /\s/.test(char) && source.slice(index).trimStart().startsWith('@')) {
116
+ return source.slice(0, index).trim();
117
+ }
118
+ }
119
+ return source.trim();
120
+ }
121
+
122
+ function readBodyLines(source, block) {
123
+ return readTextLines(source, block.bodyStartOffset, block.bodyEndOffset);
124
+ }
125
+
126
+ function readTextLines(source, startOffset, endOffset) {
127
+ const body = source.slice(startOffset, endOffset);
128
+ const lines = body.split('\n');
129
+ const records = [];
130
+ let lineStart = startOffset;
131
+ for (const rawLine of lines) {
132
+ const rawEnd = lineStart + rawLine.length;
133
+ const leading = /^\s*/.exec(rawLine)?.[0].length ?? 0;
134
+ const trailing = /\s*$/.exec(rawLine)?.[0].length ?? 0;
135
+ const startOffset = lineStart + leading;
136
+ const endOffset = Math.max(startOffset, rawEnd - trailing);
137
+ records.push({ text: rawLine.trim(), startOffset, endOffset });
138
+ lineStart = rawEnd + 1;
139
+ }
140
+ return records;
141
+ }
142
+
143
+ function sourcePosition(source, offset) {
144
+ const lines = source.slice(0, offset).split('\n');
145
+ return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
146
+ }
147
+
148
+ function sourceSpan(source, block, startOffset, endOffset, options = {}) {
149
+ return cleanRecord({
150
+ sourceId: options.documentId,
151
+ path: options.sourcePath,
152
+ blockId: block.id,
153
+ blockKind: block.kind,
154
+ startOffset,
155
+ endOffset,
156
+ start: sourcePosition(source, startOffset),
157
+ end: sourcePosition(source, endOffset)
158
+ });
159
+ }
160
+
161
+ function readInlineType(label, text = '') { return new RegExp('(?:^|\\s)' + label + '\\s+(.+)$').exec(text)?.[1]?.trim(); }
162
+ function safeId(value) { return String(value).replace(/[^A-Za-z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'row'; }
163
+ function cleanRecord(record) {
164
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
165
+ }
166
+
167
+ function closingDelimiterFor(char) {
168
+ if (char === '<') return '>';
169
+ if (char === '(') return ')';
170
+ if (char === '[') return ']';
171
+ return '}';
172
+ }
@@ -8,17 +8,38 @@ export function parseTypeExpression(value) {
8
8
  const text = value.trim();
9
9
  const application = readTypeApplication(text);
10
10
  if (!application) return text;
11
+ if (application.name === 'Record') return readRecordTypeExpression(application.body) ?? text;
12
+ if (application.name === 'Union') return readUnionTypeExpression(application.body) ?? text;
11
13
  const args = splitTopLevelCommaList(application.body);
12
14
  if (args.some((arg) => !arg)) return text;
13
15
  const parsedArgs = args.map(parseTypeExpression);
14
16
  if (application.name === 'Set' && parsedArgs.length === 1) return { kind: 'set', item: parsedArgs[0] };
15
17
  if (application.name === 'List' && parsedArgs.length === 1) return { kind: 'list', item: parsedArgs[0] };
16
18
  if (application.name === 'Map' && parsedArgs.length === 2) return { kind: 'map', key: parsedArgs[0], value: parsedArgs[1] };
17
- if (application.name === 'Record') return readRecordTypeExpression(application.body) ?? { kind: 'ref', name: application.name, args: parsedArgs };
18
- if (application.name === 'Union') return readUnionTypeExpression(application.body) ?? { kind: 'ref', name: application.name, args: parsedArgs };
19
19
  return { kind: 'ref', name: application.name, args: parsedArgs };
20
20
  }
21
21
 
22
+ export function inspectTypeExpressionSyntax(value) {
23
+ const text = value.trim();
24
+ if (!text) return { ok: false, reason: 'missing-type-expression' };
25
+ const application = readTypeApplication(text);
26
+ if (!application) {
27
+ return hasReservedStructuralTypeSyntax(text)
28
+ ? { ok: false, reason: 'malformed-structural-type-expression' }
29
+ : { ok: true };
30
+ }
31
+ if (application.name === 'Record') return inspectRecordTypeExpression(application.body);
32
+ if (application.name === 'Union') return inspectUnionTypeExpression(application.body);
33
+ if (!hasReservedStructuralTypeSyntax(application.body)) return { ok: true };
34
+ const args = splitTopLevelCommaListStrict(application.body);
35
+ if (!args.ok) return { ok: false, reason: args.reason };
36
+ for (const arg of args.parts) {
37
+ const inspected = inspectTypeExpressionSyntax(arg);
38
+ if (!inspected.ok) return inspected;
39
+ }
40
+ return { ok: true };
41
+ }
42
+
22
43
  function readTypeApplication(text) {
23
44
  const match = /^([A-Za-z_$][\w$]*)</.exec(text);
24
45
  if (!match || !text.endsWith('>')) return undefined;
@@ -35,34 +56,98 @@ function readTypeApplication(text) {
35
56
  }
36
57
 
37
58
  function readRecordTypeExpression(body) {
59
+ const split = splitTopLevelCommaListStrict(body);
60
+ if (!split.ok) return undefined;
38
61
  const fields = [];
39
- for (const source of splitTopLevelCommaList(body)) {
62
+ const seen = new Set();
63
+ const seenNames = new Set();
64
+ for (const source of split.parts) {
40
65
  const field = readInlineTypeField(source, 'record_field');
41
66
  if (!field) return undefined;
67
+ if (seen.has(field.id)) return undefined;
68
+ seen.add(field.id);
69
+ if (seenNames.has(field.name)) return undefined;
70
+ seenNames.add(field.name);
42
71
  fields.push(field);
43
72
  }
44
73
  return fields.length ? { kind: 'record', fields } : undefined;
45
74
  }
46
75
 
47
76
  function readUnionTypeExpression(body) {
77
+ const split = splitTopLevelCommaListStrict(body);
78
+ if (!split.ok) return undefined;
48
79
  const variants = [];
49
- for (const source of splitTopLevelCommaList(body)) {
80
+ const seenNames = new Set();
81
+ const seenIds = new Set();
82
+ for (const source of split.parts) {
50
83
  const variant = readInlineTypeVariant(source);
51
84
  if (!variant) return undefined;
85
+ if (seenNames.has(variant.name)) return undefined;
86
+ seenNames.add(variant.name);
87
+ if (variant.id) {
88
+ if (seenIds.has(variant.id)) return undefined;
89
+ seenIds.add(variant.id);
90
+ }
52
91
  variants.push(variant);
53
92
  }
54
93
  return variants.length ? { kind: 'union', variants } : undefined;
55
94
  }
56
95
 
96
+ function inspectRecordTypeExpression(body) {
97
+ const split = splitTopLevelCommaListStrict(body);
98
+ if (!split.ok) return { ok: false, reason: split.reason };
99
+ if (!split.parts.length) return { ok: false, reason: 'empty-structural-record-type' };
100
+ const seen = new Set();
101
+ const seenNames = new Set();
102
+ for (const source of split.parts) {
103
+ const field = readInlineTypeField(source, 'record_field');
104
+ if (!field) return { ok: false, reason: 'malformed-structural-record-field' };
105
+ if (seen.has(field.id)) return { ok: false, reason: 'duplicate-structural-record-field-id' };
106
+ seen.add(field.id);
107
+ if (seenNames.has(field.name)) return { ok: false, reason: 'duplicate-structural-record-field-name' };
108
+ seenNames.add(field.name);
109
+ }
110
+ return { ok: true, fieldCount: split.parts.length };
111
+ }
112
+
113
+ function inspectUnionTypeExpression(body) {
114
+ const split = splitTopLevelCommaListStrict(body);
115
+ if (!split.ok) return { ok: false, reason: split.reason };
116
+ if (!split.parts.length) return { ok: false, reason: 'empty-structural-union-type' };
117
+ const seenNames = new Set();
118
+ const seenIds = new Set();
119
+ for (const source of split.parts) {
120
+ const inspected = inspectInlineTypeVariant(source);
121
+ if (!inspected.ok) return inspected;
122
+ const variant = readInlineTypeVariant(source);
123
+ if (seenNames.has(variant.name)) return { ok: false, reason: 'duplicate-structural-union-variant-name' };
124
+ seenNames.add(variant.name);
125
+ if (variant.id) {
126
+ if (seenIds.has(variant.id)) return { ok: false, reason: 'duplicate-structural-union-variant-id' };
127
+ seenIds.add(variant.id);
128
+ }
129
+ }
130
+ return { ok: true, variantCount: split.parts.length };
131
+ }
132
+
57
133
  function readInlineTypeVariant(source) {
58
134
  const match = /^\s*([A-Za-z_$][\w$]*)(?:\s+@id\(\s*["']([^"']+)["']\s*\))?(?:\((.*)\))?\s*$/.exec(source);
59
135
  if (!match) return undefined;
60
136
  const payload = match[3]?.trim();
61
137
  const fields = [];
62
- if (payload) {
63
- for (const fieldSource of splitTopLevelCommaList(payload)) {
138
+ if (match[3] !== undefined) {
139
+ if (!payload) return undefined;
140
+ const split = splitTopLevelCommaListStrict(payload);
141
+ if (!split.ok) return undefined;
142
+ const seen = new Set();
143
+ const seenNames = new Set();
144
+ for (const fieldSource of split.parts) {
64
145
  const field = readInlineTypeField(fieldSource, `variant_field_${match[1]}`);
65
146
  if (!field) return undefined;
147
+ if (seen.has(field.id)) return undefined;
148
+ seen.add(field.id);
149
+ if (seenNames.has(field.name)) return undefined;
150
+ seenNames.add(field.name);
66
151
  fields.push(field);
67
152
  }
68
153
  }
@@ -73,14 +158,92 @@ function readInlineTypeVariant(source) {
73
158
  };
74
159
  }
75
160
 
161
+ function inspectInlineTypeVariant(source) {
162
+ const match = /^\s*([A-Za-z_$][\w$]*)(?:\s+@id\(\s*["']([^"']+)["']\s*\))?(?:\((.*)\))?\s*$/.exec(source);
163
+ if (!match) return { ok: false, reason: 'malformed-structural-union-variant' };
164
+ const payload = match[3]?.trim();
165
+ if (match[3] === undefined) return { ok: true };
166
+ if (!payload) return { ok: false, reason: 'empty-structural-union-variant-payload' };
167
+ const split = splitTopLevelCommaListStrict(payload);
168
+ if (!split.ok) return { ok: false, reason: split.reason };
169
+ const seen = new Set();
170
+ const seenNames = new Set();
171
+ for (const fieldSource of split.parts) {
172
+ const field = readInlineTypeField(fieldSource, `variant_field_${match[1]}`);
173
+ if (!field) return { ok: false, reason: 'malformed-structural-union-variant-field' };
174
+ if (seen.has(field.id)) return { ok: false, reason: 'duplicate-structural-union-variant-field-id' };
175
+ seen.add(field.id);
176
+ if (seenNames.has(field.name)) return { ok: false, reason: 'duplicate-structural-union-variant-field-name' };
177
+ seenNames.add(field.name);
178
+ }
179
+ return { ok: true };
180
+ }
181
+
76
182
  function readInlineTypeField(source, idPrefix) {
77
183
  const match = /^\s*([A-Za-z_$][\w$]*)(\?)?(?:\s+@id\(\s*["']([^"']+)["']\s*\))?(\?)?\s*:\s*(.+)\s*$/.exec(source);
78
184
  if (!match || !match[5].trim()) return undefined;
185
+ const typeSource = match[5].trim();
186
+ if (!inspectTypeExpressionSyntax(typeSource).ok) return undefined;
79
187
  const optional = Boolean(match[2] || match[4]);
80
188
  return {
81
189
  id: match[3] ?? `${idPrefix}_${match[1]}`,
82
190
  name: match[1],
83
- type: parseTypeExpression(match[5].trim()),
191
+ type: parseTypeExpression(typeSource),
84
192
  ...(optional ? { optional: true } : {})
85
193
  };
86
194
  }
195
+
196
+ function hasReservedStructuralTypeSyntax(text) {
197
+ return /\b(?:Record|Union)\s*</.test(text);
198
+ }
199
+
200
+ function splitTopLevelCommaListStrict(source) {
201
+ const parts = [];
202
+ const stack = [];
203
+ let start = 0;
204
+ let quote = '';
205
+ let escaped = false;
206
+ for (let index = 0; index < source.length; index++) {
207
+ const char = source[index];
208
+ if (quote) {
209
+ if (escaped) {
210
+ escaped = false;
211
+ } else if (char === '\\') {
212
+ escaped = true;
213
+ } else if (char === quote) {
214
+ quote = '';
215
+ }
216
+ continue;
217
+ }
218
+ if (char === '"' || char === "'") {
219
+ quote = char;
220
+ continue;
221
+ }
222
+ if (char === '<' || char === '(' || char === '[' || char === '{') {
223
+ stack.push(closingDelimiterFor(char));
224
+ continue;
225
+ }
226
+ if (char === '>' || char === ')' || char === ']' || char === '}') {
227
+ if (stack.pop() !== char) return { ok: false, reason: 'unbalanced-structural-type-expression' };
228
+ continue;
229
+ }
230
+ if (char === ',' && stack.length === 0) {
231
+ const part = source.slice(start, index).trim();
232
+ if (!part) return { ok: false, reason: 'empty-structural-type-expression-part' };
233
+ parts.push(part);
234
+ start = index + 1;
235
+ }
236
+ }
237
+ if (quote || stack.length) return { ok: false, reason: 'unbalanced-structural-type-expression' };
238
+ const last = source.slice(start).trim();
239
+ if (!last) return { ok: false, reason: 'empty-structural-type-expression-part' };
240
+ parts.push(last);
241
+ return { ok: true, parts };
242
+ }
243
+
244
+ function closingDelimiterFor(char) {
245
+ if (char === '<') return '>';
246
+ if (char === '(') return ')';
247
+ if (char === '[') return ']';
248
+ return '}';
249
+ }
@@ -12,18 +12,24 @@ export function readVariantPayloadFields(rest, variantName, parseTypeExpression)
12
12
  return fields;
13
13
  }
14
14
 
15
- export function inspectVariantPayload(rest) {
15
+ export function inspectVariantPayload(rest, inspectTypeExpressionSyntax) {
16
16
  const withoutId = stripLeadingVariantId(rest);
17
17
  if (!withoutId) return { ok: true };
18
18
  const payload = /^\((.*)\)$/.exec(withoutId);
19
19
  if (!payload || !payload[1].trim()) return { ok: false, reason: 'malformed-type-variant-payload' };
20
20
  const fieldIds = [];
21
21
  const seen = new Set();
22
+ const seenNames = new Set();
22
23
  for (const fieldSource of splitTopLevelCommaList(payload[1])) {
23
24
  const field = parseVariantPayloadField(fieldSource);
24
25
  if (!field) return { ok: false, reason: 'malformed-type-variant-payload' };
25
26
  if (seen.has(field.id)) return { ok: false, reason: 'duplicate-type-variant-field-id' };
26
27
  seen.add(field.id);
28
+ if (seenNames.has(field.name)) return { ok: false, reason: 'duplicate-type-variant-field-name' };
29
+ seenNames.add(field.name);
30
+ const typeSource = readVariantPayloadFieldTypeSource(fieldSource);
31
+ const inspected = inspectTypeExpressionSyntax?.(typeSource);
32
+ if (inspected && !inspected.ok) return { ok: false, reason: inspected.reason };
27
33
  fieldIds.push(field.id);
28
34
  }
29
35
  return { ok: true, fieldCount: fieldIds.length, fieldIds };
@@ -41,6 +47,10 @@ function parseVariantPayloadField(source, variantName, parseTypeExpression) {
41
47
  };
42
48
  }
43
49
 
50
+ function readVariantPayloadFieldTypeSource(source) {
51
+ return /^\s*[A-Za-z_$][\w$]*(\?)?(?:\s+@id\(\s*["'][^"']+["']\s*\))?(\?)?\s*:\s*(.+)\s*$/.exec(source)?.[3]?.trim();
52
+ }
53
+
44
54
  export function splitTopLevelCommaList(source) {
45
55
  const parts = [];
46
56
  let depth = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.60",
3
+ "version": "0.3.62",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",