parseTypeExpr only handled angle-bracket generics (Map<K,V>, Set<T>, List<T>) but SpeckDL canonical syntax uses parentheses: Map(Nat, Account), Set(Nat), List(LogEntry). This caused two bugs: 1. State property types emitted as raw strings (accounts: Map(Nat, Account)) 2. Parametric types treated as enum interface names (TigerBeetleLedgerMap(Nat, Account)) Added List(T), Set(T), Map(K,V) handlers alongside existing angle-bracket handlers. All 41/41 tests passing. TigerBeetle now generates clean TS types: - accounts: Map<number, Account> (was Map(Nat, Account)) - pending: Set<number> (was Set(Nat)) - log: LogEntry[] (was List(LogEntry))
913 lines
26 KiB
TypeScript
913 lines
26 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
// AST node types for SpeckDL
|
|
export interface SpeckNode {
|
|
type: 'speck';
|
|
name: string;
|
|
members: MemberNode[];
|
|
}
|
|
|
|
export interface ImportNode {
|
|
type: 'import';
|
|
path: string;
|
|
alias?: string;
|
|
version?: string;
|
|
hash?: string;
|
|
}
|
|
|
|
export interface InputNode {
|
|
type: 'input';
|
|
typeExpr: TypeExpr;
|
|
}
|
|
|
|
export interface OutputNode {
|
|
type: 'output';
|
|
typeExpr: TypeExpr;
|
|
}
|
|
|
|
export interface ConstraintNode {
|
|
type: 'constraint';
|
|
expr: string;
|
|
}
|
|
|
|
export interface VerifyNode {
|
|
type: 'verify';
|
|
temporalExpr: string;
|
|
}
|
|
|
|
export interface InterfaceNode {
|
|
type: 'interface';
|
|
name: string;
|
|
methods: MethodSignature[];
|
|
}
|
|
|
|
export interface MethodSignature {
|
|
name: string;
|
|
params: { name: string; type: TypeExpr }[];
|
|
returnType: TypeExpr;
|
|
}
|
|
|
|
export interface EventNode {
|
|
type: 'event';
|
|
name: string;
|
|
fields: { name: string; type: TypeExpr }[];
|
|
}
|
|
|
|
export interface ProvenanceNode {
|
|
type: 'provenance';
|
|
clauses: ProvenanceClause[];
|
|
}
|
|
|
|
export interface ProvenanceClause {
|
|
type: 'regulation' | 'design_decision' | 'parent_spec' | 'external_doc';
|
|
value: string;
|
|
location?: string; // for external_doc
|
|
}
|
|
|
|
export interface ReviewNode {
|
|
type: 'review';
|
|
kind: 'manual' | 'auto' | 'hybrid';
|
|
}
|
|
|
|
export interface DerivesNode {
|
|
type: 'derives';
|
|
from: string;
|
|
via?: string;
|
|
}
|
|
|
|
export interface SatisfiesNode {
|
|
type: 'satisfies';
|
|
requirement: string;
|
|
clause?: string;
|
|
}
|
|
|
|
export interface AuthorNode {
|
|
type: 'author';
|
|
name: string;
|
|
email?: string;
|
|
}
|
|
|
|
export interface SourceNode {
|
|
type: 'source';
|
|
kind: 'conversation' | 'meeting' | 'document' | 'regulation' |
|
|
'architecture_review' | 'threat_model' | 'compliance_audit';
|
|
ref?: string;
|
|
}
|
|
|
|
export interface BOMNode {
|
|
type: 'bom';
|
|
compiler?: { name: string; version?: string };
|
|
solver?: { name: string; version?: string };
|
|
runtime?: { name: string; version?: string };
|
|
license?: string;
|
|
hash?: string;
|
|
}
|
|
|
|
// State machine extension (v0.3)
|
|
export interface StateNode {
|
|
type: 'state';
|
|
variables: StateVar[];
|
|
}
|
|
|
|
export interface StateVar {
|
|
name: string;
|
|
typeExpr: TypeExpr;
|
|
defaultInit?: string; // default initializer expression as raw string
|
|
}
|
|
|
|
export interface InitNode {
|
|
type: 'init';
|
|
assignments: { name: string; expr: string }[];
|
|
}
|
|
|
|
export interface ActionNode {
|
|
type: 'action';
|
|
name: string;
|
|
params: { name: string; type: TypeExpr }[];
|
|
statements: ActionStatement[];
|
|
}
|
|
|
|
export type ActionStatement =
|
|
| { type: 'assign'; target: string; expr: string }
|
|
| { type: 'let'; name: string; expr: string }
|
|
| { type: 'require'; expr: string }
|
|
| { type: 'precondition'; expr: string }
|
|
| { type: 'postcondition'; expr: string }
|
|
| { type: 'emit'; event: string; fields: { name: string; value: string }[] }
|
|
| { type: 'return'; expr: string };
|
|
|
|
export type MemberNode =
|
|
| ImportNode
|
|
| InputNode
|
|
| OutputNode
|
|
| ConstraintNode
|
|
| VerifyNode
|
|
| InterfaceNode
|
|
| EventNode
|
|
| ProvenanceNode
|
|
| ReviewNode
|
|
| DerivesNode
|
|
| SatisfiesNode
|
|
| AuthorNode
|
|
| SourceNode
|
|
| BOMNode
|
|
| StateNode
|
|
| InitNode
|
|
| ActionNode;
|
|
|
|
export interface TypeExpr {
|
|
type: 'primitive' | 'record' | 'list' | 'set' | 'map' | 'ident';
|
|
name?: string;
|
|
fields?: { name: string; type: TypeExpr }[];
|
|
elementType?: TypeExpr;
|
|
keyType?: TypeExpr; // for Map<K, V>
|
|
valueType?: TypeExpr; // for Map<K, V>
|
|
}
|
|
|
|
export interface AST {
|
|
specks: SpeckNode[];
|
|
}
|
|
|
|
/**
|
|
* Parse a .speck file and return the AST
|
|
*/
|
|
export function parseSpeckFile(filePath: string): AST {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
return parseSpeckContent(content, filePath);
|
|
}
|
|
|
|
/**
|
|
* Parse SpeckDL content and return the AST
|
|
*/
|
|
export function parseSpeckContent(content: string, filePath?: string): AST {
|
|
const specks: SpeckNode[] = [];
|
|
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: string[], startIndex: number): SpeckNode | null {
|
|
const headerMatch = lines[startIndex].match(/^speck\s+([\w-]+)\s*{/);
|
|
if (!headerMatch) return null;
|
|
|
|
const name = headerMatch[1];
|
|
const members: MemberNode[] = [];
|
|
|
|
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: string[], startIndex: number, startBraceCount: number): number {
|
|
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: string[], startIndex: number): MemberNode | null {
|
|
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: string): MemberNode | null {
|
|
// 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() as 'manual' | 'auto' | 'hybrid';
|
|
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: string): StateNode {
|
|
const variables: StateVar[] = [];
|
|
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: string): InitNode {
|
|
const assignments: { name: string; expr: string }[] = [];
|
|
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: string): ActionNode | null {
|
|
const match = line.match(/^action\s+(\w+)\s*(?:\(([^)]*)\))?\s*\{/);
|
|
if (!match) return null;
|
|
|
|
const name = match[1];
|
|
const paramsStr = match[2] || '';
|
|
const params: { name: string; type: TypeExpr }[] = [];
|
|
|
|
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: string): ImportNode {
|
|
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: string): TypeExpr {
|
|
expr = expr.trim();
|
|
|
|
// Primitive types
|
|
const primitives = ['Nat', 'Int', 'Real', 'Bool', 'String', 'Bytes'];
|
|
if (primitives.includes(expr)) {
|
|
return { type: 'primitive', name: expr };
|
|
}
|
|
|
|
// List<T>
|
|
if (expr.startsWith('List<') && expr.endsWith('>')) {
|
|
const inner = expr.substring(5, expr.length - 1).trim();
|
|
return { type: 'list', elementType: parseTypeExpr(inner) };
|
|
}
|
|
|
|
// Set<T>
|
|
if (expr.startsWith('Set<') && expr.endsWith('>')) {
|
|
const inner = expr.substring(4, expr.length - 1).trim();
|
|
return { type: 'set', elementType: parseTypeExpr(inner) };
|
|
}
|
|
|
|
// Map<K, V>
|
|
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: { name: string; type: TypeExpr }[] = [];
|
|
|
|
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: string): InterfaceNode {
|
|
// 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: string): EventNode {
|
|
// 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: string[], line: string): ProvenanceNode {
|
|
// This is a simplified parser - in a real implementation, we'd parse the full block
|
|
return {
|
|
type: 'provenance',
|
|
clauses: [],
|
|
};
|
|
}
|
|
|
|
function parseDerives(line: string): DerivesNode {
|
|
// 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: string): SatisfiesNode {
|
|
// 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: string): AuthorNode {
|
|
// 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: string): SourceNode {
|
|
// 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 as any,
|
|
ref: ref || undefined,
|
|
};
|
|
}
|
|
|
|
function parseStateBlockMultiline(lines: string[], startIndex: number, startBraceCount: number): StateNode {
|
|
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: string[], startIndex: number, startBraceCount: number): InitNode {
|
|
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: string[], startIndex: number, startBraceCount: number): ActionNode | null {
|
|
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: ActionStatement[] = [];
|
|
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: { name: string; value: string }[] = [];
|
|
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: string[], startIndex: number, startBraceCount: number): EventNode {
|
|
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: { name: string; type: TypeExpr }[] = [];
|
|
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: string[], startIndex: number, startBraceCount: number): InterfaceNode {
|
|
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: MethodSignature[] = [];
|
|
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: { name: string; type: TypeExpr }[] = [];
|
|
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 };
|
|
}
|