fix(speckl): strip inline // comments from event/interface field types (cycle 62)

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.
This commit is contained in:
Scott Scoble 2026-06-28 22:26:06 -07:00
parent a6bdbce915
commit 0e03aa4e04
2 changed files with 56 additions and 2 deletions

View file

@ -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');
}
});
});
});

View file

@ -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*(.+?),?$/);