Claude Code transcript - page 1/2

Implement the following plan:

Plan: Support dotted protocol tags (Issue #68)

Context

Occam variant protocol tags can contain dots (e.g., bar.data, bar.terminate, bar.blank), as seen in real-world code like kroc/modules/course/examples/bar.occ. The issue requests support for this.

After investigation, the lexer already handles dots as part of identifiers (lexer.go:280readIdentifier() loops while isLetter || isDigit || ch == '.'), so bar.data is already a single IDENT token. The codegen already calls goIdent() on tag names, converting dots to underscores. The core functionality likely already works, but has no test coverage.

Changes

1. Add parser unit test for dotted variant protocol tags

File: parser/parser_test.go

Add TestVariantProtocolDeclDottedTags — a protocol with dotted tag names (bar.data ; INT, bar.terminate, bar.blank ; INT). Verify each ProtocolVariant.Tag preserves the dotted name (e.g., "bar.data").

2. Add codegen unit test for dotted variant protocol types

File: codegen/codegen_test.go

Add TestVariantProtocolDottedTags — verify generated Go output contains underscore-converted names: - type _proto_BAR_PROTO_bar_data struct { ... } - type _proto_BAR_PROTO_bar_terminate struct{} - etc.

3. Add e2e test for dotted protocol tags (send + receive)

File: codegen/e2e_protocol_test.go

Add TestE2E_VariantProtocolDottedTags — a full transpile→compile→run test: - Protocol with dotted tags (with and without payloads) - Variant send with dotted tag + payload (c ! bar.data ; 42) - Variant send with dotted tag + no payload (c ! bar.blank) - Variant receive with dotted tags in CASE arms - Verify expected output

4. Fix any issues found during testing

If the tests reveal bugs, fix them in the relevant parser/codegen code.

5. Update TODO.md

Document that dotted protocol tags are now supported.

Verification

go test ./parser -run TestVariantProtocolDeclDottedTags -v
go test ./codegen -run TestVariantProtocolDottedTags -v
go test ./codegen -run TestE2E_VariantProtocolDottedTags -v
go test ./...   # full suite to check for regressions

If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/david/.claude/projects/-home-david-projects-code-associates-occam2go/e4fce02a-e445-42c2-8859-d822a1765d80.jsonl

Thinking

Let me implement the plan for supporting dotted protocol tags. I need to:

  1. Add parser unit test
  2. Add codegen unit test
  3. Add e2e test
  4. Fix any issues found
  5. Update TODO.md

Let me start by reading the relevant test files to understand existing patterns.

I'll start by reading the existing test files to understand the patterns, then implement all the tests.

Task
Explore existing test patterns
{
  "prompt": "Read the following files and return their content so I can understand existing test patterns for variant protocols:\n\n1. Find and read existing variant protocol parser tests in `parser/parser_test.go` - search for \"Variant\" or \"variant\" in test function names\n2. Find and read existing variant protocol codegen tests in `codegen/codegen_test.go` - search for \"Variant\" or \"variant\" \n3. Find and read existing variant protocol e2e tests in `codegen/e2e_protocol_test.go` - read the whole file\n4. Read `TODO.md` to see how features are documented\n\nReturn the relevant test code snippets and the TODO.md content.",
  "subagent_type": "Explore"
}
[
  {
    "type": "text",
    "text": "Perfect! Now let me provide you with a comprehensive summary of the existing test patterns for variant protocols:\n\n## Existing Variant Protocol Test Patterns\n\n### 1. **Parser Tests** (`parser/parser_test.go` - lines 1629-1685)\n\n**Test: `TestVariantProtocolDecl`**\n\nTests parsing of variant protocol declarations with mixed payloads:\n\n```go\nfunc TestVariantProtocolDecl(t *testing.T) {\n\tinput := `PROTOCOL MSG\n  CASE\n    text; INT\n    number; INT; INT\n    quit\n`\n\tl := lexer.New(input)\n\tp := New(l)\n\tprogram := p.ParseProgram()\n\tcheckParserErrors(t, p)\n\n\tif len(program.Statements) != 1 {\n\t\tt.Fatalf(\"expected 1 statement, got %d\", len(program.Statements))\n\t}\n\n\tproto, ok := program.Statements[0].(*ast.ProtocolDecl)\n\tif !ok {\n\t\tt.Fatalf(\"expected ProtocolDecl, got %T\", program.Statements[0])\n\t}\n\n\tif proto.Name != \"MSG\" {\n\t\tt.Errorf(\"expected name 'MSG', got %s\", proto.Name)\n\t}\n\n\tif proto.Kind != \"variant\" {\n\t\tt.Errorf(\"expected kind 'variant', got %s\", proto.Kind)\n\t}\n\n\tif len(proto.Variants) != 3 {\n\t\tt.Fatalf(\"expected 3 variants, got %d\", len(proto.Variants))\n\t}\n\n\t// text; INT\n\tif proto.Variants[0].Tag != \"text\" {\n\t\tt.Errorf(\"expected tag 'text', got %s\", proto.Variants[0].Tag)\n\t}\n\tif len(proto.Variants[0].Types) != 1 || proto.Variants[0].Types[0] != \"INT\" {\n\t\tt.Errorf(\"expected types [INT] for text, got %v\", proto.Variants[0].Types)\n\t}\n\n\t// number; INT; INT\n\tif proto.Variants[1].Tag != \"number\" {\n\t\tt.Errorf(\"expected tag 'number', got %s\", proto.Variants[1].Tag)\n\t}\n\tif len(proto.Variants[1].Types) != 2 {\n\t\tt.Errorf(\"expected 2 types for number, got %d\", len(proto.Variants[1].Types))\n\t}\n\n\t// quit (no payload)\n\tif proto.Variants[2].Tag != \"quit\" {\n\t\tt.Errorf(\"expected tag 'quit', got %s\", proto.Variants[2].Tag)\n\t}\n\tif len(proto.Variants[2].Types) != 0 {\n\t\tt.Errorf(\"expected 0 types for quit, got %d\", len(proto.Variants[2].Types))\n\t}\n}\n```\n\n**Key Patterns:**\n- Checks `proto.Kind == \"variant\"`\n- Validates `proto.Variants` slice has expected count\n- Tests each variant's `Tag` and `Types` slice\n- Tests both payloaded and no-payload variants\n\n---\n\n### 2. **Codegen Unit Tests** (`codegen/codegen_test.go` - lines 504-524)\n\n**Test: `TestVariantProtocolType`**\n\nTests code generation of variant protocol type definitions:\n\n```go\nfunc TestVariantProtocolType(t *testing.T) {\n\tinput := `PROTOCOL MSG\n  CASE\n    text; INT\n    quit\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"type _proto_MSG interface {\") {\n\t\tt.Errorf(\"expected interface declaration in output, got:\\n%s\", output)\n\t}\n\tif !strings.Contains(output, \"_is_MSG()\") {\n\t\tt.Errorf(\"expected marker method in output, got:\\n%s\", output)\n\t}\n\tif !strings.Contains(output, \"type _proto_MSG_text struct {\") {\n\t\tt.Errorf(\"expected text struct in output, got:\\n%s\", output)\n\t}\n\tif !strings.Contains(output, \"type _proto_MSG_quit struct{}\") {\n\t\tt.Errorf(\"expected quit struct in output, got:\\n%s\", output)\n\t}\n}\n```\n\n**Key Patterns:**\n- Uses `transpile()` helper to transpile occam to Go\n- Uses `strings.Contains()` to check for expected Go output patterns\n- Tests for:\n  - Interface declaration: `type _proto_MSG interface {`\n  - Marker method: `_is_MSG()`\n  - Concrete struct for each variant: `type _proto_MSG_<tag> struct {`\n\n---\n\n### 3. **End-to-End Protocol Tests** (`codegen/e2e_protocol_test.go`)\n\n**Test: `TestE2E_VariantProtocol` (lines 44-68)**\n\nEnd-to-end test that transpiles → compiles → runs variant protocol code:\n\n```go\nfunc TestE2E_VariantProtocol(t *testing.T) {\n\t// Variant protocol: tagged union with CASE receive\n\toccam := `PROTOCOL MSG\n  CASE\n    data; INT\n    quit\n\nSEQ\n  CHAN OF MSG c:\n  INT result:\n  result := 0\n  PAR\n    c ! data ; 42\n    c ? CASE\n      data ; result\n        print.int(result)\n      quit\n        print.int(0)\n`\n\toutput := transpileCompileRun(t, occam)\n\texpected := \"42\\n\"\n\tif output != expected {\n\t\tt.Errorf(\"expected %q, got %q\", expected, output)\n\t}\n}\n```\n\n**Test: `TestE2E_VariantProtocolNoPayload` (lines 70-94)**\n\nEnd-to-end test for variant without payload:\n\n```go\nfunc TestE2E_VariantProtocolNoPayload(t *testing.T) {\n\t// Variant protocol with no-payload tag\n\toccam := `PROTOCOL MSG\n  CASE\n    data; INT\n    quit\n\nSEQ\n  CHAN OF MSG c:\n  INT result:\n  result := 0\n  PAR\n    c ! quit\n    c ? CASE\n      data ; result\n        print.int(result)\n      quit\n        print.int(99)\n`\n\toutput := transpileCompileRun(t, occam)\n\texpected := \"99\\n\"\n\tif output != expected {\n\t\tt.Errorf(\"expected %q, got %q\", expected, output)\n\t}\n}\n```\n\n**Key Patterns:**\n- Uses `transpileCompileRun()` helper\n- Tests complete program execution flow\n- Verifies output matches expected stdout\n- Tests different code paths (different variants matched in switch)\n\n---\n\n### 4. **TODO.md Documentation** (line 60)\n\nVariant protocols are marked as **Fully Implemented**:\n\n```\n### Protocols\n- **Simple** — `PROTOCOL SIG IS INT` (type alias)\n- **Sequential** — `PROTOCOL PAIR IS INT ; BYTE` (struct)\n- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types)\n```\n\n---\n\n## Summary of Test Patterns\n\n| Layer | Test File | Pattern |\n|-------|-----------|---------|\n| **Parser** | `parser_test.go` | Check AST structure: `proto.Kind`, `proto.Variants[i].Tag`, `proto.Variants[i].Types` |\n| **Codegen Unit** | `codegen_test.go` | Use `transpile()` helper + `strings.Contains()` for Go output patterns |\n| **Codegen E2E** | `e2e_protocol_test.go` | Use `transpileCompileRun()` helper + verify stdout output |\n| **Docs** | `TODO.md` | List as \"Fully Implemented\" |\n\n**Test Files Locations:**\n- `/home/david/projects/code-associates/occam2go/parser/parser_test.go` (parser unit tests)\n- `/home/david/projects/code-associates/occam2go/codegen/codegen_test.go` (codegen unit tests)\n- `/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go` (e2e tests)\n- `/home/david/projects/code-associates/occam2go/TODO.md` (feature documentation)"
  },
  {
    "type": "text",
    "text": "agentId: a647ffb3b63483cd2 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 28041\ntool_uses: 10\nduration_ms: 19883</usage>"
  }
]

Now let me read the exact locations where I need to insert the new tests, and the TODO.md.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "offset": 1680,
  "limit": 20
}
  1680→		t.Errorf("expected tag 'quit', got %s", proto.Variants[2].Tag)
  1681→	}
  1682→	if len(proto.Variants[2].Types) != 0 {
  1683→		t.Errorf("expected 0 types for quit, got %d", len(proto.Variants[2].Types))
  1684→	}
  1685→}
  1686→
  1687→func TestChanDeclWithProtocol(t *testing.T) {
  1688→	input := `PROTOCOL SIGNAL IS INT
  1689→CHAN OF SIGNAL c:
  1690→`
  1691→	l := lexer.New(input)
  1692→	p := New(l)
  1693→	program := p.ParseProgram()
  1694→	checkParserErrors(t, p)
  1695→
  1696→	if len(program.Statements) != 2 {
  1697→		t.Fatalf("expected 2 statements, got %d", len(program.Statements))
  1698→	}
  1699→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go",
  "offset": 520,
  "limit": 20
}
   520→	}
   521→	if !strings.Contains(output, "type _proto_MSG_quit struct{}") {
   522→		t.Errorf("expected quit struct in output, got:\n%s", output)
   523→	}
   524→}
   525→
   526→func TestRecordType(t *testing.T) {
   527→	input := `RECORD POINT
   528→  INT x:
   529→  INT y:
   530→`
   531→	output := transpile(t, input)
   532→
   533→	if !strings.Contains(output, "type POINT struct {") {
   534→		t.Errorf("expected 'type POINT struct {' in output, got:\n%s", output)
   535→	}
   536→	if !strings.Contains(output, "x int") {
   537→		t.Errorf("expected 'x int' field in output, got:\n%s", output)
   538→	}
   539→	if !strings.Contains(output, "y int") {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go",
  "offset": 90,
  "limit": 20
}
    90→	expected := "99\n"
    91→	if output != expected {
    92→		t.Errorf("expected %q, got %q", expected, output)
    93→	}
    94→}
    95→
    96→func TestE2E_ProtocolWithProc(t *testing.T) {
    97→	// Protocol channel passed as PROC parameter
    98→	occam := `PROTOCOL PAIR IS INT ; INT
    99→
   100→PROC sender(CHAN OF PAIR out)
   101→  out ! 3 ; 7
   102→
   103→SEQ
   104→  CHAN OF PAIR c:
   105→  INT a, b:
   106→  PAR
   107→    sender(c)
   108→    c ? a ; b
   109→  print.int(a + b)
Grep
{
  "pattern": "Variant.*protocol|dotted|Dotted",
  "path": "/home/david/projects/code-associates/occam2go/TODO.md",
  "output_mode": "content",
  "-n": true
}
No matches found
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/TODO.md"
}
     1→# occam2go — Implementation Status
     2→
     3→## Fully Implemented
     4→
     5→### Core Constructs
     6→- **SEQ** — Sequential execution, with replicators (`SEQ i = 0 FOR n`) and optional STEP
     7→- **PAR** — Parallel execution via goroutines + sync.WaitGroup, with replicators
     8→- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators; supports multi-statement bodies (declarations scoped before process)
     9→- **WHILE** — Loops, maps to Go `for` loops; supports multi-statement bodies
    10→- **CASE** — Pattern matching with multiple cases and ELSE branch; supports multi-statement bodies
    11→- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, multi-statement bodies, and replicators (`ALT i = 0 FOR n` using `reflect.Select`)
    12→- **SKIP** — No-op process
    13→- **STOP** — Error + deadlock
    14→
    15→### Data Types & Declarations
    16→- **INT, INT16, INT32, INT64, BYTE, BOOL, REAL, REAL32, REAL64** — Scalar types (INT16/32/64 map to int16/32/64, REAL/REAL64 map to float64, REAL32 maps to float32)
    17→- **Variable declarations** — `INT x, y, z:`
    18→- **Arrays** — `[n]TYPE arr:` with index expressions
    19→- **Channels** — `CHAN OF TYPE c:` with send (`!`) and receive (`?`); `CHAN BYTE` shorthand (without `OF`)
    20→- **Channel arrays** — `[n]CHAN OF TYPE cs:` with indexed send/receive and `[]CHAN OF TYPE` proc params
    21→- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only); direction annotations at call sites (`out!`, `in?`) accepted and ignored
    22→- **Timers** — `TIMER tim:` with reads and `AFTER` expressions
    23→- **Abbreviations** — `VAL INT x IS 1:`, `INT y IS z:`, untyped `VAL x IS expr:` — named constants and aliases
    24→- **INITIAL declarations** — `INITIAL INT x IS 42:` — mutable variables with initial values
    25→- **Byte literals** — `'A'`, `'0'` with occam escape sequences (`*n`, `*c`, `*t`)
    26→- **Hex integer literals** — `#FF`, `#80000000`
    27→
    28→### Procedures & Functions
    29→- **PROC** — Declaration with VAL, reference, CHAN OF, and open array (`[]TYPE`) parameters
    30→- **PROC calls** — With automatic `&`/`*` for reference params, pass-through for channels
    31→- **FUNCTION (IS form)** — `INT FUNCTION square(VAL INT x) IS x * x`
    32→- **FUNCTION (VALOF form)** — Local declarations + VALOF body + RESULT
    33→- **Multi-result FUNCTIONs** — `INT, INT FUNCTION f(...)` returning multiple values via `RESULT a, b`
    34→- **Nested PROCs/FUNCTIONs** — Local definitions inside a PROC body, compiled as Go closures
    35→- **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
    36→- **INLINE modifier** — `INT INLINE FUNCTION f(...)` — accepted and ignored (optimization hint only)
    37→- **Built-in print** — `print.int`, `print.bool`, `print.string`, `print.newline`
    38→
    39→### Expressions & Operators
    40→- **Arithmetic** — `+`, `-`, `*`, `/`, `\` (modulo)
    41→- **Comparison** — `=`, `<>`, `<`, `>`, `<=`, `>=`
    42→- **Logical** — `AND`, `OR`, `NOT`
    43→- **Bitwise** — `/\`, `\/`, `><`, `~`, `<<`, `>>`
    44→- **AFTER** — As boolean expression (maps to `>`)
    45→- **Parenthesized expressions**
    46→- **Array indexing** — `arr[i]`, `arr[expr]`
    47→- **String literals** — Double-quoted strings
    48→- **Type conversions** — `INT expr`, `INT16 expr`, `INT32 expr`, `INT64 expr`, `BYTE expr`, `BOOL expr`, `REAL32 expr`, `REAL64 expr` (including BOOL↔numeric conversions, and ROUND/TRUNC qualifiers for float↔int conversions)
    49→- **Checked arithmetic** — `PLUS`, `MINUS`, `TIMES` — modular (wrapping) operators
    50→- **MOSTNEG/MOSTPOS** — Type min/max constants for INT, INT16, INT32, INT64, BYTE, REAL32, REAL64
    51→- **SIZE operator** — `SIZE arr`, `SIZE "str"` maps to `len()`
    52→- **Array slices** — `[arr FROM n FOR m]` with slice assignment
    53→- **Array literals** — `[1, 2, 3]` — inline array/table expressions
    54→- **Multi-assignment** — `a, b := f(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`
    55→- **Multi-line expression continuation** — Binary operators and `:=` at end of line continue expression on next line
    56→
    57→### Protocols
    58→- **Simple** — `PROTOCOL SIG IS INT` (type alias)
    59→- **Sequential** — `PROTOCOL PAIR IS INT ; BYTE` (struct)
    60→- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types)
    61→
    62→### Records
    63→- **RECORD** — Struct types with field access via bracket syntax (`p[x]`)
    64→
    65→### Type Reinterpretation & Intrinsics
    66→- **RETYPES** — Bit-level type reinterpretation (`VAL INT X RETYPES X :` for float32→int, `VAL [2]INT X RETYPES X :` for float64→int pair)
    67→- **Transputer intrinsics** — `LONGPROD`, `LONGDIV`, `LONGSUM`, `LONGDIFF`, `NORMALISE`, `SHIFTLEFT`, `SHIFTRIGHT` — extended-precision arithmetic as Go helper functions
    68→- **CAUSEERROR** — Error-raising primitive, maps to `panic("CAUSEERROR")`
    69→
    70→### Preprocessor
    71→- **`#IF` / `#ELSE` / `#ENDIF`** — Conditional compilation with `TRUE`, `FALSE`, `DEFINED()`, `NOT`, equality
    72→- **`#DEFINE`** — Symbol definition
    73→- **`#INCLUDE`** — File inclusion with search paths and include guards
    74→- **`#COMMENT` / `#PRAGMA` / `#USE`** — Ignored (blank lines)
    75→- **Predefined symbols** — `TARGET.BITS.PER.WORD = 64`
    76→
    77→### Tooling
    78→- **gen-module** — Generate `.module` files from KRoC SConscript build files
    79→
    80→---
    81→
    82→## Not Yet Implemented
    83→
    84→### Required for shared_screen module (extends course module)
    85→
    86→| Feature | Notes | Used in |
    87→|---------|-------|---------|
    88→| **`DATA TYPE X IS TYPE:`** | Simple type alias (e.g. `DATA TYPE COLOUR IS BYTE:`). | shared_screen.inc |
    89→| **`DATA TYPE X RECORD`** | Alternative record syntax (vs current `RECORD X`). | shared_screen.inc |
    90→| **Counted array protocol** | `BYTE::[]BYTE` — length-prefixed array in protocols. | shared_screen.inc, shared_screen.occ |
    91→| **`RESULT` param qualifier** | `RESULT INT len` on PROC params (output-only, like a write-only reference). | float_io.occ |
    92→
    93→### Other language features
    94→
    95→| Feature | Notes |
    96→|---------|-------|
    97→| **PRI ALT / PRI PAR** | Priority variants of ALT and PAR. |
    98→| **PLACED PAR** | Assigning processes to specific hardware. |
    99→| **PORT OF** | Hardware port mapping. |
   100→| **`VAL []BYTE` abbreviations** | `VAL []BYTE cmap IS "0123456789ABCDEF":` — named string constants. |
   101→| **`#PRAGMA DEFINED`** | Compiler hint to suppress definedness warnings. Can be ignored. |
   102→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go"
}
     1→package codegen
     2→
     3→import "testing"
     4→
     5→func TestE2E_SimpleProtocol(t *testing.T) {
     6→	// Simple protocol: just a named type alias
     7→	occam := `PROTOCOL SIGNAL IS INT
     8→
     9→SEQ
    10→  CHAN OF SIGNAL c:
    11→  INT result:
    12→  PAR
    13→    c ! 42
    14→    c ? result
    15→  print.int(result)
    16→`
    17→	output := transpileCompileRun(t, occam)
    18→	expected := "42\n"
    19→	if output != expected {
    20→		t.Errorf("expected %q, got %q", expected, output)
    21→	}
    22→}
    23→
    24→func TestE2E_SequentialProtocol(t *testing.T) {
    25→	// Sequential protocol: send/receive multiple values
    26→	occam := `PROTOCOL PAIR IS INT ; INT
    27→
    28→SEQ
    29→  CHAN OF PAIR c:
    30→  INT x, y:
    31→  PAR
    32→    c ! 10 ; 20
    33→    c ? x ; y
    34→  print.int(x)
    35→  print.int(y)
    36→`
    37→	output := transpileCompileRun(t, occam)
    38→	expected := "10\n20\n"
    39→	if output != expected {
    40→		t.Errorf("expected %q, got %q", expected, output)
    41→	}
    42→}
    43→
    44→func TestE2E_VariantProtocol(t *testing.T) {
    45→	// Variant protocol: tagged union with CASE receive
    46→	occam := `PROTOCOL MSG
    47→  CASE
    48→    data; INT
    49→    quit
    50→
    51→SEQ
    52→  CHAN OF MSG c:
    53→  INT result:
    54→  result := 0
    55→  PAR
    56→    c ! data ; 42
    57→    c ? CASE
    58→      data ; result
    59→        print.int(result)
    60→      quit
    61→        print.int(0)
    62→`
    63→	output := transpileCompileRun(t, occam)
    64→	expected := "42\n"
    65→	if output != expected {
    66→		t.Errorf("expected %q, got %q", expected, output)
    67→	}
    68→}
    69→
    70→func TestE2E_VariantProtocolNoPayload(t *testing.T) {
    71→	// Variant protocol with no-payload tag
    72→	occam := `PROTOCOL MSG
    73→  CASE
    74→    data; INT
    75→    quit
    76→
    77→SEQ
    78→  CHAN OF MSG c:
    79→  INT result:
    80→  result := 0
    81→  PAR
    82→    c ! quit
    83→    c ? CASE
    84→      data ; result
    85→        print.int(result)
    86→      quit
    87→        print.int(99)
    88→`
    89→	output := transpileCompileRun(t, occam)
    90→	expected := "99\n"
    91→	if output != expected {
    92→		t.Errorf("expected %q, got %q", expected, output)
    93→	}
    94→}
    95→
    96→func TestE2E_ProtocolWithProc(t *testing.T) {
    97→	// Protocol channel passed as PROC parameter
    98→	occam := `PROTOCOL PAIR IS INT ; INT
    99→
   100→PROC sender(CHAN OF PAIR out)
   101→  out ! 3 ; 7
   102→
   103→SEQ
   104→  CHAN OF PAIR c:
   105→  INT a, b:
   106→  PAR
   107→    sender(c)
   108→    c ? a ; b
   109→  print.int(a + b)
   110→`
   111→	output := transpileCompileRun(t, occam)
   112→	expected := "10\n"
   113→	if output != expected {
   114→		t.Errorf("expected %q, got %q", expected, output)
   115→	}
   116→}
   117→
Thinking

Now I have all the context I need. Let me create the three tests and update TODO.md. I'll add them all in parallel.

Now I have all the patterns. Let me create the three tests and update TODO.md.

✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
func TestChanDeclWithProtocol(t *testing.T) {
+
func TestVariantProtocolDeclDottedTags(t *testing.T) {
	input := `PROTOCOL BAR.PROTO
  CASE
    bar.data; INT
    bar.terminate
    bar.blank; INT
`
	l := lexer.New(input)
	p := New(l)
	program := p.ParseProgram()
	checkParserErrors(t, p)

	if len(program.Statements) != 1 {
		t.Fatalf("expected 1 statement, got %d", len(program.Statements))
	}

	proto, ok := program.Statements[0].(*ast.ProtocolDecl)
	if !ok {
		t.Fatalf("expected ProtocolDecl, got %T", program.Statements[0])
	}

	if proto.Name != "BAR.PROTO" {
		t.Errorf("expected name 'BAR.PROTO', got %s", proto.Name)
	}

	if proto.Kind != "variant" {
		t.Errorf("expected kind 'variant', got %s", proto.Kind)
	}

	if len(proto.Variants) != 3 {
		t.Fatalf("expected 3 variants, got %d", len(proto.Variants))
	}

	// bar.data; INT
	if proto.Variants[0].Tag != "bar.data" {
		t.Errorf("expected tag 'bar.data', got %s", proto.Variants[0].Tag)
	}
	if len(proto.Variants[0].Types) != 1 || proto.Variants[0].Types[0] != "INT" {
		t.Errorf("expected types [INT] for bar.data, got %v", proto.Variants[0].Types)
	}

	// bar.terminate (no payload)
	if proto.Variants[1].Tag != "bar.terminate" {
		t.Errorf("expected tag 'bar.terminate', got %s", proto.Variants[1].Tag)
	}
	if len(proto.Variants[1].Types) != 0 {
		t.Errorf("expected 0 types for bar.terminate, got %d", len(proto.Variants[1].Types))
	}

	// bar.blank; INT
	if proto.Variants[2].Tag != "bar.blank" {
		t.Errorf("expected tag 'bar.blank', got %s", proto.Variants[2].Tag)
	}
	if len(proto.Variants[2].Types) != 1 || proto.Variants[2].Types[0] != "INT" {
		t.Errorf("expected types [INT] for bar.blank, got %v", proto.Variants[2].Types)
	}
}

func TestChanDeclWithProtocol(t *testing.T) {
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
✏️ Edit codegen_test.go
/home/david/projects/code-associates/occam2go/codegen/codegen_test.go
func TestRecordType(t *testing.T) {
+
func TestVariantProtocolDottedTags(t *testing.T) {
	input := `PROTOCOL BAR.PROTO
  CASE
    bar.data; INT
    bar.terminate
    bar.blank; INT
`
	output := transpile(t, input)

	if !strings.Contains(output, "type _proto_BAR_PROTO interface {") {
		t.Errorf("expected interface declaration in output, got:\n%s", output)
	}
	if !strings.Contains(output, "type _proto_BAR_PROTO_bar_data struct {") {
		t.Errorf("expected bar_data struct in output, got:\n%s", output)
	}
	if !strings.Contains(output, "type _proto_BAR_PROTO_bar_terminate struct{}") {
		t.Errorf("expected bar_terminate struct in output, got:\n%s", output)
	}
	if !strings.Contains(output, "type _proto_BAR_PROTO_bar_blank struct {") {
		t.Errorf("expected bar_blank struct in output, got:\n%s", output)
	}
}

func TestRecordType(t *testing.T) {
The file /home/david/projects/code-associates/occam2go/codegen/codegen_test.go has been updated successfully.
✏️ Edit e2e_protocol_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go
func TestE2E_ProtocolWithProc(t *testing.T) {
+
func TestE2E_VariantProtocolDottedTags(t *testing.T) {
	// Variant protocol with dotted tag names (e.g., bar.data)
	occam := `PROTOCOL BAR.PROTO
  CASE
    bar.data; INT
    bar.terminate
    bar.blank; INT

SEQ
  CHAN OF BAR.PROTO c:
  INT result:
  result := 0
  PAR
    SEQ
      c ! bar.data ; 42
      c ! bar.terminate
    SEQ
      c ? CASE
        bar.data ; result
          print.int(result)
        bar.terminate
          print.int(0)
        bar.blank ; result
          print.int(result)
      c ? CASE
        bar.data ; result
          print.int(result)
        bar.terminate
          print.int(99)
        bar.blank ; result
          print.int(result)
`
	output := transpileCompileRun(t, occam)
	expected := "42\n99\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_ProtocolWithProc(t *testing.T) {
The file /home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go has been updated successfully.

Now let me run the three new tests to see if they pass.

$ Bash
Run parser test for dotted protocol tags
go test ./parser -run TestVariantProtocolDeclDottedTags -v 2>&1
=== RUN   TestVariantProtocolDeclDottedTags
--- PASS: TestVariantProtocolDeclDottedTags (0.00s)
PASS
ok  	github.com/codeassociates/occam2go/parser	0.001s
$ Bash
Run codegen test for dotted protocol tags
go test ./codegen -run TestVariantProtocolDottedTags -v 2>&1
=== RUN   TestVariantProtocolDottedTags
--- PASS: TestVariantProtocolDottedTags (0.00s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.001s
$ Bash
Run e2e test for dotted protocol tags
go test ./codegen -run TestE2E_VariantProtocolDottedTags -v 2>&1
Exit code 1
=== RUN   TestE2E_VariantProtocolDottedTags
    e2e_protocol_test.go:128: compilation failed: exit status 1
        Output: # command-line-arguments
        /tmp/occam2go-test-828436456/main.go:26:17: undefined: _proto_BAR
        
        Go code:
        package main
        
        import (
        	"fmt"
        	"sync"
        )
        
        type _proto_BAR_PROTO interface {
        	_is_BAR_PROTO()
        }
        
        type _proto_BAR_PROTO_bar_data struct {
        	_0 int
        }
        func (_proto_BAR_PROTO_bar_data) _is_BAR_PROTO() {}
        
        type _proto_BAR_PROTO_bar_terminate struct{}
        func (_proto_BAR_PROTO_bar_terminate) _is_BAR_PROTO() {}
        
        type _proto_BAR_PROTO_bar_blank struct {
        	_0 int
        }
        func (_proto_BAR_PROTO_bar_blank) _is_BAR_PROTO() {}
        
        func main() {
        	c := make(chan _proto_BAR.PROTO)
        	var result int
        	_ = result
        	result = 0
        	var wg sync.WaitGroup
        	wg.Add(2)
        	go func() {
        		defer wg.Done()
        		c <- _proto_BAR_PROTO_bar_data{42}
        		c <- _proto_BAR_PROTO_bar_terminate{}
        	}()
        	go func() {
        		defer wg.Done()
        		switch _v := (<-c).(type) {
        		case _proto_BAR_PROTO_bar_data:
        			result = _v._0
        			fmt.Println(result)
        		case _proto_BAR_PROTO_bar_terminate:
        			fmt.Println(0)
        		case _proto_BAR_PROTO_bar_blank:
        			result = _v._0
        			fmt.Println(result)
        		}
        		switch _v := (<-c).(type) {
        		case _proto_BAR_PROTO_bar_data:
        			result = _v._0
        			fmt.Println(result)
        		case _proto_BAR_PROTO_bar_terminate:
        			fmt.Println(99)
        		case _proto_BAR_PROTO_bar_blank:
        			result = _v._0
        			fmt.Println(result)
        		}
        	}()
        	wg.Wait()
        }
--- FAIL: TestE2E_VariantProtocolDottedTags (0.03s)
FAIL
FAIL	github.com/codeassociates/occam2go/codegen	0.034s
FAIL

=== RUN   TestE2E_VariantProtocolDottedTags
    e2e_protocol_test.go:128: compilation failed: exit status 1
        Output: # command-line-arguments
        /tmp/occam2go-test-828436456/main.go:26:17: undefined: _proto_BAR
        
        Go code:
        package main
        
        import (
        	"fmt"
        	"sync"
        )
        
        type _proto_BAR_PROTO interface {
        	_is_BAR_PROTO()
        }
        
        type _proto_BAR_PROTO_bar_data struct {
        	_0 int
        }
        func (_proto_BAR_PROTO_bar_data) _is_BAR_PROTO() {}
        
        type _proto_BAR_PROTO_bar_terminate struct{}
        func (_proto_BAR_PROTO_bar_terminate) _is_BAR_PROTO() {}
        
        type _proto_BAR_PROTO_bar_blank struct {
        	_0 int
        }
        func (_proto_BAR_PROTO_bar_blank) _is_BAR_PROTO() {}
        
        func main() {
        	c := make(chan _proto_BAR.PROTO)
        	var result int
        	_ = result
        	result = 0
        	var wg sync.WaitGroup
        	wg.Add(2)
        	go func() {
        		defer wg.Done()
        		c <- _proto_BAR_PROTO_bar_data{42}
        		c <- _proto_BAR_PROTO_bar_terminate{}
        	}()
        	go func() {
        		defer wg.Done()
        		switch _v := (<-c).(type) {
        		case _proto_BAR_PROTO_bar_data:
        			result = _v._0
        			fmt.Println(result)
        		case _proto_BAR_PROTO_bar_terminate:
        			fmt.Println(0)
        		case _proto_BAR_PROTO_bar_blank:
        			result = _v._0
        			fmt.Println(result)
        		}
        		switch _v := (<-c).(type) {
        		case _proto_BAR_PROTO_bar_data:
        			result = _v._0
        			fmt.Println(result)
        		case _proto_BAR_PROTO_bar_terminate:
        			fmt.Println(99)
        		case _proto_BAR_PROTO_bar_blank:
        			result = _v._0
        			fmt.Println(result)
        		}
        	}()
        	wg.Wait()
        }
--- FAIL: TestE2E_VariantProtocolDottedTags (0.03s)
FAIL
FAIL	github.com/codeassociates/occam2go/codegen	0.034s
FAIL
Thinking

The e2e test reveals a bug: CHAN OF BAR.PROTO c: generates c := make(chan _proto_BAR.PROTO) — the dot is not being converted to underscore in the channel type reference. The protocol type definitions themselves are fine (they use _proto_BAR_PROTO), but the channel declaration doesn't call goIdent() on the protocol name.

Let me find where channel declarations with protocols are generated.

The e2e test found a bug: CHAN OF BAR.PROTO c: generates make(chan _proto_BAR.PROTO) — the dot isn't converted to underscore in the channel type. Let me find where channel types reference protocol names in codegen.

Grep
{
  "pattern": "_proto_.*ChanType|chanType.*proto|ProtocolName|protocolName|CHAN OF.*proto",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "\"_proto_\"",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
1564:			return "_proto_" + occamType
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1555,
  "limit": 20
}
  1555→	case "REAL":
  1556→		return "float64"
  1557→	case "REAL32":
  1558→		return "float32"
  1559→	case "REAL64":
  1560→		return "float64"
  1561→	default:
  1562→		// Check if it's a protocol name
  1563→		if _, ok := g.protocolDefs[occamType]; ok {
  1564→			return "_proto_" + occamType
  1565→		}
  1566→		// Check if it's a record type name
  1567→		if _, ok := g.recordDefs[occamType]; ok {
  1568→			return occamType
  1569→		}
  1570→		return occamType // pass through unknown types
  1571→	}
  1572→}
  1573→
  1574→func isOccamIntType(t string) bool {

Found it. Line 1564: "_proto_" + occamType doesn't apply goIdent() to convert dots to underscores. Let me fix this.

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
		if _, ok := g.protocolDefs[occamType]; ok {
			return "_proto_" + occamType
		}
+
		if _, ok := g.protocolDefs[occamType]; ok {
			return "_proto_" + goIdent(occamType)
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now let me also check if there are other places where protocol names are used without goIdent() — there could be similar issues in protocol type definitions, variant send/receive, etc.

Grep
{
  "pattern": "\"_proto_\" \\+",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
1564:			return "_proto_" + goIdent(occamType)

Only one place uses the _proto_ prefix directly. Let me also check for the other pattern where protocol/variant names are assembled.

Grep
{
  "pattern": "proto_.*\\+|_proto_",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
1172:		// Variant send with explicit tag: c <- _proto_NAME_tag{values...}
1173:		g.write(fmt.Sprintf("_proto_%s_%s{", gProtoName, goIdent(send.VariantTag)))
1184:			g.write(fmt.Sprintf("_proto_%s_%s{}", gProtoName, goIdent(ident.Value)))
1189:		// Sequential send: c <- _proto_NAME{val1, val2, ...}
1190:		g.write(fmt.Sprintf("_proto_%s{", gProtoName))
1251:		g.writeLine(fmt.Sprintf("type _proto_%s = %s", gName, goType))
1254:		g.writeLine(fmt.Sprintf("type _proto_%s struct {", gName))
1265:		g.writeLine(fmt.Sprintf("type _proto_%s interface {", gName))
1276:				g.writeLine(fmt.Sprintf("type _proto_%s_%s struct{}", gName, gTag))
1278:				g.writeLine(fmt.Sprintf("type _proto_%s_%s struct {", gName, gTag))
1287:			g.writeLine(fmt.Sprintf("func (_proto_%s_%s) _is_%s() {}", gName, gTag, gName))
1311:		g.writeLine(fmt.Sprintf("case _proto_%s_%s:", gProtoName, goIdent(vc.Tag)))
1564:			return "_proto_" + goIdent(occamType)

Let me see how gProtoName and gName are set in these contexts.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1155,
  "limit": 45
}
  1155→}
  1156→
  1157→func (g *Generator) generateSend(send *ast.Send) {
  1158→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1159→	g.write(goIdent(send.Channel))
  1160→	if send.ChannelIndex != nil {
  1161→		g.write("[")
  1162→		g.generateExpression(send.ChannelIndex)
  1163→		g.write("]")
  1164→	}
  1165→	g.write(" <- ")
  1166→
  1167→	protoName := g.chanProtocols[send.Channel]
  1168→	proto := g.protocolDefs[protoName]
  1169→	gProtoName := goIdent(protoName)
  1170→
  1171→	if send.VariantTag != "" && proto != nil && proto.Kind == "variant" {
  1172→		// Variant send with explicit tag: c <- _proto_NAME_tag{values...}
  1173→		g.write(fmt.Sprintf("_proto_%s_%s{", gProtoName, goIdent(send.VariantTag)))
  1174→		for i, val := range send.Values {
  1175→			if i > 0 {
  1176→				g.write(", ")
  1177→			}
  1178→			g.generateExpression(val)
  1179→		}
  1180→		g.write("}")
  1181→	} else if proto != nil && proto.Kind == "variant" && send.Value != nil && len(send.Values) == 0 {
  1182→		// Check if the send value is a bare identifier matching a variant tag
  1183→		if ident, ok := send.Value.(*ast.Identifier); ok && g.isVariantTag(protoName, ident.Value) {
  1184→			g.write(fmt.Sprintf("_proto_%s_%s{}", gProtoName, goIdent(ident.Value)))
  1185→		} else {
  1186→			g.generateExpression(send.Value)
  1187→		}
  1188→	} else if len(send.Values) > 0 && proto != nil && proto.Kind == "sequential" {
  1189→		// Sequential send: c <- _proto_NAME{val1, val2, ...}
  1190→		g.write(fmt.Sprintf("_proto_%s{", gProtoName))
  1191→		g.generateExpression(send.Value)
  1192→		for _, val := range send.Values {
  1193→			g.write(", ")
  1194→			g.generateExpression(val)
  1195→		}
  1196→		g.write("}")
  1197→	} else {
  1198→		// Simple send
  1199→		g.generateExpression(send.Value)
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1240,
  "limit": 55
}
  1240→			varRef = "*" + varRef
  1241→		}
  1242→		g.writeLine(fmt.Sprintf("%s = <-%s", varRef, chanRef))
  1243→	}
  1244→}
  1245→
  1246→func (g *Generator) generateProtocolDecl(proto *ast.ProtocolDecl) {
  1247→	gName := goIdent(proto.Name)
  1248→	switch proto.Kind {
  1249→	case "simple":
  1250→		goType := g.occamTypeToGoBase(proto.Types[0])
  1251→		g.writeLine(fmt.Sprintf("type _proto_%s = %s", gName, goType))
  1252→		g.writeLine("")
  1253→	case "sequential":
  1254→		g.writeLine(fmt.Sprintf("type _proto_%s struct {", gName))
  1255→		g.indent++
  1256→		for i, t := range proto.Types {
  1257→			goType := g.occamTypeToGoBase(t)
  1258→			g.writeLine(fmt.Sprintf("_%d %s", i, goType))
  1259→		}
  1260→		g.indent--
  1261→		g.writeLine("}")
  1262→		g.writeLine("")
  1263→	case "variant":
  1264→		// Interface type
  1265→		g.writeLine(fmt.Sprintf("type _proto_%s interface {", gName))
  1266→		g.indent++
  1267→		g.writeLine(fmt.Sprintf("_is_%s()", gName))
  1268→		g.indent--
  1269→		g.writeLine("}")
  1270→		g.writeLine("")
  1271→		// Concrete types for each variant
  1272→		for _, v := range proto.Variants {
  1273→			gTag := goIdent(v.Tag)
  1274→			if len(v.Types) == 0 {
  1275→				// No-payload variant: empty struct
  1276→				g.writeLine(fmt.Sprintf("type _proto_%s_%s struct{}", gName, gTag))
  1277→			} else {
  1278→				g.writeLine(fmt.Sprintf("type _proto_%s_%s struct {", gName, gTag))
  1279→				g.indent++
  1280→				for i, t := range v.Types {
  1281→					goType := g.occamTypeToGoBase(t)
  1282→					g.writeLine(fmt.Sprintf("_%d %s", i, goType))
  1283→				}
  1284→				g.indent--
  1285→				g.writeLine("}")
  1286→			}
  1287→			g.writeLine(fmt.Sprintf("func (_proto_%s_%s) _is_%s() {}", gName, gTag, gName))
  1288→			g.writeLine("")
  1289→		}
  1290→	}
  1291→}
  1292→
  1293→func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
  1294→	protoName := g.chanProtocols[vr.Channel]
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1295,
  "limit": 25
}
  1295→	gProtoName := goIdent(protoName)
  1296→	chanRef := goIdent(vr.Channel)
  1297→	if vr.ChannelIndex != nil {
  1298→		var buf strings.Builder
  1299→		buf.WriteString(goIdent(vr.Channel))
  1300→		buf.WriteString("[")
  1301→		oldBuilder := g.builder
  1302→		g.builder = strings.Builder{}
  1303→		g.generateExpression(vr.ChannelIndex)
  1304→		buf.WriteString(g.builder.String())
  1305→		g.builder = oldBuilder
  1306→		buf.WriteString("]")
  1307→		chanRef = buf.String()
  1308→	}
  1309→	g.writeLine(fmt.Sprintf("switch _v := (<-%s).(type) {", chanRef))
  1310→	for _, vc := range vr.Cases {
  1311→		g.writeLine(fmt.Sprintf("case _proto_%s_%s:", gProtoName, goIdent(vc.Tag)))
  1312→		g.indent++
  1313→		for i, v := range vc.Variables {
  1314→			g.writeLine(fmt.Sprintf("%s = _v._%d", goIdent(v), i))
  1315→		}
  1316→		if vc.Body != nil {
  1317→			g.generateStatement(vc.Body)
  1318→		}
  1319→		g.indent--

All the protocol codegen already uses goIdent() on protocol names and tags. The only issue was in occamTypeToGoBase() at line 1564, which I've already fixed. Let me re-run the e2e test.

$ Bash
Re-run e2e test after fix
go test ./codegen -run TestE2E_VariantProtocolDottedTags -v 2>&1
=== RUN   TestE2E_VariantProtocolDottedTags
--- PASS: TestE2E_VariantProtocolDottedTags (0.09s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.088s

All three tests pass. Now let me run the full test suite to check for regressions, and update TODO.md.

$ Bash
Run full test suite for regressions
go test ./... 2>&1
✏️ Edit TODO.md
/home/david/projects/code-associates/occam2go/TODO.md
- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types)
+
- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types), including dotted tag names (`bar.data`, `bar.terminate`)
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	17.829s
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	0.002s
ok  	github.com/codeassociates/occam2go/preproc	(cached)
The file /home/david/projects/code-associates/occam2go/TODO.md has been updated successfully.

All tests pass with no regressions. Let me also update CLAUDE.md to document the dotted tag support.

Grep
{
  "pattern": "variant protocol|Variant protocol|variant.*tag",
  "path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "output_mode": "content",
  "-n": true
}
102:| `c ! tag ; val` (variant send) | `c <- _proto_X_tag{val}` |

The CLAUDE.md "What's Implemented" section mentions protocols but doesn't specifically list variant protocol details. The existing coverage in the mapping table is sufficient. The TODO.md update captures the new feature.

Here's a summary of all changes:

Bug fix: - codegen/codegen.go:1564occamTypeToGoBase() now applies goIdent() to protocol names, converting dots to underscores. Previously CHAN OF BAR.PROTO would generate chan _proto_BAR.PROTO (invalid Go) instead of chan _proto_BAR_PROTO.

Tests added: - parser/parser_test.goTestVariantProtocolDeclDottedTags: verifies parser preserves dotted tag names (bar.data, bar.terminate, bar.blank) in the AST - codegen/codegen_test.goTestVariantProtocolDottedTags: verifies generated Go type names use underscores (_proto_BAR_PROTO_bar_data, etc.) - codegen/e2e_protocol_test.goTestE2E_VariantProtocolDottedTags: full transpile→compile→run test with dotted tags (with and without payloads), variant send, and variant CASE receive

Documentation: - TODO.md — Updated variant protocol entry to note dotted tag name support

Thank you. Please create a PR for this fix.

Let me check the current state and create the PR.

$ Bash
Show working tree status
git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   TODO.md
	modified:   codegen/codegen.go
	modified:   codegen/codegen_test.go
	modified:   codegen/e2e_protocol_test.go
	modified:   parser/parser_test.go

no changes added to commit (use "git add" and/or "git commit -a")
$ Bash
Show all changes
git diff
diff --git a/TODO.md b/TODO.md
index a6de63c..dbfdfe1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -57,7 +57,7 @@
 ### Protocols
 - **Simple** — `PROTOCOL SIG IS INT` (type alias)
 - **Sequential** — `PROTOCOL PAIR IS INT ; BYTE` (struct)
-- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types)
+- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types), including dotted tag names (`bar.data`, `bar.terminate`)
 
 ### Records
 - **RECORD** — Struct types with field access via bracket syntax (`p[x]`)
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 21b3655..dec7efd 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -1561,7 +1561,7 @@ func (g *Generator) occamTypeToGo(occamType string) string {
 	default:
 		// Check if it's a protocol name
 		if _, ok := g.protocolDefs[occamType]; ok {
-			return "_proto_" + occamType
+			return "_proto_" + goIdent(occamType)
 		}
 		// Check if it's a record type name
 		if _, ok := g.recordDefs[occamType]; ok {
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index c6cda3d..ccecd68 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -523,6 +523,29 @@ func TestVariantProtocolType(t *testing.T) {
 	}
 }
 
+func TestVariantProtocolDottedTags(t *testing.T) {
+	input := `PROTOCOL BAR.PROTO
+  CASE
+    bar.data; INT
+    bar.terminate
+    bar.blank; INT
+`
+	output := transpile(t, input)
+
+	if !strings.Contains(output, "type _proto_BAR_PROTO interface {") {
+		t.Errorf("expected interface declaration in output, got:\n%s", output)
+	}
+	if !strings.Contains(output, "type _proto_BAR_PROTO_bar_data struct {") {
+		t.Errorf("expected bar_data struct in output, got:\n%s", output)
+	}
+	if !strings.Contains(output, "type _proto_BAR_PROTO_bar_terminate struct{}") {
+		t.Errorf("expected bar_terminate struct in output, got:\n%s", output)
+	}
+	if !strings.Contains(output, "type _proto_BAR_PROTO_bar_blank struct {") {
+		t.Errorf("expected bar_blank struct in output, got:\n%s", output)
+	}
+}
+
 func TestRecordType(t *testing.T) {
 	input := `RECORD POINT
   INT x:
diff --git a/codegen/e2e_protocol_test.go b/codegen/e2e_protocol_test.go
index a5e9afb..8b51339 100644
--- a/codegen/e2e_protocol_test.go
+++ b/codegen/e2e_protocol_test.go
@@ -93,6 +93,45 @@ SEQ
 	}
 }
 
+func TestE2E_VariantProtocolDottedTags(t *testing.T) {
+	// Variant protocol with dotted tag names (e.g., bar.data)
+	occam := `PROTOCOL BAR.PROTO
+  CASE
+    bar.data; INT
+    bar.terminate
+    bar.blank; INT
+
+SEQ
+  CHAN OF BAR.PROTO c:
+  INT result:
+  result := 0
+  PAR
+    SEQ
+      c ! bar.data ; 42
+      c ! bar.terminate
+    SEQ
+      c ? CASE
+        bar.data ; result
+          print.int(result)
+        bar.terminate
+          print.int(0)
+        bar.blank ; result
+          print.int(result)
+      c ? CASE
+        bar.data ; result
+          print.int(result)
+        bar.terminate
+          print.int(99)
+        bar.blank ; result
+          print.int(result)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "42\n99\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
 func TestE2E_ProtocolWithProc(t *testing.T) {
 	// Protocol channel passed as PROC parameter
 	occam := `PROTOCOL PAIR IS INT ; INT
diff --git a/parser/parser_test.go b/parser/parser_test.go
index f8ce1fc..b154094 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -1684,6 +1684,64 @@ func TestVariantProtocolDecl(t *testing.T) {
 	}
 }
 
+func TestVariantProtocolDeclDottedTags(t *testing.T) {
+	input := `PROTOCOL BAR.PROTO
+  CASE
+    bar.data; INT
+    bar.terminate
+    bar.blank; INT
+`
+	l := lexer.New(input)
+	p := New(l)
+	program := p.ParseProgram()
+	checkParserErrors(t, p)
+
+	if len(program.Statements) != 1 {
+		t.Fatalf("expected 1 statement, got %d", len(program.Statements))
+	}
+
+	proto, ok := program.Statements[0].(*ast.ProtocolDecl)
+	if !ok {
+		t.Fatalf("expected ProtocolDecl, got %T", program.Statements[0])
+	}
+
+	if proto.Name != "BAR.PROTO" {
+		t.Errorf("expected name 'BAR.PROTO', got %s", proto.Name)
+	}
+
+	if proto.Kind != "variant" {
+		t.Errorf("expected kind 'variant', got %s", proto.Kind)
+	}
+
+	if len(proto.Variants) != 3 {
+		t.Fatalf("expected 3 variants, got %d", len(proto.Variants))
+	}
+
+	// bar.data; INT
+	if proto.Variants[0].Tag != "bar.data" {
+		t.Errorf("expected tag 'bar.data', got %s", proto.Variants[0].Tag)
+	}
+	if len(proto.Variants[0].Types) != 1 || proto.Variants[0].Types[0] != "INT" {
+		t.Errorf("expected types [INT] for bar.data, got %v", proto.Variants[0].Types)
+	}
+
+	// bar.terminate (no payload)
+	if proto.Variants[1].Tag != "bar.terminate" {
+		t.Errorf("expected tag 'bar.terminate', got %s", proto.Variants[1].Tag)
+	}
+	if len(proto.Variants[1].Types) != 0 {
+		t.Errorf("expected 0 types for bar.terminate, got %d", len(proto.Variants[1].Types))
+	}
+
+	// bar.blank; INT
+	if proto.Variants[2].Tag != "bar.blank" {
+		t.Errorf("expected tag 'bar.blank', got %s", proto.Variants[2].Tag)
+	}
+	if len(proto.Variants[2].Types) != 1 || proto.Variants[2].Types[0] != "INT" {
+		t.Errorf("expected types [INT] for bar.blank, got %v", proto.Variants[2].Types)
+	}
+}
+
 func TestChanDeclWithProtocol(t *testing.T) {
 	input := `PROTOCOL SIGNAL IS INT
 CHAN OF SIGNAL c:
$ Bash
Show recent commits
git log --oneline -5
6faf05d Merge pull request #59 from codeassociates/fix/chan-array-dir-params
235652f Drop direction from channel array params to fix Go slice covariance issue
5a513cd Merge pull request #58 from codeassociates/fix/scope-boolvars-per-proc
347f176 Scope boolVars per proc/function body to prevent cross-proc leakage
36a462d Merge pull request #55 from codeassociates/fix/typed-abbreviation-type-loss
$ Bash
Create feature branch
git checkout -b fix/dotted-protocol-tags
Switched to a new branch 'fix/dotted-protocol-tags'
$ Bash
Stage and commit all changes
git add codegen/codegen.go codegen/codegen_test.go codegen/e2e_protocol_test.go parser/parser_test.go TODO.md && git commit -m "$(cat <<'EOF'
Support dotted protocol tags (e.g., bar.data, bar.terminate)

Apply goIdent() to protocol names in occamTypeToGo() so dotted names
like BAR.PROTO get converted to BAR_PROTO in channel type references.
The protocol type definitions and variant send/receive already handled
this correctly, but channel declarations (CHAN OF BAR.PROTO) did not.

Adds parser, codegen, and e2e tests for dotted variant protocol tags.

Closes #68

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
5 files changed, 122 insertions(+), 2 deletions(-)
$ Bash
Push branch to remote
git push -u origin fix/dotted-protocol-tags 2>&1
remote: 
remote: Create a pull request for 'fix/dotted-protocol-tags' on GitHub by visiting:        
remote:      https://github.com/codeassociates/occam2go/pull/new/fix/dotted-protocol-tags        
remote: 
To associate-1.github.com:codeassociates/occam2go.git
 * [new branch]      fix/dotted-protocol-tags -> fix/dotted-protocol-tags
branch 'fix/dotted-protocol-tags' set up to track 'origin/fix/dotted-protocol-tags'.
$ Bash
Create pull request
gh pr create --title "Support dotted protocol tags" --body "$(cat <<'EOF'
## Summary
- Fix `occamTypeToGo()` to apply `goIdent()` on protocol names, converting dots to underscores in channel type references (e.g., `CHAN OF BAR.PROTO` → `chan _proto_BAR_PROTO` instead of broken `chan _proto_BAR.PROTO`)
- Add parser, codegen unit, and e2e tests for variant protocols with dotted tag names (`bar.data`, `bar.terminate`, `bar.blank`)
- Update TODO.md to document dotted tag name support

Closes #68

## Test plan
- [x] `go test ./parser -run TestVariantProtocolDeclDottedTags` — parser preserves dotted tag names in AST
- [x] `go test ./codegen -run TestVariantProtocolDottedTags` — codegen converts dots to underscores in type names
- [x] `go test ./codegen -run TestE2E_VariantProtocolDottedTags` — full transpile→compile→run with dotted tags (send + CASE receive, with and without payloads)
- [x] `go test ./...` — no regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
https://github.com/codeassociates/occam2go/pull/69

PR created: https://github.com/codeassociates/occam2go/pull/69

Thank you. I've approved the PR. Please merge.

$ Bash
Merge PR #69
gh pr merge 69 --merge 2>&1
$ Bash
Switch to main and pull merged changes
git checkout main && git pull 2>&1
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
From associate-1.github.com:codeassociates/occam2go
   6faf05d..25ad300  main       -> origin/main
Updating 6faf05d..25ad300
Fast-forward
 TODO.md                      |  2 +-
 codegen/codegen.go           |  2 +-
 codegen/codegen_test.go      | 23 ++++++++++++++++++
 codegen/e2e_protocol_test.go | 39 +++++++++++++++++++++++++++++
 parser/parser_test.go        | 58 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 122 insertions(+), 2 deletions(-)

Merged and main is up to date.

Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.

/exit exit