@shapeshift-labs/frontier-lang-parser 0.3.23 → 0.3.25

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/README.md CHANGED
@@ -220,6 +220,46 @@ npm install @shapeshift-labs/frontier-lang-parser
220
220
 
221
221
  The parser projects text into `@shapeshift-labs/frontier-lang-kernel` documents. The syntax is intentionally small and experimental.
222
222
 
223
+ ## Authored view render graph syntax
224
+
225
+ `.frontier` view blocks can describe UI render graphs directly. Nested `render`
226
+ blocks are flattened into `view.renders`, and each parent stores stable child
227
+ render IDs in `children`. This keeps authored UI structural and target-neutral:
228
+ HTML, JSX, SwiftUI, or another target can lower the same semantic graph without
229
+ the parser claiming browser or runtime equivalence.
230
+
231
+ ```frontier
232
+ view TodoList @id("view_todo_list") {
233
+ reads TodoDb.todos
234
+ dispatches action_add
235
+ prop disabled @id("view_prop_disabled"): Boolean
236
+ event save @id("view_event_save") action action_add input TodoInput
237
+
238
+ render Article @id("render_todo_root") {
239
+ key todo-list-root
240
+
241
+ render Button @id("render_save_button") {
242
+ identity save
243
+ text "Save"
244
+ prop disabled disabled
245
+ on press save
246
+ }
247
+
248
+ render SaveIcon kind component @id("render_save_icon") {
249
+ component Icon
250
+ key save-icon
251
+ prop name "check"
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ The root render becomes a graph node whose `children` reference
258
+ `render_save_button` and `render_save_icon` in source order. Child props, text,
259
+ and events stay attached to the child render node instead of leaking onto the
260
+ parent. `kind component` records `component` instead of a literal HTML tag, so
261
+ target adapters can decide how to project it.
262
+
223
263
  ## Authored target projection syntax
224
264
 
225
265
  `.frontier` target blocks can carry projection contracts next to their emit settings. These rows describe what a target lowering claims to represent, what it still needs proof for, and which losses or missing evidence must stay visible to merge and translation tooling.
@@ -328,6 +368,21 @@ conversion TodoJavascriptToRust @id("conversion_todo_js_rust") {
328
368
 
329
369
  `sourceRuntime` and `targetRuntime` become runtime maps. `runtimeRequirement` rows become proof obligations for host/runtime capabilities, including authored `requiredSignals` denominators such as source hashes, target hashes, probe ids, runtime commands, telemetry hashes, and capability-specific trace hashes. `proofEvidence` and `evidence` attach evidence ids, but the compiler still requires bound evidence records before a proof obligation is satisfied. `dialect` and `extern` rows preserve dialect-specific constructs, projection readiness, loss/evidence ids, and binding metadata without requiring the authored Frontier file to drop down to raw JSON.
330
370
 
371
+ ## Authored dialect registry syntax
372
+
373
+ `.frontier` files can carry reusable dialect registries with `dialectRegistry` or `universalDialectRegistry` blocks. These blocks describe language-specific constructs that should stay visible during translation instead of being silently collapsed into generic stubs.
374
+
375
+ ```frontier
376
+ dialectRegistry RuntimeDialects @id("dialect_registry_runtime") {
377
+ language javascript
378
+ sourcePath src/runtime.ts
379
+ dialect nodeProcess @id("dialect_registry_node_process") dialect node.runtime kind runtime name process.env target rust disposition unsupported readiness blocked loss loss_node_process_projection evidence evidence_node_runtime sourceMap sourcemap_todo_ts
380
+ extern viteRoutes @id("dialect_registry_vite_routes") dialect vite.plugin.virtual-module externKind generatorArtifact target rust disposition runtime-required readiness needs-review evidence evidence_vite_routes_manifest bindingSymbol virtual:routes module vite
381
+ }
382
+ ```
383
+
384
+ The parser projects these rows into `metadata.dialects` as a `frontier.lang.universalDialectRegistry`-shaped object. `dialect` rows become universal dialect records, `extern` rows become universal extern records, and projection fields preserve target disposition, readiness, evidence ids, loss ids, source-map ids, and binding metadata. Registry records are evidence for route scoring, not proof of equivalent behavior: authored metadata keeps `autoMergeClaim` and `semanticEquivalenceClaim` false.
385
+
331
386
  ## Authored possibility spaces
332
387
 
333
388
  `.frontier` files can describe a governed space of valid implementations with `constraintSpace` or `possibilitySpace` blocks. These blocks are metadata-only: they do not add kernel nodes, but they preserve the variables, hard constraints, preferences, collapse strategies, and admission rules that let tools reason about semantic merge, translation, refactoring, and runtime projection as constraint satisfaction.
@@ -0,0 +1,184 @@
1
+ export function parseDialectRegistryBlock(block) {
2
+ const name = nameFrom(block.header);
3
+ const registry = {
4
+ kind: 'frontier.lang.universalDialectRegistry',
5
+ version: 1,
6
+ id: idFrom(block.header, `dialect_registry_${name}`),
7
+ language: readWord('language', block.body),
8
+ dialect: readWord('dialect', block.body),
9
+ sourcePath: readWord('sourcePath', block.body) ?? readWord('path', block.body),
10
+ dialects: [],
11
+ externs: [],
12
+ metadata: {
13
+ authoredDialectRegistry: true,
14
+ authoredDialectRegistryBlockName: name,
15
+ semanticEquivalenceClaim: false,
16
+ autoMergeClaim: false
17
+ }
18
+ };
19
+ for (const rawLine of block.body.split('\n')) {
20
+ const line = rawLine.trim();
21
+ if (!line || line.startsWith('#')) continue;
22
+ const dialect = /^(?:dialect|record)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
23
+ const extern = /^extern\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
24
+ if (dialect) registry.dialects.push(dialectRecord(registry, dialect[1], dialect[2]));
25
+ if (extern) registry.externs.push(externRecord(registry, extern[1], extern[2]));
26
+ }
27
+ return summarizeRegistry(registry);
28
+ }
29
+
30
+ export function mergeDialectRegistryBlocks(blocks = []) {
31
+ if (!blocks.length) return undefined;
32
+ return summarizeRegistry({
33
+ kind: 'frontier.lang.universalDialectRegistry',
34
+ version: 1,
35
+ id: blocks.length === 1 ? blocks[0].id : 'dialect_registry:source',
36
+ language: first(blocks.map((block) => block.language)),
37
+ dialects: uniqueById(blocks.flatMap((block) => block.dialects ?? [])),
38
+ externs: uniqueById(blocks.flatMap((block) => block.externs ?? [])),
39
+ metadata: {
40
+ authoredDialectRegistryBlockIds: blocks.map((block) => block.id),
41
+ semanticEquivalenceClaim: false,
42
+ autoMergeClaim: false
43
+ }
44
+ });
45
+ }
46
+
47
+ function dialectRecord(registry, name, text) {
48
+ return cleanRecord({
49
+ kind: 'frontier.lang.universalDialectRecord',
50
+ version: 1,
51
+ id: idFrom(text, `dialect_${name}`),
52
+ language: readInlineWord('language', text) ?? registry.language,
53
+ dialect: readInlineWord('dialect', text) ?? registry.dialect ?? name,
54
+ constructKind: readInlineWord('constructKind', text) ?? readInlineWord('kind', text) ?? 'runtime',
55
+ name: readInlineWord('name', text) ?? name,
56
+ nativeKind: readInlineWord('nativeKind', text),
57
+ sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text) ?? registry.sourcePath,
58
+ nativeSourceId: readInlineWord('nativeSource', text) ?? readInlineWord('nativeSourceId', text),
59
+ nativeAstId: readInlineWord('nativeAst', text) ?? readInlineWord('nativeAstId', text),
60
+ nativeAstNodeId: readInlineWord('nativeAstNode', text) ?? readInlineWord('nativeAstNodeId', text),
61
+ semanticNodeId: readInlineWord('semanticNode', text) ?? readInlineWord('semanticNodeId', text),
62
+ semanticSymbolId: readInlineWord('symbol', text) ?? readInlineWord('semanticSymbol', text) ?? readInlineWord('semanticSymbolId', text),
63
+ semanticOccurrenceId: readInlineWord('semanticOccurrence', text) ?? readInlineWord('semanticOccurrenceId', text),
64
+ sourceMapId: readInlineWord('sourceMap', text) ?? readInlineWord('sourceMapId', text),
65
+ sourceMapMappingId: readInlineWord('sourceMapMapping', text) ?? readInlineWord('sourceMapMappingId', text),
66
+ externIds: readInlineList(text, 'extern', 'externId', 'externIds') ?? [],
67
+ lossIds: readInlineList(text, 'loss', 'lossId', 'lossIds') ?? [],
68
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceId', 'evidenceIds') ?? [],
69
+ projection: projectionRecord(text),
70
+ metadata: { semanticEquivalenceClaim: false, autoMergeClaim: false }
71
+ });
72
+ }
73
+
74
+ function externRecord(registry, name, text) {
75
+ const binding = cleanRecord({
76
+ module: readInlineWord('module', text),
77
+ path: readInlineWord('bindingPath', text) ?? readInlineWord('path', text),
78
+ symbol: readInlineWord('bindingSymbol', text) ?? readInlineWord('symbol', text),
79
+ abi: readInlineWord('abi', text),
80
+ version: readInlineWord('version', text)
81
+ });
82
+ return cleanRecord({
83
+ kind: 'frontier.lang.universalExternRecord',
84
+ version: 1,
85
+ id: idFrom(text, `extern_${name}`),
86
+ language: readInlineWord('language', text) ?? registry.language,
87
+ dialect: readInlineWord('dialect', text) ?? registry.dialect ?? name,
88
+ externKind: readInlineWord('externKind', text) ?? readInlineWord('kind', text) ?? 'foreignSymbol',
89
+ name: readInlineWord('name', text) ?? name,
90
+ ...(Object.keys(binding).length ? { binding } : {}),
91
+ sourcePath: readInlineWord('sourcePath', text) ?? registry.sourcePath,
92
+ nativeSourceId: readInlineWord('nativeSource', text) ?? readInlineWord('nativeSourceId', text),
93
+ nativeAstId: readInlineWord('nativeAst', text) ?? readInlineWord('nativeAstId', text),
94
+ nativeAstNodeId: readInlineWord('nativeAstNode', text) ?? readInlineWord('nativeAstNodeId', text),
95
+ semanticNodeId: readInlineWord('semanticNode', text) ?? readInlineWord('semanticNodeId', text),
96
+ semanticSymbolId: readInlineWord('semanticSymbol', text) ?? readInlineWord('semanticSymbolId', text),
97
+ sourceMapId: readInlineWord('sourceMap', text) ?? readInlineWord('sourceMapId', text),
98
+ sourceMapMappingId: readInlineWord('sourceMapMapping', text) ?? readInlineWord('sourceMapMappingId', text),
99
+ lossIds: readInlineList(text, 'loss', 'lossId', 'lossIds') ?? [],
100
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceId', 'evidenceIds') ?? [],
101
+ projection: projectionRecord(text),
102
+ metadata: { semanticEquivalenceClaim: false, autoMergeClaim: false }
103
+ });
104
+ }
105
+
106
+ function projectionRecord(text) {
107
+ const lossIds = readInlineList(text, 'projectionLoss', 'projectionLossIds', 'loss', 'lossId', 'lossIds') ?? [];
108
+ const disposition = readInlineWord('disposition', text) ?? (lossIds.length ? 'lossy' : 'review-required');
109
+ return cleanRecord({
110
+ disposition,
111
+ readiness: readInlineWord('readiness', text) ?? readinessForDisposition(disposition),
112
+ targets: readInlineList(text, 'target', 'targets', 'targetLanguage') ?? [],
113
+ lossIds,
114
+ evidenceIds: readInlineList(text, 'projectionEvidence', 'projectionEvidenceIds', 'evidence', 'evidenceId', 'evidenceIds') ?? [],
115
+ sourceMapIds: readInlineList(text, 'sourceMap', 'sourceMapId', 'sourceMapIds'),
116
+ sourceMapMappingIds: readInlineList(text, 'sourceMapMapping', 'sourceMapMappingId', 'sourceMapMappingIds'),
117
+ notes: readInlineList(text, 'note', 'notes')
118
+ });
119
+ }
120
+
121
+ function summarizeRegistry(registry) {
122
+ const dialects = uniqueById(registry.dialects ?? []);
123
+ const externs = uniqueById(registry.externs ?? []);
124
+ const records = [...dialects, ...externs];
125
+ return cleanRecord({
126
+ ...registry,
127
+ dialects,
128
+ externs,
129
+ summary: {
130
+ dialects: dialects.length,
131
+ externs: externs.length,
132
+ records: records.length,
133
+ languages: unique(records.map((record) => record.language)),
134
+ dialectNames: unique(records.map((record) => record.dialect)),
135
+ constructKinds: countBy(dialects.map((record) => record.constructKind)),
136
+ externKinds: countBy(externs.map((record) => record.externKind)),
137
+ lossIds: unique(records.flatMap((record) => record.lossIds ?? [])),
138
+ evidenceIds: unique(records.flatMap((record) => record.evidenceIds ?? [])),
139
+ sourceMapIds: unique(records.flatMap((record) => [record.sourceMapId, ...(record.projection?.sourceMapIds ?? [])])),
140
+ projectionDispositions: countBy(records.map((record) => record.projection?.disposition)),
141
+ projectionReadiness: records.reduce((readiness, record) => maxReadiness(readiness, record.projection?.readiness ?? 'ready'), 'ready'),
142
+ recordsWithLosses: records.filter((record) => (record.lossIds ?? []).length).length,
143
+ recordsWithProjectionEvidence: records.filter((record) => (record.projection?.evidenceIds ?? []).length).length
144
+ }
145
+ });
146
+ }
147
+
148
+ function readinessForDisposition(disposition) {
149
+ if (disposition === 'preserved' || disposition === 'lossless') return 'ready';
150
+ if (disposition === 'lossy' || disposition === 'declaration-only') return 'ready-with-losses';
151
+ if (disposition === 'stub-only' || disposition === 'unsupported') return 'blocked';
152
+ return 'needs-review';
153
+ }
154
+
155
+ function maxReadiness(left, right) {
156
+ const order = ['ready', 'ready-with-losses', 'needs-review', 'blocked'];
157
+ return order.indexOf(right) > order.indexOf(left) ? right : left;
158
+ }
159
+
160
+ function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
161
+ function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Unnamed'; }
162
+ function readWord(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\s,]+)', 'm').exec(body)?.[1]?.trim(); }
163
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
164
+ function readInlineList(text, ...labels) {
165
+ for (const label of labels) {
166
+ const value = readInlineWord(label, text);
167
+ if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
168
+ }
169
+ return undefined;
170
+ }
171
+ function cleanRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
172
+ function unique(values) { return [...new Set(values.filter(Boolean))]; }
173
+ function first(values) { return values.find(Boolean); }
174
+ function uniqueById(records) {
175
+ const seen = new Set();
176
+ return records.filter((record) => {
177
+ if (!record?.id || seen.has(record.id)) return false;
178
+ seen.add(record.id);
179
+ return true;
180
+ });
181
+ }
182
+ function countBy(values) {
183
+ return values.filter(Boolean).reduce((counts, value) => ({ ...counts, [value]: (counts[value] ?? 0) + 1 }), {});
184
+ }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { actionNode, capabilityNode, createDocument, effectNode, entityNode, ext
2
2
  import { parseConstraintSpaceBlock } from './constraint-space.js';
3
3
  import { parseConversionBlock } from './conversion.js';
4
4
  import { parseDecisionGraphBlock } from './decision-graph.js';
5
+ import { parseDialectRegistryBlock } from './dialect-registry.js';
5
6
  import { parseInterlinguaBlock } from './interlingua.js';
6
7
  import { createParsedMetadata } from './metadata.js';
7
8
  import { parseSemanticOperationsBlock } from './operations.js';
@@ -20,6 +21,7 @@ export function parseFrontierSource(source, options = {}) {
20
21
  const conversionBlocks = [];
21
22
  const constraintSpaceBlocks = [];
22
23
  const decisionGraphBlocks = [];
24
+ const dialectRegistryBlocks = [];
23
25
  const interlinguaBlocks = [];
24
26
  const resourceGraphBlocks = [];
25
27
  const nativeSourceBlocks = [];
@@ -48,10 +50,11 @@ export function parseFrontierSource(source, options = {}) {
48
50
  if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') conversionBlocks.push(parseConversionBlock(block));
49
51
  if (block.kind === 'constraintSpace' || block.kind === 'possibilitySpace') constraintSpaceBlocks.push(parseConstraintSpaceBlock(block));
50
52
  if (block.kind === 'decisionGraph' || block.kind === 'admissionGraph') decisionGraphBlocks.push(parseDecisionGraphBlock(block));
53
+ if (block.kind === 'dialectRegistry' || block.kind === 'universalDialectRegistry') dialectRegistryBlocks.push(parseDialectRegistryBlock(block));
51
54
  if (block.kind === 'interlingua' || block.kind === 'universalInterlingua') interlinguaBlocks.push(parseInterlinguaBlock(block));
52
55
  if (block.kind === 'resourceGraph' || block.kind === 'semanticResourceGraph') resourceGraphBlocks.push(parseResourceGraphBlock(block));
53
56
  }
54
- const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, interlinguaBlocks, resourceGraphBlocks, nativeSourceBlocks });
57
+ const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, dialectRegistryBlocks, interlinguaBlocks, resourceGraphBlocks, nativeSourceBlocks });
55
58
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
56
59
  }
57
60
 
@@ -61,7 +64,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
61
64
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
62
65
  function readBlocks(source) {
63
66
  const blocks = [];
64
- const header = /\b(entity|state|action|view|migration|capability|effect|type|extern|lattice|nativeSource|target|proof|paradigm|paradigmSemantics|operations|semanticOperations|conversion|universalConversionPlan|constraintSpace|possibilitySpace|decisionGraph|admissionGraph|interlingua|universalInterlingua|resourceGraph|semanticResourceGraph)\s+([^{}]+)\{/g;
67
+ const header = /\b(entity|state|action|view|migration|capability|effect|type|extern|lattice|nativeSource|target|proof|paradigm|paradigmSemantics|operations|semanticOperations|conversion|universalConversionPlan|constraintSpace|possibilitySpace|decisionGraph|admissionGraph|dialectRegistry|universalDialectRegistry|interlingua|universalInterlingua|resourceGraph|semanticResourceGraph)\s+([^{}]+)\{/g;
65
68
  let match;
66
69
  while ((match = header.exec(source))) {
67
70
  let depth = 1; let index = header.lastIndex;
package/dist/metadata.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { mergeDialectRegistryBlocks } from './dialect-registry.js';
2
+
1
3
  const PROOF_GROUPS = ['contracts', 'refinements', 'invariants', 'termination', 'temporal', 'obligations', 'artifacts', 'assumptions'];
2
4
  const PARADIGM_GROUPS = [
3
5
  'bindingScopes',
@@ -21,7 +23,7 @@ const PARADIGM_GROUPS = [
21
23
  'loweringRecords'
22
24
  ];
23
25
 
24
- export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], interlinguaBlocks = [], resourceGraphBlocks = [], nativeSourceBlocks = [] } = {}) {
26
+ export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], dialectRegistryBlocks = [], interlinguaBlocks = [], resourceGraphBlocks = [], nativeSourceBlocks = [] } = {}) {
25
27
  const metadata = {};
26
28
  if (proofBlocks.length) metadata.proof = mergeProofBlocks(proofBlocks);
27
29
  if (paradigmBlocks.length) metadata.paradigmSemantics = mergeParadigmBlocks(paradigmBlocks);
@@ -29,6 +31,7 @@ export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], op
29
31
  if (conversionBlocks.length) metadata.universalConversionPlan = mergeConversionBlocks(conversionBlocks);
30
32
  if (constraintSpaceBlocks.length) metadata.constraintSpaces = mergeConstraintSpaceBlocks(constraintSpaceBlocks);
31
33
  if (decisionGraphBlocks.length) metadata.decisionGraph = mergeDecisionGraphBlocks(decisionGraphBlocks);
34
+ if (dialectRegistryBlocks.length) metadata.dialects = mergeDialectRegistryBlocks(dialectRegistryBlocks);
32
35
  if (interlinguaBlocks.length) metadata.universalInterlingua = mergeInterlinguaBlocks(interlinguaBlocks);
33
36
  if (resourceGraphBlocks.length) metadata.semanticResourceGraphs = mergeResourceGraphBlocks(resourceGraphBlocks);
34
37
  if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
package/dist/view.js CHANGED
@@ -34,20 +34,32 @@ function readViewEvents(body) {
34
34
  }
35
35
 
36
36
  function readRenderNodes(body) {
37
- return readNestedBlocks('render', body).map((block) => {
38
- const tagName = nameFrom(block.header);
39
- const props = readRenderProps(block.body);
40
- const events = readRenderEvents(block.body);
41
- return {
42
- id: idFrom(block.header, `render_${tagName}`),
43
- kind: readInlineWord('kind', block.header) ?? 'element',
44
- tagName,
45
- identityKey: readLine('identity', block.body) ?? readLine('key', block.body),
46
- text: readQuotedLine('text', block.body),
47
- props: props.length ? props : undefined,
48
- events: events.length ? events : undefined
49
- };
50
- });
37
+ const renders = [];
38
+ for (const block of readNestedBlocks('render', body)) pushRenderNode(block, renders);
39
+ return renders;
40
+ }
41
+
42
+ function pushRenderNode(block, renders) {
43
+ const headerName = nameFrom(block.header);
44
+ const ownBody = stripNestedBlocks('render', block.body);
45
+ const kind = readInlineWord('kind', block.header) ?? readLine('kind', ownBody) ?? 'element';
46
+ const props = readRenderProps(ownBody);
47
+ const events = readRenderEvents(ownBody);
48
+ const childBlocks = readNestedBlocks('render', block.body);
49
+ const explicitChildren = readList('children', ownBody) ?? readList('child', ownBody);
50
+ const nestedChildren = childBlocks.map((child) => idFrom(child.header, `render_${nameFrom(child.header)}`));
51
+ renders.push(compactRecord({
52
+ id: idFrom(block.header, `render_${headerName}`),
53
+ kind,
54
+ tagName: readLine('tag', ownBody) ?? readLine('tagName', ownBody) ?? (kind === 'component' || kind === 'text' ? undefined : headerName),
55
+ component: readLine('component', ownBody) ?? (kind === 'component' ? headerName : undefined),
56
+ identityKey: readLine('identity', ownBody) ?? readLine('key', ownBody),
57
+ text: readQuotedLine('text', ownBody),
58
+ props: props.length ? props : undefined,
59
+ events: events.length ? events : undefined,
60
+ children: uniqueStrings([...(explicitChildren ?? []), ...nestedChildren])
61
+ }));
62
+ for (const child of childBlocks) pushRenderNode(child, renders);
51
63
  }
52
64
 
53
65
  function readRenderProps(body) {
@@ -92,6 +104,8 @@ function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n
92
104
  function readQuotedLine(label, body) { return new RegExp(`^\\s*${label}\\s+["']([^"']+)["']`, 'm').exec(body)?.[1]; }
93
105
  function readInlineWord(label, text = '') { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
94
106
  function readRenderValue(value) { const quoted = /^["']([^"']+)["']$/.exec(value.trim()); return quoted ? { value: quoted[1] } : { expression: value.trim() }; }
107
+ function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
108
+ function uniqueStrings(values) { const result = []; for (const value of values) if (value && !result.includes(value)) result.push(value); return result.length ? result : undefined; }
95
109
  function parseOptionalTypeExpression(value) { return value ? parseTypeExpression(value.trim()) : undefined; }
96
110
  function parseTypeExpression(value) {
97
111
  const text = value.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
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/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs",
25
+ "test": "npm run build && node test/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",