From 0e03aa4e0408584a0eba8321ea78827cb452fa86 Mon Sep 17 00:00:00 2001 From: Scott Scoble Date: Sun, 28 Jun 2026 22:26:06 -0700 Subject: [PATCH] fix(speckl): strip inline // comments from event/interface field types (cycle 62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end compile of FederatedMeetup.speckdl surfaced a real generator input bug: inline line comments on field declarations were bleeding into the parsed type expression. A field like mesh_ip: Bytes // 4 bytes (IPv4 overlay) produced a TypeExpr whose name was literally 'Bytes // 4 bytes (IPv4 overlay)' which the .proto generator emitted verbatim: bytes // 4 bytes (IPv4 overlay) mesh_ip = 2; — broken syntax (comment in the type position). Fix: both parseEventBlockMultiline and parseInterfaceBlockMultiline now strip '// ...' suffixes from each inner line BEFORE matching the 'name: type' regex or splitting on commas. The state-block parser already had this; cycle 62 brings event and interface to parity. Tests: 2 new parser tests covering event and interface blocks, both with the FederatedMeetup-style inline comment. All 54 tests pass (52 baseline + 2 new). tsc build clean. End-to-end verified: running generateProtobuf against the real FederatedMeetup.speckdl now emits clean 'bytes mesh_ip = 2;' for the InitialMeshPeer interface, where cycle 62's pre-fix output was the broken 'Bytes // 4 bytes (IPv4 overlay) mesh_ip = 2;'. This unblocks the next phase of the federated-meetup Speckl migration: the consumer Go code can now bind to the generated proto without a half-broken field in the way. --- compiler/__tests__/parser.test.ts | 39 +++++++++++++++++++++++++++++++ compiler/src/parser.ts | 19 +++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) 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*(.+?),?$/);