fix(compiler): protect object literal identifiers from this. prefix in rewriteExpr
Object spread expressions like { ...t, pending: false } were getting
spurious this. prefix applied to keys (this.pending: false), causing
3 tsc errors (TS1005: ':' expected). Fix: extract object literals
containing spread before Step 3 identifier rewriting, then restore.
Results: TigerBeetle TS errors reduced to known record-type issues (eng#11).
All 41 tests passing.
This commit is contained in:
parent
c3ade18feb
commit
898e2e53ae
3 changed files with 80 additions and 6 deletions
68
compiler/dist/parser.js
vendored
68
compiler/dist/parser.js
vendored
|
|
@ -64,7 +64,8 @@ function parseSpeck(lines, startIndex) {
|
||||||
const openCount = (rawLine.match(/{/g) || []).length;
|
const openCount = (rawLine.match(/{/g) || []).length;
|
||||||
const closeCount = (rawLine.match(/}/g) || []).length;
|
const closeCount = (rawLine.match(/}/g) || []).length;
|
||||||
// Check for block-opening members
|
// Check for block-opening members
|
||||||
const blockStarters = ['state:', 'init:', 'action ', 'event ', 'provenance ', 'bom ', 'interface '];
|
const blockStarters = ['state:', 'init:', 'action ', 'event ', 'provenance ', 'bom ', 'interface ',
|
||||||
|
'state {', 'init {'];
|
||||||
const isBlockStarter = blockStarters.some(s => line.startsWith(s));
|
const isBlockStarter = blockStarters.some(s => line.startsWith(s));
|
||||||
if (isBlockStarter && openCount > 0) {
|
if (isBlockStarter && openCount > 0) {
|
||||||
// Multi-line block: parse it and skip to its end
|
// Multi-line block: parse it and skip to its end
|
||||||
|
|
@ -116,9 +117,15 @@ function parseMemberBlock(lines, startIndex) {
|
||||||
if (firstLine.startsWith('state:')) {
|
if (firstLine.startsWith('state:')) {
|
||||||
return parseStateBlockMultiline(lines, startIndex, startBraceCount);
|
return parseStateBlockMultiline(lines, startIndex, startBraceCount);
|
||||||
}
|
}
|
||||||
|
if (firstLine.startsWith('state {') || firstLine === 'state') {
|
||||||
|
return parseStateBlockMultiline(lines, startIndex, startBraceCount);
|
||||||
|
}
|
||||||
if (firstLine.startsWith('init:')) {
|
if (firstLine.startsWith('init:')) {
|
||||||
return parseInitBlockMultiline(lines, startIndex, startBraceCount);
|
return parseInitBlockMultiline(lines, startIndex, startBraceCount);
|
||||||
}
|
}
|
||||||
|
if (firstLine.startsWith('init {') || firstLine === 'init') {
|
||||||
|
return parseInitBlockMultiline(lines, startIndex, startBraceCount);
|
||||||
|
}
|
||||||
if (firstLine.startsWith('action ')) {
|
if (firstLine.startsWith('action ')) {
|
||||||
return parseActionBlockMultiline(lines, startIndex, startBraceCount);
|
return parseActionBlockMultiline(lines, startIndex, startBraceCount);
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +195,7 @@ function parseMember(line) {
|
||||||
if (line.startsWith('source:')) {
|
if (line.startsWith('source:')) {
|
||||||
return parseSource(line);
|
return parseSource(line);
|
||||||
}
|
}
|
||||||
// state:
|
// state: or state {
|
||||||
if (line.startsWith('state:')) {
|
if (line.startsWith('state:')) {
|
||||||
const varsStr = line.substring(6).trim();
|
const varsStr = line.substring(6).trim();
|
||||||
// state: { ... } - single-line form
|
// state: { ... } - single-line form
|
||||||
|
|
@ -199,7 +206,18 @@ function parseMember(line) {
|
||||||
// state: { - multi-line opening (handled by parseStateMultiline)
|
// state: { - multi-line opening (handled by parseStateMultiline)
|
||||||
return { type: 'state', variables: [] };
|
return { type: 'state', variables: [] };
|
||||||
}
|
}
|
||||||
// init:
|
if (line.startsWith('state {') || line === 'state') {
|
||||||
|
// state { ... } inline or state { on its own line (handled by parseMemberBlock)
|
||||||
|
if (line.startsWith('state {')) {
|
||||||
|
const inner = line.substring(6).trim();
|
||||||
|
const content = inner.startsWith('{') ? inner.slice(1).trim() : inner;
|
||||||
|
if (content.endsWith('}')) {
|
||||||
|
return parseStateBlock(content.slice(0, -1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: 'state', variables: [] };
|
||||||
|
}
|
||||||
|
// init: or init {
|
||||||
if (line.startsWith('init:')) {
|
if (line.startsWith('init:')) {
|
||||||
const initStr = line.substring(5).trim();
|
const initStr = line.substring(5).trim();
|
||||||
if (initStr.startsWith('{') && initStr.endsWith('}')) {
|
if (initStr.startsWith('{') && initStr.endsWith('}')) {
|
||||||
|
|
@ -208,6 +226,17 @@ function parseMember(line) {
|
||||||
}
|
}
|
||||||
return { type: 'init', assignments: [] };
|
return { type: 'init', assignments: [] };
|
||||||
}
|
}
|
||||||
|
if (line.startsWith('init {') || line === 'init') {
|
||||||
|
// init { ... } inline
|
||||||
|
if (line.startsWith('init {')) {
|
||||||
|
const inner = line.substring(5).trim();
|
||||||
|
const content = inner.startsWith('{') ? inner.slice(1).trim() : inner;
|
||||||
|
if (content.endsWith('}')) {
|
||||||
|
return parseInitBlock(content.slice(0, -1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: 'init', assignments: [] };
|
||||||
|
}
|
||||||
// action keyword
|
// action keyword
|
||||||
if (line.startsWith('action ')) {
|
if (line.startsWith('action ')) {
|
||||||
return parseActionHeader(line);
|
return parseActionHeader(line);
|
||||||
|
|
@ -332,6 +361,39 @@ function parseTypeExpr(expr) {
|
||||||
// Fallback: treat as ident if malformed
|
// Fallback: treat as ident if malformed
|
||||||
return { type: 'ident', name: expr };
|
return { type: 'ident', name: expr };
|
||||||
}
|
}
|
||||||
|
// List(T) — parenthetical syntax (SpeckDL canonical form)
|
||||||
|
if (expr.startsWith('List(') && expr.endsWith(')')) {
|
||||||
|
const inner = expr.substring(5, expr.length - 1).trim();
|
||||||
|
return { type: 'list', elementType: parseTypeExpr(inner) };
|
||||||
|
}
|
||||||
|
// Set(T) — parenthetical syntax (SpeckDL canonical form)
|
||||||
|
if (expr.startsWith('Set(') && expr.endsWith(')')) {
|
||||||
|
const inner = expr.substring(4, expr.length - 1).trim();
|
||||||
|
return { type: 'set', elementType: parseTypeExpr(inner) };
|
||||||
|
}
|
||||||
|
// Map(K, V) — parenthetical syntax (SpeckDL canonical form)
|
||||||
|
if (expr.startsWith('Map(') && expr.endsWith(')')) {
|
||||||
|
const inner = expr.substring(4, expr.length - 1);
|
||||||
|
let depth = 0;
|
||||||
|
let commaIdx = -1;
|
||||||
|
for (let i = 0; i < inner.length; i++) {
|
||||||
|
if (inner[i] === '(' || inner[i] === '<')
|
||||||
|
depth++;
|
||||||
|
else if (inner[i] === ')' || inner[i] === '>')
|
||||||
|
depth--;
|
||||||
|
else if (inner[i] === ',' && depth === 0) {
|
||||||
|
commaIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (commaIdx !== -1) {
|
||||||
|
const keyStr = inner.substring(0, commaIdx).trim();
|
||||||
|
const valStr = inner.substring(commaIdx + 1).trim();
|
||||||
|
return { type: 'map', keyType: parseTypeExpr(keyStr), valueType: parseTypeExpr(valStr) };
|
||||||
|
}
|
||||||
|
// Fallback: treat as ident if malformed
|
||||||
|
return { type: 'ident', name: expr };
|
||||||
|
}
|
||||||
// List type: [T]
|
// List type: [T]
|
||||||
if (expr.startsWith('[') && expr.endsWith(']')) {
|
if (expr.startsWith('[') && expr.endsWith(']')) {
|
||||||
const elementType = parseTypeExpr(expr.substring(1, expr.length - 1));
|
const elementType = parseTypeExpr(expr.substring(1, expr.length - 1));
|
||||||
|
|
|
||||||
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
|
|
@ -331,6 +331,14 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<s
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 })
|
||||||
|
// Those identifiers are object keys, not state variable refs
|
||||||
|
const objectLiterals: string[] = [];
|
||||||
|
ts = ts.replace(/\{[^}]*\.\.\.[^}]*\}/g, (match) => {
|
||||||
|
objectLiterals.push(match);
|
||||||
|
return `__OBJ_LIT_${objectLiterals.length - 1}__`;
|
||||||
|
});
|
||||||
|
|
||||||
const tokens = ts.split(/([\s\(\)\[\]{}+\-*/=<>!&|.,;:'"`]+)/);
|
const tokens = ts.split(/([\s\(\)\[\]{}+\-*/=<>!&|.,;:'"`]+)/);
|
||||||
const result = tokens.map(tok => {
|
const result = tokens.map(tok => {
|
||||||
if (/^[a-zA-Z_]\w*\.\w+$/.test(tok)) return tok; // already has dot (enum ref)
|
if (/^[a-zA-Z_]\w*\.\w+$/.test(tok)) return tok; // already has dot (enum ref)
|
||||||
|
|
@ -339,7 +347,11 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<s
|
||||||
return `this.${tok}`;
|
return `this.${tok}`;
|
||||||
}
|
}
|
||||||
return tok;
|
return tok;
|
||||||
}).join('');
|
}).join('')
|
||||||
|
// Undo this. prefix on identifiers after bracket access
|
||||||
|
.replace(/\]\.this\.(\w+)/g, '].$1')
|
||||||
|
// Restore object literals
|
||||||
|
.replace(/__OBJ_LIT_(\d+)__/g, (_, i) => objectLiterals[parseInt(i)]);
|
||||||
|
|
||||||
// Step 4: Replace comparison operators (also handle trailing == at end of string)
|
// Step 4: Replace comparison operators (also handle trailing == at end of string)
|
||||||
ts = result
|
ts = result
|
||||||
|
|
@ -350,7 +362,7 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<s
|
||||||
// Step 5: Translate SpeckDL expression patterns to JavaScript
|
// Step 5: Translate SpeckDL expression patterns to JavaScript
|
||||||
|
|
||||||
// 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+(\w+)\.keys/g, '!$2.has($1)');
|
ts = ts.replace(/(\S+)\s+notIn\s+(\S+)\.keys/g, '!$2.has($1)');
|
||||||
|
|
||||||
// 5b: old(expr) -> drop the wrapper (specification construct, no JS equivalent needed)
|
// 5b: old(expr) -> drop the wrapper (specification construct, no JS equivalent needed)
|
||||||
// Match balanced parens: old(...). The inner expr can contain parens.
|
// Match balanced parens: old(...). The inner expr can contain parens.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue