@lokascript/framework 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +111 -24
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +133 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/tokenization/index.js +133 -0
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
- package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +3 -1
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
- package/dist/core/types.d.ts +3 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +564 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +556 -46
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +62 -7
- package/dist/ir/explicit-parser.d.ts.map +1 -1
- package/dist/ir/explicit-renderer.d.ts +18 -1
- package/dist/ir/explicit-renderer.d.ts.map +1 -1
- package/dist/ir/index.d.ts +4 -2
- package/dist/ir/index.d.ts.map +1 -1
- package/dist/ir/index.js +423 -46
- package/dist/ir/index.js.map +1 -1
- package/dist/ir/protocol-json.d.ts.map +1 -1
- package/dist/ir/to-runtime-ast.d.ts +53 -0
- package/dist/ir/to-runtime-ast.d.ts.map +1 -0
- package/dist/ir/types.d.ts +14 -4
- package/dist/ir/types.d.ts.map +1 -1
- package/dist/parsing/index.js.map +1 -1
- package/dist/testing/index.js +8671 -8261
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
- package/src/core/tokenization/morphology/index.ts +3 -1
- package/src/core/types.ts +4 -2
- package/src/index.ts +7 -0
- package/src/ir/conformance.test.ts +120 -0
- package/src/ir/explicit-parser.test.ts +307 -1
- package/src/ir/explicit-parser.ts +403 -29
- package/src/ir/explicit-renderer.test.ts +87 -2
- package/src/ir/explicit-renderer.ts +57 -5
- package/src/ir/index.ts +13 -2
- package/src/ir/protocol-json.test.ts +4 -2
- package/src/ir/protocol-json.ts +30 -24
- package/src/ir/to-runtime-ast.ts +203 -0
- package/src/ir/types.ts +14 -4
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Serializes SemanticNode to the universal [command role:value ...] bracket syntax.
|
|
5
5
|
* Zero dependencies beyond core types — no language-specific logic.
|
|
6
|
+
*
|
|
7
|
+
* Also renders annotations (@name(value)) and multi-line documents.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type {
|
|
@@ -13,24 +15,78 @@ import type {
|
|
|
13
15
|
EventHandlerSemanticNode,
|
|
14
16
|
ConditionalSemanticNode,
|
|
15
17
|
LoopSemanticNode,
|
|
18
|
+
LSEEnvelope,
|
|
16
19
|
} from '../core/types';
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Render a semantic node as explicit bracket syntax.
|
|
20
23
|
*
|
|
24
|
+
* Handles annotations (v1.2) — if the node has annotations, they are
|
|
25
|
+
* prepended as @name or @name(value) before the bracket output.
|
|
26
|
+
*
|
|
21
27
|
* @example
|
|
22
28
|
* ```typescript
|
|
23
29
|
* renderExplicit(node) // "[toggle patient:.active destination:#button]"
|
|
30
|
+
* // With annotations:
|
|
31
|
+
* renderExplicit(annotatedNode) // "@timeout(5s) [fetch source:\"/api/users\"]"
|
|
24
32
|
* ```
|
|
25
33
|
*/
|
|
26
34
|
export function renderExplicit(node: SemanticNode): string {
|
|
35
|
+
let result: string;
|
|
36
|
+
|
|
27
37
|
// Handle compound nodes
|
|
28
38
|
if (node.kind === 'compound') {
|
|
29
39
|
const compoundNode = node as CompoundSemanticNode;
|
|
30
40
|
const renderedStatements = compoundNode.statements.map(stmt => renderExplicit(stmt));
|
|
31
|
-
|
|
41
|
+
result = renderedStatements.join(` ${compoundNode.chainType} `);
|
|
42
|
+
} else {
|
|
43
|
+
result = renderBracketCommand(node);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Prepend annotations if present
|
|
47
|
+
if (node.annotations && node.annotations.length > 0) {
|
|
48
|
+
const annParts = node.annotations.map(ann =>
|
|
49
|
+
ann.value !== undefined ? `@${ann.name}(${ann.value})` : `@${ann.name}`
|
|
50
|
+
);
|
|
51
|
+
return annParts.join(' ') + ' ' + result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Render an LSEEnvelope as a multi-line document string.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* renderDocument(envelope)
|
|
63
|
+
* // "#!lse 1.2\n[toggle patient:.active]\n[add patient:.highlight]"
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function renderDocument(envelope: LSEEnvelope): string {
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
|
|
69
|
+
// Emit version header if not the default
|
|
70
|
+
if (envelope.lseVersion && envelope.lseVersion !== '1.0') {
|
|
71
|
+
lines.push(`#!lse ${envelope.lseVersion}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Emit each node on its own line
|
|
75
|
+
for (const node of envelope.nodes) {
|
|
76
|
+
lines.push(renderExplicit(node));
|
|
32
77
|
}
|
|
33
78
|
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Internal Helpers
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Render a single bracket command (non-compound node).
|
|
88
|
+
*/
|
|
89
|
+
function renderBracketCommand(node: SemanticNode): string {
|
|
34
90
|
const parts: string[] = [node.action];
|
|
35
91
|
|
|
36
92
|
// Add roles
|
|
@@ -109,10 +165,6 @@ export function renderExplicit(node: SemanticNode): string {
|
|
|
109
165
|
return `[${parts.join(' ')}]`;
|
|
110
166
|
}
|
|
111
167
|
|
|
112
|
-
// =============================================================================
|
|
113
|
-
// Internal Helpers
|
|
114
|
-
// =============================================================================
|
|
115
|
-
|
|
116
168
|
/**
|
|
117
169
|
* Convert a semantic value to its explicit syntax string form.
|
|
118
170
|
*/
|
package/src/ir/index.ts
CHANGED
|
@@ -32,10 +32,17 @@ export type {
|
|
|
32
32
|
export { DEFAULT_REFERENCES, isValidReference } from './references';
|
|
33
33
|
|
|
34
34
|
// Explicit syntax parser
|
|
35
|
-
export {
|
|
35
|
+
export {
|
|
36
|
+
parseExplicit,
|
|
37
|
+
parseCompound,
|
|
38
|
+
parseDocument,
|
|
39
|
+
isExplicitSyntax,
|
|
40
|
+
isCompoundSyntax,
|
|
41
|
+
isDocumentSyntax,
|
|
42
|
+
} from './explicit-parser';
|
|
36
43
|
|
|
37
44
|
// Explicit syntax renderer
|
|
38
|
-
export { renderExplicit } from './explicit-renderer';
|
|
45
|
+
export { renderExplicit, renderDocument } from './explicit-renderer';
|
|
39
46
|
|
|
40
47
|
// JSON schema conversion (deprecated — use fromProtocolJSON/validateProtocolJSON/toProtocolJSON)
|
|
41
48
|
export { jsonToSemanticNode, validateSemanticJSON, semanticNodeToJSON } from './json-schema';
|
|
@@ -64,3 +71,7 @@ export {
|
|
|
64
71
|
|
|
65
72
|
// Interchange AST → SemanticNode converter
|
|
66
73
|
export { fromInterchangeNode, convertValue, renderExpr } from './from-interchange';
|
|
74
|
+
|
|
75
|
+
// SemanticNode → Runtime AST converter (for core runtime execution)
|
|
76
|
+
export { semanticNodeToRuntimeAST, semanticValueToAST } from './to-runtime-ast';
|
|
77
|
+
export type { RuntimeASTNode, RuntimeCommandNode, RuntimeEventNode } from './to-runtime-ast';
|
|
@@ -995,10 +995,10 @@ describe('v1.2: diagnostics', () => {
|
|
|
995
995
|
...createCommandNode('toggle', { patient: createSelector('#button', 'id') }),
|
|
996
996
|
diagnostics: [
|
|
997
997
|
{
|
|
998
|
-
|
|
999
|
-
role: 'patient',
|
|
998
|
+
severity: 'error',
|
|
1000
999
|
message: "toggle.patient expects selector kind [class, attribute], got 'id'",
|
|
1001
1000
|
code: 'SCHEMA_SELECTOR_KIND_MISMATCH',
|
|
1001
|
+
source: 'schema',
|
|
1002
1002
|
},
|
|
1003
1003
|
],
|
|
1004
1004
|
};
|
|
@@ -1006,6 +1006,7 @@ describe('v1.2: diagnostics', () => {
|
|
|
1006
1006
|
expect(json.diagnostics).toHaveLength(1);
|
|
1007
1007
|
expect(json.diagnostics![0].level).toBe('error');
|
|
1008
1008
|
expect(json.diagnostics![0].code).toBe('SCHEMA_SELECTOR_KIND_MISMATCH');
|
|
1009
|
+
expect(json.diagnostics![0].source).toBe('schema');
|
|
1009
1010
|
});
|
|
1010
1011
|
|
|
1011
1012
|
it('deserializes diagnostics from protocol JSON', () => {
|
|
@@ -1025,6 +1026,7 @@ describe('v1.2: diagnostics', () => {
|
|
|
1025
1026
|
const node = fromProtocolJSON(json);
|
|
1026
1027
|
expect(node.diagnostics).toHaveLength(1);
|
|
1027
1028
|
expect(node.diagnostics![0].code).toBe('SCHEMA_VALUE_TYPE_MISMATCH');
|
|
1029
|
+
expect(node.diagnostics![0].severity).toBe('error');
|
|
1028
1030
|
});
|
|
1029
1031
|
|
|
1030
1032
|
describe('fixture conformance: type-constraints.json', () => {
|
package/src/ir/protocol-json.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import type {
|
|
55
55
|
ProtocolNodeJSON,
|
|
56
56
|
ProtocolValueJSON,
|
|
57
|
+
ProtocolDiagnosticJSON,
|
|
57
58
|
ProtocolChainType,
|
|
58
59
|
IRDiagnostic,
|
|
59
60
|
LSEEnvelopeJSON,
|
|
@@ -164,14 +165,18 @@ export function toProtocolJSON(node: SemanticNode): ProtocolNodeJSON {
|
|
|
164
165
|
);
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
// v1.2: diagnostics (all node kinds)
|
|
168
|
+
// v1.2/v1.2.1: diagnostics (all node kinds)
|
|
168
169
|
if (node.diagnostics && node.diagnostics.length > 0) {
|
|
169
|
-
result.diagnostics = node.diagnostics.map(d =>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
result.diagnostics = node.diagnostics.map(d => {
|
|
171
|
+
const json: ProtocolDiagnosticJSON = {
|
|
172
|
+
level: d.severity,
|
|
173
|
+
message: d.message,
|
|
174
|
+
};
|
|
175
|
+
if (d.code) json.code = d.code;
|
|
176
|
+
if (d.source) json.source = d.source;
|
|
177
|
+
if (d.suggestions && d.suggestions.length > 0) json.suggestions = [...d.suggestions];
|
|
178
|
+
return json;
|
|
179
|
+
});
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
return result;
|
|
@@ -252,10 +257,11 @@ export function fromProtocolJSON(json: ProtocolNodeJSON): SemanticNode {
|
|
|
252
257
|
a.value !== undefined ? { name: a.name, value: a.value } : { name: a.name }
|
|
253
258
|
);
|
|
254
259
|
const diagnostics = json.diagnostics?.map(d => ({
|
|
255
|
-
|
|
256
|
-
role: d.role,
|
|
260
|
+
severity: d.level as 'error' | 'warning' | 'info',
|
|
257
261
|
message: d.message,
|
|
258
|
-
code: d.code,
|
|
262
|
+
...(d.code ? { code: d.code } : {}),
|
|
263
|
+
...(d.source ? { source: d.source } : {}),
|
|
264
|
+
...(d.suggestions && d.suggestions.length > 0 ? { suggestions: d.suggestions } : {}),
|
|
259
265
|
}));
|
|
260
266
|
|
|
261
267
|
// trigger sugar: wrap command in event handler (check before kind dispatch)
|
|
@@ -361,7 +367,13 @@ function applyV12Metadata(
|
|
|
361
367
|
node: SemanticNode,
|
|
362
368
|
annotations: Array<{ name: string; value?: string }> | undefined,
|
|
363
369
|
diagnostics:
|
|
364
|
-
| Array<{
|
|
370
|
+
| Array<{
|
|
371
|
+
severity: 'error' | 'warning' | 'info';
|
|
372
|
+
message: string;
|
|
373
|
+
code?: string;
|
|
374
|
+
source?: string;
|
|
375
|
+
suggestions?: string[];
|
|
376
|
+
}>
|
|
365
377
|
| undefined
|
|
366
378
|
): SemanticNode {
|
|
367
379
|
if ((!annotations || annotations.length === 0) && (!diagnostics || diagnostics.length === 0)) {
|
|
@@ -460,7 +472,7 @@ const VALID_VALUE_TYPES = new Set([
|
|
|
460
472
|
]);
|
|
461
473
|
const VALID_CHAIN_TYPES = new Set(['then', 'and', 'async', 'sequential', 'pipe']);
|
|
462
474
|
const VALID_ASYNC_VARIANTS = new Set(['all', 'race']);
|
|
463
|
-
const VALID_DIAGNOSTIC_LEVELS = new Set(['error', 'warning']);
|
|
475
|
+
const VALID_DIAGNOSTIC_LEVELS = new Set(['error', 'warning', 'info']);
|
|
464
476
|
|
|
465
477
|
/**
|
|
466
478
|
* Validate that an unknown value conforms to the protocol JSON format.
|
|
@@ -637,14 +649,7 @@ export function validateProtocolJSON(json: unknown): IRDiagnostic[] {
|
|
|
637
649
|
diagnostics.push({
|
|
638
650
|
severity: 'error',
|
|
639
651
|
code: 'INVALID_DIAGNOSTIC_LEVEL',
|
|
640
|
-
message: `diagnostics[${i}].level must be "error" or "
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
if (typeof d.role !== 'string') {
|
|
644
|
-
diagnostics.push({
|
|
645
|
-
severity: 'error',
|
|
646
|
-
code: 'MISSING_DIAGNOSTIC_ROLE',
|
|
647
|
-
message: `diagnostics[${i}] missing required field: role`,
|
|
652
|
+
message: `diagnostics[${i}].level must be "error", "warning", or "info"`,
|
|
648
653
|
});
|
|
649
654
|
}
|
|
650
655
|
if (typeof d.message !== 'string') {
|
|
@@ -654,11 +659,12 @@ export function validateProtocolJSON(json: unknown): IRDiagnostic[] {
|
|
|
654
659
|
message: `diagnostics[${i}] missing required field: message`,
|
|
655
660
|
});
|
|
656
661
|
}
|
|
657
|
-
|
|
662
|
+
// v1.2.1: role and code are now optional
|
|
663
|
+
if ('code' in d && typeof d.code !== 'string') {
|
|
658
664
|
diagnostics.push({
|
|
659
|
-
severity: '
|
|
660
|
-
code: '
|
|
661
|
-
message: `diagnostics[${i}]
|
|
665
|
+
severity: 'warning',
|
|
666
|
+
code: 'INVALID_DIAGNOSTIC_CODE',
|
|
667
|
+
message: `diagnostics[${i}].code must be a string if present`,
|
|
662
668
|
});
|
|
663
669
|
}
|
|
664
670
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SemanticNode → Runtime AST Converter
|
|
3
|
+
*
|
|
4
|
+
* Converts framework SemanticNode (LSE IR) to a structural AST format
|
|
5
|
+
* that the core runtime can execute directly. Uses structural typing
|
|
6
|
+
* to avoid a dependency on the core package.
|
|
7
|
+
*
|
|
8
|
+
* This is the canonical converter — AOT compiler delegates to it.
|
|
9
|
+
*
|
|
10
|
+
* The output matches the core runtime's expected node shapes:
|
|
11
|
+
* - `{ type: 'command', name, args, roles }` → processCommand()
|
|
12
|
+
* - `{ type: 'event', event, modifiers, body }` → adapted to executeEventHandler()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
SemanticNode,
|
|
17
|
+
SemanticValue,
|
|
18
|
+
EventHandlerSemanticNode,
|
|
19
|
+
ConditionalSemanticNode,
|
|
20
|
+
LoopSemanticNode,
|
|
21
|
+
} from '../core/types';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Structural output types (no core package dependency)
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/** Minimal AST node that the core runtime can execute. */
|
|
28
|
+
export interface RuntimeASTNode {
|
|
29
|
+
readonly type: string;
|
|
30
|
+
readonly [key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Command node shape expected by runtime's processCommand(). */
|
|
34
|
+
export interface RuntimeCommandNode extends RuntimeASTNode {
|
|
35
|
+
readonly type: 'command';
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly args: RuntimeASTNode[];
|
|
38
|
+
readonly roles?: Readonly<Record<string, RuntimeASTNode>>;
|
|
39
|
+
readonly modifiers?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Event node shape expected by runtime's event adapter. */
|
|
43
|
+
export interface RuntimeEventNode extends RuntimeASTNode {
|
|
44
|
+
readonly type: 'event';
|
|
45
|
+
readonly event: string;
|
|
46
|
+
readonly modifiers: Record<string, unknown>;
|
|
47
|
+
readonly body: RuntimeASTNode[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Converter
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert a SemanticNode to a RuntimeASTNode that the core runtime can execute.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* import { parseExplicit } from '@lokascript/framework/ir';
|
|
60
|
+
* import { semanticNodeToRuntimeAST } from '@lokascript/framework/ir';
|
|
61
|
+
*
|
|
62
|
+
* const node = parseExplicit('[toggle patient:.active]');
|
|
63
|
+
* const ast = semanticNodeToRuntimeAST(node);
|
|
64
|
+
* // { type: 'command', name: 'toggle', args: [{ type: 'selector', value: '.active' }], roles: { patient: ... } }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function semanticNodeToRuntimeAST(node: SemanticNode): RuntimeASTNode {
|
|
68
|
+
switch (node.kind) {
|
|
69
|
+
case 'event-handler':
|
|
70
|
+
return convertEventHandler(node as EventHandlerSemanticNode);
|
|
71
|
+
|
|
72
|
+
case 'conditional':
|
|
73
|
+
return convertConditional(node as ConditionalSemanticNode);
|
|
74
|
+
|
|
75
|
+
case 'loop':
|
|
76
|
+
return convertLoop(node as LoopSemanticNode);
|
|
77
|
+
|
|
78
|
+
case 'compound': {
|
|
79
|
+
const compound = node as SemanticNode & { statements?: SemanticNode[]; chainType?: string };
|
|
80
|
+
const commands = (compound.statements ?? []).map(semanticNodeToRuntimeAST);
|
|
81
|
+
return {
|
|
82
|
+
type: 'CommandSequence',
|
|
83
|
+
commands,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'command':
|
|
88
|
+
default:
|
|
89
|
+
return convertCommand(node);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function convertEventHandler(eh: EventHandlerSemanticNode): RuntimeEventNode {
|
|
94
|
+
const eventValue = eh.roles.get('event');
|
|
95
|
+
const eventName = eventValue && 'value' in eventValue ? String(eventValue.value) : 'click';
|
|
96
|
+
|
|
97
|
+
const modifiers: Record<string, unknown> = {};
|
|
98
|
+
if (eh.eventModifiers) {
|
|
99
|
+
if (eh.eventModifiers.once) modifiers.once = true;
|
|
100
|
+
if (eh.eventModifiers.debounce) modifiers.debounce = eh.eventModifiers.debounce;
|
|
101
|
+
if (eh.eventModifiers.throttle) modifiers.throttle = eh.eventModifiers.throttle;
|
|
102
|
+
if (eh.eventModifiers.queue) modifiers.queue = eh.eventModifiers.queue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type: 'event',
|
|
107
|
+
event: eventName,
|
|
108
|
+
modifiers,
|
|
109
|
+
body: (eh.body ?? []).map(semanticNodeToRuntimeAST),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function convertCommand(node: SemanticNode): RuntimeCommandNode {
|
|
114
|
+
const roles: Record<string, RuntimeASTNode> = {};
|
|
115
|
+
const args: RuntimeASTNode[] = [];
|
|
116
|
+
|
|
117
|
+
for (const [roleName, value] of node.roles) {
|
|
118
|
+
const astValue = semanticValueToAST(value);
|
|
119
|
+
roles[roleName] = astValue;
|
|
120
|
+
args.push(astValue);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'command',
|
|
125
|
+
name: node.action,
|
|
126
|
+
args,
|
|
127
|
+
roles,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function convertConditional(node: ConditionalSemanticNode): RuntimeASTNode {
|
|
132
|
+
const conditionValue = node.roles.get('condition');
|
|
133
|
+
return {
|
|
134
|
+
type: 'command',
|
|
135
|
+
name: 'if',
|
|
136
|
+
args: conditionValue ? [semanticValueToAST(conditionValue)] : [],
|
|
137
|
+
condition: conditionValue ? semanticValueToAST(conditionValue) : undefined,
|
|
138
|
+
thenBranch: (node.thenBranch ?? []).map(semanticNodeToRuntimeAST),
|
|
139
|
+
elseBranch: (node.elseBranch ?? []).map(semanticNodeToRuntimeAST),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function convertLoop(node: LoopSemanticNode): RuntimeASTNode {
|
|
144
|
+
const countValue = node.roles.get('patient') || node.roles.get('count');
|
|
145
|
+
return {
|
|
146
|
+
type: 'command',
|
|
147
|
+
name: 'repeat',
|
|
148
|
+
args: countValue ? [semanticValueToAST(countValue)] : [],
|
|
149
|
+
loopVariant: node.loopVariant ?? 'forever',
|
|
150
|
+
body: (node.body ?? []).map(semanticNodeToRuntimeAST),
|
|
151
|
+
loopVariable: node.loopVariable,
|
|
152
|
+
indexVariable: node.indexVariable,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert a SemanticValue to a RuntimeASTNode.
|
|
158
|
+
*/
|
|
159
|
+
export function semanticValueToAST(value: SemanticValue): RuntimeASTNode {
|
|
160
|
+
switch (value.type) {
|
|
161
|
+
case 'selector':
|
|
162
|
+
return { type: 'selector', value: value.value as string };
|
|
163
|
+
|
|
164
|
+
case 'reference':
|
|
165
|
+
return { type: 'identifier', value: value.value as string };
|
|
166
|
+
|
|
167
|
+
case 'literal': {
|
|
168
|
+
const lit = value as { value: unknown; dataType?: string };
|
|
169
|
+
if (lit.dataType === 'duration') {
|
|
170
|
+
const str = String(lit.value);
|
|
171
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(str);
|
|
172
|
+
if (match) {
|
|
173
|
+
const ms = match[2] === 's' ? parseFloat(match[1]) * 1000 : parseFloat(match[1]);
|
|
174
|
+
return { type: 'literal', value: ms };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { type: 'literal', value: lit.value as string | number | boolean | null };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'property-path': {
|
|
181
|
+
const pp = value as import('../core/types').PropertyPathValue;
|
|
182
|
+
const objectAST = semanticValueToAST(pp.object);
|
|
183
|
+
return {
|
|
184
|
+
type: 'member',
|
|
185
|
+
object: objectAST,
|
|
186
|
+
property: pp.property,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case 'expression': {
|
|
191
|
+
const expr = value as { raw: string };
|
|
192
|
+
return { type: 'expression', value: expr.raw };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case 'flag': {
|
|
196
|
+
const flag = value as { name: string; enabled: boolean };
|
|
197
|
+
return { type: 'literal', value: flag.enabled };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
return { type: 'literal', value: String((value as { value?: unknown }).value ?? '') };
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/ir/types.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface ParseExplicitOptions {
|
|
|
67
67
|
schemaLookup?: SchemaLookup;
|
|
68
68
|
/** Custom set of valid reference names (defaults to DEFAULT_REFERENCES) */
|
|
69
69
|
referenceSet?: ReadonlySet<string>;
|
|
70
|
+
/**
|
|
71
|
+
* When true, validation errors are collected as diagnostics on the
|
|
72
|
+
* returned node instead of throwing. Fatal parse errors (missing brackets,
|
|
73
|
+
* empty input) still throw. Default: false.
|
|
74
|
+
*/
|
|
75
|
+
collectDiagnostics?: boolean;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
// =============================================================================
|
|
@@ -153,12 +159,16 @@ export interface ProtocolNodeJSON {
|
|
|
153
159
|
// v1.2 Sub-types
|
|
154
160
|
// =============================================================================
|
|
155
161
|
|
|
156
|
-
/** A
|
|
162
|
+
/** A diagnostic in protocol wire format (v1.2, extended v1.2.1). */
|
|
157
163
|
export interface ProtocolDiagnosticJSON {
|
|
158
|
-
level: 'error' | 'warning';
|
|
159
|
-
role
|
|
164
|
+
level: 'error' | 'warning' | 'info';
|
|
165
|
+
role?: string;
|
|
160
166
|
message: string;
|
|
161
|
-
code
|
|
167
|
+
code?: string;
|
|
168
|
+
/** v1.2.1: source parser/stage that produced this diagnostic. */
|
|
169
|
+
source?: string;
|
|
170
|
+
/** v1.2.1: actionable fix suggestions. */
|
|
171
|
+
suggestions?: string[];
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
/** A metadata annotation in protocol wire format (v1.2). */
|