@shapeshift-labs/frontier-lang-parser 0.3.34 → 0.3.36
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 +2 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2 -11
- package/dist/source-syntax-report.js +176 -28
- package/dist/view.js +2 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,11 +230,11 @@ import { inspectFrontierSourceSyntax } from '@shapeshift-labs/frontier-lang-pars
|
|
|
230
230
|
const report = inspectFrontierSourceSyntax(source);
|
|
231
231
|
|
|
232
232
|
if (report.summary.failClosed) {
|
|
233
|
-
console.error(report.summary.unknownKinds);
|
|
233
|
+
console.error(report.summary.unknownKinds, report.diagnostics);
|
|
234
234
|
}
|
|
235
235
|
```
|
|
236
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 is evidence about parser coverage only. It keeps `autoMergeClaim` and `semanticEquivalenceClaim` false.
|
|
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
238
|
|
|
239
239
|
## Authored view render graph syntax
|
|
240
240
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
export interface ParseFrontierOptions { readonly id?: string; readonly name?: string; }
|
|
3
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
|
+
}
|
|
4
9
|
export interface FrontierSourceBlockSyntaxRecord {
|
|
5
10
|
readonly kind: string;
|
|
6
11
|
readonly name: string;
|
|
@@ -14,6 +19,8 @@ export interface FrontierSourceBlockSyntaxRecord {
|
|
|
14
19
|
readonly moduleId?: string;
|
|
15
20
|
readonly moduleName?: string;
|
|
16
21
|
readonly recognized: boolean;
|
|
22
|
+
readonly malformed?: boolean;
|
|
23
|
+
readonly diagnostics?: readonly FrontierSourceSyntaxDiagnostic[];
|
|
17
24
|
}
|
|
18
25
|
export interface FrontierUnknownSourceBlockSyntaxRecord extends FrontierSourceBlockSyntaxRecord {
|
|
19
26
|
readonly recognized: false;
|
|
@@ -31,11 +38,14 @@ export interface FrontierSourceSyntaxReport {
|
|
|
31
38
|
readonly blockCount: number;
|
|
32
39
|
readonly recognizedBlockCount: number;
|
|
33
40
|
readonly unknownBlockCount: number;
|
|
41
|
+
readonly malformedBlockCount: number;
|
|
42
|
+
readonly diagnosticCount: number;
|
|
34
43
|
readonly recognizedKinds: readonly string[];
|
|
35
44
|
readonly unknownKinds: readonly string[];
|
|
36
45
|
readonly failClosed: boolean;
|
|
37
46
|
readonly unsupportedSyntax: boolean;
|
|
38
47
|
};
|
|
48
|
+
readonly diagnostics: readonly FrontierSourceSyntaxDiagnostic[];
|
|
39
49
|
readonly metadata: {
|
|
40
50
|
readonly sourceBytes: number;
|
|
41
51
|
readonly autoMergeClaim: false;
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ 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';
|
|
19
|
+
import { FrontierSourceBlockKinds, readFrontierSourceBlocks } from './source-syntax-report.js';
|
|
20
20
|
export { FrontierSourceBlockKinds, inspectFrontierSourceSyntax } from './source-syntax-report.js';
|
|
21
21
|
|
|
22
22
|
export function parseFrontierSource(source, options = {}) {
|
|
@@ -77,16 +77,7 @@ export function parseFrontierFile(name, source) { return parseFrontierSource(sou
|
|
|
77
77
|
function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[1]; }
|
|
78
78
|
function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
|
|
79
79
|
function readBlocks(source) {
|
|
80
|
-
|
|
81
|
-
const header = new RegExp('\\b(' + FrontierSourceBlockKinds.join('|') + ')\\s+([^{}]+)\\{', 'g');
|
|
82
|
-
let match;
|
|
83
|
-
while ((match = header.exec(source))) {
|
|
84
|
-
let depth = 1; let index = header.lastIndex;
|
|
85
|
-
while (index < source.length && depth > 0) { const ch = source[index++]; if (ch === '{') depth++; if (ch === '}') depth--; }
|
|
86
|
-
blocks.push({ kind: match[1], header: match[2].trim(), body: source.slice(header.lastIndex, index - 1) });
|
|
87
|
-
header.lastIndex = index;
|
|
88
|
-
}
|
|
89
|
-
return blocks;
|
|
80
|
+
return readFrontierSourceBlocks(source);
|
|
90
81
|
}
|
|
91
82
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
92
83
|
function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Unnamed'; }
|
|
@@ -48,7 +48,8 @@ const FrontierSourceBlockKindSet = new Set(FrontierSourceBlockKinds);
|
|
|
48
48
|
export function inspectFrontierSourceSyntax(source, options = {}) {
|
|
49
49
|
const documentId = options.id ?? readId(source) ?? 'mod_frontier';
|
|
50
50
|
const documentName = options.name ?? readName(source) ?? 'FrontierModule';
|
|
51
|
-
const
|
|
51
|
+
const structure = scanFrontierStructure(source);
|
|
52
|
+
const blocks = readCandidateDeclarationBlocks(source, structure).map((block) => ({
|
|
52
53
|
...block,
|
|
53
54
|
recognized: FrontierSourceBlockKindSet.has(block.kind)
|
|
54
55
|
}));
|
|
@@ -57,6 +58,22 @@ export function inspectFrontierSourceSyntax(source, options = {}) {
|
|
|
57
58
|
...block,
|
|
58
59
|
reason: 'unsupported-top-level-block'
|
|
59
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;
|
|
60
77
|
return {
|
|
61
78
|
kind: 'frontier.lang.sourceSyntaxReport',
|
|
62
79
|
version: 1,
|
|
@@ -69,21 +86,57 @@ export function inspectFrontierSourceSyntax(source, options = {}) {
|
|
|
69
86
|
blockCount: blocks.length,
|
|
70
87
|
recognizedBlockCount: recognizedBlocks.length,
|
|
71
88
|
unknownBlockCount: unknownBlocks.length,
|
|
89
|
+
malformedBlockCount: malformedBlocks.length,
|
|
90
|
+
diagnosticCount: diagnostics.length,
|
|
72
91
|
recognizedKinds: unique(recognizedBlocks.map((block) => block.kind)),
|
|
73
92
|
unknownKinds: unique(unknownBlocks.map((block) => block.kind)),
|
|
74
|
-
failClosed
|
|
93
|
+
failClosed,
|
|
75
94
|
unsupportedSyntax: unknownBlocks.length > 0
|
|
76
95
|
},
|
|
96
|
+
diagnostics,
|
|
77
97
|
metadata: {
|
|
78
|
-
sourceBytes: source
|
|
98
|
+
sourceBytes: utf8ByteLength(source),
|
|
79
99
|
autoMergeClaim: false,
|
|
80
100
|
semanticEquivalenceClaim: false
|
|
81
101
|
}
|
|
82
102
|
};
|
|
83
103
|
}
|
|
84
104
|
|
|
85
|
-
function
|
|
86
|
-
const
|
|
105
|
+
export function readFrontierSourceBlocks(source, options = {}) {
|
|
106
|
+
const report = inspectFrontierSourceSyntax(source, options);
|
|
107
|
+
return report.recognizedBlocks.filter((block) => !block.malformed).map((block) => ({
|
|
108
|
+
kind: block.kind,
|
|
109
|
+
header: block.header,
|
|
110
|
+
body: source.slice(block.bodyStartOffset, block.bodyEndOffset),
|
|
111
|
+
syntax: block
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function readFrontierNestedBlocks(kind, source) {
|
|
116
|
+
const structure = scanFrontierStructure(source);
|
|
117
|
+
const blocks = [];
|
|
118
|
+
const header = new RegExp('\\b' + escapeRegExp(kind) + '\\s+([^{}]+?)\\{', 'g');
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = header.exec(source))) {
|
|
121
|
+
const start = match.index;
|
|
122
|
+
const open = header.lastIndex - 1;
|
|
123
|
+
if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
|
|
124
|
+
const close = findMatchingBrace(structure, open);
|
|
125
|
+
if (close < 0) continue;
|
|
126
|
+
blocks.push({
|
|
127
|
+
kind,
|
|
128
|
+
header: match[1].trim(),
|
|
129
|
+
body: source.slice(header.lastIndex, close),
|
|
130
|
+
start,
|
|
131
|
+
end: close + 1
|
|
132
|
+
});
|
|
133
|
+
header.lastIndex = close + 1;
|
|
134
|
+
}
|
|
135
|
+
return blocks;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readCandidateDeclarationBlocks(source, structure) {
|
|
139
|
+
const moduleRanges = readModuleRanges(source, structure);
|
|
87
140
|
const blocks = [];
|
|
88
141
|
const header = /(^|\n)\s*([A-Za-z_$][\w$]*)\s+([^{}\n]+)\{/g;
|
|
89
142
|
let match;
|
|
@@ -94,8 +147,12 @@ function readCandidateDeclarationBlocks(source) {
|
|
|
94
147
|
const kind = match[2];
|
|
95
148
|
if (kind === 'module') continue;
|
|
96
149
|
const open = header.lastIndex - 1;
|
|
97
|
-
|
|
98
|
-
const
|
|
150
|
+
if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
|
|
151
|
+
const close = findMatchingBrace(structure, open);
|
|
152
|
+
const malformed = close < 0;
|
|
153
|
+
const end = malformed ? source.length : close + 1;
|
|
154
|
+
const bodyEnd = malformed ? source.length : close;
|
|
155
|
+
const depth = braceDepthBefore(structure, start);
|
|
99
156
|
const moduleRange = moduleRanges.find((range) => start > range.open && start < range.close);
|
|
100
157
|
const declarationDepth = moduleRange ? moduleRange.depth + 1 : 0;
|
|
101
158
|
if (depth !== declarationDepth) continue;
|
|
@@ -106,18 +163,24 @@ function readCandidateDeclarationBlocks(source) {
|
|
|
106
163
|
id: idFrom(headerText),
|
|
107
164
|
header: headerText,
|
|
108
165
|
startOffset: start,
|
|
109
|
-
endOffset:
|
|
166
|
+
endOffset: end,
|
|
110
167
|
bodyStartOffset: open + 1,
|
|
111
|
-
bodyEndOffset:
|
|
168
|
+
bodyEndOffset: bodyEnd,
|
|
112
169
|
location: sourcePosition(source, start),
|
|
113
170
|
moduleId: moduleRange?.id,
|
|
114
|
-
moduleName: moduleRange?.name
|
|
171
|
+
moduleName: moduleRange?.name,
|
|
172
|
+
malformed,
|
|
173
|
+
diagnostics: malformed ? [{
|
|
174
|
+
reason: 'unterminated-block',
|
|
175
|
+
message: `Block "${kind}" has no matching closing brace.`,
|
|
176
|
+
location: sourcePosition(source, open)
|
|
177
|
+
}] : []
|
|
115
178
|
});
|
|
116
179
|
}
|
|
117
180
|
return blocks;
|
|
118
181
|
}
|
|
119
182
|
|
|
120
|
-
function readModuleRanges(source) {
|
|
183
|
+
function readModuleRanges(source, structure) {
|
|
121
184
|
const ranges = [];
|
|
122
185
|
const header = /(^|\n)\s*module\s+([^{}\n]+)\{/g;
|
|
123
186
|
let match;
|
|
@@ -126,36 +189,120 @@ function readModuleRanges(source) {
|
|
|
126
189
|
const leading = /^\s*/.exec(source.slice(fullStart))?.[0].length ?? 0;
|
|
127
190
|
const start = fullStart + leading;
|
|
128
191
|
const open = header.lastIndex - 1;
|
|
129
|
-
|
|
192
|
+
if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
|
|
193
|
+
const close = findMatchingBrace(structure, open);
|
|
194
|
+
const malformed = close < 0;
|
|
130
195
|
ranges.push({
|
|
131
196
|
start,
|
|
132
197
|
open,
|
|
133
|
-
close,
|
|
134
|
-
depth: braceDepthBefore(
|
|
198
|
+
close: malformed ? source.length : close,
|
|
199
|
+
depth: braceDepthBefore(structure, start),
|
|
135
200
|
name: nameFrom(match[2].trim()),
|
|
136
|
-
id: idFrom(match[2].trim())
|
|
201
|
+
id: idFrom(match[2].trim()),
|
|
202
|
+
malformed
|
|
137
203
|
});
|
|
138
204
|
}
|
|
139
205
|
return ranges;
|
|
140
206
|
}
|
|
141
207
|
|
|
142
|
-
function findMatchingBrace(
|
|
143
|
-
|
|
144
|
-
for (let index = open + 1; index < source.length; index++) {
|
|
145
|
-
if (source[index] === '{') depth++;
|
|
146
|
-
if (source[index] === '}') depth--;
|
|
147
|
-
if (depth === 0) return index;
|
|
148
|
-
}
|
|
149
|
-
return source.length - 1;
|
|
208
|
+
function findMatchingBrace(structure, open) {
|
|
209
|
+
return structure.bracePairs.get(open) ?? -1;
|
|
150
210
|
}
|
|
151
211
|
|
|
152
|
-
function braceDepthBefore(
|
|
212
|
+
function braceDepthBefore(structure, offset) {
|
|
213
|
+
return structure.depthBefore[Math.min(Math.max(offset, 0), structure.depthBefore.length - 1)] ?? 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function scanFrontierStructure(source) {
|
|
217
|
+
const depthBefore = new Int32Array(source.length + 1);
|
|
218
|
+
const codeOffsets = new Uint8Array(source.length);
|
|
219
|
+
const stack = [];
|
|
220
|
+
const bracePairs = new Map();
|
|
221
|
+
const unmatchedCloseBraces = [];
|
|
153
222
|
let depth = 0;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
223
|
+
let state = 'code';
|
|
224
|
+
let quote = '';
|
|
225
|
+
for (let index = 0; index < source.length; index++) {
|
|
226
|
+
depthBefore[index] = depth;
|
|
227
|
+
const char = source[index];
|
|
228
|
+
const next = source[index + 1];
|
|
229
|
+
if (state === 'line-comment') {
|
|
230
|
+
if (char === '\n') state = 'code';
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (state === 'block-comment') {
|
|
234
|
+
if (char === '*' && next === '/') {
|
|
235
|
+
depthBefore[index + 1] = depth;
|
|
236
|
+
index++;
|
|
237
|
+
state = 'code';
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (state === 'string') {
|
|
242
|
+
if (char === '\\') {
|
|
243
|
+
depthBefore[index + 1] = depth;
|
|
244
|
+
index++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (char === quote) {
|
|
248
|
+
state = 'code';
|
|
249
|
+
quote = '';
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
codeOffsets[index] = 1;
|
|
254
|
+
if (char === '/' && next === '/') {
|
|
255
|
+
codeOffsets[index + 1] = 0;
|
|
256
|
+
depthBefore[index + 1] = depth;
|
|
257
|
+
index++;
|
|
258
|
+
state = 'line-comment';
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (char === '/' && next === '*') {
|
|
262
|
+
codeOffsets[index + 1] = 0;
|
|
263
|
+
depthBefore[index + 1] = depth;
|
|
264
|
+
index++;
|
|
265
|
+
state = 'block-comment';
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (char === '#' && isLineLeadingWhitespace(source, index)) {
|
|
269
|
+
state = 'line-comment';
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
273
|
+
state = 'string';
|
|
274
|
+
quote = char;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (char === '{') {
|
|
278
|
+
stack.push(index);
|
|
279
|
+
depth++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (char === '}') {
|
|
283
|
+
const open = stack.pop();
|
|
284
|
+
if (open === undefined) {
|
|
285
|
+
unmatchedCloseBraces.push(index);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
depth = Math.max(0, depth - 1);
|
|
289
|
+
bracePairs.set(open, index);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
depthBefore[source.length] = depth;
|
|
293
|
+
return { bracePairs, codeOffsets, depthBefore, unmatchedCloseBraces, unmatchedOpenBraces: stack };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isLineLeadingWhitespace(source, offset) {
|
|
297
|
+
for (let index = offset - 1; index >= 0 && source[index] !== '\n'; index--) {
|
|
298
|
+
if (!/\s/.test(source[index])) return false;
|
|
157
299
|
}
|
|
158
|
-
return
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function utf8ByteLength(source) {
|
|
304
|
+
if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(source).length;
|
|
305
|
+
return unescape(encodeURIComponent(source)).length;
|
|
159
306
|
}
|
|
160
307
|
|
|
161
308
|
function sourcePosition(source, offset) {
|
|
@@ -168,3 +315,4 @@ function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']
|
|
|
168
315
|
function idFrom(header) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1]; }
|
|
169
316
|
function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Unnamed'; }
|
|
170
317
|
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
318
|
+
function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
package/dist/view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { viewNode } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { readFrontierNestedBlocks } from './source-syntax-report.js';
|
|
2
3
|
|
|
3
4
|
export function parseViewBlock(block) {
|
|
4
5
|
const name = nameFrom(block.header);
|
|
@@ -79,16 +80,7 @@ function readRenderEvents(body) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function readNestedBlocks(kind, source) {
|
|
82
|
-
|
|
83
|
-
const header = new RegExp('\\b' + kind + '\\s+([^{}]+)\\{', 'g');
|
|
84
|
-
let match;
|
|
85
|
-
while ((match = header.exec(source))) {
|
|
86
|
-
let depth = 1; let index = header.lastIndex;
|
|
87
|
-
while (index < source.length && depth > 0) { const ch = source[index++]; if (ch === '{') depth++; if (ch === '}') depth--; }
|
|
88
|
-
blocks.push({ kind, header: match[1].trim(), body: source.slice(header.lastIndex, index - 1), start: match.index, end: index });
|
|
89
|
-
header.lastIndex = index;
|
|
90
|
-
}
|
|
91
|
-
return blocks;
|
|
83
|
+
return readFrontierNestedBlocks(kind, source);
|
|
92
84
|
}
|
|
93
85
|
|
|
94
86
|
function stripNestedBlocks(kind, source) {
|