Skip to content

Commit e4feff4

Browse files
committed
Recur for loops
Signed-off-by: James Hamlin <jfhamlin@gmail.com>
1 parent 801a74a commit e4feff4

File tree

4 files changed

+117
-28
lines changed

4 files changed

+117
-28
lines changed

pkg/codegen/codegen.go

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@ type varScope struct {
1818
names map[string]string // maps Clojure names to Go variable names
1919
}
2020

21+
// recurContext represents the context for a loop/recur form
22+
type recurContext struct {
23+
loopID *lang.Symbol // The loop ID to match recur with its loop
24+
bindings []string // Go variable names for loop bindings (in order)
25+
}
26+
2127
// Generator handles the conversion of AST nodes to Go code
2228
type Generator struct {
2329
originalWriter io.Writer
2430
w io.Writer
25-
varScopes []varScope // stack of variable scopes
31+
varScopes []varScope // stack of variable scopes
32+
recurStack []recurContext // stack of recur contexts for nested loops
2633
}
2734

2835
// New creates a new code generator
@@ -31,6 +38,7 @@ func New(w io.Writer) *Generator {
3138
originalWriter: w,
3239
w: w,
3340
varScopes: []varScope{{nextNum: 0, names: make(map[string]string)}},
41+
recurStack: []recurContext{},
3442
}
3543
}
3644

@@ -259,7 +267,7 @@ func (g *Generator) generateFn(fn *runtime.Fn) string {
259267
g.writef(" }\n")
260268

261269
// Generate method body
262-
g.generateFnMethod(methodNode, "args", 0)
270+
g.generateFnMethod(methodNode, "args")
263271

264272
g.writef("})\n")
265273
} else {
@@ -277,7 +285,7 @@ func (g *Generator) generateFn(fn *runtime.Fn) string {
277285
}
278286

279287
g.writef(" case %d:\n", methodNode.FixedArity)
280-
g.generateFnMethod(methodNode, "args", 1)
288+
g.generateFnMethod(methodNode, "args")
281289
}
282290

283291
// Generate default case for variadic method
@@ -287,7 +295,7 @@ func (g *Generator) generateFn(fn *runtime.Fn) string {
287295
g.writef(" if len(args) < %d {\n", variadicMethodNode.FixedArity)
288296
g.writef(" panic(lang.NewIllegalArgumentError(\"wrong number of arguments (\" + fmt.Sprint(len(args)) + \")\"))\n")
289297
g.writef(" }\n")
290-
g.generateFnMethod(variadicMethodNode, "args", 1)
298+
g.generateFnMethod(variadicMethodNode, "args")
291299
} else {
292300
// No variadic method - error on any other arity
293301
g.writef(" default:\n")
@@ -311,17 +319,12 @@ func (g *Generator) generateFn(fn *runtime.Fn) string {
311319
}
312320

313321
// generateFnMethod generates the body of a function method
314-
func (g *Generator) generateFnMethod(methodNode *ast.FnMethodNode, argsVar string, indentLevel int) {
315-
indent := strings.Repeat(" ", indentLevel)
316-
322+
func (g *Generator) generateFnMethod(methodNode *ast.FnMethodNode, argsVar string) {
317323
// Push a new scope for the method body
318324
g.pushVarScope()
319325
defer g.popVarScope()
320326

321-
// TODO: Handle recur with a label when we implement recur
322-
// if methodNode.LoopID != nil {
323-
// g.writef("%sRecur_%s:\n", indent, mungeID(methodNode.LoopID.Name()))
324-
// }
327+
// TODO: Handle recur with a label
325328

326329
// Bind parameters
327330
for i, param := range methodNode.Params {
@@ -330,16 +333,16 @@ func (g *Generator) generateFnMethod(methodNode *ast.FnMethodNode, argsVar strin
330333

331334
if i < methodNode.FixedArity {
332335
// Regular parameter
333-
g.writef("%s %s := %s[%d]\n", indent, paramVar, argsVar, i)
336+
g.writef("%s := %s[%d]\n", paramVar, argsVar, i)
334337
} else {
335338
// Variadic parameter - collect rest args
336-
g.writef("%s %s := lang.NewList(%s[%d:]...)\n", indent, paramVar, argsVar, methodNode.FixedArity)
339+
g.writef("%s := lang.NewList(%s[%d:]...)\n", paramVar, argsVar, methodNode.FixedArity)
337340
}
338341
}
339342

340343
// Generate the body
341344
bodyVar := g.generateASTNode(methodNode.Body)
342-
g.writef("%s return %s\n", indent, bodyVar)
345+
g.writef("return %s\n", bodyVar)
343346
}
344347

345348
// generateASTNode generates code for an AST node
@@ -354,6 +357,8 @@ func (g *Generator) generateASTNode(node *ast.Node) string {
354357
return g.allocateVar(localNode.Name.Name())
355358
case ast.OpDo:
356359
return g.generateDo(node)
360+
case ast.OpLet:
361+
return g.generateLet(node, false)
357362
case ast.OpLoop:
358363
return g.generateLet(node, true)
359364
case ast.OpIf:
@@ -451,11 +456,11 @@ func (g *Generator) generateIf(node *ast.Node) string {
451456
testExpr := g.generateASTNode(ifNode.Test)
452457
g.writef("if lang.IsTruthy(%s) {\n", testExpr)
453458
thenExpr := g.generateASTNode(ifNode.Then)
454-
g.writef(" %s = %s\n", resultVar, thenExpr)
459+
g.writeAssign(resultVar, thenExpr)
455460
g.writef("} else {\n")
456461
if ifNode.Else != nil {
457462
elsExpr := g.generateASTNode(ifNode.Else)
458-
g.writef(" %s = %s\n", resultVar, elsExpr)
463+
g.writeAssign(resultVar, elsExpr)
459464
} else {
460465
g.writef(" %s = nil\n", resultVar)
461466
}
@@ -523,6 +528,12 @@ func (g *Generator) generateLet(node *ast.Node, isLoop bool) string {
523528
g.pushVarScope()
524529
defer g.popVarScope()
525530

531+
// Collect binding variable names for recur context if this is a loop
532+
var bindingVars []string
533+
if isLoop {
534+
bindingVars = make([]string, 0, len(letNode.Bindings))
535+
}
536+
526537
// Emit bindings directly to g.w
527538
for _, binding := range letNode.Bindings {
528539
bindingNode := binding.Sub.(*ast.BindingNode)
@@ -534,19 +545,34 @@ func (g *Generator) generateLet(node *ast.Node, isLoop bool) string {
534545

535546
// Generate initialization code
536547
initCode := g.generateASTNode(init)
537-
g.writef("%s := %s\n", varName, initCode)
548+
g.writef("var %s any = %s\n", varName, initCode)
549+
550+
// Collect binding variables for loop
551+
if isLoop {
552+
bindingVars = append(bindingVars, varName)
553+
}
538554
}
539555

540556
resultId := g.allocateVar("letResult")
541557
if isLoop {
558+
// Push recur context for this loop
559+
g.recurStack = append(g.recurStack, recurContext{
560+
loopID: letNode.LoopID,
561+
bindings: bindingVars,
562+
})
563+
defer func() {
564+
// Pop recur context when done
565+
g.recurStack = g.recurStack[:len(g.recurStack)-1]
566+
}()
567+
542568
g.writef("var %s any\n", resultId)
543569
g.writef("for {\n")
544570
}
545571

546572
// Return the body expression (r-value)
547573
result := g.generateASTNode(letNode.Body)
548574
if isLoop {
549-
g.writef(" %s = %s\n", resultId, result)
575+
g.writeAssign(resultId, result)
550576
g.writef(" break\n") // Break out of the loop after the body
551577
g.writef("}\n")
552578
return resultId
@@ -558,14 +584,45 @@ func (g *Generator) generateLet(node *ast.Node, isLoop bool) string {
558584
func (g *Generator) generateRecur(node *ast.Node) string {
559585
recurNode := node.Sub.(*ast.RecurNode)
560586

561-
exprs := recurNode.Exprs
562-
for _, expr := range exprs {
563-
val, err := noRecurEnv.EvalAST(expr)
564-
if err != nil {
565-
return nil, err
587+
// Find the matching recur context
588+
var ctx *recurContext
589+
for i := len(g.recurStack) - 1; i >= 0; i-- {
590+
if lang.Equals(g.recurStack[i].loopID, recurNode.LoopID) {
591+
ctx = &g.recurStack[i]
592+
break
566593
}
567-
vals = append(vals, val)
568594
}
595+
596+
if ctx == nil {
597+
panic(fmt.Sprintf("recur without matching loop for ID: %v", recurNode.LoopID))
598+
}
599+
600+
// Verify the number of recur expressions matches the number of loop bindings
601+
if len(recurNode.Exprs) != len(ctx.bindings) {
602+
panic(fmt.Sprintf("recur expects %d arguments, got %d", len(ctx.bindings), len(recurNode.Exprs)))
603+
}
604+
605+
// Generate temporary variables to hold the new values
606+
// This prevents issues with bindings that reference each other
607+
tempVars := make([]string, len(recurNode.Exprs))
608+
for i, expr := range recurNode.Exprs {
609+
tempVar := g.allocateVar(fmt.Sprintf("recurTemp%d", i))
610+
tempVars[i] = tempVar
611+
exprCode := g.generateASTNode(expr)
612+
g.writef("var %s any = %s\n", tempVar, exprCode)
613+
}
614+
615+
// Assign the temporary values to the loop bindings
616+
for i, bindingVar := range ctx.bindings {
617+
g.writef("%s = %s\n", bindingVar, tempVars[i])
618+
}
619+
620+
// Continue the loop
621+
g.writef("continue\n")
622+
623+
// Return empty string since recur doesn't produce a value
624+
// (control flow never reaches past the continue)
625+
return ""
569626
}
570627

571628
////////////////////////////////////////////////////////////////////////////////
@@ -594,6 +651,14 @@ func (g *Generator) writef(format string, args ...any) error {
594651
return err
595652
}
596653

654+
// writeAssign writes an assignment iff the r-value string is non-empty
655+
func (g *Generator) writeAssign(varName, rValue string) {
656+
if rValue == "" {
657+
return
658+
}
659+
g.writef("%s = %s\n", varName, rValue)
660+
}
661+
597662
////////////////////////////////////////////////////////////////////////////////
598663
// Variable Scope Management
599664

pkg/codegen/testdata/codegen/test/loop_simple.glj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
(defn simple-loop []
44
(loop [i 0]
55
(if (< i 10)
6-
(recur (inc i)))))
6+
(recur (inc i))
7+
i)))

pkg/codegen/testdata/codegen/test/loop_simple.go

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
_ "github.com/glojurelang/glojure/pkg/codegen/testdata/codegen/test"
7+
"github.com/glojurelang/glojure/pkg/glj"
8+
)
9+
10+
func main() {
11+
run := glj.Var("codegen.test.loop-simple", "simple-loop")
12+
result := run.Invoke()
13+
fmt.Printf("%v (%T)\n", result, result)
14+
}

0 commit comments

Comments
 (0)