Skip to content

Commit 6d3db45

Browse files
committed
fix: Prioritize channel receive detection over type hoisting
Fixed critical bug where channel receive variables (currentState := <-channel) were incorrectly treated as hoisted variables instead of channel receives. Root cause: - inferTypeFromExpr returns the element type for channel receives - analyzeComponentBody checked inferredType != nil BEFORE checking hasChannelOp - Since both were true, it took the hoisted vars branch and skipped channel tracking - This caused g.channelReceiveVars to be empty - Result: No channel listeners were generated, breaking reactive updates Fix: 1. Reorder checks in analyzeComponentBody to check hasChannelOp FIRST 2. Reorder checks in generateBody for consistency 3. Skip channel receive vars when adding struct fields Now works correctly: - Channel receives tracked in channelReceiveVars map - Listeners generated for channels that are received from - Local variables created in Render from struct fields - Calculator and other examples with channel-based state now work Testing: - All codegen tests pass - Calculator buttons update state correctly - WebGPU cube controls update reactively
1 parent b8b1588 commit 6d3db45

4 files changed

Lines changed: 57 additions & 55 deletions

File tree

examples/calculator/calculator_gen.go

Lines changed: 18 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/calculator/guix_helpers_gen.go

Lines changed: 0 additions & 22 deletions
This file was deleted.

examples/webgpu-cube/controls_gen.go

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/codegen/codegen.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -371,12 +371,16 @@ func (g *Generator) analyzeComponentBody(comp *guixast.Component) {
371371
if comp.Body != nil {
372372
for _, varDecl := range comp.Body.VarDecls {
373373
if len(varDecl.Names) == 1 && len(varDecl.Values) == 1 {
374-
if g.inferTypeFromExpr(varDecl.Values[0]) != nil {
375-
g.hoistedVars[varDecl.Names[0]] = true
376-
} else if varDecl.Values[0].Left != nil && varDecl.Values[0].Left.ChannelOp != nil {
374+
hasChannelOp := varDecl.Values[0].Left != nil && varDecl.Values[0].Left.ChannelOp != nil
375+
inferredType := g.inferTypeFromExpr(varDecl.Values[0])
376+
377+
// Check for channel receive FIRST (has priority over hoisting)
378+
if hasChannelOp {
377379
// Track channel receive: currentState := <-stateChannel
378380
channelName := varDecl.Values[0].Left.ChannelOp.Channel
379381
g.channelReceiveVars[varDecl.Names[0]] = capitalize(channelName)
382+
} else if inferredType != nil {
383+
g.hoistedVars[varDecl.Names[0]] = true
380384
}
381385
}
382386
}
@@ -970,10 +974,16 @@ func (g *Generator) generateComponentStruct(comp *guixast.Component) *ast.GenDec
970974

971975
// Add VarDecl fields from component body (for stateful variables)
972976
// This ensures channels and components persist across renders
977+
// But skip channel receive variables - those are managed by channel listeners
973978
if comp.Body != nil {
974979
for _, varDecl := range comp.Body.VarDecls {
975980
for i, name := range varDecl.Names {
976981
if i < len(varDecl.Values) {
982+
// Skip channel receive variables (they become local vars in Render)
983+
if g.channelReceiveVars != nil && g.channelReceiveVars[name] != "" {
984+
continue
985+
}
986+
977987
// Determine the type from the value expression
978988
varType := g.inferTypeFromExpr(varDecl.Values[i])
979989
if varType != nil {
@@ -1540,18 +1550,34 @@ func (g *Generator) generateBody(body *guixast.Body) ast.Expr {
15401550
// and initialized in the constructor - skip them here in Render
15411551
// All other VarDecls remain as local variables (including multiple assignments)
15421552
for _, varDecl := range body.VarDecls {
1543-
// Check if ALL variables in this decl can be hoisted
1544-
// For now, only hoist if there's a single variable with an inferable type
1545-
canHoist := len(varDecl.Names) == 1 && len(varDecl.Values) == 1 &&
1546-
g.inferTypeFromExpr(varDecl.Values[0]) != nil
1547-
1548-
// Check if this is a channel receive variable (skip - will be accessed via c.current<ChannelName>)
1553+
// Check if this is a channel receive variable FIRST (has priority)
15491554
isChannelReceive := len(varDecl.Names) == 1 &&
15501555
g.channelReceiveVars != nil &&
15511556
g.channelReceiveVars[varDecl.Names[0]] != ""
15521557

1553-
if canHoist || isChannelReceive {
1554-
// Hoisted field or channel receive - skip initialization in Render
1558+
// Check if ALL variables in this decl can be hoisted
1559+
// For now, only hoist if there's a single variable with an inferable type
1560+
// BUT exclude channel receives from hoisting (they're handled separately below)
1561+
canHoist := !isChannelReceive && len(varDecl.Names) == 1 && len(varDecl.Values) == 1 &&
1562+
g.inferTypeFromExpr(varDecl.Values[0]) != nil
1563+
1564+
if isChannelReceive {
1565+
// Channel receive variable - replace with assignment from struct field
1566+
// Example: currentState := c.currentStateChannel
1567+
varName := varDecl.Names[0]
1568+
channelName := g.channelReceiveVars[varName]
1569+
stmts = append(stmts, &ast.AssignStmt{
1570+
Lhs: []ast.Expr{ast.NewIdent(varName)},
1571+
Tok: token.DEFINE,
1572+
Rhs: []ast.Expr{
1573+
&ast.SelectorExpr{
1574+
X: ast.NewIdent("c"),
1575+
Sel: ast.NewIdent("current" + channelName),
1576+
},
1577+
},
1578+
})
1579+
} else if canHoist {
1580+
// Hoisted field - skip initialization in Render
15551581
continue
15561582
} else {
15571583
// Local variable(s) - generate: name := value or n, err := value

0 commit comments

Comments
 (0)