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
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.
{
"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>"
}
]The exploration agent did an excellent job. Let me now plan the implementation. The issue is clear:
boolVars is a flat global map - BOOL variables from any scope can pollute lookups in other scopesrefParams pattern (full replacement save/restore) since boolVars tracks variable names (not proc signatures)chanProtocols and recordVars which have the same issue, but the issue specifically asks about boolVarsLet 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
boolVarsThe 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.
{
"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{
"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){
"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(")Now let me also check the generateVarDecl to see where boolVars is populated during generation, and the collectBoolVars function.
{
"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 {{
"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) {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)
generateProcDecl and generateFuncDecl: save/restore boolVars like refParams, and register BOOL params from proc/func parametersgenerateVarDecl already adds BOOL vars dynamically during generation, which handles the restThe 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.
{
"pattern": "BOOL.*param|param.*BOOL|p\\.Type.*BOOL",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content"
}{
"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→ }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.
{
"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]
{
"pattern": "generateInitialDecl\"",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content"
}{
"pattern": "InitialDecl\"",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"pattern": "InitialDecl",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"pattern": "INITIAL|Initial",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"pattern": "Initial",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"output_mode": "content",
"-n": true
}{
"pattern": "IsInitial|initialDecl|initial.decl",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"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
So INITIAL declarations use the Abbreviation AST node. Let me check if generateAbbreviation tracks BOOL vars.
{
"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→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:
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.
# 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 ./... ```
{
"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.