diff --git a/compiler/__tests__/parser.test.ts b/compiler/__tests__/parser.test.ts index 0a92083..e56a0eb 100644 --- a/compiler/__tests__/parser.test.ts +++ b/compiler/__tests__/parser.test.ts @@ -794,4 +794,43 @@ speck Second { expect(b.specks[0].eventSuffix).toBe('Payload'); }); }); + + // Cycle 62: inline `//` comments on field declarations were bleeding + // into the type expression. Real federated-meetup had: + // mesh_ip: Bytes // 4 bytes (IPv4 overlay) + // emitted as `Bytes // 4 bytes (IPv4 overlay) mesh_ip = 2;` — + // broken syntax. The fix strips the comment before parsing the type. + describe('inline comment stripping (cycle 62)', () => { + it('should strip inline // comments from event field types', () => { + const content = `speck One { + event InitialMeshPeer { + host_wg_key: Bytes, // 32 bytes + mesh_ip: Bytes // 4 bytes (IPv4 overlay) + } +}`; + const ast = parseSpeckContent(content); + const ev = ast.specks[0].members.find(m => m.name === 'InitialMeshPeer'); + if (ev?.type === 'event') { + const meshIp = ev.fields.find(f => f.name === 'mesh_ip'); + expect(meshIp?.type.name).toBe('Bytes'); + expect(meshIp?.type.name).not.toContain('IPv4'); + } + }); + + it('should strip inline // comments from interface field types', () => { + const content = `speck One { + interface InitialMeshPeer { + host_wg_key: Bytes, // X25519 + mesh_ip: Bytes // 4 bytes (IPv4 overlay) + } +}`; + const ast = parseSpeckContent(content); + const iface = ast.specks[0].members.find(m => m.name === 'InitialMeshPeer'); + if (iface?.type === 'interface') { + const meshIp = iface.fields.find(f => f.name === 'mesh_ip'); + expect(meshIp?.type.name).toBe('Bytes'); + expect(meshIp?.type.name).not.toContain('IPv4'); + } + }); + }); }); diff --git a/compiler/src/parser.ts b/compiler/src/parser.ts index e4c66cb..45f1dd8 100644 --- a/compiler/src/parser.ts +++ b/compiler/src/parser.ts @@ -1294,7 +1294,15 @@ function parseEventBlockMultiline(lines: string[], startIndex: number, startBrac const fields: { name: string; type: TypeExpr }[] = []; for (const line of innerLines) { - const match = line.match(/^(\w+)\s*:\s*(.+?),?$/); + // Cycle 62: strip inline `//` line-comments from the type expression + // before parsing. Without this, an event field like + // mesh_ip: Bytes // 4 bytes (IPv4 overlay) + // produces a TypeExpr whose name includes the comment text, which + // the .proto generator emits verbatim: + // bytes // 4 bytes (IPv4 overlay) mesh_ip = 2; + // (broken syntax — comment is in the type position). + const noComment = line.replace(/\/\/.*$/, '').trim(); + const match = noComment.match(/^(\w+)\s*:\s*(.+?),?$/); if (match) { fields.push({ name: match[1], type: parseTypeExpr(match[2].replace(/,$/, '').trim()) }); } @@ -1313,8 +1321,15 @@ function parseInterfaceBlockMultiline(lines: string[], startIndex: number, start const fields: InterfaceField[] = []; for (const line of innerLines) { + // Cycle 62: strip inline `//` line-comments from each line before + // token-splitting. Without this, a field like + // mesh_ip: Bytes // 4 bytes (IPv4 overlay) + // has its comment text bleed into the parsed type expression, + // which the .proto generator emits verbatim — broken syntax. + const noComment = line.replace(/\/\/.*$/, '').trim(); + if (!noComment) continue; // Split on comma so multi-variant lines like "Idle, Parsing, Done" all parse - const tokens = line.split(',').map(s => s.trim()).filter(Boolean); + const tokens = noComment.split(',').map(s => s.trim()).filter(Boolean); for (const token of tokens) { // Check 1: method signature form: name(params) -> returnType const methodMatch = token.match(/^(\w+)\s*\(([^)]*)\)\s*->\s*(.+?),?$/);