fix: correct enum value rewriting and parser quoted-string handling #16
4 changed files with 110 additions and 18 deletions
30
compiler/dist/parser.js
vendored
30
compiler/dist/parser.js
vendored
|
|
@ -498,7 +498,35 @@ function parseActionBlockMultiline(lines, startIndex, startBraceCount) {
|
|||
const fieldsStr = emitMatch[2];
|
||||
const fields = [];
|
||||
if (fieldsStr) {
|
||||
const fieldPairs = fieldsStr.split(',').map(s => s.trim()).filter(Boolean);
|
||||
// Split on commas but respect quoted strings (don't split inside "...")
|
||||
const fieldPairs = [];
|
||||
let depth = 0;
|
||||
let inQuote = false;
|
||||
let current = '';
|
||||
for (let i = 0; i < fieldsStr.length; i++) {
|
||||
const ch = fieldsStr[i];
|
||||
if (ch === '"' && (i === 0 || fieldsStr[i - 1] !== '\\')) {
|
||||
inQuote = !inQuote;
|
||||
current += ch;
|
||||
}
|
||||
else if (!inQuote && ch === '(') {
|
||||
depth++;
|
||||
current += ch;
|
||||
}
|
||||
else if (!inQuote && ch === ')') {
|
||||
depth--;
|
||||
current += ch;
|
||||
}
|
||||
else if (!inQuote && depth === 0 && ch === ',') {
|
||||
fieldPairs.push(current.trim());
|
||||
current = '';
|
||||
}
|
||||
else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current.trim())
|
||||
fieldPairs.push(current.trim());
|
||||
for (const fp of fieldPairs) {
|
||||
const colonIdx = fp.indexOf(':');
|
||||
if (colonIdx > 0) {
|
||||
|
|
|
|||
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
|
|
@ -185,12 +185,21 @@ function emitSpeck(speck: SpeckNode): string {
|
|||
allKnownStateValues.push(...values);
|
||||
}
|
||||
|
||||
// Build value -> enumName map for correct enum prefixing in init
|
||||
const initValueToEnum = new Map<string, string>();
|
||||
for (const [enumName, values] of enumMap) {
|
||||
const fullEnumName = `${speck.name}${enumName}`;
|
||||
for (const v of values) {
|
||||
initValueToEnum.set(v, fullEnumName);
|
||||
}
|
||||
}
|
||||
|
||||
const initAssignments = initNode
|
||||
? initNode.assignments.map(a => {
|
||||
const name = cleanName(a.name);
|
||||
const expr = cleanExpr(a.expr);
|
||||
// Replace bare state literals with enum refs in init
|
||||
const enumRef = replaceStateWithEnum(expr, stateEnumName, allKnownStateValues);
|
||||
const enumRef = replaceStateWithEnum(expr, stateEnumName, allKnownStateValues, initValueToEnum);
|
||||
return ` this.${name} = ${enumRef};`;
|
||||
}).join('\n')
|
||||
: '';
|
||||
|
|
@ -238,6 +247,7 @@ ${verifyChecks}
|
|||
const file =
|
||||
`// Auto-generated by speckl-compile from ${speck.name}\n` +
|
||||
`// DO NOT EDIT MANUALLY — regenerate from .speckdl source\n\n` +
|
||||
`import path from 'path';\n\n` +
|
||||
`${stateEnum}\n\n` +
|
||||
`${eventInterfaces}\n\n` +
|
||||
`${classBody}`;
|
||||
|
|
@ -256,7 +266,15 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
const requires = action.statements
|
||||
.filter(s => s.type === 'require')
|
||||
.map(s => {
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName, knownStateValues);
|
||||
// Build value -> enumName map for correct enum prefixing
|
||||
const valueToEnum = new Map<string, string>();
|
||||
for (const [enumName, values] of enumMap) {
|
||||
const fullEnumName = `${speckName}${enumName}`;
|
||||
for (const v of values) {
|
||||
valueToEnum.set(v, fullEnumName);
|
||||
}
|
||||
}
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName, knownStateValues, valueToEnum);
|
||||
return ` if (!(${expr})) throw new Error('Guard failed: ${expr.replace(/'/g, "\\'")}');`;
|
||||
})
|
||||
.join('\n');
|
||||
|
|
@ -265,7 +283,8 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
.filter(s => s.type === 'assign')
|
||||
.map(s => {
|
||||
const target = cleanName(s.target);
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName, knownStateValues);
|
||||
// For assignments, no enum value rewriting needed
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName, knownStateValues, new Map());
|
||||
return ` this.${target} = ${expr};`;
|
||||
})
|
||||
.join('\n');
|
||||
|
|
@ -276,8 +295,16 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
const fields = s.fields
|
||||
.map(f => {
|
||||
const name = cleanName(f.name);
|
||||
const val = rewriteExpr(f.value, stateVarNames, localNames, stateEnumName, knownStateValues);
|
||||
return ` ${name}: ${val}`;
|
||||
// Emit fields also need correct enum prefixing for non-state enum values
|
||||
const emitValueToEnum = new Map();
|
||||
for (const [enumName, values] of enumMap) {
|
||||
const fullEnumName = `${speckName}${enumName}`;
|
||||
for (const v of values) {
|
||||
emitValueToEnum.set(v, fullEnumName);
|
||||
}
|
||||
}
|
||||
const expr = rewriteExpr(f.value, stateVarNames, localNames, stateEnumName, knownStateValues, emitValueToEnum);
|
||||
return ` ${name}: ${expr}`;
|
||||
})
|
||||
.join(',\n');
|
||||
return ` this.decisionLog.push({\n${fields}\n } as ${s.event});`;
|
||||
|
|
@ -290,7 +317,7 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
|
||||
// Rewrite SpeckDL expressions to valid TypeScript
|
||||
// Only prefix identifiers with `this.` if they are state variables AND not local params
|
||||
function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<string>, stateEnumName: string, knownStateValues: string[]): string {
|
||||
function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<string>, stateEnumName: string, knownStateValues: string[], valueToEnum?: Map<string, string>): string {
|
||||
function shouldPrefix(id: string): boolean {
|
||||
return stateVarNames.has(id) && !localNames.has(id);
|
||||
}
|
||||
|
|
@ -299,16 +326,18 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<s
|
|||
let ts = expr
|
||||
.replace(/\bnow\(\)/g, 'Date.now()')
|
||||
.replace(/\blen\(([^)]+)\)/g, '$1.length')
|
||||
.replace(/\bjoin\(([^,]+),\s*([^)]+)\)/g, '$1.join($2)')
|
||||
.replace(/\bjoin\(([^,]+),\s*([^)]+)\)/g, 'path.join($1, $2)')
|
||||
.replace(/\bforall\s+(\w+)\s+in\s+([^:]+):\s*(.+)/g, '$2.every(($1) => $3)');
|
||||
|
||||
// Step 2: Replace state enum literals with enum refs BEFORE identifier replacement
|
||||
const allStateValues = new Set<string>(knownStateValues);
|
||||
// Add common fallback states
|
||||
['Open','Grooming','Groomed','Assigned','InProgress','InReview','Merged','Blocked','Closed','Idle','Parsing','Parsed','ProvenanceGenerated','BOMsGenerated','TypeScriptGenerated','WASMGenerated','Done'].forEach(s => allStateValues.add(s));
|
||||
if (allStateValues.size > 0) {
|
||||
const stateValueRegex = new RegExp('\\b(' + Array.from(allStateValues).join('|') + ')\\b', 'g');
|
||||
ts = ts.replace(stateValueRegex, `${stateEnumName}.$1`);
|
||||
// Step 2: Replace enum values with their correct enum refs BEFORE identifier replacement
|
||||
if (valueToEnum && valueToEnum.size > 0) {
|
||||
// Sort by length (longest first) to avoid partial matches
|
||||
const sortedValues = Array.from(valueToEnum.keys()).sort((a, b) => b.length - a.length);
|
||||
for (const value of sortedValues) {
|
||||
const enumRef = valueToEnum.get(value)!;
|
||||
const regex = new RegExp(`\\b${value}\\b`, 'g');
|
||||
ts = ts.replace(regex, `${enumRef}.${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Replace identifiers that are state variables but not local params
|
||||
|
|
@ -359,7 +388,18 @@ function cleanName(s: string): string {
|
|||
return s.replace(/,$/, '').trim();
|
||||
}
|
||||
|
||||
function replaceStateWithEnum(expr: string, enumName: string, knownStateValues: string[]): string {
|
||||
function replaceStateWithEnum(expr: string, enumName: string, knownStateValues: string[], valueToEnum?: Map<string, string>): string {
|
||||
if (valueToEnum && valueToEnum.size > 0) {
|
||||
// Sort by length (longest first) to avoid partial matches
|
||||
const sortedValues = Array.from(valueToEnum.keys()).sort((a, b) => b.length - a.length);
|
||||
for (const value of sortedValues) {
|
||||
const enumRef = valueToEnum.get(value)!;
|
||||
const regex = new RegExp(`\\b${value}\\b`, 'g');
|
||||
expr = expr.replace(regex, `${enumRef}.${value}`);
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
const allStateValues = new Set<string>(knownStateValues);
|
||||
['Open','Grooming','Groomed','Assigned','InProgress','InReview','Merged','Blocked','Closed','Idle','Parsing','Parsed','ProvenanceGenerated','BOMsGenerated','TypeScriptGenerated','WASMGenerated','Done'].forEach(s => allStateValues.add(s));
|
||||
if (allStateValues.size === 0) return expr;
|
||||
|
|
|
|||
|
|
@ -746,7 +746,31 @@ function parseActionBlockMultiline(lines: string[], startIndex: number, startBra
|
|||
const fieldsStr = emitMatch[2];
|
||||
const fields: { name: string; value: string }[] = [];
|
||||
if (fieldsStr) {
|
||||
const fieldPairs = fieldsStr.split(',').map(s => s.trim()).filter(Boolean);
|
||||
// Split on commas but respect quoted strings (don't split inside "...")
|
||||
const fieldPairs: string[] = [];
|
||||
let depth = 0;
|
||||
let inQuote = false;
|
||||
let current = '';
|
||||
for (let i = 0; i < fieldsStr.length; i++) {
|
||||
const ch = fieldsStr[i];
|
||||
if (ch === '"' && (i === 0 || fieldsStr[i - 1] !== '\\')) {
|
||||
inQuote = !inQuote;
|
||||
current += ch;
|
||||
} else if (!inQuote && ch === '(') {
|
||||
depth++;
|
||||
current += ch;
|
||||
} else if (!inQuote && ch === ')') {
|
||||
depth--;
|
||||
current += ch;
|
||||
} else if (!inQuote && depth === 0 && ch === ',') {
|
||||
fieldPairs.push(current.trim());
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current.trim()) fieldPairs.push(current.trim());
|
||||
|
||||
for (const fp of fieldPairs) {
|
||||
const colonIdx = fp.indexOf(':');
|
||||
if (colonIdx > 0) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue