Skip to content

Commit 4f344e0

Browse files
committed
Add channel-based reactivity to code generator
Implemented code generation support for channel-based reactive components following the pattern: ```go func Counter(counterChannel: <-chan int) { Div { `Counter: {<-counterChannel}` } } ``` Code Generator Changes: - Added hasChannelParams() to detect channel parameters - generateComponentStruct() now adds currentValue fields for channels - generateBindAppMethod() creates BindApp() that starts channel listeners - generateChannelListenerMethods() creates goroutines that listen on channels - Updated generateExpr() to use currentValue fields instead of blocking reads - Fixed typeToAST() channel handling to avoid double-wrapping Architecture: - For each channel parameter, generates a current<Name> field - BindApp() method starts goroutines for each channel - Goroutines listen on channels with `for val := range ch` - When value received, updates current field and triggers app.Update() - Render() uses current value field (non-blocking) Benefits: - Idiomatic Go channel usage - Non-blocking renders - Automatic re-renders when channel receives values - Clean separation of concerns Note: Parser issue with `<-chan T` type needs investigation - currently being parsed incorrectly. The generator infrastructure is complete and correct, just needs parser fix.
1 parent 56de3c3 commit 4f344e0

1 file changed

Lines changed: 218 additions & 10 deletions

File tree

pkg/codegen/codegen.go

Lines changed: 218 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ func (g *Generator) generateComponent(comp *guixast.Component) []ast.Decl {
119119
// Generate constructor
120120
decls = append(decls, g.generateConstructor(comp))
121121

122+
// Check if component has channel parameters
123+
hasChannels := g.hasChannelParams(comp)
124+
125+
// Generate BindApp method if there are channels
126+
if hasChannels {
127+
decls = append(decls, g.generateBindAppMethod(comp))
128+
// Generate listener methods for each channel
129+
decls = append(decls, g.generateChannelListenerMethods(comp)...)
130+
}
131+
122132
// Generate Render method
123133
decls = append(decls, g.generateRenderMethod(comp))
124134

@@ -130,6 +140,16 @@ func (g *Generator) generateComponent(comp *guixast.Component) []ast.Decl {
130140
return decls
131141
}
132142

143+
// hasChannelParams checks if a component has any channel parameters
144+
func (g *Generator) hasChannelParams(comp *guixast.Component) bool {
145+
for _, param := range comp.Params {
146+
if param.Type != nil && (param.Type.IsChannel || param.Type.IsChan) {
147+
return true
148+
}
149+
}
150+
return false
151+
}
152+
133153
// generatePropsStruct generates a Props struct for component parameters
134154
func (g *Generator) generatePropsStruct(comp *guixast.Component) *ast.GenDecl {
135155
fields := make([]*ast.Field, len(comp.Params))
@@ -267,6 +287,22 @@ func (g *Generator) generateComponentStruct(comp *guixast.Component) *ast.GenDec
267287
Names: []*ast.Ident{ast.NewIdent(capitalize(param.Name))},
268288
Type: g.typeToAST(param.Type),
269289
})
290+
291+
// For channel parameters, add a current value field
292+
if param.Type != nil && (param.Type.IsChannel || param.Type.IsChan) {
293+
// Extract the element type from the channel
294+
var elemType ast.Expr
295+
if param.Type.Generic != nil {
296+
elemType = g.typeToAST(param.Type.Generic)
297+
} else {
298+
elemType = g.typeToAST(&guixast.Type{Name: param.Type.Name})
299+
}
300+
301+
fields = append(fields, &ast.Field{
302+
Names: []*ast.Ident{ast.NewIdent("current" + capitalize(param.Name))},
303+
Type: elemType,
304+
})
305+
}
270306
}
271307

