Claude Code transcript - page 1/2

Implement the following plan:

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 #INCLUDEd 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.gogenerateProcDecl, 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

# 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 ./...

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/f025241a-2f05-4a0f-bd8b-62efa9622376.jsonl

Thinking

Let me implement the plan to scope boolVars per proc/function body. I need to read the relevant sections of codegen/codegen.go first.

Let me read the relevant sections of codegen.go to understand the current code.

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": 2090,
  "limit": 100
}
  2090→		// Wrap string literals with []byte() when passed to []BYTE parameters
  2091→		if _, isStr := arg.(*ast.StringLiteral); isStr && i < len(params) && params[i].IsOpenArray && params[i].Type == "BYTE" {
  2092→			g.write("[]byte(")
  2093→			g.generateExpression(arg)
  2094→			g.write(")")
  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(")
  2165→			g.generateExpression(arg)
  2166→			g.write(")")
  2167→		} else {
  2168→			g.generateExpression(arg)
  2169→		}
  2170→	}
  2171→	g.write(")")
  2172→}
  2173→
  2174→func (g *Generator) generateMultiAssignment(stmt *ast.MultiAssignment) {
  2175→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  2176→	for i, target := range stmt.Targets {
  2177→		if i > 0 {
  2178→			g.write(", ")
  2179→		}
  2180→		if target.Index != nil {
  2181→			// Check if this is a record field access
  2182→			if _, ok := g.recordVars[target.Name]; ok {
  2183→				if ident, ok := target.Index.(*ast.Identifier); ok {
  2184→					g.write(goIdent(target.Name))
  2185→					g.write(".")
  2186→					g.write(goIdent(ident.Value))
  2187→					continue
  2188→				}
  2189→			}
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1080,
  "limit": 15
}
  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)
Thinking

Now I have a good understanding of the code. Let me also check how generateFuncDecl handles nested procs and scoping, and look at the boolVars field definition and collectBoolVars.

Let me also check how boolVars is defined and the collectBoolVars function.

Grep
{
  "pattern": "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": "collectBoolVars",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true,
  "-C": 5
}
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) {
--
1392-			}
1393-		}
1394-	}
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-

