Skip to content

Commit 0e1526a

Browse files
committed
Fix implicit return from if/else and return inside try/or
if/else blocks as the last expression in a function or lambda now correctly return the branch value instead of nil: def classify(x) if x > 10 "big" else "small" end end classify(20) # => "big" (was nil) return statements inside try/or error handlers no longer cause a compile error. They now set the handler result value: matches = try find_stuff(input) or err return nil # sets matches = nil (previously failed to compile) end
1 parent 96d9d50 commit 0e1526a

4 files changed

Lines changed: 201 additions & 40 deletions

File tree

compiler/codegen.go

Lines changed: 144 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type codeGen struct {
5252
currentFunc *ast.FuncDef // current function being generated (for type lookups)
5353
varTypeScope string // override scope key for varType lookups (test/bench blocks)
5454
inSpawn int // nesting depth of spawn blocks (>0 means inside spawn)
55+
inTryHandler int // nesting depth of try/or handler defers (>0 means inside handler)
5556
lambdaDepth int // nesting depth of lambda bodies (>0 means inside fn)
5657
lambdaScopeBase []int // scope index at each lambda entry (stack)
5758
lambdaOuterFunc []*ast.FuncDef // enclosing function at each lambda entry (stack)
@@ -978,16 +979,14 @@ func (g *codeGen) writeFunc(f *ast.FuncDef) error {
978979
g.inFunc = true
979980
hasImplicitReturn := false
980981
for i, s := range f.Body {
981-
// Implicit return: last expression in function body becomes the return value.
982+
// Implicit return: last expression or if/else in function body becomes the return value.
982983
if i == len(f.Body)-1 {
983-
if es, ok := s.(*ast.ExprStmt); ok {
984-
g.emitLineDirective(es.SourceLine)
985-
expr, err := g.exprString(es.Expression)
986-
if err != nil {
987-
return err
988-
}
989-
g.writef("return %s\n", expr)
990-
hasImplicitReturn = true
984+
handled, allCovered, err := g.writeLastStmtAs(s, "return %s\n")
985+
if err != nil {
986+
return err
987+
}
988+
if handled {
989+
hasImplicitReturn = allCovered
991990
continue
992991
}
993992
}
@@ -1220,6 +1219,51 @@ func (g *codeGen) writeExprStmt(e *ast.ExprStmt) error {
12201219
func (g *codeGen) writeIf(i *ast.IfStmt) error {
12211220
// Pre-declare variables assigned in any branch so they're visible
12221221
// after the if block (Ruby-like scoping: if/else doesn't create a new scope).
1222+
g.predeclareIfVars(i)
1223+
1224+
cond, err := g.exprString(i.Condition)
1225+
if err != nil {
1226+
return err
1227+
}
1228+
g.writef("if %s {\n", g.condExpr(cond, i.Condition))
1229+
g.indent++
1230+
for _, s := range i.Body {
1231+
if err := g.writeStmt(s); err != nil {
1232+
return err
1233+
}
1234+
}
1235+
g.indent--
1236+
for _, ec := range i.ElsifClauses {
1237+
cond, err := g.exprString(ec.Condition)
1238+
if err != nil {
1239+
return err
1240+
}
1241+
g.writef("} else if %s {\n", g.condExpr(cond, ec.Condition))
1242+
g.indent++
1243+
for _, s := range ec.Body {
1244+
if err := g.writeStmt(s); err != nil {
1245+
return err
1246+
}
1247+
}
1248+
g.indent--
1249+
}
1250+
if len(i.ElseBody) > 0 {
1251+
g.writeln("} else {")
1252+
g.indent++
1253+
for _, s := range i.ElseBody {
1254+
if err := g.writeStmt(s); err != nil {
1255+
return err
1256+
}
1257+
}
1258+
g.indent--
1259+
}
1260+
g.writeln("}")
1261+
return nil
1262+
}
1263+
1264+
// predeclareIfVars pre-declares variables assigned in any branch of an if/else
1265+
// so they're visible after the if block (Ruby-like scoping).
1266+
func (g *codeGen) predeclareIfVars(i *ast.IfStmt) {
12231267
var allBranches []ast.Statement
12241268
allBranches = append(allBranches, i.Body...)
12251269
for _, ec := range i.ElsifClauses {
@@ -1237,44 +1281,91 @@ func (g *codeGen) writeIf(i *ast.IfStmt) error {
12371281
g.declareVar(name)
12381282
}
12391283
}
1284+
}
1285+
1286+
// writeLastStmtAs tries to write a statement as an implicit return or assignment.
1287+
// format is the format string for the last expression (e.g. "return %s\n" or "r = %s\n").
1288+
// Returns handled=true if the statement was written, allCovered=true if all code paths
1289+
// produce a value (e.g. if/elsif/else with all branches handled).
1290+
func (g *codeGen) writeLastStmtAs(s ast.Statement, format string) (handled bool, allCovered bool, err error) {
1291+
switch st := s.(type) {
1292+
case *ast.ExprStmt:
1293+
g.emitLineDirective(st.StmtLine())
1294+
expr, err := g.exprString(st.Expression)
1295+
if err != nil {
1296+
return false, false, err
1297+
}
1298+
g.writef(format, expr)
1299+
return true, true, nil
1300+
case *ast.IfStmt:
1301+
allCovered, err := g.writeIfWithLastAction(st, format)
1302+
if err != nil {
1303+
return false, false, err
1304+
}
1305+
return true, allCovered, nil
1306+
default:
1307+
return false, false, nil
1308+
}
1309+
}
1310+
1311+
// writeIfWithLastAction writes an if/elsif/else block where the last expression
1312+
// in each branch is formatted with the given format string (for implicit returns).
1313+
func (g *codeGen) writeIfWithLastAction(i *ast.IfStmt, format string) (bool, error) {
1314+
g.predeclareIfVars(i)
12401315

12411316
cond, err := g.exprString(i.Condition)
12421317
if err != nil {
1243-
return err
1318+
return false, err
12441319
}
12451320
g.writef("if %s {\n", g.condExpr(cond, i.Condition))
12461321
g.indent++
1247-
for _, s := range i.Body {
1248-
if err := g.writeStmt(s); err != nil {
1249-
return err
1250-
}
1322+
if err := g.writeBodyWithLastAction(i.Body, format); err != nil {
1323+
return false, err
12511324
}
12521325
g.indent--
12531326
for _, ec := range i.ElsifClauses {
12541327
cond, err := g.exprString(ec.Condition)
12551328
if err != nil {
1256-
return err
1329+
return false, err
12571330
}
12581331
g.writef("} else if %s {\n", g.condExpr(cond, ec.Condition))
12591332
g.indent++
1260-
for _, s := range ec.Body {
1261-
if err := g.writeStmt(s); err != nil {
1262-
return err
1263-
}
1333+
if err := g.writeBodyWithLastAction(ec.Body, format); err != nil {
1334+
return false, err
12641335
}
12651336
g.indent--
12661337
}
1338+
allCovered := false
12671339
if len(i.ElseBody) > 0 {
12681340
g.writeln("} else {")
12691341
g.indent++
1270-
for _, s := range i.ElseBody {
1271-
if err := g.writeStmt(s); err != nil {
1272-
return err
1273-
}
1342+
if err := g.writeBodyWithLastAction(i.ElseBody, format); err != nil {
1343+
return false, err
12741344
}
12751345
g.indent--
1346+
allCovered = true
12761347
}
12771348
g.writeln("}")
1349+
return allCovered, nil
1350+
}
1351+
1352+
// writeBodyWithLastAction writes a list of statements, converting the last
1353+
// expression into the given format (e.g. "return %s\n").
1354+
func (g *codeGen) writeBodyWithLastAction(body []ast.Statement, format string) error {
1355+
for i, s := range body {
1356+
if i == len(body)-1 {
1357+
handled, _, err := g.writeLastStmtAs(s, format)
1358+
if err != nil {
1359+
return err
1360+
}
1361+
if handled {
1362+
continue
1363+
}
1364+
}
1365+
if err := g.writeStmt(s); err != nil {
1366+
return err
1367+
}
1368+
}
12781369
return nil
12791370
}
12801371

@@ -1476,6 +1567,18 @@ func (g *codeGen) rangeIntExpr(e ast.Expr) string {
14761567
}
14771568

14781569
func (g *codeGen) writeReturn(r *ast.ReturnStmt) error {
1570+
// Inside a try/or handler defer, return sets the handler result (r).
1571+
if g.inTryHandler > 0 {
1572+
if r.Value != nil {
1573+
expr, err := g.exprString(r.Value)
1574+
if err != nil {
1575+
return err
1576+
}
1577+
g.writef("r = %s\n", expr)
1578+
}
1579+
g.writeln("return")
1580+
return nil
1581+
}
14791582
// Inside a spawn block, return EXPR must assign to t.result and
14801583
// use a bare return (the goroutine closure has no return value).
14811584
if g.inSpawn > 0 {
@@ -2145,27 +2248,30 @@ func (g *codeGen) tryExpr(e *ast.TryExpr) (string, error) {
21452248
handlerCode, cerr := g.captureOutput(func() error {
21462249
g.pushScope()
21472250
g.declareVar(e.ErrVar)
2251+
g.inTryHandler++
21482252

21492253
for i, s := range e.Handler {
21502254
isLast := i == len(e.Handler)-1
21512255
if isLast {
2152-
// Last statement: if it's a bare expression, assign to r (return value)
2153-
if es, ok := s.(*ast.ExprStmt); ok {
2154-
val, verr := g.exprString(es.Expression)
2155-
if verr != nil {
2156-
g.popScope()
2157-
return verr
2158-
}
2159-
g.writef("r = %s\n", val)
2256+
// Last statement: if it's a bare expression or if/else, assign to r (return value)
2257+
handled, _, herr := g.writeLastStmtAs(s, "r = %s\n")
2258+
if herr != nil {
2259+
g.inTryHandler--
2260+
g.popScope()
2261+
return herr
2262+
}
2263+
if handled {
21602264
continue
21612265
}
21622266
}
21632267
if werr := g.writeStmt(s); werr != nil {
2268+
g.inTryHandler--
21642269
g.popScope()
21652270
return werr
21662271
}
21672272
}
21682273

2274+
g.inTryHandler--
21692275
g.popScope()
21702276
return nil
21712277
})
@@ -2274,15 +2380,13 @@ func (g *codeGen) fnExpr(e *ast.FnExpr) (string, error) {
22742380
for i, s := range e.Body {
22752381
isLast := i == len(e.Body)-1
22762382
if isLast {
2277-
// Last statement: if it's a bare expression, make it the return value
2278-
if es, ok := s.(*ast.ExprStmt); ok {
2279-
g.emitLineDirective(es.StmtLine())
2280-
val, verr := g.exprString(es.Expression)
2281-
if verr != nil {
2282-
restoreLambda()
2283-
return verr
2284-
}
2285-
g.writef("return %s\n", val)
2383+
// Last statement: if it's a bare expression or if/else, make it the return value
2384+
handled, _, herr := g.writeLastStmtAs(s, "return %s\n")
2385+
if herr != nil {
2386+
restoreLambda()
2387+
return herr
2388+
}
2389+
if handled {
22862390
continue
22872391
}
22882392
}

rats/core/09_try_or_test.rugo

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,9 @@ rats "shell fallback with try"
3131
test.assert_eq(result["status"], 0)
3232
test.assert_contains(result["output"], "continued")
3333
end
34+
35+
rats "return inside try/or handler sets handler result"
36+
result = test.run("rugo run rats/core/fixtures/bug_try_or_return_type.rugo")
37+
test.assert_eq(result["status"], 0)
38+
test.assert_eq(result["output"], "1")
39+
end

rats/core/54_implicit_return_test.rugo

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,37 @@ rats "explicit return still works"
8181
test.assert_eq(result["status"], 0)
8282
test.assert_eq(result["output"], "small\nbig")
8383
end
84+
85+
rats "implicit return from if/else in function"
86+
source = <<~RUGO
87+
def classify(x)
88+
if x > 10
89+
"big"
90+
else
91+
"small"
92+
end
93+
end
94+
puts(classify(20))
95+
puts(classify(5))
96+
RUGO
97+
result = eval.run(source)
98+
test.assert_eq(result["status"], 0)
99+
test.assert_eq(result["output"], "big\nsmall")
100+
end
101+
102+
rats "implicit return from if/else in lambda"
103+
source = <<~RUGO
104+
arr = [1, 2, 3, nil]
105+
result = arr.map(fn(x)
106+
if x != nil
107+
x * 2
108+
else
109+
nil
110+
end
111+
end)
112+
puts result
113+
RUGO
114+
result = eval.run(source)
115+
test.assert_eq(result["status"], 0)
116+
test.assert_eq(result["output"], "[2 4 6 <nil>]")
117+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
def find_stuff(query)
2+
return [1, 2, 3]
3+
end
4+
5+
def resolve(input)
6+
matches = try find_stuff(input) or err
7+
return nil
8+
end
9+
10+
if len(matches) == 0
11+
return nil
12+
end
13+
14+
return matches[0]
15+
end
16+
17+
puts resolve("test")

0 commit comments

Comments
 (0)