import fs from 'fs'; /** * Parse a .speck file and return the AST */ export function parseSpeckFile(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); return parseSpeckContent(content, filePath); } /** * Parse SpeckDL content and return the AST */ export function parseSpeckContent(content, filePath) { const specks = []; const lines = content.split('\n'); let i = 0; while (i < lines.length) { const line = lines[i].trim(); // Skip empty lines and comments if (!line || line.startsWith('//') || line.startsWith('/*')) { i++; continue; } // Handle import statements if (line.startsWith('import ')) { const importNode = parseImportStatement(line); // Import is a special case - it's not part of a speck // We'll handle it separately in a future update i++; continue; } // Handle speck definitions if (line.startsWith('speck ')) { const speck = parseSpeck(lines, i); if (speck) { specks.push(speck); // Move index past the speck we just parsed i++; // Increment by 1, the loop will continue from next line } else { i++; } continue; } i++; } return { specks }; } function parseSpeck(lines, startIndex) { const headerMatch = lines[startIndex].match(/^speck\s+([\w-]+)\s*{/); if (!headerMatch) return null; const name = headerMatch[1]; const members = []; let i = startIndex + 1; let braceCount = 1; while (i < lines.length && braceCount > 0) { const rawLine = lines[i]; const line = rawLine.trim(); if (!line || line.startsWith('//') || line.startsWith('/*')) { i++; continue; } // Count braces on this line first const openCount = (rawLine.match(/{/g) || []).length; const closeCount = (rawLine.match(/}/g) || []).length; // Check for block-opening members const blockStarters = ['state:', 'init:', 'action ', 'event ', 'provenance ', 'bom ', 'interface ', 'state {', 'init {']; const isBlockStarter = blockStarters.some(s => line.startsWith(s)); if (isBlockStarter && openCount > 0) { // Multi-line block: parse it and skip to its end const member = parseMemberBlock(lines, i); if (member) { members.push(member); } // Skip past this block const endIdx = findBlockEnd(lines, i + 1, braceCount + openCount); i = endIdx + 1; continue; } // Single-line: update brace count, then parse if still inside braceCount += openCount - closeCount; if (braceCount <= 0) { i++; break; } const member = parseMember(line); if (member) { members.push(member); } i++; } return { type: 'speck', name, members }; } function findBlockEnd(lines, startIndex, startBraceCount) { let braceCount = startBraceCount; let i = startIndex; while (i < lines.length) { const line = lines[i]; for (const ch of line) { if (ch === '{') braceCount++; if (ch === '}') { braceCount--; if (braceCount < startBraceCount) { return i; } } } i++; } return i; } function parseMemberBlock(lines, startIndex) { const firstLine = lines[startIndex].trim(); const startBraceCount = (firstLine.match(/{/g) || []).length; 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); } if (firstLine.startsWith('event ')) { return parseEventBlockMultiline(lines, startIndex, startBraceCount); } if (firstLine.startsWith('interface ')) { return parseInterfaceBlockMultiline(lines, startIndex, startBraceCount); } if (firstLine.startsWith('provenance ')) { return { type: 'provenance', clauses: [] }; } if (firstLine.startsWith('bom ')) { return { type: 'bom' }; } return null; } function parseMember(line) { // input: if (line.startsWith('input:')) { const typeExpr = parseTypeExpr(line.substring(6).trim()); return { type: 'input', typeExpr }; } // output: if (line.startsWith('output:')) { const typeExpr = parseTypeExpr(line.substring(7).trim()); return { type: 'output', typeExpr }; } // constraint: if (line.startsWith('constraint:')) { return { type: 'constraint', expr: line.substring(11).trim() }; } // verify: if (line.startsWith('verify:')) { return { type: 'verify', temporalExpr: line.substring(7).trim() }; } // interface: if (line.startsWith('interface ')) { return parseInterface(line); } // event: if (line.startsWith('event ')) { return parseEvent(line); } // provenance { if (line.startsWith('provenance ')) { return { type: 'provenance', clauses: [] }; } // review: if (line.startsWith('review:')) { const kind = line.substring(7).trim(); return { type: 'review', kind }; } // derives from: if (line.startsWith('derives from ')) { return parseDerives(line); } // satisfies: if (line.startsWith('satisfies ')) { return parseSatisfies(line); } // author: if (line.startsWith('author:')) { return parseAuthor(line); } // source: if (line.startsWith('source:')) { return parseSource(line); } // state: or state { if (line.startsWith('state:')) { const varsStr = line.substring(6).trim(); // state: { ... } - single-line form if (varsStr.startsWith('{') && varsStr.endsWith('}')) { const inner = varsStr.slice(1, -1).trim(); return parseStateBlock(inner); } // 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: or init { if (line.startsWith('init:')) { const initStr = line.substring(5).trim(); if (initStr.startsWith('{') && initStr.endsWith('}')) { const inner = initStr.slice(1, -1).trim(); return parseInitBlock(inner); } 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 ')) { return parseActionHeader(line); } // bom { if (line.startsWith('bom ')) { // Simplified - full BOM block parsing would need multi-line context return { type: 'bom', }; } return null; } function parseStateBlock(inner) { const variables = []; if (!inner.trim()) return { type: 'state', variables }; const varStrs = inner.split(/;|\n/).map(s => s.trim()).filter(Boolean); for (const vStr of varStrs) { const withDefault = vStr.match(/^(\w+)\s*:\s*(.+?)\s*:=\s*(.+)$/); if (withDefault) { variables.push({ name: withDefault[1], typeExpr: parseTypeExpr(withDefault[2].trim()), defaultInit: withDefault[3].trim(), }); continue; } const withoutDefault = vStr.match(/^(\w+)\s*:\s*(.+)$/); if (withoutDefault) { variables.push({ name: withoutDefault[1], typeExpr: parseTypeExpr(withoutDefault[2].trim()), }); } } return { type: 'state', variables }; } function parseInitBlock(inner) { const assignments = []; if (!inner.trim()) return { type: 'init', assignments }; const assignStrs = inner.split(/;|\n/).map(s => s.trim()).filter(Boolean); for (const aStr of assignStrs) { const match = aStr.match(/^(\w+)\s*:=\s*(.+)$/); if (match) { assignments.push({ name: match[1], expr: match[2].trim() }); } } return { type: 'init', assignments }; } function parseActionHeader(line) { const match = line.match(/^action\s+(\w+)\s*(?:\(([^)]*)\))?\s*\{/); if (!match) return null; const name = match[1]; const paramsStr = match[2] || ''; const params = []; if (paramsStr.trim()) { const paramPairs = paramsStr.split(',').map(s => s.trim()).filter(Boolean); for (const p of paramPairs) { const pMatch = p.match(/^(\w+)\s*:\s*(.+)$/); if (pMatch) { params.push({ name: pMatch[1], type: parseTypeExpr(pMatch[2].trim()) }); } } } return { type: 'action', name, params, statements: [] }; } function parseImportStatement(line) { const match = line.match(/^import\s+(['"])(.+?)\1(?:\s+as\s+(\w+))?(?:\s+version\s+(['"])(.+?)\4)?(?:\s+hash\s+(['"])(.+?)\6)?/); if (!match) { throw new Error(`Failed to parse import statement: ${line}`); } const [, , path, , alias, , version, , hash] = match; return { type: 'import', path, alias, version: version || undefined, hash: hash || undefined, }; } function parseTypeExpr(expr) { expr = expr.trim(); // Primitive types const primitives = ['Nat', 'Int', 'Real', 'Bool', 'String', 'Bytes']; if (primitives.includes(expr)) { return { type: 'primitive', name: expr }; } // List if (expr.startsWith('List<') && expr.endsWith('>')) { const inner = expr.substring(5, expr.length - 1).trim(); return { type: 'list', elementType: parseTypeExpr(inner) }; } // Set if (expr.startsWith('Set<') && expr.endsWith('>')) { const inner = expr.substring(4, expr.length - 1).trim(); return { type: 'set', elementType: parseTypeExpr(inner) }; } // Map if (expr.startsWith('Map<') && expr.endsWith('>')) { const inner = expr.substring(4, expr.length - 1); // Find the first-level comma (not inside nested generics) let depth = 0; let commaIdx = -1; for (let i = 0; i < inner.length; i++) { if (inner[i] === '<') depth++; else if (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(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(']')) { const elementType = parseTypeExpr(expr.substring(1, expr.length - 1)); return { type: 'list', elementType }; } // Record type: { ... } if (expr.startsWith('{') && expr.endsWith('}')) { const content = expr.substring(1, expr.length - 1).trim(); const fields = []; if (content) { // Split by comma and parse each field const fieldStrs = content.split(',').map(s => s.trim()); for (const fieldStr of fieldStrs) { const fieldMatch = fieldStr.match(/^(\w+)\s*:\s*(.+)$/); if (fieldMatch) { const [, name, typeStr] = fieldMatch; fields.push({ name, type: parseTypeExpr(typeStr.trim()) }); } } } return { type: 'record', fields }; } // Identifier (for interface references, etc.) return { type: 'ident', name: expr }; } function parseInterface(line) { // interface Name { const match = line.match(/^interface\s+(\w+)\s*{/); if (!match) { throw new Error(`Failed to parse interface: ${line}`); } const name = match[1]; // This is a simplified parser - in a real implementation, we'd need to handle multi-line interfaces return { type: 'interface', name, methods: [], }; } function parseEvent(line) { // event Name { const match = line.match(/^event\s+(\w+)\s*{/); if (!match) { throw new Error(`Failed to parse event: ${line}`); } const name = match[1]; return { type: 'event', name, fields: [], }; } function parseProvenanceBlock(lines, line) { // This is a simplified parser - in a real implementation, we'd parse the full block return { type: 'provenance', clauses: [], }; } function parseDerives(line) { // derives from Name via "rationale" const match = line.match(/^derives\s+from\s+(\w+(?:-\w+)*)(?:\s+via\s+(['"])(.+?)\2)?/); if (!match) { console.warn(`Failed to parse derives statement: ${line}`); return { type: 'derives', from: 'unknown' }; } const [, from, , via] = match; return { type: 'derives', from, via: via || undefined, }; } function parseSatisfies(line) { // satisfies Name [clause "X.Y.Z"] // e.g., satisfies RESILIENCE-REQ-01 clause "3.1.2" // e.g., satisfies RESILIENCE-REQ-02 const match = line.match(/^satisfies\s+([\w-]+(?:-[\w]+)*)(?:\s+clause\s+(['"])(.+?)\2)?/); if (!match) { console.warn(`Failed to parse satisfies statement: ${line}`); return { type: 'satisfies', requirement: 'unknown' }; } const [, requirement, , clause] = match; return { type: 'satisfies', requirement, clause: clause || undefined, }; } function parseAuthor(line) { // author: "Name" <"email"> // e.g., author: "Scott Scoble" <"scott@scoble.me"> const match = line.match(/^author:\s+(['"])(.+?)\1(?:\s+<(['"])(.+?)\3>)?/); if (!match) { // Try without angle brackets or with different format console.warn(`Failed to parse author statement: ${line}`); return { type: 'author', name: 'unknown' }; } const [, , name, , email] = match; return { type: 'author', name, email: email || undefined, }; } function parseSource(line) { // source: kind ref "identifier" // e.g., source: architecture_review ref "AR-2024-0215" const match = line.match(/^source:\s+(\w+)(?:\s+ref\s+(['"])(.+?)\2)?/); if (!match) { console.warn(`Failed to parse source statement: ${line}`); return { type: 'source', kind: 'document' }; } const [, kind, , ref] = match; const validKinds = [ 'conversation', 'meeting', 'document', 'regulation', 'architecture_review', 'threat_model', 'compliance_audit' ]; if (!validKinds.includes(kind)) { console.warn(`Invalid source kind: ${kind}, defaulting to document`); return { type: 'source', kind: 'document', ref: ref || undefined }; } return { type: 'source', kind: kind, ref: ref || undefined, }; } function parseStateBlockMultiline(lines, startIndex, 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 inner = innerLines.join('; '); return parseStateBlock(inner); } function parseInitBlockMultiline(lines, startIndex, 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 inner = innerLines.join('; '); return parseInitBlock(inner); } function parseActionBlockMultiline(lines, startIndex, startBraceCount) { const firstLine = lines[startIndex].trim(); const header = parseActionHeader(firstLine); if (!header) return null; const endIndex = findBlockEnd(lines, startIndex + 1, startBraceCount); const bodyLines = lines.slice(startIndex + 1, endIndex).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('/*')); const statements = []; for (let i = 0; i < bodyLines.length; i++) { const stmt = bodyLines[i]; // precondition: expr if (stmt.startsWith('precondition:')) { let expr = stmt.substring(13).trim(); // support 'or' continuation lines while (i + 1 < bodyLines.length && bodyLines[i + 1].startsWith('or ')) { i++; expr += ' || ' + bodyLines[i].substring(3).trim(); } statements.push({ type: 'precondition', expr }); continue; } // postcondition: expr if (stmt.startsWith('postcondition:')) { let expr = stmt.substring(14).trim(); // support 'or' continuation lines while (i + 1 < bodyLines.length && bodyLines[i + 1].startsWith('or ')) { i++; expr += ' || ' + bodyLines[i].substring(3).trim(); } statements.push({ type: 'postcondition', expr }); continue; } // require expr if (stmt.startsWith('require ')) { statements.push({ type: 'require', expr: stmt.substring(8).trim() }); continue; } // return expr if (stmt.startsWith('return ')) { statements.push({ type: 'return', expr: stmt.substring(7).trim() }); continue; } // emit Event { fields } const emitMatch = stmt.match(/^emit\s+(\w+)\s*\{\s*(.*)\s*\}$/); if (emitMatch) { const eventName = emitMatch[1]; const fieldsStr = emitMatch[2]; const fields = []; if (fieldsStr) { const fieldPairs = fieldsStr.split(',').map(s => s.trim()).filter(Boolean); for (const fp of fieldPairs) { const colonIdx = fp.indexOf(':'); if (colonIdx > 0) { fields.push({ name: fp.substring(0, colonIdx).trim(), value: fp.substring(colonIdx + 1).trim(), }); } } } statements.push({ type: 'emit', event: eventName, fields }); continue; } // let name := expr const letMatch = stmt.match(/^let\s+(\w+)\s*:=\s*(.+)$/); if (letMatch) { statements.push({ type: 'let', name: letMatch[1], expr: letMatch[2].trim() }); continue; } // assignment: target := expr const assignMatch = stmt.match(/^(\w+(?:\[.*?\])?)\s*:=\s*(.+)$/); if (assignMatch) { statements.push({ type: 'assign', target: assignMatch[1], expr: assignMatch[2].trim() }); } } return { ...header, statements }; } function parseEventBlockMultiline(lines, startIndex, startBraceCount) { const firstLine = lines[startIndex].trim(); const header = parseEvent(firstLine); 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 fields = []; for (const line of innerLines) { const match = line.match(/^(\w+)\s*:\s*(.+?),?$/); if (match) { fields.push({ name: match[1], type: parseTypeExpr(match[2].replace(/,$/, '').trim()) }); } } return { ...header, fields }; } function parseInterfaceBlockMultiline(lines, startIndex, startBraceCount) { const firstLine = lines[startIndex].trim(); const header = parseInterface(firstLine); 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 methods = []; for (const line of innerLines) { // name(params) -> returnType const match = line.match(/^(\w+)\s*\(([^)]*)\)\s*-\u003e\s*(.+?),?$/); if (match) { const name = match[1]; const paramsStr = match[2]; const returnTypeStr = match[3].replace(/,$/, '').trim(); const params = []; if (paramsStr.trim()) { const paramPairs = paramsStr.split(',').map(s => s.trim()).filter(Boolean); for (const p of paramPairs) { const pMatch = p.match(/^(\w+)\s*:\s*(.+)$/); if (pMatch) { params.push({ name: pMatch[1], type: parseTypeExpr(pMatch[2].trim()) }); } } } methods.push({ name, params, returnType: parseTypeExpr(returnTypeStr) }); } } return { ...header, methods }; } //# sourceMappingURL=parser.js.map