Fix multiline type/object parsing, Map inline records, set-diff for Maps
- Add braceDepth tracking to Map/Set type parsing for inline records - Fix multiline state block joining to preserve type expressions - Fix multiline object literal parsing in assignments - Add smart set-difference for Maps vs Sets - Fix bare notIn for Sets - Add non-null assertion (!) to Map.get() calls - Add implicit event interface generation from emit statements - Protect object literals from this. prefix injection
This commit is contained in:
parent
8b520284cb
commit
a392163630
4 changed files with 201 additions and 36 deletions
77
compiler/dist/parser.js
vendored
77
compiler/dist/parser.js
vendored
|
|
@ -141,6 +141,25 @@ function findBlockEnd(lines, startIndex, startBraceCount) {
|
||||||
}
|
}
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
// Check if an expression has balanced braces, parens, and brackets
|
||||||
|
function isExpressionComplete(expr) {
|
||||||
|
let braces = 0, parens = 0, brackets = 0;
|
||||||
|
for (const char of expr) {
|
||||||
|
if (char === '{')
|
||||||
|
braces++;
|
||||||
|
else if (char === '}')
|
||||||
|
braces--;
|
||||||
|
else if (char === '(')
|
||||||
|
parens++;
|
||||||
|
else if (char === ')')
|
||||||
|
parens--;
|
||||||
|
else if (char === '[')
|
||||||
|
brackets++;
|
||||||
|
else if (char === ']')
|
||||||
|
brackets--;
|
||||||
|
}
|
||||||
|
return braces === 0 && parens === 0 && brackets === 0;
|
||||||
|
}
|
||||||
function parseMemberBlock(lines, startIndex) {
|
function parseMemberBlock(lines, startIndex) {
|
||||||
const firstLine = lines[startIndex].trim();
|
const firstLine = lines[startIndex].trim();
|
||||||
const startBraceCount = (firstLine.match(/{/g) || []).length;
|
const startBraceCount = (firstLine.match(/{/g) || []).length;
|
||||||
|
|
@ -398,15 +417,20 @@ function parseTypeExpr(expr) {
|
||||||
// Map<K, V>
|
// Map<K, V>
|
||||||
if (expr.startsWith('Map<') && expr.endsWith('>')) {
|
if (expr.startsWith('Map<') && expr.endsWith('>')) {
|
||||||
const inner = expr.substring(4, expr.length - 1);
|
const inner = expr.substring(4, expr.length - 1);
|
||||||
// Find the first-level comma (not inside nested generics)
|
// Find the first-level comma (not inside nested generics or record literals)
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
let braceDepth = 0;
|
||||||
let commaIdx = -1;
|
let commaIdx = -1;
|
||||||
for (let i = 0; i < inner.length; i++) {
|
for (let i = 0; i < inner.length; i++) {
|
||||||
if (inner[i] === '<')
|
if (inner[i] === '<' || inner[i] === '(')
|
||||||
depth++;
|
depth++;
|
||||||
else if (inner[i] === '>')
|
else if (inner[i] === '>' || inner[i] === ')')
|
||||||
depth--;
|
depth--;
|
||||||
else if (inner[i] === ',' && depth === 0) {
|
else if (inner[i] === '{')
|
||||||
|
braceDepth++;
|
||||||
|
else if (inner[i] === '}')
|
||||||
|
braceDepth--;
|
||||||
|
else if (inner[i] === ',' && depth === 0 && braceDepth === 0) {
|
||||||
commaIdx = i;
|
commaIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -433,13 +457,18 @@ function parseTypeExpr(expr) {
|
||||||
if (expr.startsWith('Map(') && expr.endsWith(')')) {
|
if (expr.startsWith('Map(') && expr.endsWith(')')) {
|
||||||
const inner = expr.substring(4, expr.length - 1);
|
const inner = expr.substring(4, expr.length - 1);
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
let braceDepth = 0;
|
||||||
let commaIdx = -1;
|
let commaIdx = -1;
|
||||||
for (let i = 0; i < inner.length; i++) {
|
for (let i = 0; i < inner.length; i++) {
|
||||||
if (inner[i] === '(' || inner[i] === '<')
|
if (inner[i] === '(' || inner[i] === '<')
|
||||||
depth++;
|
depth++;
|
||||||
else if (inner[i] === ')' || inner[i] === '>')
|
else if (inner[i] === ')' || inner[i] === '>')
|
||||||
depth--;
|
depth--;
|
||||||
else if (inner[i] === ',' && depth === 0) {
|
else if (inner[i] === '{')
|
||||||
|
braceDepth++;
|
||||||
|
else if (inner[i] === '}')
|
||||||
|
braceDepth--;
|
||||||
|
else if (inner[i] === ',' && depth === 0 && braceDepth === 0) {
|
||||||
commaIdx = i;
|
commaIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -583,13 +612,36 @@ function parseSource(line) {
|
||||||
function parseStateBlockMultiline(lines, startIndex, startBraceCount) {
|
function parseStateBlockMultiline(lines, startIndex, startBraceCount) {
|
||||||
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
||||||
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
||||||
const inner = innerLines.join('; ');
|
// Join lines: use '; ' between complete variable declarations
|
||||||
|
const inner = innerLines.reduce((acc, line, i) => {
|
||||||
|
if (i === 0)
|
||||||
|
return line;
|
||||||
|
const prevLine = innerLines[i - 1];
|
||||||
|
// Check if this line starts a new variable declaration (name: type)
|
||||||
|
const isNewVar = /^\w+\s*:/.test(line);
|
||||||
|
// Check if prev line ends with a continuation character
|
||||||
|
const prevIsContinuation = /[{(,]\s*$/.test(prevLine);
|
||||||
|
if (isNewVar && !prevIsContinuation) {
|
||||||
|
return acc + '; ' + line;
|
||||||
|
}
|
||||||
|
return acc + ' ' + line;
|
||||||
|
}, '');
|
||||||
return parseStateBlock(inner);
|
return parseStateBlock(inner);
|
||||||
}
|
}
|
||||||
function parseInitBlockMultiline(lines, startIndex, startBraceCount) {
|
function parseInitBlockMultiline(lines, startIndex, startBraceCount) {
|
||||||
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
||||||
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
||||||
const inner = innerLines.join('; ');
|
const inner = innerLines.reduce((acc, line, i) => {
|
||||||
|
if (i === 0)
|
||||||
|
return line;
|
||||||
|
const prevLine = innerLines[i - 1];
|
||||||
|
const isNewVar = /^\w+\s*:/.test(line);
|
||||||
|
const prevIsContinuation = /[{(,]\s*$/.test(prevLine);
|
||||||
|
if (isNewVar && !prevIsContinuation) {
|
||||||
|
return acc + '; ' + line;
|
||||||
|
}
|
||||||
|
return acc + ' ' + line;
|
||||||
|
}, '');
|
||||||
return parseInitBlock(inner);
|
return parseInitBlock(inner);
|
||||||
}
|
}
|
||||||
function parseActionBlockMultiline(lines, startIndex, startBraceCount) {
|
function parseActionBlockMultiline(lines, startIndex, startBraceCount) {
|
||||||
|
|
@ -675,10 +727,17 @@ function parseActionBlockMultiline(lines, startIndex, startBraceCount) {
|
||||||
statements.push({ type: 'let', name: letMatch[1], expr: letMatch[2].trim() });
|
statements.push({ type: 'let', name: letMatch[1], expr: letMatch[2].trim() });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// assignment: target := expr
|
// assignment: target := expr (may span multiple lines)
|
||||||
const assignMatch = stmt.match(/^(\w+(?:\[.*?\])?)\s*:=\s*(.+)$/);
|
const assignMatch = stmt.match(/^(\w+(?:\[.*?\])?)\s*:=\s*(.+)$/);
|
||||||
if (assignMatch) {
|
if (assignMatch) {
|
||||||
statements.push({ type: 'assign', target: assignMatch[1], expr: assignMatch[2].trim() });
|
let target = assignMatch[1];
|
||||||
|
let expr = assignMatch[2].trim();
|
||||||
|
// Handle multiline expressions: join subsequent lines until braces/parens are balanced
|
||||||
|
while (i + 1 < bodyLines.length && !isExpressionComplete(expr)) {
|
||||||
|
i++;
|
||||||
|
expr += ' ' + bodyLines[i];
|
||||||
|
}
|
||||||
|
statements.push({ type: 'assign', target, expr });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ...header, statements };
|
return { ...header, statements };
|
||||||
|
|
|
||||||
2
compiler/dist/parser.js.map
vendored
2
compiler/dist/parser.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -38,6 +38,19 @@ function emitSpeck(speck: SpeckNode): string {
|
||||||
const initNode = members.find(m => m.type === 'init') as InitNode | undefined;
|
const initNode = members.find(m => m.type === 'init') as InitNode | undefined;
|
||||||
const actions = members.filter(m => m.type === 'action') as ActionNode[];
|
const actions = members.filter(m => m.type === 'action') as ActionNode[];
|
||||||
const events = members.filter(m => m.type === 'event') as EventNode[];
|
const events = members.filter(m => m.type === 'event') as EventNode[];
|
||||||
|
|
||||||
|
// Collect implicit event types from emit statements
|
||||||
|
const implicitEvents = new Map<string, { name: string; fields: { name: string; value: string }[] }>();
|
||||||
|
for (const action of actions) {
|
||||||
|
for (const stmt of action.statements) {
|
||||||
|
if (stmt.type === 'emit') {
|
||||||
|
const existing = implicitEvents.get(stmt.event);
|
||||||
|
if (!existing) {
|
||||||
|
implicitEvents.set(stmt.event, { name: stmt.event, fields: stmt.fields });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const constraints = members.filter(m => m.type === 'constraint') as ConstraintNode[];
|
const constraints = members.filter(m => m.type === 'constraint') as ConstraintNode[];
|
||||||
const verifies = members.filter(m => m.type === 'verify') as VerifyNode[];
|
const verifies = members.filter(m => m.type === 'verify') as VerifyNode[];
|
||||||
|
|
||||||
|
|
@ -158,13 +171,21 @@ function emitSpeck(speck: SpeckNode): string {
|
||||||
}
|
}
|
||||||
const stateEnum = allEnums.join('\n\n');
|
const stateEnum = allEnums.join('\n\n');
|
||||||
|
|
||||||
const eventInterfaces = events.map(e => {
|
const eventInterfaces = [
|
||||||
const fields = e.fields.map(f => {
|
// Explicit event declarations
|
||||||
const name = cleanName(f.name);
|
...events.map(e => {
|
||||||
return ` ${name}: ${tsType(f.type, speck.name, enumMap)};`;
|
const fields = e.fields.map(f => {
|
||||||
}).join('\n');
|
const name = cleanName(f.name);
|
||||||
return `export interface ${e.name} {\n${fields}\n}`;
|
return ` ${name}: ${tsType(f.type, speck.name, enumMap)};`;
|
||||||
}).join('\n\n');
|
}).join('\n');
|
||||||
|
return `export interface ${e.name} {\n${fields}\n}`;
|
||||||
|
}),
|
||||||
|
// Implicit events from emit statements
|
||||||
|
...Array.from(implicitEvents.values()).map(e => {
|
||||||
|
const fields = e.fields.map((f: any) => ` ${f.name}: any;`).join('\n');
|
||||||
|
return `export interface ${e.name} {\n${fields}\n}`;
|
||||||
|
})
|
||||||
|
].join('\n\n');
|
||||||
|
|
||||||
const className = `${speck.name}Machine`;
|
const className = `${speck.name}Machine`;
|
||||||
const stateVars = (stateNode?.variables ?? []).map(v => ({
|
const stateVars = (stateNode?.variables ?? []).map(v => ({
|
||||||
|
|
@ -310,10 +331,26 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, mapVarNames:
|
||||||
.map(s => {
|
.map(s => {
|
||||||
const target = cleanName(s.target);
|
const target = cleanName(s.target);
|
||||||
const expr = rewriteExpr(s.expr, stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues);
|
const expr = rewriteExpr(s.expr, stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues);
|
||||||
|
// Map assignment: map[key] = val -> map.set(key, val)
|
||||||
|
const mapBracketMatch = target.match(/^(\w+)\[([^\]]+)\]$/);
|
||||||
|
if (mapBracketMatch && mapVarNames.has(mapBracketMatch[1])) {
|
||||||
|
const varName = mapBracketMatch[1];
|
||||||
|
const key = mapBracketMatch[2];
|
||||||
|
return ` this.${varName}.set(${key}, ${expr});`;
|
||||||
|
}
|
||||||
return ` this.${target} = ${expr};`;
|
return ` this.${target} = ${expr};`;
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
|
// Handle let statements: let x := expr -> const x = expr;
|
||||||
|
const lets = action.statements
|
||||||
|
.filter(s => s.type === 'let')
|
||||||
|
.map(s => {
|
||||||
|
const expr = rewriteExpr(s.expr, stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues);
|
||||||
|
return ` const ${s.name} = ${expr};`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
const emits = action.statements
|
const emits = action.statements
|
||||||
.filter(s => s.type === 'emit')
|
.filter(s => s.type === 'emit')
|
||||||
.map(s => {
|
.map(s => {
|
||||||
|
|
@ -339,9 +376,9 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, mapVarNames:
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
// Preconditions + requires first (guards), then mutations (assign + emit), then postconditions
|
// Preconditions + requires first (guards), then mutations (let + assign + emit), then postconditions
|
||||||
const guards = [preconditions, requires].filter(Boolean).join('\n');
|
const guards = [preconditions, requires].filter(Boolean).join('\n');
|
||||||
const mutations = [assigns, emits].filter(Boolean).join('\n');
|
const mutations = [lets, assigns, emits].filter(Boolean).join('\n');
|
||||||
const bodyParts = [guards, mutations, postconditions].filter(Boolean).join('\n');
|
const bodyParts = [guards, mutations, postconditions].filter(Boolean).join('\n');
|
||||||
return ` ${action.name}(${params}) {\n${bodyParts}\n }`;
|
return ` ${action.name}(${params}) {\n${bodyParts}\n }`;
|
||||||
}
|
}
|
||||||
|
|
@ -371,10 +408,10 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, mapVarNames: Set<
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Replace identifiers that are state variables but not local params
|
// Step 3: Replace identifiers that are state variables but not local params
|
||||||
// First, protect identifiers inside object literals (e.g. { ...t, pending: false })
|
// First, protect object literals (those { ... } blocks are object values, not state refs)
|
||||||
// Those identifiers are object keys, not state variable refs
|
|
||||||
const objectLiterals: string[] = [];
|
const objectLiterals: string[] = [];
|
||||||
ts = ts.replace(/\{[^}]*\.\.\.[^}]*\}/g, (match) => {
|
// Match { ... } including nested braces (simplistic: match balanced braces)
|
||||||
|
ts = ts.replace(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g, (match) => {
|
||||||
objectLiterals.push(match);
|
objectLiterals.push(match);
|
||||||
return `__OBJ_LIT_${objectLiterals.length - 1}__`;
|
return `__OBJ_LIT_${objectLiterals.length - 1}__`;
|
||||||
});
|
});
|
||||||
|
|
@ -404,6 +441,12 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, mapVarNames: Set<
|
||||||
|
|
||||||
// 5a: notIn -> !<map>.has(<key>) (e.g. "t.id notIn transfers.keys" -> "!transfers.has(t.id)")
|
// 5a: notIn -> !<map>.has(<key>) (e.g. "t.id notIn transfers.keys" -> "!transfers.has(t.id)")
|
||||||
ts = ts.replace(/(\S+)\s+notIn\s+(\S+)\.keys/g, '!$2.has($1)');
|
ts = ts.replace(/(\S+)\s+notIn\s+(\S+)\.keys/g, '!$2.has($1)');
|
||||||
|
// 5a1: notIn for Sets (no .keys suffix), e.g. "acctId notIn accounts" -> "!accounts.has(acctId)"
|
||||||
|
ts = ts.replace(/(\S+)\s+notIn\s+(\S+)/g, (match, key, collection) => {
|
||||||
|
// Skip if collection looks like a JS keyword
|
||||||
|
if (/^(true|false|null|undefined|new|typeof|instanceof|void|in|of|if|else|return|throw|let|const|var|function|class|import|export|from|for|while|do|switch|case|break|continue|default|try|catch|finally|async|await|yield|this)$/.test(collection)) return match;
|
||||||
|
return `!${collection}.has(${key})`;
|
||||||
|
});
|
||||||
// 5a2: x in <map>.keys -> <map>.has(x) (e.g. "t.from in accounts.keys" -> "accounts.has(t.from)")
|
// 5a2: x in <map>.keys -> <map>.has(x) (e.g. "t.from in accounts.keys" -> "accounts.has(t.from)")
|
||||||
ts = ts.replace(/(\S+)\s+in\s+(\S+)\.keys/g, '$2.has($1)');
|
ts = ts.replace(/(\S+)\s+in\s+(\S+)\.keys/g, '$2.has($1)');
|
||||||
// 5a3: x in <set> -> <set>.has(x) (e.g. "id in pending" -> "pending.has(id)")
|
// 5a3: x in <set> -> <set>.has(x) (e.g. "id in pending" -> "pending.has(id)")
|
||||||
|
|
@ -421,9 +464,23 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, mapVarNames: Set<
|
||||||
// "pending union {t.id}" -> "new Set([...pending, t.id])"
|
// "pending union {t.id}" -> "new Set([...pending, t.id])"
|
||||||
ts = ts.replace(/(\S+)\s+union\s+\{([^}]+)\}/g, 'new Set([...$1, $2])');
|
ts = ts.replace(/(\S+)\s+union\s+\{([^}]+)\}/g, 'new Set([...$1, $2])');
|
||||||
|
|
||||||
// 5d: \\ -> set difference via filter
|
// 5d: \\ -> set difference via filter (handles both \\ and \\\\)
|
||||||
// "pending \\ {id}" -> "new Set([...pending].filter(x => x !== id))"
|
// "pending \\ {id}" or "pending \ {id}" -> "new Set([...pending].filter(x => x !== id))"
|
||||||
ts = ts.replace(/(\S+)\s+\\\\\s+\{([^}]+)\}/g, 'new Set([...$1].filter(x => x !== $2))');
|
// For Maps: "pending \\ {id}" -> "new Map([...pending].filter(([k, v]) => k !== id))"
|
||||||
|
ts = ts.replace(/(\S+)\s+\\\\\s+\{([^}]+)\}/g, (match, collection, key) => {
|
||||||
|
const base = collection.replace(/^this\./, '');
|
||||||
|
if (mapVarNames.has(base)) {
|
||||||
|
return `new Map([...${collection}].filter(([k, v]) => k !== ${key}))`;
|
||||||
|
}
|
||||||
|
return `new Set([...${collection}].filter(x => x !== ${key}))`;
|
||||||
|
});
|
||||||
|
ts = ts.replace(/(\S+)\s+\\\s+\{([^}]+)\}/g, (match, collection, key) => {
|
||||||
|
const base = collection.replace(/^this\./, '');
|
||||||
|
if (mapVarNames.has(base)) {
|
||||||
|
return `new Map([...${collection}].filter(([k, v]) => k !== ${key}))`;
|
||||||
|
}
|
||||||
|
return `new Set([...${collection}].filter(x => x !== ${key}))`;
|
||||||
|
});
|
||||||
|
|
||||||
// 5e: Clean up trailing empty comparisons (===) — expression has no RHS after old() removal
|
// 5e: Clean up trailing empty comparisons (===) — expression has no RHS after old() removal
|
||||||
// "accounts[t.from].debits_posted ===" -> "accounts[t.from].debits_posted === accounts[t.from].debits_posted"
|
// "accounts[t.from].debits_posted ===" -> "accounts[t.from].debits_posted === accounts[t.from].debits_posted"
|
||||||
|
|
@ -434,13 +491,14 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, mapVarNames: Set<
|
||||||
|
|
||||||
// 5g: Map access via bracket -> .get() (e.g. "transfers[id]" -> "transfers.get(id)")
|
// 5g: Map access via bracket -> .get() (e.g. "transfers[id]" -> "transfers.get(id)")
|
||||||
// Only applies to state variables that are Map types
|
// Only applies to state variables that are Map types
|
||||||
|
// Add non-null assertion (!) because Map.get() returns T | undefined
|
||||||
for (const mapVar of mapVarNames) {
|
for (const mapVar of mapVarNames) {
|
||||||
// Match: mapVar[expr] where mapVar is a known Map state variable
|
// Match: mapVar[expr] where mapVar is a known Map state variable
|
||||||
// Handle both bare mapVar[...] and this.mapVar[...]
|
// Handle both bare mapVar[...] and this.mapVar[...]
|
||||||
const barePattern = new RegExp('\\b(' + mapVar + ')\\[([^\\]]+)\\]', 'g');
|
const barePattern = new RegExp('\\b(' + mapVar + ')\\[([^\\]]+)\\]', 'g');
|
||||||
ts = ts.replace(barePattern, '$1.get($2)');
|
ts = ts.replace(barePattern, '$1.get($2)!');
|
||||||
const thisPattern = new RegExp('\\b(this\\.' + mapVar + ')\\[([^\\]]+)\\]', 'g');
|
const thisPattern = new RegExp('\\b(this\\.' + mapVar + ')\\[([^\\]]+)\\]', 'g');
|
||||||
ts = ts.replace(thisPattern, '$1.get($2)');
|
ts = ts.replace(thisPattern, '$1.get($2)!');
|
||||||
}
|
}
|
||||||
// 5h: .size() -> .size (Set/Map size property, not a method in JS)
|
// 5h: .size() -> .size (Set/Map size property, not a method in JS)
|
||||||
ts = ts.replace(/\.size\(\)/g, '.size');
|
ts = ts.replace(/\.size\(\)/g, '.size');
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,20 @@ function findBlockEnd(lines: string[], startIndex: number, startBraceCount: numb
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if an expression has balanced braces, parens, and brackets
|
||||||
|
function isExpressionComplete(expr: string): boolean {
|
||||||
|
let braces = 0, parens = 0, brackets = 0;
|
||||||
|
for (const char of expr) {
|
||||||
|
if (char === '{') braces++;
|
||||||
|
else if (char === '}') braces--;
|
||||||
|
else if (char === '(') parens++;
|
||||||
|
else if (char === ')') parens--;
|
||||||
|
else if (char === '[') brackets++;
|
||||||
|
else if (char === ']') brackets--;
|
||||||
|
}
|
||||||
|
return braces === 0 && parens === 0 && brackets === 0;
|
||||||
|
}
|
||||||
|
|
||||||
function parseMemberBlock(lines: string[], startIndex: number): MemberNode | null {
|
function parseMemberBlock(lines: string[], startIndex: number): MemberNode | null {
|
||||||
const firstLine = lines[startIndex].trim();
|
const firstLine = lines[startIndex].trim();
|
||||||
const startBraceCount = (firstLine.match(/{/g) || []).length;
|
const startBraceCount = (firstLine.match(/{/g) || []).length;
|
||||||
|
|
@ -624,13 +638,16 @@ function parseTypeExpr(expr: string): TypeExpr {
|
||||||
// Map<K, V>
|
// Map<K, V>
|
||||||
if (expr.startsWith('Map<') && expr.endsWith('>')) {
|
if (expr.startsWith('Map<') && expr.endsWith('>')) {
|
||||||
const inner = expr.substring(4, expr.length - 1);
|
const inner = expr.substring(4, expr.length - 1);
|
||||||
// Find the first-level comma (not inside nested generics)
|
// Find the first-level comma (not inside nested generics or record literals)
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
let braceDepth = 0;
|
||||||
let commaIdx = -1;
|
let commaIdx = -1;
|
||||||
for (let i = 0; i < inner.length; i++) {
|
for (let i = 0; i < inner.length; i++) {
|
||||||
if (inner[i] === '<') depth++;
|
if (inner[i] === '<' || inner[i] === '(') depth++;
|
||||||
else if (inner[i] === '>') depth--;
|
else if (inner[i] === '>' || inner[i] === ')') depth--;
|
||||||
else if (inner[i] === ',' && depth === 0) {
|
else if (inner[i] === '{') braceDepth++;
|
||||||
|
else if (inner[i] === '}') braceDepth--;
|
||||||
|
else if (inner[i] === ',' && depth === 0 && braceDepth === 0) {
|
||||||
commaIdx = i;
|
commaIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -660,11 +677,14 @@ function parseTypeExpr(expr: string): TypeExpr {
|
||||||
if (expr.startsWith('Map(') && expr.endsWith(')')) {
|
if (expr.startsWith('Map(') && expr.endsWith(')')) {
|
||||||
const inner = expr.substring(4, expr.length - 1);
|
const inner = expr.substring(4, expr.length - 1);
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
let braceDepth = 0;
|
||||||
let commaIdx = -1;
|
let commaIdx = -1;
|
||||||
for (let i = 0; i < inner.length; i++) {
|
for (let i = 0; i < inner.length; i++) {
|
||||||
if (inner[i] === '(' || inner[i] === '<') depth++;
|
if (inner[i] === '(' || inner[i] === '<') depth++;
|
||||||
else if (inner[i] === ')' || inner[i] === '>') depth--;
|
else if (inner[i] === ')' || inner[i] === '>') depth--;
|
||||||
else if (inner[i] === ',' && depth === 0) {
|
else if (inner[i] === '{') braceDepth++;
|
||||||
|
else if (inner[i] === '}') braceDepth--;
|
||||||
|
else if (inner[i] === ',' && depth === 0 && braceDepth === 0) {
|
||||||
commaIdx = i;
|
commaIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -834,14 +854,35 @@ function parseSource(line: string): SourceNode {
|
||||||
function parseStateBlockMultiline(lines: string[], startIndex: number, startBraceCount: number): StateNode {
|
function parseStateBlockMultiline(lines: string[], startIndex: number, startBraceCount: number): StateNode {
|
||||||
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
||||||
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
||||||
const inner = innerLines.join('; ');
|
// Join lines: use '; ' between complete variable declarations
|
||||||
|
const inner = innerLines.reduce((acc, line, i) => {
|
||||||
|
if (i === 0) return line;
|
||||||
|
const prevLine = innerLines[i - 1];
|
||||||
|
// Check if this line starts a new variable declaration (name: type)
|
||||||
|
const isNewVar = /^\w+\s*:/.test(line);
|
||||||
|
// Check if prev line ends with a continuation character
|
||||||
|
const prevIsContinuation = /[{(,]\s*$/.test(prevLine);
|
||||||
|
if (isNewVar && !prevIsContinuation) {
|
||||||
|
return acc + '; ' + line;
|
||||||
|
}
|
||||||
|
return acc + ' ' + line;
|
||||||
|
}, '');
|
||||||
return parseStateBlock(inner);
|
return parseStateBlock(inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseInitBlockMultiline(lines: string[], startIndex: number, startBraceCount: number): InitNode {
|
function parseInitBlockMultiline(lines: string[], startIndex: number, startBraceCount: number): InitNode {
|
||||||
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount);
|
||||||
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
const innerLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*'));
|
||||||
const inner = innerLines.join('; ');
|
const inner = innerLines.reduce((acc, line, i) => {
|
||||||
|
if (i === 0) return line;
|
||||||
|
const prevLine = innerLines[i - 1];
|
||||||
|
const isNewVar = /^\w+\s*:/.test(line);
|
||||||
|
const prevIsContinuation = /[{(,]\s*$/.test(prevLine);
|
||||||
|
if (isNewVar && !prevIsContinuation) {
|
||||||
|
return acc + '; ' + line;
|
||||||
|
}
|
||||||
|
return acc + ' ' + line;
|
||||||
|
}, '');
|
||||||
return parseInitBlock(inner);
|
return parseInitBlock(inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -929,10 +970,17 @@ function parseActionBlockMultiline(lines: string[], startIndex: number, startBra
|
||||||
statements.push({ type: 'let', name: letMatch[1], expr: letMatch[2].trim() });
|
statements.push({ type: 'let', name: letMatch[1], expr: letMatch[2].trim() });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// assignment: target := expr
|
// assignment: target := expr (may span multiple lines)
|
||||||
const assignMatch = stmt.match(/^(\w+(?:\[.*?\])?)\s*:=\s*(.+)$/);
|
const assignMatch = stmt.match(/^(\w+(?:\[.*?\])?)\s*:=\s*(.+)$/);
|
||||||
if (assignMatch) {
|
if (assignMatch) {
|
||||||
statements.push({ type: 'assign', target: assignMatch[1], expr: assignMatch[2].trim() });
|
let target = assignMatch[1];
|
||||||
|
let expr = assignMatch[2].trim();
|
||||||
|
// Handle multiline expressions: join subsequent lines until braces/parens are balanced
|
||||||
|
while (i + 1 < bodyLines.length && !isExpressionComplete(expr)) {
|
||||||
|
i++;
|
||||||
|
expr += ' ' + bodyLines[i];
|
||||||
|
}
|
||||||
|
statements.push({ type: 'assign', target, expr });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue