Claude Code transcript - page 1/1

Please look at issue #56.

$ Bash
View GitHub issue #56
gh issue view 56
title:	boolVars map is global: name collisions cause invalid Go code
state:	OPEN
author:	associate-1
labels:	
comments:	0
assignees:	
projects:	
milestone:	
number:	56
--
## Summary

The `boolVars` map in `codegen.go` is a flat, unscoped map keyed by variable name. When multiple scopes declare variables with the same name but different types, the map produces incorrect codegen.

## Reproduction

Transpile `cast.occ` with the course module included:

```bash
./occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/cast.go kroc/modules/course/examples/cast.occ
go vet /tmp/cast.go
```

```
vet: /tmp/cast.go:2702:25: cannot use b (variable of type byte) as bool value in argument to _boolToInt
```

## Root cause

`string.occ` (included via `course.module`) declares `BOOL b:` inside a proc. This registers `"b"` in the global `boolVars` map. Later, `cast.occ` declares `BYTE b:` in a different scope. When `INT16 b` is encountered, `isBoolExpression()` checks `g.boolVars["b"]` → `true`, so it emits `int16(_boolToInt(b))` instead of the correct `int16(b)`.

The generated Go code:
```go
// wrong: b is byte, not bool
i16 = int16(_boolToInt(b))

// correct:
i16 = int16(b)
```

## Affected files

- `cast.occ`
- `casting.occ`
- Potentially any program that includes modules declaring BOOL variables with common names

## Suggested fix

Scope `boolVars` per proc/function body, or use a stack-based approach (push on proc entry, pop on proc exit), similar to how `procSigs` scoping works with `collectNestedProcSigsScoped()`.

## Related

Tracking issue: #43
EnterPlanMode
{}
Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.

In plan mode, you should:
1. Thoroughly explore the codebase to understand existing patterns
2. Identify similar features and architectural approaches
3. Consider multiple approaches and their trade-offs
4. Use AskUserQuestion if you need to clarify the approach
5. Design a concrete implementation strategy
6. When ready, use ExitPlanMode to present your plan for approval

Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.
Task
Explore boolVars scoping
{
  "subagent_type": "Explore",
  "prompt": "I need to understand how `boolVars` is used in the codegen package and how similar scoping problems have been solved. Please explore:\n\n1. How `boolVars` is declared, populated, and used in `codegen/codegen.go`. Find all references to `boolVars` and `isBoolExpression`.\n2. How `procSigs` scoping works with `collectNestedProcSigsScoped()` - this is the pattern mentioned as a model for fixing boolVars.\n3. Are there any other maps/state in the Generator struct that might have similar scoping issues? Look at the Generator struct definition and note any variable-tracking maps.\n4. How proc/function bodies are generated - specifically the entry/exit points where scope push/pop could be added.\n\nBe very thorough - include line numbers and relevant code snippets."
}
[
  {
    "type": "text",
    "text": "Perfect! Now I have all the information I need. Let me create a comprehensive report.\n\n## Comprehensive Exploration Report: `boolVars` Scoping Analysis\n\nI've thoroughly analyzed how `boolVars` is used, how scoping is handled in the codebase, and identified similar issues with other variable-tracking maps. Here's my detailed report:\n\n---\n\n### 1. **How `boolVars` is Declared, Populated, and Used**\n\n#### Struct Definition (lines 38-39):\n```go\n// Bool variable tracking (for type conversion codegen)\nboolVars map[string]bool\n```\n\n#### Initialization (line 112):\nIn `Generate()` method:\n```go\ng.boolVars = make(map[string]bool)\n```\n\n#### Populated in Two Ways:\n\n**A. Package-level VarDecl (lines 1084-1088):**\n```go\nfunc (g *Generator) generateVarDecl(decl *ast.VarDecl) {\n    // ... \n    // Track BOOL variables for type conversion codegen\n    if decl.Type == \"BOOL\" {\n        for _, n := range decl.Names {\n            g.boolVars[n] = true\n        }\n    }\n}\n```\n\n**B. Pre-pass collection (lines 115-117):**\n```go\n// Pre-pass: collect BOOL variable names (needed before containsBoolConversion)\nfor _, stmt := range program.Statements {\n    g.collectBoolVars(stmt)\n}\n```\n\n#### `collectBoolVars()` Traversal (lines 1397-1441):\nRecursively traverses all statements to collect BOOL variable names:\n- **VarDecl case** (lines 1399-1404): Adds BOOL-typed variables\n- Recurses through: **SeqBlock, ParBlock, ProcDecl, FuncDecl, WhileLoop, IfStatement, CaseStatement**\n\n#### Usage (lines 2673-2688):\nIn `isBoolExpression()` method for type conversion codegen:\n```go\nfunc (g *Generator) isBoolExpression(expr ast.Expression) bool {\n    switch e := expr.(type) {\n    case *ast.BooleanLiteral:\n        return true\n    case *ast.Identifier:\n        return g.boolVars[e.Value]  // <-- Lookup here (line 2675)\n    case *ast.UnaryExpr:\n        return e.Operator == \"NOT\"\n    case *ast.TypeConversion:\n        return e.TargetType == \"BOOL\"\n    case *ast.ParenExpr:\n        return g.isBoolExpression(e.Expr)\n    }\n    return false\n}\n```\n\nAlso used in line 2665:\n```go\nreturn tc.TargetType != \"BOOL\" && g.isBoolExpression(tc.Expr)\n```\n\n---\n\n### 2. **How `procSigs` Scoping Works with `collectNestedProcSigsScoped()`**\n\nThis is the **model pattern for fixing `boolVars`**.\n\n#### Initial Collection (lines 149-155):\nPackage-level collection during pre-pass:\n```go\nif proc, ok := stmt.(*ast.ProcDecl); ok {\n    g.procSigs[proc.Name] = proc.Params\n    g.collectNestedProcSigs(proc.Body)  // Non-scoped, adds all nested procs globally\n}\n```\n\n#### Non-Scoped Traversal (lines 325-352):\n`collectNestedProcSigs()` adds ALL nested procs/funcs globally (problematic):\n```go\nfunc (g *Generator) collectNestedProcSigs(stmts []ast.Statement) {\n    for _, stmt := range stmts {\n        switch s := stmt.(type) {\n        case *ast.ProcDecl:\n            g.procSigs[s.Name] = s.Params  // Global, may conflict\n            g.collectNestedProcSigs(s.Body)\n        // ...\n        }\n    }\n}\n```\n\n#### Scoped Traversal (lines 354-389):\n`collectNestedProcSigsScoped()` saves old values to restore after scope:\n```go\nfunc (g *Generator) collectNestedProcSigsScoped(stmts []ast.Statement, oldSigs map[string][]ast.ProcParam) {\n    for _, stmt := range stmts {\n        switch s := stmt.(type) {\n        case *ast.ProcDecl:\n            if _, saved := oldSigs[s.Name]; !saved {\n                oldSigs[s.Name] = g.procSigs[s.Name] // Save old value (nil if not set)\n            }\n            g.procSigs[s.Name] = s.Params  // Override\n            g.collectNestedProcSigsScoped(s.Body, oldSigs)\n        // ...\n        }\n    }\n}\n```\n\n#### Scope Entry/Exit in `generateProcDecl()` (lines 1927-2018):\n\n**Entry** (lines 1994-1995):\n```go\n// Inside generateProcDecl() after writing func signature\noldSigs := make(map[string][]ast.ProcParam)\ng.collectNestedProcSigsScoped(proc.Body, oldSigs)\n\nfor _, stmt := range proc.Body {\n    g.generateStatement(stmt)\n}\n```\n\n**Exit/Restoration** (lines 2001-2008):\n```go\n// Restore overwritten signatures\nfor name, params := range oldSigs {\n    if params == nil {\n        delete(g.procSigs, name)\n    } else {\n        g.procSigs[name] = params\n    }\n}\n```\n\n---\n\n### 3. **Other Maps in Generator Struct with Similar Scoping Issues**\n\nI examined the struct definition (lines 11-48). Here are **ALL variable-tracking maps**:\n\n| Field | Type | Usage | Scoping |\n|-------|------|-------|---------|\n| `procSigs` | `map[string][]ast.ProcParam` | Proc/func signatures | **SCOPED** (via `collectNestedProcSigsScoped()`) |\n| `refParams` | `map[string]bool` | Reference parameters of current proc | **SCOPED** (saved/restored in `generateProcDecl()`, lines 1929, 2016) |\n| `protocolDefs` | `map[string]*ast.ProtocolDecl` | Protocol definitions | Global only (package-level) |\n| `chanProtocols` | `map[string]string` | Channel name → protocol mapping | **NOT SCOPED** ⚠️ |\n| `recordDefs` | `map[string]*ast.RecordDecl` | Record type definitions | Global only (package-level) |\n| `recordVars` | `map[string]string` | Variable name → record type | **NOT SCOPED** ⚠️ |\n| `boolVars` | `map[string]bool` | Variable name → is BOOL | **NOT SCOPED** ⚠️ |\n| `retypesRenames` | `map[string]string` | RETYPES param renames | **SCOPED** (saved/restored in `generateProcDecl()`, lines 1962, 2017) |\n\n**Three maps need scoping**: `boolVars`, `chanProtocols`, `recordVars`\n\n---\n\n### 4. **Proc/Function Body Generation Entry/Exit Points**\n\n#### In `generateProcDecl()` (lines 1927-2018):\n\n```\nEntry point (after line 1987, before line 1997):\n  L1987:  g.writeLine(fmt.Sprintf(\"func %s(%s) {\", gName, params))\n  L1988:  g.indent++\n  L1989:  g.nestingLevel++\n  \n  L1991-1995: [SCOPE SETUP]\n               oldSigs := make(map[string][]ast.ProcParam)\n               g.collectNestedProcSigsScoped(proc.Body, oldSigs)\n  \n  L1997-1999: [BODY GENERATION]\n               for _, stmt := range proc.Body {\n                   g.generateStatement(stmt)\n               }\n  \n  L2001-2008: [SCOPE RESTORATION]\n               for name, params := range oldSigs {\n                   if params == nil {\n                       delete(g.procSigs, name)\n                   } else {\n                       g.procSigs[name] = params\n                   }\n               }\n  \n  L2010:  g.nestingLevel--\n  L2011:  g.indent--\n  L2012:  g.writeLine(\"}\")\n```\n\n#### In `generateFuncDecl()` (lines 2103-2148):\n\n```\nEntry point (after line 2123, before line 2128):\n  L2123:  g.writeLine(fmt.Sprintf(\"func %s(%s) %s {\", gName, params, returnTypeStr))\n  L2125:  g.indent++\n  L2126:  g.nestingLevel++\n  \n  L2128-2130: [BODY GENERATION - NO SCOPING!]\n               for _, stmt := range fn.Body {\n                   g.generateStatement(stmt)\n               }\n  \n  L2144:  g.nestingLevel--\n  L2145:  g.indent--\n  L2146:  g.writeLine(\"}\")\n```\n\n**Important observation**: `generateFuncDecl()` has **no scoping logic** (no saved/restored maps), while `generateProcDecl()` has full scoping for `procSigs`, `refParams`, and `retypesRenames`.\n\n---\n\n### 5. **Detailed Reference Parameter Scoping Example** (lines 1927-2018)\n\nReference parameters are scoped similarly but with subtle differences:\n\n```go\nfunc (g *Generator) generateProcDecl(proc *ast.ProcDecl) {\n    // SAVE OLD CONTEXT\n    oldRefParams := g.refParams\n    newRefParams := make(map[string]bool)\n    \n    // INHERIT parent's ref params for closure captures\n    if g.nestingLevel > 0 {\n        for k, v := range oldRefParams {\n            newRefParams[k] = v\n        }\n    }\n    \n    // BUILD NEW CONTEXT from this proc's params\n    for _, p := range proc.Params {\n        if !p.IsVal && !p.IsChan && ... {\n            newRefParams[p.Name] = true\n        } else {\n            delete(newRefParams, p.Name)  // Own param shadows inherited\n        }\n        // Register protocol/record mappings\n    }\n    g.refParams = newRefParams  // ACTIVATE NEW CONTEXT\n    \n    // ... proc body generation ...\n    \n    // RESTORE OLD CONTEXT\n    g.refParams = oldRefParams\n    g.retypesRenames = oldRenames\n}\n```\n\nKey difference from `procSigs`:\n- **`refParams`**: Completely replaced (new map) and restored (old map reinstated)\n- **`procSigs`**: Selectively overwritten and restored entry-by-entry\n\n---\n\n### 6. **How `collectBoolVars` is Called**\n\nOnly called **once** in the entire pipeline (line 116):\n```go\n// Pre-pass: collect BOOL variable names (needed before containsBoolConversion)\nfor _, stmt := range program.Statements {\n    g.collectBoolVars(stmt)\n}\n```\n\nThis is during the **package-level pre-pass** in `Generate()`, meaning:\n- All BOOL variables from the entire program are added to `g.boolVars`\n- **No scoping boundaries** → nested procs' BOOL variables pollute the global map\n- A BOOL variable in one nested proc can be seen as BOOL in a sibling nested proc\n\n---\n\n### Summary Table: State Scoping in Generator\n\n| Map | Scope | Entry | Exit | Model |\n|-----|-------|-------|------|-------|\n| `procSigs` | Per-proc | `collectNestedProcSigsScoped()` (L1995) | Restore loop (L2002-2008) | Selective restore |\n| `refParams` | Per-proc | Saved & replaced (L1929, L1957) | Restored (L2016) | Full replacement |\n| `retypesRenames` | Per-proc | Saved & cleared (L1962-1963) | Restored (L2017) | Full replacement |\n| `boolVars` | **GLOBAL** (broken) | Pre-pass only (L116) | Never | Needs scoping |\n| `chanProtocols` | **GLOBAL** (broken) | Pre-pass only (L162) | Never | Needs scoping |\n| `recordVars` | **GLOBAL** (broken) | Pre-pass only (L163) | Never | Needs scoping |\n\n---\n\nThis completes the thorough exploration. The scoping pattern is clear: **entry saves old state, body generation uses new state, exit restores old state**. Both `refParams` (full replacement) and `procSigs` (selective entry-by-entry) patterns are viable models for fixing `boolVars`, `chanProtocols`, and `recordVars`."
  },
  {
    "type": "text",
    "text": "agentId: aeff3118953ab822c (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 44747\ntool_uses: 37\nduration_ms: 75280</usage>"
  }
]
Thinking

The exploration agent did an excellent job. Let me now plan the implementation. The issue is clear:

  1. boolVars is a flat global map - BOOL variables from any scope can pollute lookups in other scopes
  2. The fix should follow the existing refParams pattern (full replacement save/restore) since boolVars tracks variable names (not proc signatures)
  3. We also need to consider chanProtocols and recordVars which have the same issue, but the issue specifically asks about boolVars

Let me think about the approach:

For boolVars: - At proc/func entry: save old boolVars, create new map inheriting parent scope entries, add this proc's own BOOL params/variables

  • At proc/func exit: restore old boolVars

The tricky part is that collectBoolVars does a full AST traversal upfront, so I need to decide between scoping it per proc during generation versus removing the pre-pass and tracking dynamically as I generate code.

Option B feels right—similar to how refParams works—where I add variables to boolVars when I encounter a BOOL VarDecl during generation, then save/restore the map on proc entry/exit. But the pre-pass serves a different purpose: it determines whether we need the _boolToInt helper at all. So I should keep the pre-pass for that, but make the runtime boolVars tracking scoped per procedure. the pre-pass collects all BOOL variables globally for the containsBoolConversion check, but during code generation I need to scope boolVars per procedure/function by saving and restoring the map at entry and exit, while letting generateVarDecl dynamically add local BOOL variables during body generation. for generateFuncDecl. The real issue is that the pre-pass collects everything globally, but during generation I need to scope variables per-procedure without inheriting from unrelated procedures. I think the cleanest approach is to clear boolVars before generation starts and rebuild it dynamically as I traverse each scope, so top-level procs stay isolated while nested procs can still inherit from their parent.

Now I'm looking at how collectBoolVars feeds into containsBoolConversion(), and once that check completes I can reset the map. Then during code generation, each level manages its own scope: top-level declarations add to boolVars, procedure declarations save and restore the map while adding their BOOL parameters, and nested declarations within a procedure body add to the current scope's boolVars. Let me trace through the actual codegen.go to verify this works.