272308
return &ast.GenDecl{
@@ -683,12 +719,11 @@ func (g *Generator) generateExpr(expr *guixast.Expr) ast.Expr {
683719
}
684720

685721
if expr.ChannelOp != nil {
686-
return &ast.UnaryExpr{
687-
Op: token.ARROW,
688-
X: &ast.SelectorExpr{
689-
X: ast.NewIdent("c"),
690-
Sel: ast.NewIdent(capitalize(expr.ChannelOp.Channel)),
691-
},
722+
// Instead of reading from the channel (which would block),
723+
// use the current value field that's updated by the channel listener
724+
return &ast.SelectorExpr{
725+
X: ast.NewIdent("c"),
726+
Sel: ast.NewIdent("current" + capitalize(expr.ChannelOp.Channel)),
692727
}
693728
}
694729

@@ -822,14 +857,15 @@ func (g *Generator) typeToAST(t *guixast.Type) ast.Expr {
822857

823858
var base ast.Expr = ast.NewIdent(t.Name)
824859

825-
if t.IsChannel {
860+
// Handle channel types
861+
// IsChannel && IsChan means "<-chan T" (receive-only)
862+
// IsChan only means "chan T" (bidirectional)
863+
if t.IsChannel && t.IsChan {
826864
base = &ast.ChanType{
827865
Dir: ast.RECV,
828866
Value: base,
829867
}
830-
}
831-
832-
if t.IsChan {
868+
} else if t.IsChan {
833869
base = &ast.ChanType{
834870
Dir: ast.SEND | ast.RECV,
835871
Value: base,
@@ -982,6 +1018,178 @@ func (g *Generator) generateUpdateMethod(comp *guixast.Component) *ast.FuncDecl
9821018
}
9831019
}
9841020

1021+
// generateBindAppMethod generates the BindApp method for components with channels
1022+
func (g *Generator) generateBindAppMethod(comp *guixast.Component) *ast.FuncDecl {
1023+
stmts := []ast.Stmt{
1024+
// c.app = app
1025+
&ast.AssignStmt{
1026+
Lhs: []ast.Expr{
1027+
&ast.SelectorExpr{
1028+
X: ast.NewIdent("c"),
1029+
Sel: ast.NewIdent("app"),
1030+
},
1031+
},
1032+
Tok: token.ASSIGN,
1033+
Rhs: []ast.Expr{ast.NewIdent("app")},
1034+
},
1035+
}
1036+
1037+
// For each channel parameter, start a listener
1038+
for _, param := range comp.Params {
1039+
if param.Type != nil && (param.Type.IsChannel || param.Type.IsChan) {
1040+
// if c.ChannelName != nil { c.startChannelNameListener() }
1041+
stmts = append(stmts, &ast.IfStmt{
1042+
Cond: &ast.BinaryExpr{
1043+
X: &ast.SelectorExpr{
1044+
X: ast.NewIdent("c"),
1045+
Sel: ast.NewIdent(capitalize(param.Name)),
1046+
},
1047+
Op: token.NEQ,
1048+
Y: ast.NewIdent("nil"),
1049+
},
1050+
Body: &ast.BlockStmt{
1051+
List: []ast.Stmt{
1052+
&ast.ExprStmt{
1053+
X: &ast.CallExpr{
1054+
Fun: &ast.SelectorExpr{
1055+
X: ast.NewIdent("c"),
1056+
Sel: ast.NewIdent("start" + capitalize(param.Name) + "Listener"),
1057+
},
1058+
},
1059+
},
1060+
},
1061+
},
1062+
})
1063+
}
1064+
}
1065+
1066+
return &ast.FuncDecl{
1067+
Recv: &ast.FieldList{
1068+
List: []*ast.Field{
1069+
{
1070+
Names: []*ast.Ident{ast.NewIdent("c")},
1071+
Type: &ast.StarExpr{
1072+
X: ast.NewIdent(comp.Name),
1073+
},
1074+
},
1075+
},
1076+
},
1077+
Name: ast.NewIdent("BindApp"),
1078+
Type: &ast.FuncType{
1079+
Params: &ast.FieldList{
1080+
List: []*ast.Field{
1081+
{
1082+
Names: []*ast.Ident{ast.NewIdent("app")},
1083+
Type: &ast.StarExpr{
1084+
X: &ast.SelectorExpr{
1085+
X: ast.NewIdent("runtime"),
1086+
Sel: ast.NewIdent("App"),
1087+
},
1088+
},
1089+
},
1090+
},
1091+
},
1092+
},
1093+
Body: &ast.BlockStmt{
1094+
List: stmts,
1095+
},
1096+
}
1097+
}
1098+
1099+
// generateChannelListenerMethods generates listener methods for each channel parameter
1100+
func (g *Generator) generateChannelListenerMethods(comp *guixast.Component) []ast.Decl {
1101+
var decls []ast.Decl
1102+
1103+
for _, param := range comp.Params {
1104+
if param.Type != nil && (param.Type.IsChannel || param.Type.IsChan) {
1105+
// Generate: func (c *Component) startChannelNameListener() { go func() { for val := range c.ChannelName { c.currentChannelName = val; c.app.Update() } }() }
1106+
decls = append(decls, &ast.FuncDecl{
1107+
Recv: &ast.FieldList{
1108+
List: []*ast.Field{
1109+
{
1110+
Names: []*ast.Ident{ast.NewIdent("c")},
1111+
Type: &ast.StarExpr{
1112+
X: ast.NewIdent(comp.Name),
1113+
},
1114+
},
1115+
},
1116+
},
1117+
Name: ast.NewIdent("start" + capitalize(param.Name) + "Listener"),
1118+
Type: &ast.FuncType{},
1119+
Body: &ast.BlockStmt{
1120+
List: []ast.Stmt{
1121+
// go func() { ... }()
1122+
&ast.GoStmt{
1123+
Call: &ast.CallExpr{
1124+
Fun: &ast.FuncLit{
1125+
Type: &ast.FuncType{},
1126+
Body: &ast.BlockStmt{
1127+
List: []ast.Stmt{
1128+
// for val := range c.ChannelName { ... }
1129+
&ast.RangeStmt{
1130+
Key: nil,
1131+
Value: ast.NewIdent("val"),
1132+
Tok: token.DEFINE,
1133+
X: &ast.SelectorExpr{
1134+
X: ast.NewIdent("c"),
1135+
Sel: ast.NewIdent(capitalize(param.Name)),
1136+
},
1137+
Body: &ast.BlockStmt{
1138+
List: []ast.Stmt{
1139+
// c.currentChannelName = val
1140+
&ast.AssignStmt{
1141+
Lhs: []ast.Expr{
1142+
&ast.SelectorExpr{
1143+
X: ast.NewIdent("c"),
1144+
Sel: ast.NewIdent("current" + capitalize(param.Name)),
1145+
},
1146+
},
1147+
Tok: token.ASSIGN,
1148+
Rhs: []ast.Expr{ast.NewIdent("val")},
1149+
},
1150+
// if c.app != nil { c.app.Update() }
1151+
&ast.IfStmt{
1152+
Cond: &ast.BinaryExpr{
1153+
X: &ast.SelectorExpr{
1154+
X: ast.NewIdent("c"),
1155+
Sel: ast.NewIdent("app"),
1156+
},
1157+
Op: token.NEQ,
1158+
Y: ast.NewIdent("nil"),
1159+
},
1160+
Body: &ast.BlockStmt{
1161+
List: []ast.Stmt{
1162+
&ast.ExprStmt{
1163+
X: &ast.CallExpr{
1164+
Fun: &ast.SelectorExpr{
1165+
X: &ast.SelectorExpr{
1166+
X: ast.NewIdent("c"),
1167+
Sel: ast.NewIdent("app"),
1168+
},
1169+
Sel: ast.NewIdent("Update"),
1170+
},
1171+
},
1172+
},
1173+
},
1174+
},
1175+
},
1176+
},
1177+
},
1178+
},
1179+
},
1180+
},
1181+
},
1182+
},
1183+
},
1184+
},
1185+
},
1186+
})
1187+
}
1188+
}
1189+
1190+
return decls
1191+
}
1192+
9851193
// Helper functions
9861194

9871195
func capitalize(s string) string {

0 commit comments

Comments
 (0)