Now let me check line 1402 context (the generateAbbreviation BOOL tracking):

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1091,
  "limit": 30
}
  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 {

Good. Now I have all the context I need. Let me implement the changes.

TaskCreate
Add save/restore of g.boolVars in generateProcDecl and generateFuncDecl, following the existing refParams scoping pattern.
{
  "subject": "Scope boolVars per proc/function body",
  "activeForm": "Scoping boolVars per proc/function body"
}
Task #1 created successfully: Scope boolVars per proc/function body
TaskUpdate
{
  "taskId": "1",
  "status": "in_progress"
}
Updated task #1 status

Now let me apply the changes. First, generateProcDecl — add boolVars scoping after the refParams save:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	// Track reference parameters for this procedure
	oldRefParams := g.refParams
	newRefParams := make(map[string]bool)
	// Inherit parent's ref params for closure captures when nested
	if g.nestingLevel > 0 {
		for k, v := range oldRefParams {
			newRefParams[k] = v
		}
	}
	for _, p := range proc.Params {
		if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray && p.ArraySize == "" {
			newRefParams[p.Name] = true
		} else {
			// Own param shadows any inherited ref param with same name
			delete(newRefParams, p.Name)
		}
+
	// Track reference parameters for this procedure
	oldRefParams := g.refParams
	newRefParams := make(map[string]bool)
	// Scope boolVars per proc body
	oldBoolVars := g.boolVars
	newBoolVars := make(map[string]bool)
	// Inherit parent's ref params and boolVars for closure captures when nested
	if g.nestingLevel > 0 {
		for k, v := range oldRefParams {
			newRefParams[k] = v
		}
		for k, v := range oldBoolVars {
			newBoolVars[k] = v
		}
	}
	for _, p := range proc.Params {
		if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray && p.ArraySize == "" {
			newRefParams[p.Name] = true
		} else {
			// Own param shadows any inherited ref param with same name
			delete(newRefParams, p.Name)
		}
		// Track BOOL params; delete non-BOOL params that shadow inherited names
		if p.Type == "BOOL" && !p.IsChan && !p.IsChanArray {
			newBoolVars[p.Name] = true
		} else {
			delete(newBoolVars, p.Name)
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now add g.boolVars = newBoolVars right after g.refParams = newRefParams:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1962,
  "limit": 10
}
  1962→		// Register record-typed params
  1963→		if !p.IsChan {
  1964→			if _, ok := g.recordDefs[p.Type]; ok {
  1965→				g.recordVars[p.Name] = p.Type
  1966→			}
  1967→		}
  1968→	}
  1969→	g.refParams = newRefParams
  1970→
  1971→	// Scan proc body for RETYPES declarations that shadow parameters.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	g.refParams = newRefParams

	// Scan proc body for RETYPES declarations that shadow parameters.
+
	g.refParams = newRefParams
	g.boolVars = newBoolVars

	// Scan proc body for RETYPES declarations that shadow parameters.
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now add the restore of boolVars at the end of generateProcDecl:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 2025,
  "limit": 10
}
  2025→	g.writeLine("}")
  2026→	g.writeLine("")
  2027→
  2028→	// Restore previous context
  2029→	g.refParams = oldRefParams
  2030→	g.retypesRenames = oldRenames
  2031→}
  2032→
  2033→func (g *Generator) generateProcParams(params []ast.ProcParam) string {
  2034→	var parts []string
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	// Restore previous context
	g.refParams = oldRefParams
	g.retypesRenames = oldRenames
}
+
	// Restore previous context
	g.refParams = oldRefParams
	g.boolVars = oldBoolVars
	g.retypesRenames = oldRenames
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now do the same for generateFuncDecl. Let me read its current state:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 2116,
  "limit": 50
}
  2116→
  2117→func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
  2118→	params := g.generateProcParams(fn.Params)
  2119→
  2120→	// Build return type string
  2121→	var returnTypeStr string
  2122→	if len(fn.ReturnTypes) == 1 {
  2123→		returnTypeStr = g.occamTypeToGo(fn.ReturnTypes[0])
  2124→	} else {
  2125→		goTypes := make([]string, len(fn.ReturnTypes))
  2126→		for i, rt := range fn.ReturnTypes {
  2127→			goTypes[i] = g.occamTypeToGo(rt)
  2128→		}
  2129→		returnTypeStr = "(" + strings.Join(goTypes, ", ") + ")"
  2130→	}
  2131→
  2132→	gName := goIdent(fn.Name)
  2133→	if g.nestingLevel > 0 {
  2134→		// Nested FUNCTION: generate as Go closure
  2135→		g.writeLine(fmt.Sprintf("%s := func(%s) %s {", gName, params, returnTypeStr))
  2136→	} else {
  2137→		g.writeLine(fmt.Sprintf("func %s(%s) %s {", gName, params, returnTypeStr))
  2138→	}
  2139→	g.indent++
  2140→	g.nestingLevel++
  2141→
  2142→	for _, stmt := range fn.Body {
  2143→		g.generateStatement(stmt)
  2144→	}
  2145→
  2146→	if len(fn.ResultExprs) > 0 {
  2147→		g.builder.WriteString(strings.Repeat("\t", g.indent))
  2148→		g.write("return ")
  2149→		for i, expr := range fn.ResultExprs {
  2150→			if i > 0 {
  2151→				g.write(", ")
  2152→			}
  2153→			g.generateExpression(expr)
  2154→		}
  2155→		g.write("\n")
  2156→	}
  2157→
  2158→	g.nestingLevel--
  2159→	g.indent--
  2160→	g.writeLine("}")
  2161→	g.writeLine("")
  2162→}
  2163→
  2164→func (g *Generator) generateFuncCallExpr(call *ast.FuncCall) {
  2165→	if transpIntrinsics[call.Name] {
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
	params := g.generateProcParams(fn.Params)

	// Build return type string
	var returnTypeStr string
	if len(fn.ReturnTypes) == 1 {
		returnTypeStr = g.occamTypeToGo(fn.ReturnTypes[0])
	} else {
		goTypes := make([]string, len(fn.ReturnTypes))
		for i, rt := range fn.ReturnTypes {
			goTypes[i] = g.occamTypeToGo(rt)
		}
		returnTypeStr = "(" + strings.Join(goTypes, ", ") + ")"
	}

	gName := goIdent(fn.Name)
	if g.nestingLevel > 0 {
		// Nested FUNCTION: generate as Go closure
		g.writeLine(fmt.Sprintf("%s := func(%s) %s {", gName, params, returnTypeStr))
	} else {
		g.writeLine(fmt.Sprintf("func %s(%s) %s {", gName, params, returnTypeStr))
	}
	g.indent++
	g.nestingLevel++

	for _, stmt := range fn.Body {
		g.generateStatement(stmt)
	}

	if len(fn.ResultExprs) > 0 {
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write("return ")
		for i, expr := range fn.ResultExprs {
			if i > 0 {
				g.write(", ")
			}
			g.generateExpression(expr)
		}
		g.write("\n")
	}

	g.nestingLevel--
	g.indent--
	g.writeLine("}")
	g.writeLine("")
}
+
func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
	params := g.generateProcParams(fn.Params)

	// Build return type string
	var returnTypeStr string
	if len(fn.ReturnTypes) == 1 {
		returnTypeStr = g.occamTypeToGo(fn.ReturnTypes[0])
	} else {
		goTypes := make([]string, len(fn.ReturnTypes))
		for i, rt := range fn.ReturnTypes {
			goTypes[i] = g.occamTypeToGo(rt)
		}
		returnTypeStr = "(" + strings.Join(goTypes, ", ") + ")"
	}

	// Scope boolVars per function body
	oldBoolVars := g.boolVars
	newBoolVars := make(map[string]bool)
	if g.nestingLevel > 0 {
		for k, v := range oldBoolVars {
			newBoolVars[k] = v
		}
	}
	for _, p := range fn.Params {
		if p.Type == "BOOL" && !p.IsChan && !p.IsChanArray {
			newBoolVars[p.Name] = true
		} else {
			delete(newBoolVars, p.Name)
		}
	}
	g.boolVars = newBoolVars

	gName := goIdent(fn.Name)
	if g.nestingLevel > 0 {
		// Nested FUNCTION: generate as Go closure
		g.writeLine(fmt.Sprintf("%s := func(%s) %s {", gName, params, returnTypeStr))
	} else {
		g.writeLine(fmt.Sprintf("func %s(%s) %s {", gName, params, returnTypeStr))
	}
	g.indent++
	g.nestingLevel++

	for _, stmt := range fn.Body {
		g.generateStatement(stmt)
	}

	if len(fn.ResultExprs) > 0 {
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write("return ")
		for i, expr := range fn.ResultExprs {
			if i > 0 {
				g.write(", ")
			}
			g.generateExpression(expr)
		}
		g.write("\n")
	}

	g.nestingLevel--
	g.indent--
	g.writeLine("}")
	g.writeLine("")

	// Restore previous boolVars
	g.boolVars = oldBoolVars
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now let me verify the changes compile and run the tests:

$ Bash
Build the binary
go build -o occam2go .

Good, it compiles. Now let me run the verification steps from the plan:

$ Bash
Verify cast.occ transpiles and passes go vet (the bug reproduction)
./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
$ Bash
Verify 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
$ Bash
Run all tests
go test ./...
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	17.805s
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All tests pass. Let me mark the task complete.

TaskUpdate
{
  "taskId": "1",
  "status": "completed"
}
Updated task #1 status

Done. Here's a summary of the changes in codegen/codegen.go:

generateProcDecl — Added save/restore of g.boolVars alongside the existing refParams pattern: - Save oldBoolVars before the proc body - Create a fresh newBoolVars map (inheriting from parent if nested, for closure captures) - Register BOOL-typed params; delete non-BOOL params that shadow inherited names - Restore g.boolVars = oldBoolVars after body generation

generateFuncDecl — Same pattern: save before body, create scoped map with BOOL param registration, restore after body.

The pre-pass collectBoolVars is unchanged (still needed for containsBoolConversion to decide whether to emit the _boolToInt helper). The dynamic tracking in generateVarDecl (line 1084) naturally populates whichever scoped map is current.

Thank you. Please create a PR for this fix.

$ Bash
Show working tree status
git status
On branch fix/typed-abbreviation-type-loss
Your branch is up to date with 'origin/fix/typed-abbreviation-type-loss'.

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:   codegen/codegen.go

no changes added to commit (use "git add" and/or "git commit -a")
$ Bash
Show staged and unstaged changes
git diff
diff --git a/codegen/codegen.go b/codegen/codegen.go
index a60d2d5..0d2a53b 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -1928,11 +1928,17 @@ func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
 	// Track reference parameters for this procedure
 	oldRefParams := g.refParams
 	newRefParams := make(map[string]bool)
-	// Inherit parent's ref params for closure captures when nested
+	// Scope boolVars per proc body
+	oldBoolVars := g.boolVars
+	newBoolVars := make(map[string]bool)
+	// Inherit parent's ref params and boolVars for closure captures when nested
 	if g.nestingLevel > 0 {
 		for k, v := range oldRefParams {
 			newRefParams[k] = v
 		}
+		for k, v := range oldBoolVars {
+			newBoolVars[k] = v
+		}
 	}
 	for _, p := range proc.Params {
 		if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray && p.ArraySize == "" {
@@ -1941,6 +1947,12 @@ func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
 			// Own param shadows any inherited ref param with same name
 			delete(newRefParams, p.Name)
 		}
+		// Track BOOL params; delete non-BOOL params that shadow inherited names
+		if p.Type == "BOOL" && !p.IsChan && !p.IsChanArray {
+			newBoolVars[p.Name] = true
+		} else {
+			delete(newBoolVars, p.Name)
+		}
 		// Register chan params with protocol mappings
 		if p.IsChan || p.IsChanArray {
 			if _, ok := g.protocolDefs[p.ChanElemType]; ok {
@@ -1955,6 +1967,7 @@ func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
 		}
 	}
 	g.refParams = newRefParams
+	g.boolVars = newBoolVars
 
 	// Scan proc body for RETYPES declarations that shadow parameters.
 	// When VAL INT X RETYPES X :, Go can't redeclare X in the same scope,
@@ -2014,6 +2027,7 @@ func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
 
 	// Restore previous context
 	g.refParams = oldRefParams
+	g.boolVars = oldBoolVars
 	g.retypesRenames = oldRenames
 }
 
@@ -2115,6 +2129,23 @@ func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
 		returnTypeStr = "(" + strings.Join(goTypes, ", ") + ")"
 	}
 
