@shapeshift-labs/frontier-lang-parser 0.3.33 → 0.3.35
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 +16 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +3 -1
- package/dist/source-syntax-report.js +284 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -220,6 +220,22 @@ 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
|
+
## Source syntax reports
|
|
224
|
+
|
|
225
|
+
Use `inspectFrontierSourceSyntax` when a `.frontier` file is being treated as authored source, not only as convenient input to `parseFrontierSource`. It returns a `frontier.lang.sourceSyntaxReport` with every module-level or top-level declaration block the parser can see, whether that block kind is recognized, and any unsupported block kinds that would make a translation or merge proof fail closed.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { inspectFrontierSourceSyntax } from '@shapeshift-labs/frontier-lang-parser';
|
|
229
|
+
|
|
230
|
+
const report = inspectFrontierSourceSyntax(source);
|
|
231
|
+
|
|
232
|
+
if (report.summary.failClosed) {
|
|
233
|
+
console.error(report.summary.unknownKinds, report.diagnostics);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Nested child syntax such as `render` blocks inside a `view` is not reported as an unknown top-level block; the parent parser owns those child rows. The report ignores braces inside quoted strings and comments when finding authored declaration boundaries, reports UTF-8 `sourceBytes`, and fails closed for malformed or unterminated blocks through `diagnostics`. The report is evidence about parser coverage only. It keeps `autoMergeClaim` and `semanticEquivalenceClaim` false.
|
|
238
|
+
|
|
223
239
|
## Authored view render graph syntax
|
|
224
240
|
|
|
225
241
|
`.frontier` view blocks can describe UI render graphs directly. Nested `render`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,57 @@
|
|
|
1
1
|
import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
export interface ParseFrontierOptions { readonly id?: string; readonly name?: string; }
|
|
3
|
+
export declare const FrontierSourceBlockKinds: readonly string[];
|
|
4
|
+
export interface FrontierSourceSyntaxDiagnostic {
|
|
5
|
+
readonly reason: 'unterminated-block' | 'unmatched-close-brace';
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly location: { readonly line: number; readonly column: number; readonly offset: number };
|
|
8
|
+
}
|
|
9
|
+
export interface FrontierSourceBlockSyntaxRecord {
|
|
10
|
+
readonly kind: string;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly id?: string;
|
|
13
|
+
readonly header: string;
|
|
14
|
+
readonly startOffset: number;
|
|
15
|
+
readonly endOffset: number;
|
|
16
|
+
readonly bodyStartOffset: number;
|
|
17
|
+
readonly bodyEndOffset: number;
|
|
18
|
+
readonly location: { readonly line: number; readonly column: number; readonly offset: number };
|
|
19
|
+
readonly moduleId?: string;
|
|
20
|
+
readonly moduleName?: string;
|
|
21
|
+
readonly recognized: boolean;
|
|
22
|
+
readonly malformed?: boolean;
|
|
23
|
+
readonly diagnostics?: readonly FrontierSourceSyntaxDiagnostic[];
|
|
24
|
+
}
|
|
25
|
+
export interface FrontierUnknownSourceBlockSyntaxRecord extends FrontierSourceBlockSyntaxRecord {
|
|
26
|
+
readonly recognized: false;
|
|
27
|
+
readonly reason: 'unsupported-top-level-block';
|
|
28
|
+
}
|
|
29
|
+
export interface FrontierSourceSyntaxReport {
|
|
30
|
+
readonly kind: 'frontier.lang.sourceSyntaxReport';
|
|
31
|
+
readonly version: 1;
|
|
32
|
+
readonly documentId: string;
|
|
33
|
+
readonly documentName: string;
|
|
34
|
+
readonly blocks: readonly FrontierSourceBlockSyntaxRecord[];
|
|
35
|
+
readonly recognizedBlocks: readonly FrontierSourceBlockSyntaxRecord[];
|
|
36
|
+
readonly unknownBlocks: readonly FrontierUnknownSourceBlockSyntaxRecord[];
|
|
37
|
+
readonly summary: {
|
|
38
|
+
readonly blockCount: number;
|
|
39
|
+
readonly recognizedBlockCount: number;
|
|
40
|
+
readonly unknownBlockCount: number;
|
|
41
|
+
readonly malformedBlockCount: number;
|
|
42
|
+
readonly diagnosticCount: number;
|
|
43
|
+
readonly recognizedKinds: readonly string[];
|
|
44
|
+
readonly unknownKinds: readonly string[];
|
|
45
|
+
readonly failClosed: boolean;
|
|
46
|
+
readonly unsupportedSyntax: boolean;
|
|
47
|
+
};
|
|
48
|
+
readonly diagnostics: readonly FrontierSourceSyntaxDiagnostic[];
|
|
49
|
+
readonly metadata: {
|
|
50
|
+
readonly sourceBytes: number;
|
|
51
|
+
readonly autoMergeClaim: false;
|
|
52
|
+
readonly semanticEquivalenceClaim: false;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export declare function inspectFrontierSourceSyntax(source: string, options?: ParseFrontierOptions): FrontierSourceSyntaxReport;
|
|
3
56
|
export declare function parseFrontierSource(source: string, options?: ParseFrontierOptions): FrontierLangDocument;
|
|
4
57
|
export declare function parseFrontierFile(name: string, source: string): FrontierLangDocument;
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import { parseRuntimeCapabilityBlock } from './runtime-capability.js';
|
|
|
16
16
|
import { parseNativeSourceBlock } from './source-evidence.js';
|
|
17
17
|
import { parseTargetProjectionMetadata } from './target-projection.js';
|
|
18
18
|
import { parseViewBlock } from './view.js';
|
|
19
|
+
import { FrontierSourceBlockKinds } from './source-syntax-report.js';
|
|
20
|
+
export { FrontierSourceBlockKinds, inspectFrontierSourceSyntax } from './source-syntax-report.js';
|
|
19
21
|
|
|
20
22
|
export function parseFrontierSource(source, options = {}) {
|
|
21
23
|
const nodes = [];
|
|
@@ -76,7 +78,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
|
|
|
76
78
|
function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
|
|
77
79
|
function readBlocks(source) {
|
|
78
80
|
const blocks = [];
|
|
79
|
-
const header =
|
|
81
|
+
const header = new RegExp('\\b(' + FrontierSourceBlockKinds.join('|') + ')\\s+([^{}]+)\\{', 'g');
|
|
80
82
|
let match;
|
|
81
83
|
while ((match = header.exec(source))) {
|
|
82
84
|
let depth = 1; let index = header.lastIndex;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
export const FrontierSourceBlockKinds = Object.freeze([
|
|
2
|
+
'entity',
|
|
3
|
+
'state',
|
|
4
|
+
'action',
|
|
5
|
+
'view',
|
|
6
|
+
'migration',
|
|
7
|
+
'capability',
|
|
8
|
+
'effect',
|
|
9
|
+
'type',
|
|
10
|
+
'extern',
|
|
11
|
+
'lattice',
|
|
12
|
+
'nativeSource',
|
|
13
|
+
'target',
|
|
14
|
+
'proof',
|
|
15
|
+
'paradigm',
|
|
16
|
+
'paradigmSemantics',
|
|
17
|
+
'operations',
|
|
18
|
+
'semanticOperations',
|
|
19
|
+
'conversion',
|
|
20
|
+
'universalConversionPlan',
|
|
21
|
+
'constraintSpace',
|
|
22
|
+
'possibilitySpace',
|
|
23
|
+
'decisionGraph',
|
|
24
|
+
'admissionGraph',
|
|
25
|
+
'dialectRegistry',
|
|
26
|
+
'universalDialectRegistry',
|
|
27
|
+
'interlingua',
|
|
28
|
+
'universalInterlingua',
|
|
29
|
+
'resourceGraph',
|
|
30
|
+
'semanticResourceGraph',
|
|
31
|
+
'packageManifest',
|
|
32
|
+
'packageGraph',
|
|
33
|
+
'packageSurface',
|
|
34
|
+
'canvasSurface',
|
|
35
|
+
'canvasGraph',
|
|
36
|
+
'applicationSurface',
|
|
37
|
+
'appHost',
|
|
38
|
+
'plugin',
|
|
39
|
+
'pluginSurface',
|
|
40
|
+
'pluginContract',
|
|
41
|
+
'runtimeCapabilities',
|
|
42
|
+
'runtimeCapabilityMatrix',
|
|
43
|
+
'runtimeHosts'
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const FrontierSourceBlockKindSet = new Set(FrontierSourceBlockKinds);
|
|
47
|
+
|
|
48
|
+
export function inspectFrontierSourceSyntax(source, options = {}) {
|
|
49
|
+
const documentId = options.id ?? readId(source) ?? 'mod_frontier';
|
|
50
|
+
const documentName = options.name ?? readName(source) ?? 'FrontierModule';
|
|
51
|
+
const structure = scanFrontierStructure(source);
|
|
52
|
+
const blocks = readCandidateDeclarationBlocks(source, structure).map((block) => ({
|
|
53
|
+
...block,
|
|
54
|
+
recognized: FrontierSourceBlockKindSet.has(block.kind)
|
|
55
|
+
}));
|
|
56
|
+
const recognizedBlocks = blocks.filter((block) => block.recognized);
|
|
57
|
+
const unknownBlocks = blocks.filter((block) => !block.recognized).map((block) => ({
|
|
58
|
+
...block,
|
|
59
|
+
reason: 'unsupported-top-level-block'
|
|
60
|
+
}));
|
|
61
|
+
const malformedBlockOpenOffsets = new Set(blocks.filter((block) => block.malformed).map((block) => block.bodyStartOffset - 1));
|
|
62
|
+
const diagnostics = [
|
|
63
|
+
...blocks.flatMap((block) => block.diagnostics ?? []),
|
|
64
|
+
...structure.unmatchedOpenBraces.filter((offset) => !malformedBlockOpenOffsets.has(offset)).map((offset) => ({
|
|
65
|
+
reason: 'unterminated-block',
|
|
66
|
+
message: 'Found an opening brace without a matching closing brace.',
|
|
67
|
+
location: sourcePosition(source, offset)
|
|
68
|
+
})),
|
|
69
|
+
...structure.unmatchedCloseBraces.map((offset) => ({
|
|
70
|
+
reason: 'unmatched-close-brace',
|
|
71
|
+
message: 'Found a closing brace without a matching opening brace.',
|
|
72
|
+
location: sourcePosition(source, offset)
|
|
73
|
+
}))
|
|
74
|
+
];
|
|
75
|
+
const malformedBlocks = blocks.filter((block) => block.malformed);
|
|
76
|
+
const failClosed = unknownBlocks.length > 0 || diagnostics.length > 0;
|
|
77
|
+
return {
|
|
78
|
+
kind: 'frontier.lang.sourceSyntaxReport',
|
|
79
|
+
version: 1,
|
|
80
|
+
documentId,
|
|
81
|
+
documentName,
|
|
82
|
+
blocks,
|
|
83
|
+
recognizedBlocks,
|
|
84
|
+
unknownBlocks,
|
|
85
|
+
summary: {
|
|
86
|
+
blockCount: blocks.length,
|
|
87
|
+
recognizedBlockCount: recognizedBlocks.length,
|
|
88
|
+
unknownBlockCount: unknownBlocks.length,
|
|
89
|
+
malformedBlockCount: malformedBlocks.length,
|
|
90
|
+
diagnosticCount: diagnostics.length,
|
|
91
|
+
recognizedKinds: unique(recognizedBlocks.map((block) => block.kind)),
|
|
92
|
+
unknownKinds: unique(unknownBlocks.map((block) => block.kind)),
|
|
93
|
+
failClosed,
|
|
94
|
+
unsupportedSyntax: unknownBlocks.length > 0
|
|
95
|
+
},
|
|
96
|
+
diagnostics,
|
|
97
|
+
metadata: {
|
|
98
|
+
sourceBytes: utf8ByteLength(source),
|
|
99
|
+
autoMergeClaim: false,
|
|
100
|
+
semanticEquivalenceClaim: false
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readCandidateDeclarationBlocks(source, structure) {
|
|
106
|
+
const moduleRanges = readModuleRanges(source, structure);
|
|
107
|
+
const blocks = [];
|
|
108
|
+
const header = /(^|\n)\s*([A-Za-z_$][\w$]*)\s+([^{}\n]+)\{/g;
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = header.exec(source))) {
|
|
111
|
+
const fullStart = match.index + match[1].length;
|
|
112
|
+
const leading = /^\s*/.exec(source.slice(fullStart))?.[0].length ?? 0;
|
|
113
|
+
const start = fullStart + leading;
|
|
114
|
+
const kind = match[2];
|
|
115
|
+
if (kind === 'module') continue;
|
|
116
|
+
const open = header.lastIndex - 1;
|
|
117
|
+
if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
|
|
118
|
+
const close = findMatchingBrace(structure, open);
|
|
119
|
+
const malformed = close < 0;
|
|
120
|
+
const end = malformed ? source.length : close + 1;
|
|
121
|
+
const bodyEnd = malformed ? source.length : close;
|
|
122
|
+
const depth = braceDepthBefore(structure, start);
|
|
123
|
+
const moduleRange = moduleRanges.find((range) => start > range.open && start < range.close);
|
|
124
|
+
const declarationDepth = moduleRange ? moduleRange.depth + 1 : 0;
|
|
125
|
+
if (depth !== declarationDepth) continue;
|
|
126
|
+
const headerText = match[3].trim();
|
|
127
|
+
blocks.push({
|
|
128
|
+
kind,
|
|
129
|
+
name: nameFrom(headerText),
|
|
130
|
+
id: idFrom(headerText),
|
|
131
|
+
header: headerText,
|
|
132
|
+
startOffset: start,
|
|
133
|
+
endOffset: end,
|
|
134
|
+
bodyStartOffset: open + 1,
|
|
135
|
+
bodyEndOffset: bodyEnd,
|
|
136
|
+
location: sourcePosition(source, start),
|
|
137
|
+
moduleId: moduleRange?.id,
|
|
138
|
+
moduleName: moduleRange?.name,
|
|
139
|
+
malformed,
|
|
140
|
+
diagnostics: malformed ? [{
|
|
141
|
+
reason: 'unterminated-block',
|
|
142
|
+
message: `Block "${kind}" has no matching closing brace.`,
|
|
143
|
+
location: sourcePosition(source, open)
|
|
144
|
+
}] : []
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return blocks;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readModuleRanges(source, structure) {
|
|
151
|
+
const ranges = [];
|
|
152
|
+
const header = /(^|\n)\s*module\s+([^{}\n]+)\{/g;
|
|
153
|
+
let match;
|
|
154
|
+
while ((match = header.exec(source))) {
|
|
155
|
+
const fullStart = match.index + match[1].length;
|
|
156
|
+
const leading = /^\s*/.exec(source.slice(fullStart))?.[0].length ?? 0;
|
|
157
|
+
const start = fullStart + leading;
|
|
158
|
+
const open = header.lastIndex - 1;
|
|
159
|
+
if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
|
|
160
|
+
const close = findMatchingBrace(structure, open);
|
|
161
|
+
const malformed = close < 0;
|
|
162
|
+
ranges.push({
|
|
163
|
+
start,
|
|
164
|
+
open,
|
|
165
|
+
close: malformed ? source.length : close,
|
|
166
|
+
depth: braceDepthBefore(structure, start),
|
|
167
|
+
name: nameFrom(match[2].trim()),
|
|
168
|
+
id: idFrom(match[2].trim()),
|
|
169
|
+
malformed
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return ranges;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function findMatchingBrace(structure, open) {
|
|
176
|
+
return structure.bracePairs.get(open) ?? -1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function braceDepthBefore(structure, offset) {
|
|
180
|
+
return structure.depthBefore[Math.min(Math.max(offset, 0), structure.depthBefore.length - 1)] ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function scanFrontierStructure(source) {
|
|
184
|
+
const depthBefore = new Int32Array(source.length + 1);
|
|
185
|
+
const codeOffsets = new Uint8Array(source.length);
|
|
186
|
+
const stack = [];
|
|
187
|
+
const bracePairs = new Map();
|
|
188
|
+
const unmatchedCloseBraces = [];
|
|
189
|
+
let depth = 0;
|
|
190
|
+
let state = 'code';
|
|
191
|
+
let quote = '';
|
|
192
|
+
for (let index = 0; index < source.length; index++) {
|
|
193
|
+
depthBefore[index] = depth;
|
|
194
|
+
const char = source[index];
|
|
195
|
+
const next = source[index + 1];
|
|
196
|
+
if (state === 'line-comment') {
|
|
197
|
+
if (char === '\n') state = 'code';
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (state === 'block-comment') {
|
|
201
|
+
if (char === '*' && next === '/') {
|
|
202
|
+
depthBefore[index + 1] = depth;
|
|
203
|
+
index++;
|
|
204
|
+
state = 'code';
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (state === 'string') {
|
|
209
|
+
if (char === '\\') {
|
|
210
|
+
depthBefore[index + 1] = depth;
|
|
211
|
+
index++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (char === quote) {
|
|
215
|
+
state = 'code';
|
|
216
|
+
quote = '';
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
codeOffsets[index] = 1;
|
|
221
|
+
if (char === '/' && next === '/') {
|
|
222
|
+
codeOffsets[index + 1] = 0;
|
|
223
|
+
depthBefore[index + 1] = depth;
|
|
224
|
+
index++;
|
|
225
|
+
state = 'line-comment';
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (char === '/' && next === '*') {
|
|
229
|
+
codeOffsets[index + 1] = 0;
|
|
230
|
+
depthBefore[index + 1] = depth;
|
|
231
|
+
index++;
|
|
232
|
+
state = 'block-comment';
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (char === '#' && isLineLeadingWhitespace(source, index)) {
|
|
236
|
+
state = 'line-comment';
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
240
|
+
state = 'string';
|
|
241
|
+
quote = char;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (char === '{') {
|
|
245
|
+
stack.push(index);
|
|
246
|
+
depth++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (char === '}') {
|
|
250
|
+
const open = stack.pop();
|
|
251
|
+
if (open === undefined) {
|
|
252
|
+
unmatchedCloseBraces.push(index);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
depth = Math.max(0, depth - 1);
|
|
256
|
+
bracePairs.set(open, index);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
depthBefore[source.length] = depth;
|
|
260
|
+
return { bracePairs, codeOffsets, depthBefore, unmatchedCloseBraces, unmatchedOpenBraces: stack };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isLineLeadingWhitespace(source, offset) {
|
|
264
|
+
for (let index = offset - 1; index >= 0 && source[index] !== '\n'; index--) {
|
|
265
|
+
if (!/\s/.test(source[index])) return false;
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function utf8ByteLength(source) {
|
|
271
|
+
if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(source).length;
|
|
272
|
+
return unescape(encodeURIComponent(source)).length;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function sourcePosition(source, offset) {
|
|
276
|
+
const lines = source.slice(0, offset).split('\n');
|
|
277
|
+
return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[1]; }
|
|
281
|
+
function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
|
|
282
|
+
function idFrom(header) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1]; }
|
|
283
|
+
function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Unnamed'; }
|
|
284
|
+
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|