Excellent exploration. Let me read the critical sections of the code to design the plan.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 105,
  "limit": 65
}
   105→	g.procSigs = make(map[string][]ast.ProcParam)
   106→	g.refParams = make(map[string]bool)
   107→	g.protocolDefs = make(map[string]*ast.ProtocolDecl)
   108→	g.chanProtocols = make(map[string]string)
   109→	g.tmpCounter = 0
   110→	g.recordDefs = make(map[string]*ast.RecordDecl)
   111→	g.recordVars = make(map[string]string)
   112→	g.boolVars = make(map[string]bool)
   113→
   114→	// Pre-pass: collect BOOL variable names (needed before containsBoolConversion)
   115→	for _, stmt := range program.Statements {
   116→		g.collectBoolVars(stmt)
   117→	}
   118→
   119→	// First pass: collect procedure signatures, protocols, and check for PAR/print
   120→	for _, stmt := range program.Statements {
   121→		if g.containsPar(stmt) {
   122→			g.needSync = true
   123→		}
   124→		if g.containsPrint(stmt) {
   125→			g.needFmt = true
   126→		}
   127→		if g.containsTimer(stmt) {
   128→			g.needTime = true
   129→		}
   130→		if g.containsStop(stmt) {
   131→			g.needOs = true
   132→			g.needFmt = true
   133→		}
   134→		if g.containsMostExpr(stmt) {
   135→			g.needMath = true
   136→		}
   137→		if g.containsIntrinsics(stmt) {
   138→			g.needMathBits = true
   139→		}
   140→		if g.containsRetypes(stmt) {
   141→			g.needMath = true
   142→		}
   143→		if g.containsAltReplicator(stmt) {
   144→			g.needReflect = true
   145→		}
   146→		if g.containsBoolConversion(stmt) {
   147→			g.needBoolHelper = true
   148→		}
   149→		if proc, ok := stmt.(*ast.ProcDecl); ok {
   150→			g.procSigs[proc.Name] = proc.Params
   151→			g.collectNestedProcSigs(proc.Body)
   152→		}
   153→		if fn, ok := stmt.(*ast.FuncDecl); ok {
   154→			g.procSigs[fn.Name] = fn.Params
   155→		}
   156→		if proto, ok := stmt.(*ast.ProtocolDecl); ok {
   157→			g.protocolDefs[proto.Name] = proto
   158→		}
   159→		if rec, ok := stmt.(*ast.RecordDecl); ok {
   160→			g.recordDefs[rec.Name] = rec
   161→		}
   162→		g.collectChanProtocols(stmt)
   163→		g.collectRecordVars(stmt)
   164→	}
   165→
   166→	// Separate protocol, record, procedure declarations from other statements
   167→	var typeDecls []ast.Statement
   168→	var procDecls []ast.Statement
   169→	var mainStatements []ast.Statement
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1920,
  "limit": 110
}
  1920→		g.generateStatement(s)
  1921→	}
  1922→
  1923→	g.indent--
  1924→	g.writeLine("}")
  1925→}
  1926→
  1927→func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
  1928→	// Track reference parameters for this procedure
  1929→	oldRefParams := g.refParams
  1930→	newRefParams := make(map[string]bool)
  1931→	// Inherit parent's ref params for closure captures when nested
  1932→	if g.nestingLevel > 0 {
  1933→		for k, v := range oldRefParams {
  1934→			newRefParams[k] = v
  1935→		}
  1936→	}
  1937→	for _, p := range proc.Params {
  1938→		if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray && p.ArraySize == "" {
  1939→			newRefParams[p.Name] = true
  1940→		} else {
  1941→			// Own param shadows any inherited ref param with same name
  1942→			delete(newRefParams, p.Name)
  1943→		}
  1944→		// Register chan params with protocol mappings
  1945→		if p.IsChan || p.IsChanArray {
  1946→			if _, ok := g.protocolDefs[p.ChanElemType]; ok {
  1947→				g.chanProtocols[p.Name] = p.ChanElemType
  1948→			}
  1949→		}
  1950→		// Register record-typed params
  1951→		if !p.IsChan {
  1952→			if _, ok := g.recordDefs[p.Type]; ok {
  1953→				g.recordVars[p.Name] = p.Type
  1954→			}
  1955→		}
  1956→	}
  1957→	g.refParams = newRefParams
  1958→
  1959→	// Scan proc body for RETYPES declarations that shadow parameters.
  1960→	// When VAL INT X RETYPES X :, Go can't redeclare X in the same scope,
  1961→	// so we rename the parameter (e.g. X → _rp_X) and let RETYPES declare the original name.
  1962→	oldRenames := g.retypesRenames
  1963→	g.retypesRenames = nil
  1964→	paramNames := make(map[string]bool)
  1965→	for _, p := range proc.Params {
  1966→		paramNames[p.Name] = true
  1967→	}
  1968→	for _, stmt := range proc.Body {
  1969→		if rd, ok := stmt.(*ast.RetypesDecl); ok {
  1970→			if paramNames[rd.Source] && rd.Name == rd.Source {
  1971→				if g.retypesRenames == nil {
  1972→					g.retypesRenames = make(map[string]string)
  1973→				}
  1974→				g.retypesRenames[rd.Name] = "_rp_" + goIdent(rd.Name)
  1975→			}
  1976→		}
  1977→	}
  1978→
  1979→	// Generate function signature
  1980→	params := g.generateProcParams(proc.Params)
  1981→	gName := goIdent(proc.Name)
  1982→	if g.nestingLevel > 0 {
  1983→		// Nested PROC: generate as Go closure
  1984→		g.writeLine(fmt.Sprintf("%s := func(%s) {", gName, params))
  1985→	} else {
  1986→		g.writeLine(fmt.Sprintf("func %s(%s) {", gName, params))
  1987→	}
  1988→	g.indent++
  1989→	g.nestingLevel++
  1990→
  1991→	// Register nested proc/func signatures for this scope so that calls
  1992→	// within this proc resolve to the correct (local) signature rather than
  1993→	// a same-named proc from a different scope.
  1994→	oldSigs := make(map[string][]ast.ProcParam)
  1995→	g.collectNestedProcSigsScoped(proc.Body, oldSigs)
  1996→
  1997→	for _, stmt := range proc.Body {
  1998→		g.generateStatement(stmt)
  1999→	}
  2000→
  2001→	// Restore overwritten signatures
  2002→	for name, params := range oldSigs {
  2003→		if params == nil {
  2004→			delete(g.procSigs, name)
  2005→		} else {
  2006→			g.procSigs[name] = params
  2007→		}
  2008→	}
  2009→
  2010→	g.nestingLevel--
  2011→	g.indent--
  2012→	g.writeLine("}")
  2013→	g.writeLine("")
  2014→
  2015→	// Restore previous context
  2016→	g.refParams = oldRefParams
  2017→	g.retypesRenames = oldRenames
  2018→}
  2019→
  2020→func (g *Generator) generateProcParams(params []ast.ProcParam) string {
  2021→	var parts []string
  2022→	for _, p := range params {
  2023→		var goType string
  2024→		if p.IsChanArray {
  2025→			goType = "[]" + chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)
  2026→		} else if p.IsChan {
  2027→			goType = chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)
  2028→		} else if p.IsOpenArray {
  2029→			goType = "[]" + g.occamTypeToGo(p.Type)
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 2095,
  "limit": 70
}
  2095→		} else {
  2096→			g.generateExpression(arg)
  2097→		}
  2098→	}
  2099→	g.write(")")
  2100→	g.write("\n")
  2101→}
  2102→
  2103→func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
  2104→	params := g.generateProcParams(fn.Params)
  2105→
  2106→	// Build return type string
  2107→	var returnTypeStr string
  2108→	if len(fn.ReturnTypes) == 1 {
  2109→		returnTypeStr = g.occamTypeToGo(fn.ReturnTypes[0])
  2110→	} else {
  2111→		goTypes := make([]string, len(fn.ReturnTypes))
  2112→		for i, rt := range fn.ReturnTypes {
  2113→			goTypes[i] = g.occamTypeToGo(rt)
  2114→		}
  2115→		returnTypeStr = "(" + strings.Join(goTypes, ", ") + ")"
  2116→	}
  2117→
  2118→	gName := goIdent(fn.Name)
  2119→	if g.nestingLevel > 0 {
  2120→		// Nested FUNCTION: generate as Go closure
  2121→		g.writeLine(fmt.Sprintf("%s := func(%s) %s {", gName, params, returnTypeStr))
  2122→	} else {
  2123→		g.writeLine(fmt.Sprintf("func %s(%s) %s {", gName, params, returnTypeStr))
  2124→	}
  2125→	g.indent++
  2126→	g.nestingLevel++
  2127→
  2128→	for _, stmt := range fn.Body {
  2129→		g.generateStatement(stmt)
  2130→	}
  2131→
  2132→	if len(fn.ResultExprs) > 0 {
  2133→		g.builder.WriteString(strings.Repeat("\t", g.indent))
  2134→		g.write("return ")
  2135→		for i, expr := range fn.ResultExprs {
  2136→			if i > 0 {
  2137→				g.write(", ")
  2138→			}
  2139→			g.generateExpression(expr)
  2140→		}
  2141→		g.write("\n")
  2142→	}
  2143→
  2144→	g.nestingLevel--
  2145→	g.indent--
  2146→	g.writeLine("}")
  2147→	g.writeLine("")
  2148→}
  2149→
  2150→func (g *Generator) generateFuncCallExpr(call *ast.FuncCall) {
  2151→	if transpIntrinsics[call.Name] {
  2152→		g.write("_" + call.Name)
  2153→	} else {
  2154→		g.write(goIdent(call.Name))
  2155→	}
  2156→	g.write("(")
  2157→	params := g.procSigs[call.Name]
  2158→	for i, arg := range call.Args {
  2159→		if i > 0 {
  2160→			g.write(", ")
  2161→		}
  2162→		// Wrap string literals with []byte() when passed to []BYTE parameters
  2163→		if _, isStr := arg.(*ast.StringLiteral); isStr && i < len(params) && params[i].IsOpenArray && params[i].Type == "BYTE" {
  2164→			g.write("[]byte(")
Thinking

Now let me also check the generateVarDecl to see where boolVars is populated during generation, and the collectBoolVars function.

Now let me check how generateVarDecl populates boolVars and the collectBoolVars function.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1075,
  "limit": 25
}
  1075→	for i, n := range decl.Names {
  1076→		goNames[i] = goIdent(n)
  1077→	}
  1078→	g.writeLine(fmt.Sprintf("var %s %s", strings.Join(goNames, ", "), goType))
  1079→	// Suppress "declared and not used" for each variable
  1080→	for _, n := range goNames {
  1081→		g.writeLine(fmt.Sprintf("_ = %s", n))
  1082→	}
  1083→	// Track BOOL variables for type conversion codegen
  1084→	if decl.Type == "BOOL" {
  1085→		for _, n := range decl.Names {
  1086→			g.boolVars[n] = true
  1087→		}
  1088→	}
  1089→}
  1090→
  1091→func (g *Generator) generateAbbreviation(abbr *ast.Abbreviation) {
  1092→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1093→	if abbr.Type != "" {
  1094→		goType := g.occamTypeToGo(abbr.Type)
  1095→		if abbr.IsOpenArray {
  1096→			goType = "[]" + goType
  1097→		}
  1098→		g.write(fmt.Sprintf("var %s %s = ", goIdent(abbr.Name), goType))
  1099→	} else {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1395,
  "limit": 50
}
  1395→}
  1396→
  1397→func (g *Generator) collectBoolVars(stmt ast.Statement) {
  1398→	switch s := stmt.(type) {
  1399→	case *ast.VarDecl:
  1400→		if s.Type == "BOOL" {
  1401→			for _, name := range s.Names {
  1402→				g.boolVars[name] = true
  1403→			}
  1404→		}
  1405→	case *ast.SeqBlock:
  1406→		for _, inner := range s.Statements {
  1407→			g.collectBoolVars(inner)
  1408→		}
  1409→	case *ast.ParBlock:
  1410→		for _, inner := range s.Statements {
  1411→			g.collectBoolVars(inner)
  1412→		}
  1413→	case *ast.ProcDecl:
  1414→		for _, inner := range s.Body {
  1415→			g.collectBoolVars(inner)
  1416→		}
  1417→	case *ast.FuncDecl:
  1418→		for _, inner := range s.Body {
  1419→			g.collectBoolVars(inner)
  1420→		}
  1421→	case *ast.WhileLoop:
  1422→		for _, inner := range s.Body {
  1423→			g.collectBoolVars(inner)
  1424→		}
  1425→	case *ast.IfStatement:
  1426→		for _, choice := range s.Choices {
  1427→			if choice.NestedIf != nil {
  1428→				g.collectBoolVars(choice.NestedIf)
  1429→			}
  1430→			for _, inner := range choice.Body {
  1431→				g.collectBoolVars(inner)
  1432→			}
  1433→		}
  1434→	case *ast.CaseStatement:
  1435→		for _, choice := range s.Choices {
  1436→			for _, inner := range choice.Body {
  1437→				g.collectBoolVars(inner)
  1438→			}
  1439→		}
  1440→	}
  1441→}
  1442→
  1443→func (g *Generator) collectRecordVars(stmt ast.Statement) {
  1444→	switch s := stmt.(type) {
Thinking

Now I have a clear picture. Let me also check if BOOL params need to be tracked. Let me check if proc parameters of type BOOL need to be registered in boolVars.

Looking at the generateProcDecl function, I see that it already registers chan params with protocols and record-typed params. But it doesn't register BOOL-typed params in boolVars.

So the fix needs to: 1. Keep the pre-pass collectBoolVars for containsBoolConversion (to know whether to emit _boolToInt helper) 2. Reset boolVars before code generation starts (after all pre-pass checks)

  1. In generateProcDecl and generateFuncDecl: save/restore boolVars like refParams, and register BOOL params from proc/func parameters
  2. generateVarDecl already adds BOOL vars dynamically during generation, which handles the rest

The key insight is that resetting boolVars after the pre-pass means we only track BOOL variables during actual code generation, which is when generateVarDecl populates it anyway.

So the approach is: clear boolVars after the pre-pass, then let each scope (top-level, proc-level, func-level) build its own BOOL variable map as it generates code, with proper save/restore to prevent cross-contamination between scopes.

For nested procs, I need to inherit the parent's boolVars when entering a nested scope, similar to how reference parameters are handled, so that inner procs can see BOOL variables declared in outer scopes.

Setting up the save/restore pattern... I'm realizing the nestingLevel check at line 1932 happens before the increment at line 1989, so it correctly identifies whether we're already inside a parent procedure for scope inheritance. The same scoping pattern applies to chanProtocols and recordVars, though the issue specifically focuses on boolVars—though I should probably handle all three while I'm at it.

Looking at the actual requirement, I need to scope boolVars per procedure and function body, which means checking if BOOL-typed parameters exist in both PROC and FUNC declarations. The param structure has a Type field for non-channel params, so I can check p.Type == "BOOL" to identify them.

Let me finalize the implementation plan now.

Actually, I should verify the timing: after the pre-pass populates boolVars globally, I can reset it before the second pass since top-level variables get processed during code generation. When entering a top-level proc, I'll save the current boolVars state and restore it on exit, which handles the scoping correctly even if declarations appear in different orders.

Now let me check if I can actually reproduce the issue to confirm my understanding, though the problem itself is clear enough to fix.

Now I have a clear picture. Let me also quickly check if BOOL params are a concern.

Grep
{
  "pattern": "BOOL.*param|param.*BOOL|p\\.Type.*BOOL",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content"
}
No matches found
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1937,
  "limit": 20
}
  1937→	for _, p := range proc.Params {
  1938→		if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray && p.ArraySize == "" {
  1939→			newRefParams[p.Name] = true
  1940→		} else {
  1941→			// Own param shadows any inherited ref param with same name
  1942→			delete(newRefParams, p.Name)
  1943→		}
  1944→		// Register chan params with protocol mappings
  1945→		if p.IsChan || p.IsChanArray {
  1946→			if _, ok := g.protocolDefs[p.ChanElemType]; ok {
  1947→				g.chanProtocols[p.Name] = p.ChanElemType
  1948→			}
  1949→		}
  1950→		// Register record-typed params
  1951→		if !p.IsChan {
  1952→			if _, ok := g.recordDefs[p.Type]; ok {
  1953→				g.recordVars[p.Name] = p.Type
  1954→			}
  1955→		}
  1956→	}
Thinking

Good - I can see that proc params register chan protocols and record vars but not BOOL params. I need to also register BOOL params in the scoped boolVars.

Let me check if there's an INITIAL declaration that also needs tracking.

Let me check if INITIAL declarations with BOOL type need tracking too.

Grep
{
  "pattern": "generateInitialDecl|INITIAL.*BOOL|boolVars",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
39:	boolVars map[string]bool
112:	g.boolVars = make(map[string]bool)
1086:			g.boolVars[n] = true
1402:				g.boolVars[name] = true
2675:		return g.boolVars[e.Value]
Grep
{
  "pattern": "generateInitialDecl\"",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content"
}
No matches found
Grep
{
  "pattern": "InitialDecl\"",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "InitialDecl",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "INITIAL|Initial",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "Initial",
  "path": "/home/david/projects/code-associates/occam2go/codegen",
  "output_mode": "content",
  "-n": true
}
codegen/e2e_basic_test.go:136:func TestE2E_InitialDecl(t *testing.T) {
Grep
{
  "pattern": "IsInitial|initialDecl|initial.decl",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "INITIAL",
  "path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
  "output_mode": "content",
  "-n": true
}
488:// Abbreviation represents an abbreviation: VAL INT x IS 42:, INT y IS z:, or INITIAL INT x IS 42:
490:	Token       lexer.Token // VAL, INITIAL, or type token
492:	IsInitial   bool        // true for INITIAL declarations
Thinking

So INITIAL declarations use the Abbreviation AST node. Let me check if generateAbbreviation tracks BOOL vars.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1091,
  "limit": 55
}
  1091→func (g *Generator) generateAbbreviation(abbr *ast.Abbreviation) {
  1092→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1093→	if abbr.Type != "" {
  1094→		goType := g.occamTypeToGo(abbr.Type)
  1095→		if abbr.IsOpenArray {
  1096→			goType = "[]" + goType
  1097→		}
  1098→		g.write(fmt.Sprintf("var %s %s = ", goIdent(abbr.Name), goType))
  1099→	} else {
  1100→		g.write(fmt.Sprintf("%s := ", goIdent(abbr.Name)))
  1101→	}
  1102→	// Wrap string literals with []byte() when assigned to []byte variables
  1103→	if _, isStr := abbr.Value.(*ast.StringLiteral); isStr && abbr.IsOpenArray && abbr.Type == "BYTE" {
  1104→		g.write("[]byte(")
  1105→		g.generateExpression(abbr.Value)
  1106→		g.write(")")
  1107→	} else {
  1108→		g.generateExpression(abbr.Value)
  1109→	}
  1110→	g.write("\n")
  1111→	// Suppress "declared and not used" for abbreviations inside function bodies
  1112→	if g.nestingLevel > 0 {
  1113→		g.writeLine(fmt.Sprintf("_ = %s", goIdent(abbr.Name)))
  1114→	}
  1115→}
  1116→
  1117→func (g *Generator) generateChanDecl(decl *ast.ChanDecl) {
  1118→	goType := g.occamTypeToGo(decl.ElemType)
  1119→	if decl.IsArray {
  1120→		for _, name := range decl.Names {
  1121→			n := goIdent(name)
  1122→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1123→			g.write(fmt.Sprintf("%s := make([]chan %s, ", n, goType))
  1124→			g.generateExpression(decl.Size)
  1125→			g.write(")\n")
  1126→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1127→			g.write(fmt.Sprintf("for _i := range %s { %s[_i] = make(chan %s) }\n", n, n, goType))
  1128→		}
  1129→	} else {
  1130→		for _, name := range decl.Names {
  1131→			g.writeLine(fmt.Sprintf("%s := make(chan %s)", goIdent(name), goType))
  1132→		}
  1133→	}
  1134→}
  1135→
  1136→func (g *Generator) generateTimerDecl(decl *ast.TimerDecl) {
  1137→	for _, name := range decl.Names {
  1138→		g.writeLine(fmt.Sprintf("// TIMER %s", name))
  1139→	}
  1140→}
  1141→
  1142→func (g *Generator) generateTimerRead(tr *ast.TimerRead) {
  1143→	g.writeLine(fmt.Sprintf("%s = int(time.Now().UnixMicro())", goIdent(tr.Variable)))
  1144→}
  1145→
Thinking

I notice that generateAbbreviation does NOT track BOOL vars. So if someone writes INITIAL BOOL x IS TRUE: or BOOL x IS expr:, the variable x won't be tracked in boolVars. But the pre-pass collectBoolVars only looks at VarDecl, not Abbreviation. So abbreviations with BOOL type are already not tracked, and the scoping fix shouldn't make this worse.

Now let me also check collectBoolVars - does it handle Abbreviations?

Looking at lines 1397-1441, it only handles VarDecl, SeqBlock, ParBlock, ProcDecl, FuncDecl, WhileLoop, IfStatement, CaseStatement. So Abbreviation BOOL vars are already not tracked in boolVars. That's a pre-existing gap but not related to this issue.

Now I'm ready to outline the fix. The plan is to clear boolVars after the pre-pass, then in both generateProcDecl and generateFuncDecl I'll save and restore the scope following the same pattern used for refParams, registering BOOL-typed parameters in the new scoped boolVars while letting generateVarDecl handle dynamic registration during body generation. But I need to think through the clearing logic more carefully first.

The real issue is that the pre-pass flattens all BOOL vars from every scope into one map, so when generating code for a proc, we might incorrectly match a BOOL var from a completely different proc. The solution is to scope boolVars properly—clearing it at the start of code generation and rebuilding it scope-by-scope as we enter each function or procedure. So the real issue is that BOOL variables declared inside included files' procedures are leaking into the global scope. Since PROCs are top-level declarations that execute before the main code block, they shouldn't have access to variables outside their own scope—and when they declare their own BOOL variables, those shouldn't pollute the global map either.

The solution is to save and restore the boolVars map when entering and exiting each procedure, so declarations inside one proc don't affect others. I also need to handle the pre-pass carefully: it should identify which BOOL conversions are needed, but then clear the map before actual code generation so each proc starts fresh. The main code block can still declare its own BOOL variables normally during generation.

Now I'm weighing whether to reset boolVars after the pre-pass or just scope it per-proc. If I don't reset, top-level variables from the pre-pass would persist globally, which could cause issues when entering nested procedures.

The cleaner approach is to clear boolVars after the pre-pass and then scope it within each proc/func, ensuring each scope gets its own fresh map and avoiding cross-contamination between procedures. about this even more simply. The pattern used by refParams is:

  1. Save old
  2. Create new (inheriting from parent if nested)
  3. Generate body
  4. Restore old

This means: - At top level, boolVars has whatever the pre-pass put in - When we enter proc A, we save, create fresh map, generate body, restore - Inside proc A, only proc A's BOOL vars are visible - When we exit proc A, boolVars is restored to the pre-pass state - When we enter proc B, same thing: save, fresh map, generate, restore - Proc B never sees proc A's vars

This is exactly what we want! And we DON'T need to clear after the pre-pass because the per-proc scoping already handles it. The pre-pass data is never consulted inside a proc because we always create a fresh map on proc entry.

For nested procs, we'd inherit from the parent's scoped boolVars rather than the pre-pass global one. This approach is simpler and more correct.

Now I'm thinking through the edge case where top-level variables are declared after procs. In occam, the main code typically comes after procedure definitions, so when we encounter a VarDecl in the main sequence, it gets added to boolVars through generateVarDecl, and then the INT conversion happens afterward. That works fine. But I need to consider what happens if there's a BOOL declaration before a proc...

Actually, this can't be an issue in occam because top-level procs can't access variables from the enclosing scope unless they're passed as parameters. Nested procs can close over variables, but top-level ones are isolated. The fix is straightforward: create a fresh boolVars map on proc entry for top-level procs and inherit from the parent's scoped map for nested ones. This mirrors exactly how refParams works.

I'm ready to implement this plan now.

I have all the context I need. Let me write the plan.

📝 Write dreamy-snacking-scroll.md
/home/david/.claude/plans/dreamy-snacking-scroll.md
# Fix: Scope `boolVars` per proc/function body (Issue #56)

## Context

The `boolVars` map in `codegen.go` is flat and global. A pre-pass (`collectBoolVars`) adds ALL `BOOL`-typed variables from the entire program (including `#INCLUDE`d modules) into a single map. When generating a type conversion like `INT16 b` where `b` is a `BYTE`, the codegen checks `g.boolVars["b"]` and incorrectly finds `true` because a different proc in an included file declared `BOOL b:`. This produces invalid Go: `int16(_boolToInt(b))` instead of `int16(b)`.

## Approach

Follow the existing `refParams` scoping pattern (save/restore in `generateProcDecl`):

### Changes in `codegen/codegen.go`

**1. Scope `boolVars` in `generateProcDecl` (~line 1927)**

After the existing `oldRefParams` save (line 1929), add save/restore for `boolVars`:

- Save: `oldBoolVars := g.boolVars`
- Create new map: fresh for top-level procs (`nestingLevel == 0`), inherited copy for nested procs (closures can reference parent BOOL vars)
- Register BOOL-typed params from `proc.Params` (where `p.Type == "BOOL"` and not a chan/chanArray)
- Also delete non-BOOL params that shadow inherited names
- After body generation, restore: `g.boolVars = oldBoolVars` (next to existing `g.refParams = oldRefParams` at line 2016)

`generateVarDecl` (line 1084-1088) already adds BOOL vars dynamically during body generation, so local `BOOL` declarations will correctly populate the scoped map.

**2. Scope `boolVars` in `generateFuncDecl` (~line 2103)**

Same pattern: save before body generation, create scoped map (inheriting if nested), register BOOL params, restore after body.

### Files to modify

- `codegen/codegen.go` — `generateProcDecl`, `generateFuncDecl`

### What stays unchanged

- The pre-pass `collectBoolVars` (lines 114-117) — still needed for `containsBoolConversion` to decide whether to emit the `_boolToInt` helper function
- `generateVarDecl` BOOL tracking (lines 1084-1088) — already correct, adds to whatever the current scoped map is
- `isBoolExpression` (line 2675) — no changes needed, it already checks `g.boolVars`

## Verification

```bash
# Reproduce the bug (should fail with go vet error before fix)
./occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/cast.go kroc/modules/course/examples/cast.occ
go vet /tmp/cast.go

# Full course module still compiles
./occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module
go vet /tmp/course_out.go

# All existing tests pass
go test ./...
```
File created successfully at: /home/david/.claude/plans/dreamy-snacking-scroll.md
ExitPlanMode
{
  "plan": "# Fix: Scope `boolVars` per proc/function body (Issue #56)\n\n## Context\n\nThe `boolVars` map in `codegen.go` is flat and global. A pre-pass (`collectBoolVars`) adds ALL `BOOL`-typed variables from the entire program (including `#INCLUDE`d modules) into a single map. When generating a type conversion like `INT16 b` where `b` is a `BYTE`, the codegen checks `g.boolVars[\"b\"]` and incorrectly finds `true` because a different proc in an included file declared `BOOL b:`. This produces invalid Go: `int16(_boolToInt(b))` instead of `int16(b)`.\n\n## Approach\n\nFollow the existing `refParams` scoping pattern (save/restore in `generateProcDecl`):\n\n### Changes in `codegen/codegen.go`\n\n**1. Scope `boolVars` in `generateProcDecl` (~line 1927)**\n\nAfter the existing `oldRefParams` save (line 1929), add save/restore for `boolVars`:\n\n- Save: `oldBoolVars := g.boolVars`\n- Create new map: fresh for top-level procs (`nestingLevel == 0`), inherited copy for nested procs (closures can reference parent BOOL vars)\n- Register BOOL-typed params from `proc.Params` (where `p.Type == \"BOOL\"` and not a chan/chanArray)\n- Also delete non-BOOL params that shadow inherited names\n- After body generation, restore: `g.boolVars = oldBoolVars` (next to existing `g.refParams = oldRefParams` at line 2016)\n\n`generateVarDecl` (line 1084-1088) already adds BOOL vars dynamically during body generation, so local `BOOL` declarations will correctly populate the scoped map.\n\n**2. Scope `boolVars` in `generateFuncDecl` (~line 2103)**\n\nSame pattern: save before body generation, create scoped map (inheriting if nested), register BOOL params, restore after body.\n\n### Files to modify\n\n- `codegen/codegen.go` — `generateProcDecl`, `generateFuncDecl`\n\n### What stays unchanged\n\n- The pre-pass `collectBoolVars` (lines 114-117) — still needed for `containsBoolConversion` to decide whether to emit the `_boolToInt` helper function\n- `generateVarDecl` BOOL tracking (lines 1084-1088) — already correct, adds to whatever the current scoped map is\n- `isBoolExpression` (line 2675) — no changes needed, it already checks `g.boolVars`\n\n## Verification\n\n```bash\n# Reproduce the bug (should fail with go vet error before fix)\n./occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/cast.go kroc/modules/course/examples/cast.occ\ngo vet /tmp/cast.go\n\n# Full course module still compiles\n./occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module\ngo vet /tmp/course_out.go\n\n# All existing tests pass\ngo test ./...\n```\n"
}
The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.

[Request interrupted by user for tool use]