+	// Scope boolVars per function body
+	oldBoolVars := g.boolVars
+	newBoolVars := make(map[string]bool)
+	if g.nestingLevel > 0 {
+		for k, v := range oldBoolVars {
+			newBoolVars[k] = v
+		}
+	}
+	for _, p := range fn.Params {
+		if p.Type == "BOOL" && !p.IsChan && !p.IsChanArray {
+			newBoolVars[p.Name] = true
+		} else {
+			delete(newBoolVars, p.Name)
+		}
+	}
+	g.boolVars = newBoolVars
+
 	gName := goIdent(fn.Name)
 	if g.nestingLevel > 0 {
 		// Nested FUNCTION: generate as Go closure
@@ -2145,6 +2176,9 @@ func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
 	g.indent--
 	g.writeLine("}")
 	g.writeLine("")
+
+	// Restore previous boolVars
+	g.boolVars = oldBoolVars
 }
 
 func (g *Generator) generateFuncCallExpr(call *ast.FuncCall) {
$ Bash
Show recent commits
git log --oneline -5
74b192a Fix typed abbreviations losing declared type in local scope
c1adc5f Merge pull request #54 from codeassociates/feat/round-trunc-qualifiers
1a6da77 Support ROUND/TRUNC qualifiers on type conversions
babbe52 Merge pull request #53 from codeassociates/fix/bool-numeric-type-conversions
0df6ec8 Fix BOOL↔BYTE/numeric type conversions
$ Bash
Check if branch tracks a remote
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null; echo "exit: $?"
origin/fix/typed-abbreviation-type-loss
exit: 0
Thinking

The branch is fix/typed-abbreviation-type-loss but this fix is about scoping boolVars. Let me check - the user said to create a PR. The current branch already has a commit about typed abbreviations. This new change is about boolVars scoping. Let me create a new branch for this, or should I add to this branch?

Wait, looking more carefully - the branch name is fix/typed-abbreviation-type-loss and it already has a commit 74b192a Fix typed abbreviations losing declared type in local scope. The current changes are about scoping boolVars. This seems like it should be a separate branch/PR since it's a different issue (#56).

I need to figure out whether the typed abbreviation fix is already on main. If it's only on this branch, I should create a new branch from main for the boolVars fix, then handle the typed abbreviation commit separately.

The current branch already has a different commit. Let me create a new branch from main for this fix.

$ Bash
Stash changes and create new branch from main
git stash && git checkout -b fix/scope-boolvars-per-proc main
Saved working directory and index state WIP on fix/typed-abbreviation-type-loss: 74b192a Fix typed abbreviations losing declared type in local scope
Switched to a new branch 'fix/scope-boolvars-per-proc'
$ Bash
Re-apply stashed changes
git stash pop
Auto-merging codegen/codegen.go
On branch fix/scope-boolvars-per-proc
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:   codegen/codegen.go

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (6dfdc72295ede95e14da241ba83fbbdaf46dfcda)
$ Bash
Commit the fix
git add codegen/codegen.go && git commit -m "$(cat <<'EOF'
Scope boolVars per proc/function body to prevent cross-proc leakage

The boolVars map was flat and global, so a BOOL variable declared in one
proc (e.g. in an #INCLUDEd module) would cause type conversions like
INT16 b (where b is a BYTE) in a different proc to incorrectly emit
_boolToInt(b) instead of int16(b). Fix by saving/restoring boolVars in
generateProcDecl and generateFuncDecl, following the existing refParams
scoping pattern. Nested procs/funcs inherit parent scope for closures.

Fixes #56

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
$ Bash
Push branch to remote
git push -u origin fix/scope-boolvars-per-proc
remote: 
remote: Create a pull request for 'fix/scope-boolvars-per-proc' on GitHub by visiting:        
remote:      https://github.com/codeassociates/occam2go/pull/new/fix/scope-boolvars-per-proc        
remote: 
To associate-1.github.com:codeassociates/occam2go.git
 * [new branch]      fix/scope-boolvars-per-proc -> fix/scope-boolvars-per-proc
branch 'fix/scope-boolvars-per-proc' set up to track 'origin/fix/scope-boolvars-per-proc'.
$ Bash
Create the pull request
gh pr create --title "Fix: scope boolVars per proc/function body" --body "$(cat <<'EOF'
## Summary

- Scopes the `boolVars` map per proc/function body using save/restore, following the existing `refParams` pattern
- Prevents a `BOOL` variable declared in one proc (e.g. in an `#INCLUDE`d module) from causing incorrect `_boolToInt()` wrapping in type conversions in a different proc
- Nested procs/funcs inherit parent `boolVars` for closure captures; top-level procs start fresh

Fixes #56

## Test plan

- [x] `cast.occ` transpiles and passes `go vet` (previously failed with `_boolToInt` on a `BYTE` variable)
- [x] Full course module transpiles and passes `go vet`
- [x] All existing tests pass (`go test ./...`)

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

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

I've approved the PR. Please merge.

$ Bash
Merge PR #58
gh pr merge 58 --merge

PR #58 merged.

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