diff --git a/codegen/codegen.go b/codegen/codegen.go index e5cde52..0c63b4f 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -1878,16 +1878,12 @@ func (g *Generator) generateSeqBlock(seq *ast.SeqBlock) { g.write(fmt.Sprintf("; %s++ {\n", v)) g.indent++ } - for _, stmt := range seq.Statements { - g.generateStatement(stmt) - } + g.generateStatementsWithScoping(seq.Statements) g.indent-- g.writeLine("}") } else { // SEQ just becomes sequential Go code (Go's default) - for _, stmt := range seq.Statements { - g.generateStatement(stmt) - } + g.generateStatementsWithScoping(seq.Statements) } } @@ -2306,9 +2302,7 @@ func (g *Generator) generateProcDecl(proc *ast.ProcDecl) { oldSigs := make(map[string][]ast.ProcParam) g.collectNestedProcSigsScoped(proc.Body, oldSigs) - for _, stmt := range proc.Body { - g.generateStatement(stmt) - } + g.generateStatementsWithScoping(proc.Body) // Restore overwritten signatures for name, params := range oldSigs { @@ -2455,9 +2449,7 @@ func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) { g.indent++ g.nestingLevel++ - for _, stmt := range fn.Body { - g.generateStatement(stmt) - } + g.generateStatementsWithScoping(fn.Body) if len(fn.ResultExprs) > 0 { g.builder.WriteString(strings.Repeat("\t", g.indent)) @@ -3389,3 +3381,71 @@ func (g *Generator) emitIntrinsicHelpers() { g.writeLine("}") g.writeLine("") } + +// declaredNames returns the variable names introduced by a declaration statement. +// For non-replicated SEQ blocks (which are transparent in Go — no { } scope), +// it recursively collects names from child statements so that the parent scope +// can detect cross-block redeclarations. +func declaredNames(stmt ast.Statement) []string { + switch s := stmt.(type) { + case *ast.VarDecl: + return s.Names + case *ast.ArrayDecl: + return s.Names + case *ast.ChanDecl: + return s.Names + case *ast.TimerDecl: + return s.Names + case *ast.Abbreviation: + return []string{s.Name} + case *ast.RetypesDecl: + return []string{s.Name} + case *ast.SeqBlock: + if s.Replicator == nil { + var names []string + for _, child := range s.Statements { + names = append(names, declaredNames(child)...) + } + return names + } + return nil + default: + return nil + } +} + +// generateStatementsWithScoping emits statements, opening new Go { } scope +// blocks when a variable name is redeclared. This mirrors occam's scoping +// where each declaration starts a new scope that extends to the end of its +// enclosing block. +func (g *Generator) generateStatementsWithScoping(stmts []ast.Statement) { + declared := make(map[string]bool) + bracesOpened := 0 + + for _, stmt := range stmts { + names := declaredNames(stmt) + needScope := false + for _, n := range names { + if declared[n] { + needScope = true + break + } + } + if needScope { + g.writeLine("{") + g.indent++ + bracesOpened++ + declared = make(map[string]bool) + } + for _, n := range names { + declared[n] = true + } + g.generateStatement(stmt) + } + + for bracesOpened > 0 { + g.indent-- + g.writeLine("}") + bracesOpened-- + } +} diff --git a/codegen/e2e_basic_test.go b/codegen/e2e_basic_test.go index d039967..59dd09b 100644 --- a/codegen/e2e_basic_test.go +++ b/codegen/e2e_basic_test.go @@ -146,3 +146,27 @@ func TestE2E_InitialDecl(t *testing.T) { t.Errorf("expected %q, got %q", expected, output) } } + +func TestE2E_VarRedeclInSiblingSeqs(t *testing.T) { + // Variable 'x' is declared in two separate SEQ blocks within the same + // parent SEQ. Without scoping braces, Go reports "x redeclared in this block". + occam := `SEQ + SEQ + INT x: + x := 10 + print.int(x) + SEQ + INT x: + x := 20 + print.int(x) + SEQ + INT x: + x := 30 + print.int(x) +` + output := transpileCompileRun(t, occam) + expected := "10\n20\n30\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +}