diff --git a/compiler/src/generators/typescript-state-machine.ts b/compiler/src/generators/typescript-state-machine.ts index f080866..184c3f7 100644 --- a/compiler/src/generators/typescript-state-machine.ts +++ b/compiler/src/generators/typescript-state-machine.ts @@ -333,9 +333,12 @@ function emitAction(action: ActionNode, stateVarNames: Set, mapVarNames: case 'assign': { const target = cleanName(stmt.target); const expr = rewriteExpr(stmt.expr, stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues); - const mapBracketMatch = target.match(/^(\w+)\[([^\]]+)\]$/); + // Handle Map bracket assignment: mapVar[index] := value -> this.mapVar.set(index, value) + // Support nested brackets by finding the outermost [ ] pair + const mapBracketMatch = target.match(/^(\w+)\[([\s\S]+)\]$/); if (mapBracketMatch && mapVarNames.has(mapBracketMatch[1])) { - bodyParts.push(` this.${mapBracketMatch[1]}.set(${mapBracketMatch[2]}, ${expr});`); + const indexExpr = rewriteExpr(mapBracketMatch[2], stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues); + bodyParts.push(` this.${mapBracketMatch[1]}.set(${indexExpr}, ${expr});`); } else { bodyParts.push(` this.${target} = ${expr};`); } @@ -416,13 +419,19 @@ function rewriteExpr(expr: string, stateVarNames: Set, mapVarNames: Set< }); // Step 3b: Replace identifiers that are state variables but not local params + // Track if previous token was 'this.' to avoid double-prefixing const tokens = ts.split(/([\s\(\)\[\]{}+\-*/=<>!&|.,;:'"`]+)/); + let prevWasThisDot = false; const result = tokens.map(tok => { if (/^[a-zA-Z_]\w*\.\w+$/.test(tok)) return tok; // already has dot (enum ref) if (/^(true|false|null|undefined|\d+|\w+\.\w+)$/.test(tok)) return tok; + if (tok === 'this') { prevWasThisDot = true; return tok; } + if (tok === '.') { prevWasThisDot = true; return tok; } + if (prevWasThisDot) { prevWasThisDot = false; return tok; } // skip if preceded by this. if (/^[a-zA-Z_]\w*$/.test(tok) && shouldPrefix(tok)) { return `this.${tok}`; } + prevWasThisDot = false; return tok; }).join('') // Undo this. prefix on identifiers after bracket access @@ -478,6 +487,28 @@ function rewriteExpr(expr: string, stateVarNames: Set, mapVarNames: Set< // "not isOn" -> "!this.isOn" (will be prefixed by step 3) ts = ts.replace(/\bnot\s+/g, '!'); + // 5d2: and -> &&, or -> || (logical operators) + ts = ts.replace(/\band\b/g, '&&').replace(/\bor\b/g, '||'); + + // 5d3: Strip SpeckDL comments (// ...) before TS processing + ts = ts.replace(/\/\/.*$/gm, ''); + + // 5d4: [a..b] range syntax -> array literal [a, a+1, ..., b] + ts = ts.replace(/\[(\d+)\.\.(\d+)\]/g, (match, a, b) => { + const start = parseInt(a); + const end = parseInt(b); + const values = []; + for (let i = start; i <= end; i++) values.push(i); + return `[${values.join(', ')}]`; + }); + + // 5d5: Map literals {k: v} for Map type assignments + // If target is a Map var, convert {k: v} to new Map([[k, v]]) + ts = ts.replace(/\{(\d+):\s*([^}]+)\}/g, (match, k, v) => { + // Only convert if it looks like a Map literal (single key:value pair with number key) + return `new Map([[${k}, ${v}]])`; + }); + // 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" ts = ts.replace(/(\S+)\s*===\s*$/g, '$1 === $1'); @@ -491,10 +522,17 @@ function rewriteExpr(expr: string, stateVarNames: Set, mapVarNames: Set< for (const mapVar of mapVarNames) { // Match: mapVar[expr] where mapVar is a known Map state variable // Handle both bare mapVar[...] and this.mapVar[...] + // The inner expr needs rewriting for this-prefixing const barePattern = new RegExp('\\b(' + mapVar + ')\\[([^\\]]+)\\]', 'g'); - ts = ts.replace(barePattern, '$1.get($2)!'); + ts = ts.replace(barePattern, (match, varName, innerExpr) => { + const rewrittenInner = rewriteExpr(innerExpr, stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues); + return `${varName}.get(${rewrittenInner})!`; + }); const thisPattern = new RegExp('\\b(this\\.' + mapVar + ')\\[([^\\]]+)\\]', 'g'); - ts = ts.replace(thisPattern, '$1.get($2)!'); + ts = ts.replace(thisPattern, (match, varName, innerExpr) => { + const rewrittenInner = rewriteExpr(innerExpr, stateVarNames, mapVarNames, localNames, stateEnumName, knownStateValues); + return `${varName}.get(${rewrittenInner})!`; + }); } // 5h: .size() -> .size (Set/Map size property, not a method in JS) ts = ts.replace(/\.size\(\)/g, '.size'); @@ -571,13 +609,15 @@ function tsType(t: any, speckName: string = '', enumMap?: Map) } if (t.type === 'ident') { const rawName = cleanName(t.name); // strip trailing comma + // Map known identifier types to concrete TS types BEFORE checking enum + if (rawName === 'Integer') return 'number'; + if (rawName === 'Boolean') return 'boolean'; + if (rawName === 'Nat') return 'number'; // Check if this ident maps to a known enum interface if (enumMap && enumMap.has(rawName)) { return `${speckName}${rawName}`; } - // Map known identifier types to concrete TS types if (rawName === 'IssueState') return 'IssueWorkflowState'; - if (rawName === 'Nat') return 'number'; // Opaque interfaces become 'any' for now if (rawName === 'AST' || rawName === 'ArtifactMap') return 'any'; return rawName;