diff --git a/compiler/src/parser.ts b/compiler/src/parser.ts index da82cbb..f3ee705 100644 --- a/compiler/src/parser.ts +++ b/compiler/src/parser.ts @@ -246,7 +246,8 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { const closeCount = (rawLine.match(/}/g) || []).length; // 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)); if (isBlockStarter && openCount > 0) { @@ -304,9 +305,15 @@ function parseMemberBlock(lines: string[], startIndex: number): MemberNode | nul if (firstLine.startsWith('state:')) { return parseStateBlockMultiline(lines, startIndex, startBraceCount); } + if (firstLine.startsWith('state {') || firstLine === 'state') { + return parseStateBlockMultiline(lines, startIndex, startBraceCount); + } if (firstLine.startsWith('init:')) { return parseInitBlockMultiline(lines, startIndex, startBraceCount); } + if (firstLine.startsWith('init {') || firstLine === 'init') { + return parseInitBlockMultiline(lines, startIndex, startBraceCount); + } if (firstLine.startsWith('action ')) { return parseActionBlockMultiline(lines, startIndex, startBraceCount); } @@ -390,7 +397,7 @@ function parseMember(line: string): MemberNode | null { return parseSource(line); } - // state: + // state: or state { if (line.startsWith('state:')) { const varsStr = line.substring(6).trim(); // state: { ... } - single-line form @@ -401,8 +408,19 @@ function parseMember(line: string): MemberNode | null { // state: { - multi-line opening (handled by parseStateMultiline) return { type: 'state', variables: [] }; } + 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: + // init: or init { if (line.startsWith('init:')) { const initStr = line.substring(5).trim(); if (initStr.startsWith('{') && initStr.endsWith('}')) { @@ -411,6 +429,17 @@ function parseMember(line: string): MemberNode | null { } 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 if (line.startsWith('action ')) { @@ -553,6 +582,40 @@ function parseTypeExpr(expr: string): TypeExpr { // Fallback: treat as ident if malformed 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] if (expr.startsWith('[') && expr.endsWith(']')) {