@shapeshift-labs/frontier-lang-parser 0.3.14 → 0.3.16
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 +39 -0
- package/dist/conversion.js +61 -0
- package/dist/index.js +9 -29
- package/dist/metadata.js +19 -1
- package/dist/source-evidence.js +230 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -220,6 +220,45 @@ 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 conversion syntax
|
|
224
|
+
|
|
225
|
+
`.frontier` files can carry universal conversion evidence directly in `conversion` or `universalConversionPlan` blocks. The parser projects these records into `metadata.universalConversionPlan` for the compiler facade and downstream semantic merge tooling.
|
|
226
|
+
|
|
227
|
+
```frontier
|
|
228
|
+
conversion TodoJavascriptToRust @id("conversion_todo_js_rust") {
|
|
229
|
+
sourceLanguage javascript
|
|
230
|
+
target rust
|
|
231
|
+
sourceRuntime javascript node
|
|
232
|
+
targetRuntime rust cli
|
|
233
|
+
runtimeRequirement fetchRuntime @id("runtime_requirement_fetch") capability fetch sourceRuntime node targetRuntime cli evidence artifact_todo_title_probe
|
|
234
|
+
dialect nodeProcess @id("dialect_node_process") language javascript dialect node.runtime kind runtime target rust disposition unsupported readiness blocked loss loss_node_process_projection
|
|
235
|
+
extern viteRoutes @id("extern_vite_routes") language javascript dialect vite.plugin.virtual-module externKind generatorArtifact target rust disposition runtime-required evidence evidence_vite_routes_manifest bindingSymbol virtual:routes
|
|
236
|
+
constraint type publicApi @id("type_constraint_public_api") role source kind public-function symbol symbol:addTodo signatureHash sig_add_todo evidence artifact_todo_title_probe
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`sourceRuntime` and `targetRuntime` become runtime maps. `runtimeRequirement` rows become proof obligations for host/runtime capabilities. `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.
|
|
241
|
+
|
|
242
|
+
## Authored native source evidence
|
|
243
|
+
|
|
244
|
+
`nativeSource` blocks can also carry source-bound merge evidence. This keeps parser/source-map/merge-candidate facts in `.frontier` text instead of requiring a raw JSON sidecar for the first authored program slice.
|
|
245
|
+
|
|
246
|
+
```frontier
|
|
247
|
+
nativeSource TodoTs @id("native_todo_ts") {
|
|
248
|
+
language typescript
|
|
249
|
+
parser typescript
|
|
250
|
+
sourcePath src/todo.ts
|
|
251
|
+
sourceHash sha256:example
|
|
252
|
+
frontierNodes ent_todo, action_add
|
|
253
|
+
evidence todoTitleProbe @id("artifact_todo_title_probe") kind test status passed path reports/todo-title.json
|
|
254
|
+
sourceMap todoProjection @id("sourcemap_todo_ts") target typescript targetPath src/generated/todo.ts evidence artifact_todo_title_probe
|
|
255
|
+
mapping todoTitle @id("map_todo_title") sourceMap sourcemap_todo_ts semanticNode field_title sourceSpan src/todo.ts:1:1-1:12 generatedSpan src/generated/todo.ts:1:1-1:20 precision exact evidence artifact_todo_title_probe
|
|
256
|
+
mergeCandidate todoTitle @id("candidate_todo_title") symbol symbol:Todo.title semanticNode field_title conflictKey symbol:Todo.title readiness ready sourceMap sourcemap_todo_ts sourceMapMapping map_todo_title
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The parser projects these rows into `metadata.universalAst.sourceMaps`, `metadata.universalAst.mergeCandidates`, and `metadata.universalAst.evidence`. `nativeSource` nodes keep id links through `sourceMapIds`, `mergeCandidateIds`, and `evidenceIds`. Runtime/browser equivalence still requires separate proof artifacts.
|
|
261
|
+
|
|
223
262
|
## Benchmarks
|
|
224
263
|
|
|
225
264
|
Run the package-local benchmark with:
|
package/dist/conversion.js
CHANGED
|
@@ -46,14 +46,75 @@ export function parseConversionBlock(block) {
|
|
|
46
46
|
if (!line || line.startsWith('#')) continue;
|
|
47
47
|
const target = /^target\s+([^\s,]+)/.exec(line)?.[1];
|
|
48
48
|
const sourceLanguage = /^sourceLanguage\s+([^\s,]+)/.exec(line)?.[1] ?? /^source\s+([^\s,]+)/.exec(line)?.[1];
|
|
49
|
+
const sourceRuntime = /^sourceRuntime\s+([^\s,]+)\s+([^\s,]+)/.exec(line);
|
|
50
|
+
const targetRuntime = /^targetRuntime\s+([^\s,]+)\s+([^\s,]+)/.exec(line);
|
|
51
|
+
const runtimeRequirement = /^(?:runtimeRequirement|requiredRuntime|requiresRuntime)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
52
|
+
const dialect = /^dialect\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
53
|
+
const extern = /^extern\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
49
54
|
const constraint = /^constraint\s+([A-Za-z_$][\w$-]*)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
50
55
|
if (target) plan.targets.push(target);
|
|
51
56
|
else if (sourceLanguage) plan.sourceLanguage = sourceLanguage;
|
|
57
|
+
else if (sourceRuntime) plan.sourceRuntimes = { ...(plan.sourceRuntimes ?? {}), [sourceRuntime[1]]: sourceRuntime[2] };
|
|
58
|
+
else if (targetRuntime) plan.targetRuntimes = { ...(plan.targetRuntimes ?? {}), [targetRuntime[1]]: targetRuntime[2] };
|
|
59
|
+
else if (runtimeRequirement) addRuntimeRequirement(plan, runtimeRequirement[1], runtimeRequirement[2]);
|
|
60
|
+
else if (dialect) addDialectRecord(plan, dialect[1], dialect[2], false);
|
|
61
|
+
else if (extern) addDialectRecord(plan, extern[1], extern[2], true);
|
|
52
62
|
else if (constraint) addConstraint(plan, constraint[1], constraint[2], constraint[3]);
|
|
53
63
|
}
|
|
54
64
|
return cleanRecord({ ...plan, targets: unique(plan.targets) });
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
function addRuntimeRequirement(plan, name, text) {
|
|
68
|
+
plan.runtimeRequirements = [...(plan.runtimeRequirements ?? []), cleanRecord({
|
|
69
|
+
id: idFrom(text, `runtime_requirement_${name}`),
|
|
70
|
+
name,
|
|
71
|
+
sourceLanguage: readInlineWord('sourceLanguage', text) ?? plan.sourceLanguage,
|
|
72
|
+
target: readInlineWord('target', text) ?? readInlineWord('targetLanguage', text) ?? plan.targets[0],
|
|
73
|
+
capability: readInlineWord('capability', text) ?? readInlineWord('kind', text) ?? name,
|
|
74
|
+
kind: readInlineWord('kind', text),
|
|
75
|
+
sourceRuntime: readInlineWord('sourceRuntime', text) ?? readInlineWord('runtime', text),
|
|
76
|
+
targetRuntime: readInlineWord('targetRuntime', text),
|
|
77
|
+
sourceHost: readInlineWord('sourceHost', text),
|
|
78
|
+
targetHost: readInlineWord('targetHost', text),
|
|
79
|
+
reason: readInlineQuoted('reason', text) ?? readInlineWord('reason', text),
|
|
80
|
+
evidenceIds: readInlineList(text, 'evidence', 'evidenceIds')
|
|
81
|
+
})];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addDialectRecord(plan, name, text, isExtern) {
|
|
85
|
+
const projection = cleanRecord({
|
|
86
|
+
disposition: readInlineWord('disposition', text),
|
|
87
|
+
readiness: readInlineWord('readiness', text),
|
|
88
|
+
targets: readInlineList(text, 'target', 'targets', 'targetLanguage') ?? plan.targets,
|
|
89
|
+
evidenceIds: readInlineList(text, 'projectionEvidence', 'projectionEvidenceIds', 'evidence', 'evidenceIds'),
|
|
90
|
+
lossIds: readInlineList(text, 'projectionLoss', 'projectionLossIds', 'loss', 'lossId', 'lossIds')
|
|
91
|
+
});
|
|
92
|
+
const record = cleanRecord({
|
|
93
|
+
id: idFrom(text, `${isExtern ? 'extern' : 'dialect'}_${name}`),
|
|
94
|
+
language: readInlineWord('language', text) ?? plan.sourceLanguage,
|
|
95
|
+
dialect: readInlineWord('dialect', text) ?? name,
|
|
96
|
+
name: readInlineWord('name', text) ?? name,
|
|
97
|
+
nativeKind: readInlineWord('nativeKind', text),
|
|
98
|
+
sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text),
|
|
99
|
+
nativeSourceId: readInlineWord('nativeSource', text) ?? readInlineWord('nativeSourceId', text),
|
|
100
|
+
nativeAstId: readInlineWord('nativeAst', text) ?? readInlineWord('nativeAstId', text),
|
|
101
|
+
nativeAstNodeId: readInlineWord('nativeAstNode', text) ?? readInlineWord('nativeAstNodeId', text),
|
|
102
|
+
semanticNodeId: readInlineWord('semanticNode', text) ?? readInlineWord('semanticNodeId', text),
|
|
103
|
+
semanticSymbolId: readInlineWord('symbol', text) ?? readInlineWord('semanticSymbolId', text),
|
|
104
|
+
sourceMapId: readInlineWord('sourceMap', text) ?? readInlineWord('sourceMapId', text),
|
|
105
|
+
sourceMapMappingId: readInlineWord('sourceMapMapping', text) ?? readInlineWord('sourceMapMappingId', text),
|
|
106
|
+
lossIds: readInlineList(text, 'loss', 'lossId', 'lossIds'),
|
|
107
|
+
evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
|
|
108
|
+
projection
|
|
109
|
+
});
|
|
110
|
+
if (isExtern) {
|
|
111
|
+
const binding = cleanRecord({ module: readInlineWord('module', text), path: readInlineWord('bindingPath', text), symbol: readInlineWord('bindingSymbol', text) ?? readInlineWord('symbol', text), abi: readInlineWord('abi', text), version: readInlineWord('version', text) });
|
|
112
|
+
plan.externs = [...(plan.externs ?? []), cleanRecord({ ...record, externKind: readInlineWord('externKind', text) ?? readInlineWord('kind', text) ?? 'extern', ...(Object.keys(binding).length ? { binding } : {}) })];
|
|
113
|
+
} else {
|
|
114
|
+
plan.dialects = [...(plan.dialects ?? []), cleanRecord({ ...record, constructKind: readInlineWord('constructKind', text) ?? readInlineWord('kind', text) ?? 'runtime', externIds: readInlineList(text, 'extern', 'externIds') })];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
57
118
|
function addConstraint(plan, family, name, text) {
|
|
58
119
|
const config = FAMILIES[family] ?? { field: family.endsWith('s') ? family : `${family}Constraints`, sourceKey: 'sourceRecords', targetKey: 'targetRecords' };
|
|
59
120
|
const role = readInlineWord('role', text) ?? 'source';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { actionNode, capabilityNode, createDocument, effectNode, entityNode, externNode, latticeNode, migrationNode,
|
|
1
|
+
import { actionNode, capabilityNode, createDocument, effectNode, entityNode, externNode, latticeNode, migrationNode, stateNode, targetNode, typeNode } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import { parseConversionBlock } from './conversion.js';
|
|
3
3
|
import { createParsedMetadata } from './metadata.js';
|
|
4
4
|
import { parseSemanticOperationsBlock } from './operations.js';
|
|
5
5
|
import { parseParadigmBlock } from './paradigm.js';
|
|
6
6
|
import { parseProofBlock } from './proof.js';
|
|
7
|
+
import { parseNativeSourceBlock } from './source-evidence.js';
|
|
7
8
|
import { parseViewBlock } from './view.js';
|
|
8
9
|
|
|
9
10
|
export function parseFrontierSource(source, options = {}) {
|
|
@@ -12,6 +13,7 @@ export function parseFrontierSource(source, options = {}) {
|
|
|
12
13
|
const paradigmBlocks = [];
|
|
13
14
|
const operationBlocks = [];
|
|
14
15
|
const conversionBlocks = [];
|
|
16
|
+
const nativeSourceBlocks = [];
|
|
15
17
|
const documentId = options.id ?? readId(source) ?? 'mod_frontier';
|
|
16
18
|
const documentName = options.name ?? readName(source) ?? 'FrontierModule';
|
|
17
19
|
for (const block of readBlocks(source)) {
|
|
@@ -25,14 +27,18 @@ export function parseFrontierSource(source, options = {}) {
|
|
|
25
27
|
if (block.kind === 'type') nodes.push(parseType(block));
|
|
26
28
|
if (block.kind === 'extern') nodes.push(parseExtern(block));
|
|
27
29
|
if (block.kind === 'lattice') nodes.push(parseLattice(block));
|
|
28
|
-
if (block.kind === 'nativeSource')
|
|
30
|
+
if (block.kind === 'nativeSource') {
|
|
31
|
+
const parsed = parseNativeSourceBlock(block);
|
|
32
|
+
nodes.push(parsed.node);
|
|
33
|
+
nativeSourceBlocks.push(parsed);
|
|
34
|
+
}
|
|
29
35
|
if (block.kind === 'target') nodes.push(parseTarget(block));
|
|
30
36
|
if (block.kind === 'proof') proofBlocks.push(parseProofBlock(block));
|
|
31
37
|
if (block.kind === 'paradigm' || block.kind === 'paradigmSemantics') paradigmBlocks.push(parseParadigmBlock(block));
|
|
32
38
|
if (block.kind === 'operations' || block.kind === 'semanticOperations') operationBlocks.push(parseSemanticOperationsBlock(block));
|
|
33
39
|
if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') conversionBlocks.push(parseConversionBlock(block));
|
|
34
40
|
}
|
|
35
|
-
const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks });
|
|
41
|
+
const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, nativeSourceBlocks });
|
|
36
42
|
return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
|
|
37
43
|
}
|
|
38
44
|
|
|
@@ -173,32 +179,6 @@ function parseLattice(block) {
|
|
|
173
179
|
} : undefined
|
|
174
180
|
});
|
|
175
181
|
}
|
|
176
|
-
function parseNativeSource(block) {
|
|
177
|
-
const name = nameFrom(block.header);
|
|
178
|
-
const losses = [];
|
|
179
|
-
const lossRe = /^\s*loss\s+([A-Za-z][\w-]*)\s+["']([^"']+)["'](?:\s+severity\s+([A-Za-z][\w-]*))?/gm;
|
|
180
|
-
let match;
|
|
181
|
-
while ((match = lossRe.exec(block.body))) {
|
|
182
|
-
losses.push({
|
|
183
|
-
id: `loss_${name}_${losses.length}`,
|
|
184
|
-
kind: match[1],
|
|
185
|
-
message: match[2],
|
|
186
|
-
severity: match[3] ?? 'warning'
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
return nativeSourceNode({
|
|
190
|
-
id: idFrom(block.header, `native_${name}`),
|
|
191
|
-
name,
|
|
192
|
-
language: readWord('language', block.body) ?? name,
|
|
193
|
-
parser: readWord('parser', block.body),
|
|
194
|
-
parserVersion: readWord('parserVersion', block.body),
|
|
195
|
-
sourcePath: readWord('sourcePath', block.body) ?? readWord('path', block.body),
|
|
196
|
-
sourceHash: readWord('sourceHash', block.body),
|
|
197
|
-
symbol: readWord('symbol', block.body),
|
|
198
|
-
frontierNodeIds: readList('frontierNodes', block.body),
|
|
199
|
-
losses: losses.length ? losses : undefined
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
182
|
function parseTarget(block) {
|
|
203
183
|
const name = nameFrom(block.header);
|
|
204
184
|
return targetNode({
|
package/dist/metadata.js
CHANGED
|
@@ -21,12 +21,15 @@ const PARADIGM_GROUPS = [
|
|
|
21
21
|
'loweringRecords'
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [] } = {}) {
|
|
24
|
+
export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], nativeSourceBlocks = [] } = {}) {
|
|
25
25
|
const metadata = {};
|
|
26
26
|
if (proofBlocks.length) metadata.proof = mergeProofBlocks(proofBlocks);
|
|
27
27
|
if (paradigmBlocks.length) metadata.paradigmSemantics = mergeParadigmBlocks(paradigmBlocks);
|
|
28
28
|
if (operationBlocks.length) metadata.semanticOperations = mergeOperationBlocks(operationBlocks);
|
|
29
29
|
if (conversionBlocks.length) metadata.universalConversionPlan = mergeConversionBlocks(conversionBlocks);
|
|
30
|
+
if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
|
|
31
|
+
metadata.universalAst = mergeNativeSourceBlocks(nativeSourceBlocks);
|
|
32
|
+
}
|
|
30
33
|
return Object.keys(metadata).length ? metadata : undefined;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -66,7 +69,22 @@ function mergeConversionBlocks(blocks) {
|
|
|
66
69
|
if (block.sourceLanguage && !plan.sourceLanguage) plan.sourceLanguage = block.sourceLanguage;
|
|
67
70
|
for (const [key, value] of Object.entries(block)) {
|
|
68
71
|
if (Array.isArray(value) && key !== 'targets') plan[key] = [...(plan[key] ?? []), ...value];
|
|
72
|
+
else if ((key === 'sourceRuntimes' || key === 'targetRuntimes') && value && typeof value === 'object') {
|
|
73
|
+
plan[key] = { ...(plan[key] ?? {}), ...value };
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
return plan;
|
|
72
78
|
}
|
|
79
|
+
|
|
80
|
+
function mergeNativeSourceBlocks(blocks) {
|
|
81
|
+
return {
|
|
82
|
+
id: blocks.length === 1 ? `universalAst:${blocks[0].node.id}` : 'universalAst:source',
|
|
83
|
+
nativeSourceIds: blocks.map((block) => block.node.id),
|
|
84
|
+
sourceMaps: blocks.flatMap((block) => block.sourceMaps ?? []),
|
|
85
|
+
mergeCandidates: blocks.flatMap((block) => block.mergeCandidates ?? []),
|
|
86
|
+
evidence: blocks.flatMap((block) => block.evidence ?? []),
|
|
87
|
+
losses: blocks.flatMap((block) => block.losses ?? []),
|
|
88
|
+
metadata: { authoredNativeSourceIds: blocks.map((block) => block.node.id) }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createSemanticMergeCandidateRecord, createSourceMapRecord, nativeSourceNode } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
|
|
3
|
+
export function parseNativeSourceBlock(block) {
|
|
4
|
+
const name = nameFrom(block.header);
|
|
5
|
+
const losses = parseLosses(name, block.body);
|
|
6
|
+
const nativeSourceId = idFrom(block.header, `native_${name}`);
|
|
7
|
+
const evidence = parseEvidenceRecords(block.body);
|
|
8
|
+
const sourceMaps = parseSourceMaps(block.body, {
|
|
9
|
+
nativeSourceId,
|
|
10
|
+
sourcePath: readWord('sourcePath', block.body) ?? readWord('path', block.body),
|
|
11
|
+
sourceHash: readWord('sourceHash', block.body),
|
|
12
|
+
evidence
|
|
13
|
+
});
|
|
14
|
+
const mergeCandidates = parseMergeCandidates(block.body, {
|
|
15
|
+
nativeSourceId,
|
|
16
|
+
language: readWord('language', block.body) ?? name,
|
|
17
|
+
sourcePath: readWord('sourcePath', block.body) ?? readWord('path', block.body),
|
|
18
|
+
sourceMaps,
|
|
19
|
+
evidence
|
|
20
|
+
});
|
|
21
|
+
const node = nativeSourceNode({
|
|
22
|
+
id: nativeSourceId,
|
|
23
|
+
name,
|
|
24
|
+
language: readWord('language', block.body) ?? name,
|
|
25
|
+
parser: readWord('parser', block.body),
|
|
26
|
+
parserVersion: readWord('parserVersion', block.body),
|
|
27
|
+
sourcePath: readWord('sourcePath', block.body) ?? readWord('path', block.body),
|
|
28
|
+
sourceHash: readWord('sourceHash', block.body),
|
|
29
|
+
symbol: readWord('symbol', block.body),
|
|
30
|
+
frontierNodeIds: readList('frontierNodes', block.body),
|
|
31
|
+
sourceMapIds: sourceMaps.map((sourceMap) => sourceMap.id),
|
|
32
|
+
mergeCandidateIds: mergeCandidates.map((candidate) => candidate.id),
|
|
33
|
+
evidenceIds: evidence.map((record) => record.id),
|
|
34
|
+
losses: losses.length ? losses : undefined
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
node,
|
|
38
|
+
sourceMaps,
|
|
39
|
+
mergeCandidates,
|
|
40
|
+
evidence,
|
|
41
|
+
losses
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseLosses(name, body) {
|
|
46
|
+
const losses = [];
|
|
47
|
+
const lossRe = /^\s*loss\s+([A-Za-z][\w-]*)\s+["']([^"']+)["'](?:\s+severity\s+([A-Za-z][\w-]*))?/gm;
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = lossRe.exec(body))) {
|
|
50
|
+
losses.push({
|
|
51
|
+
id: `loss_${name}_${losses.length}`,
|
|
52
|
+
kind: match[1],
|
|
53
|
+
message: match[2],
|
|
54
|
+
severity: match[3] ?? 'warning'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return losses;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseEvidenceRecords(body) {
|
|
61
|
+
const records = [];
|
|
62
|
+
for (const { name, rest } of matchingRows(body, /^(?:evidence|proofEvidence)\s+([A-Za-z_$][\w$-]*)(.*)$/)) {
|
|
63
|
+
records.push(cleanRecord({
|
|
64
|
+
id: idFrom(rest, `evidence_${name}`),
|
|
65
|
+
kind: readInlineWord('kind', rest) ?? 'note',
|
|
66
|
+
status: readInlineWord('status', rest) ?? 'unknown',
|
|
67
|
+
path: readInlineWord('path', rest),
|
|
68
|
+
summary: readInlineQuoted('summary', rest),
|
|
69
|
+
metadata: { name }
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
return records;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseSourceMaps(body, context) {
|
|
76
|
+
const rows = matchingRows(body, /^(?:sourceMap|sourcemap)\s+([A-Za-z_$][\w$-]*)(.*)$/);
|
|
77
|
+
const evidenceById = new Map(context.evidence.map((record) => [record.id, record]));
|
|
78
|
+
const mappingRows = matchingRows(body, /^(?:mapping|sourceMapMapping)\s+([A-Za-z_$][\w$-]*)(.*)$/);
|
|
79
|
+
const defaultSourceMapId = rows.length === 1 ? idFrom(rows[0].rest, `sourcemap_${rows[0].name}`) : undefined;
|
|
80
|
+
return rows.map(({ name, rest }) => {
|
|
81
|
+
const id = idFrom(rest, `sourcemap_${name}`);
|
|
82
|
+
const evidenceIds = readInlineList(rest, 'evidence', 'evidenceIds');
|
|
83
|
+
const mappings = mappingRows
|
|
84
|
+
.filter((row) => (readInlineWord('sourceMap', row.rest) ?? readInlineWord('sourceMapId', row.rest) ?? defaultSourceMapId) === id)
|
|
85
|
+
.map((row) => parseSourceMapMapping(row.name, row.rest, {
|
|
86
|
+
sourceMapId: id,
|
|
87
|
+
nativeSourceId: context.nativeSourceId,
|
|
88
|
+
sourcePath: readInlineWord('sourcePath', rest) ?? readInlineWord('path', rest) ?? context.sourcePath
|
|
89
|
+
}));
|
|
90
|
+
return createSourceMapRecord(cleanRecord({
|
|
91
|
+
id,
|
|
92
|
+
sourcePath: readInlineWord('sourcePath', rest) ?? readInlineWord('path', rest) ?? context.sourcePath,
|
|
93
|
+
sourceHash: readInlineWord('sourceHash', rest) ?? context.sourceHash,
|
|
94
|
+
target: readInlineWord('target', rest),
|
|
95
|
+
targetPath: readInlineWord('targetPath', rest),
|
|
96
|
+
targetHash: readInlineWord('targetHash', rest),
|
|
97
|
+
semanticIndexId: readInlineWord('semanticIndex', rest) ?? readInlineWord('semanticIndexId', rest),
|
|
98
|
+
universalAstId: readInlineWord('universalAst', rest) ?? readInlineWord('universalAstId', rest),
|
|
99
|
+
nativeAstId: readInlineWord('nativeAst', rest) ?? readInlineWord('nativeAstId', rest),
|
|
100
|
+
nativeSourceId: readInlineWord('nativeSource', rest) ?? readInlineWord('nativeSourceId', rest) ?? context.nativeSourceId,
|
|
101
|
+
mappings,
|
|
102
|
+
evidence: evidenceIds?.map((evidenceId) => evidenceById.get(evidenceId) ?? { id: evidenceId, kind: 'note', status: 'unknown' })
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseSourceMapMapping(name, text, context) {
|
|
108
|
+
return cleanRecord({
|
|
109
|
+
id: idFrom(text, `mapping_${name}`),
|
|
110
|
+
semanticNodeId: readInlineWord('semanticNode', text) ?? readInlineWord('semanticNodeId', text),
|
|
111
|
+
nativeSourceId: readInlineWord('nativeSource', text) ?? readInlineWord('nativeSourceId', text) ?? context.nativeSourceId,
|
|
112
|
+
nativeAstNodeId: readInlineWord('nativeAstNode', text) ?? readInlineWord('nativeAstNodeId', text),
|
|
113
|
+
semanticSymbolId: readInlineWord('semanticSymbol', text) ?? readInlineWord('symbol', text) ?? readInlineWord('semanticSymbolId', text),
|
|
114
|
+
semanticOccurrenceId: readInlineWord('semanticOccurrence', text) ?? readInlineWord('semanticOccurrenceId', text),
|
|
115
|
+
mergeCandidateId: readInlineWord('mergeCandidate', text) ?? readInlineWord('mergeCandidateId', text),
|
|
116
|
+
sourceSpan: parseSpan(readInlineWord('sourceSpan', text), context.sourcePath),
|
|
117
|
+
generatedSpan: parseSpan(readInlineWord('generatedSpan', text), readInlineWord('targetPath', text)),
|
|
118
|
+
target: readInlineWord('target', text),
|
|
119
|
+
generatedName: readInlineWord('generatedName', text),
|
|
120
|
+
evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
|
|
121
|
+
lossIds: readInlineList(text, 'loss', 'lossIds'),
|
|
122
|
+
precision: readInlineWord('precision', text) ?? 'unknown',
|
|
123
|
+
preservation: readInlineWord('preservation', text),
|
|
124
|
+
metadata: cleanRecord({
|
|
125
|
+
sourceMapId: context.sourceMapId,
|
|
126
|
+
ownershipRegionId: readInlineWord('ownershipRegion', text) ?? readInlineWord('ownershipRegionId', text),
|
|
127
|
+
ownershipRegionKey: readInlineWord('ownershipKey', text) ?? readInlineWord('ownershipRegionKey', text)
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseMergeCandidates(body, context) {
|
|
133
|
+
const sourceMapIds = context.sourceMaps.map((sourceMap) => sourceMap.id);
|
|
134
|
+
return matchingRows(body, /^(?:mergeCandidate|candidate)\s+([A-Za-z_$][\w$-]*)(.*)$/).map(({ name, rest }) => {
|
|
135
|
+
const conflictKeys = readInlineList(rest, 'conflictKey', 'conflictKeys', 'ownershipKey', 'ownershipKeys');
|
|
136
|
+
const symbols = readInlineList(rest, 'semanticSymbol', 'semanticSymbols', 'symbol', 'symbols', 'semanticSymbolIds');
|
|
137
|
+
const nodes = readInlineList(rest, 'semanticNode', 'semanticNodes', 'semanticNodeIds');
|
|
138
|
+
const evidenceIds = readInlineList(rest, 'evidence', 'evidenceIds');
|
|
139
|
+
const sourceMapMappingIds = readInlineList(rest, 'sourceMapMapping', 'sourceMapMappings', 'sourceMapMappingIds');
|
|
140
|
+
return createSemanticMergeCandidateRecord(cleanRecord({
|
|
141
|
+
id: idFrom(rest, `merge_candidate_${name}`),
|
|
142
|
+
language: readInlineWord('language', rest) ?? context.language,
|
|
143
|
+
sourcePath: readInlineWord('sourcePath', rest) ?? readInlineWord('path', rest) ?? context.sourcePath,
|
|
144
|
+
baseHash: readInlineWord('baseHash', rest),
|
|
145
|
+
targetHash: readInlineWord('targetHash', rest),
|
|
146
|
+
touchedSymbols: symbols?.map((symbol) => ({ id: symbol, conflictKey: conflictKeys?.[0] ?? symbol })),
|
|
147
|
+
touchedSemanticNodes: nodes?.map((node) => ({ id: node, conflictKey: conflictKeys?.[0] ?? node })),
|
|
148
|
+
nativeSpans: parseCandidateNativeSpans(rest, context),
|
|
149
|
+
conflictKeys,
|
|
150
|
+
readiness: readInlineWord('readiness', rest),
|
|
151
|
+
reasons: uniqueList([readInlineQuoted('reason', rest), ...(readInlineList(rest, 'reasonCode', 'reasonCodes', 'reasons') ?? [])]),
|
|
152
|
+
evidence: evidenceIds?.map((evidenceId) => context.evidence.find((record) => record.id === evidenceId) ?? { id: evidenceId, kind: 'note', status: 'unknown' }),
|
|
153
|
+
metadata: cleanRecord({
|
|
154
|
+
name,
|
|
155
|
+
nativeSourceId: readInlineWord('nativeSource', rest) ?? readInlineWord('nativeSourceId', rest) ?? context.nativeSourceId,
|
|
156
|
+
sourceMapIds: readInlineList(rest, 'sourceMap', 'sourceMaps', 'sourceMapIds') ?? sourceMapIds,
|
|
157
|
+
sourceMapMappingIds,
|
|
158
|
+
ownershipKeys: readInlineList(rest, 'ownershipKey', 'ownershipKeys')
|
|
159
|
+
})
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseCandidateNativeSpans(text, context) {
|
|
165
|
+
const span = parseSpan(readInlineWord('sourceSpan', text), context.sourcePath);
|
|
166
|
+
const nativeAstNodeId = readInlineWord('nativeAstNode', text) ?? readInlineWord('nativeAstNodeId', text);
|
|
167
|
+
if (!span && !nativeAstNodeId) return undefined;
|
|
168
|
+
const conflictKey = readInlineList(text, 'conflictKey', 'conflictKeys')?.[0]
|
|
169
|
+
?? readInlineWord('semanticSymbol', text)
|
|
170
|
+
?? readInlineWord('symbol', text)
|
|
171
|
+
?? nativeAstNodeId
|
|
172
|
+
?? 'native-span';
|
|
173
|
+
return [cleanRecord({
|
|
174
|
+
id: readInlineWord('nativeSpan', text) ?? `native_span_${hashableId(conflictKey)}`,
|
|
175
|
+
path: span?.path ?? context.sourcePath,
|
|
176
|
+
language: readInlineWord('language', text) ?? context.language,
|
|
177
|
+
nativeAstNodeId,
|
|
178
|
+
semanticNodeId: readInlineWord('semanticNode', text) ?? readInlineWord('semanticNodeId', text),
|
|
179
|
+
symbolId: readInlineWord('semanticSymbol', text) ?? readInlineWord('symbol', text),
|
|
180
|
+
span,
|
|
181
|
+
conflictKey
|
|
182
|
+
})];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseSpan(value, fallbackPath) {
|
|
186
|
+
if (!value) return undefined;
|
|
187
|
+
const match = /^(.+?):(\d+):(\d+)-(\d+):(\d+)$/.exec(value);
|
|
188
|
+
if (!match) return { path: value };
|
|
189
|
+
return {
|
|
190
|
+
path: match[1] || fallbackPath,
|
|
191
|
+
startLine: Number(match[2]),
|
|
192
|
+
startColumn: Number(match[3]),
|
|
193
|
+
endLine: Number(match[4]),
|
|
194
|
+
endColumn: Number(match[5])
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function matchingRows(body, pattern) {
|
|
199
|
+
const rows = [];
|
|
200
|
+
for (const rawLine of body.split('\n')) {
|
|
201
|
+
const line = rawLine.trim();
|
|
202
|
+
if (!line || line.startsWith('#')) continue;
|
|
203
|
+
const match = pattern.exec(line);
|
|
204
|
+
if (match) rows.push({ name: match[1], rest: match[2] ?? '' });
|
|
205
|
+
}
|
|
206
|
+
return rows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
|
|
210
|
+
function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Unnamed'; }
|
|
211
|
+
function readWord(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\s]+)', 'm').exec(body)?.[1]?.trim(); }
|
|
212
|
+
function readList(label, body) { const line = new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]; return line ? line.split(',').map((item) => item.trim()).filter(Boolean) : undefined; }
|
|
213
|
+
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
214
|
+
function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
|
|
215
|
+
function readInlineList(text, ...labels) {
|
|
216
|
+
for (const label of labels) {
|
|
217
|
+
const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
|
|
218
|
+
if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
function cleanRecord(record) {
|
|
223
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0) && (!value || typeof value !== 'object' || Object.keys(value).length > 0)));
|
|
224
|
+
}
|
|
225
|
+
function uniqueList(values) {
|
|
226
|
+
return [...new Set(values.filter(Boolean))];
|
|
227
|
+
}
|
|
228
|
+
function hashableId(value) {
|
|
229
|
+
return String(value ?? 'unknown').replace(/[^A-Za-z0-9_$-]+/g, '_');
|
|
230
|
+
}
|