From 36ca5fde680faf1abd4b05b4ca1b96c488fcbdb8 Mon Sep 17 00:00:00 2001 From: Austin Clements Date: Thu, 30 Jul 2009 14:39:27 -0700 Subject: [PATCH] Implement labels, goto, labeled break, and labeled continue. Return checking is now done as a general flow check at the end of function compilation, since break and goto complicated the way I was doing return checking before. Goto-over-declaration checking is also done as a final flow check. Temporary variables used for effect extraction are now actually temporary. Otherwise "op=", "++", and "--" appear as declarations that cannot be jumped over. R=rsc APPROVED=rsc DELTA=421 (344 added, 38 deleted, 39 changed) OCL=32527 CL=32535 --- usr/austin/eval/compiler.go | 33 ++- usr/austin/eval/decls.go | 2 +- usr/austin/eval/expr.go | 9 +- usr/austin/eval/scope.go | 4 +- usr/austin/eval/stmt.go | 408 ++++++++++++++++++++++++++++++------ 5 files changed, 381 insertions(+), 75 deletions(-) diff --git a/usr/austin/eval/compiler.go b/usr/austin/eval/compiler.go index 641e6a293b..59858c800b 100644 --- a/usr/austin/eval/compiler.go +++ b/usr/austin/eval/compiler.go @@ -44,7 +44,27 @@ func (a *compiler) compileFuncType(b *block, typ *ast.FuncType) *FuncDecl func (a *compiler) compileArrayLen(b *block, expr ast.Expr) (int64, bool) +type label struct { + name string; + desc string; + // The PC goto statements should jump to, or nil if this label + // cannot be goto'd (such as an anonymous for loop label). + gotoPC *uint; + // The PC break statements should jump to, or nil if a break + // statement is invalid. + breakPC *uint; + // The PC continue statements should jump to, or nil if a + // continue statement is invalid. + continuePC *uint; + // The position where this label was resolved. If it has not + // been resolved yet, an invalid position. + resolved token.Position; + // The position where this label was first jumped to. + used token.Position; +} + type codeBuf struct +type flowBuf struct type FuncType struct // A funcCompiler captures information used throughout the compilation // of a single function body. @@ -55,22 +75,21 @@ type funcCompiler struct { // kinds of return statements are legal. outVarsNamed bool; *codeBuf; + flow *flowBuf; + labels map[string] *label; err bool; } +func (a *funcCompiler) checkLabels() // A blockCompiler captures information used throughout the compilation // of a single block within a function. type blockCompiler struct { *funcCompiler; block *block; - returned bool; - // The PC break statements should jump to, or nil if a break - // statement is invalid. - breakPC *uint; - // The PC continue statements should jump to, or nil if a - // continue statement is invalid. - continuePC *uint; + // The label of this block, used for finding break and + // continue labels. + label *label; // The blockCompiler for the block enclosing this one, or nil // for a function-level block. parent *blockCompiler; diff --git a/usr/austin/eval/decls.go b/usr/austin/eval/decls.go index 2f71f11f3b..439b8b2162 100644 --- a/usr/austin/eval/decls.go +++ b/usr/austin/eval/decls.go @@ -178,7 +178,7 @@ func (b *block) enterChild() *block func (b *block) exit() func (b *block) ChildScope() *Scope func (b *block) DefineVar(name string, t Type) *Variable -func (b *block) DefineTemp(t Type) *Variable +func (b *block) DefineSlot(t Type) *Variable func (b *block) DefineConst(name string, t Type, v Value) *Constant func (b *block) DefineType(name string, t Type) Type func (b *block) Lookup(name string) (level int, def Def) diff --git a/usr/austin/eval/expr.go b/usr/austin/eval/expr.go index fbd4b5ac4e..1231e22586 100644 --- a/usr/austin/eval/expr.go +++ b/usr/austin/eval/expr.go @@ -1324,7 +1324,10 @@ func (a *compiler) compileExpr(b *block, expr ast.Expr, constant bool) *exprComp // extractEffect separates out any effects that the expression may // have, returning a function that will perform those effects and a // new exprCompiler that is guaranteed to be side-effect free. These -// are the moral equivalents of "temp := &expr" and "*temp". +// are the moral equivalents of "temp := &expr" and "*temp". Because +// this creates a temporary variable, the caller should create a +// temporary block for the compilation of this expression and the +// evaluation of the results. // // Implementation limit: The expression must be addressable. func (a *exprCompiler) extractEffect() (func(f *Frame), *exprCompiler) { @@ -1337,9 +1340,7 @@ func (a *exprCompiler) extractEffect() (func(f *Frame), *exprCompiler) { // Create temporary tempBlock := a.block; tempType := NewPtrType(a.t); - // TODO(austin) These temporaries accumulate in the scope. We - // could enter a temporary block, but the caller has to exit it. - temp := tempBlock.DefineTemp(tempType); + temp := tempBlock.DefineSlot(tempType); tempIdx := temp.Index; // Generate "temp := &e" diff --git a/usr/austin/eval/scope.go b/usr/austin/eval/scope.go index 6d89d00d78..aed896f959 100644 --- a/usr/austin/eval/scope.go +++ b/usr/austin/eval/scope.go @@ -51,14 +51,14 @@ func (b *block) DefineVar(name string, t Type) *Variable { if _, ok := b.defs[name]; ok { return nil; } - v := b.DefineTemp(t); + v := b.DefineSlot(t); if v != nil { b.defs[name] = v; } return v; } -func (b *block) DefineTemp(t Type) *Variable { +func (b *block) DefineSlot(t Type) *Variable { if b.inner != nil { log.Crash("Failed to exit child block before defining variable"); } diff --git a/usr/austin/eval/stmt.go b/usr/austin/eval/stmt.go index 65d97ac3e5..88febdc376 100644 --- a/usr/austin/eval/stmt.go +++ b/usr/austin/eval/stmt.go @@ -15,6 +15,11 @@ import ( "strconv"; ) +const ( + returnPC = ^uint(0); + badPC = ^uint(1); +) + /* * Statement compiler */ @@ -22,6 +27,8 @@ import ( type stmtCompiler struct { *blockCompiler; pos token.Position; + // This statement's label, or nil if it is not labeled. + stmtLabel *label; // err should be initialized to true before visiting and set // to false when the statement is compiled successfully. The // function invoking Visit should or this with @@ -34,6 +41,183 @@ func (a *stmtCompiler) diag(format string, args ...) { a.diagAt(&a.pos, format, args); } +/* + * Flow checker + */ + +type flowEnt struct { + // Whether this flow entry is conditional. If true, flow can + // continue to the next PC. + cond bool; + // True if this will terminate flow (e.g., a return statement). + // cond must be false and jumps must be nil if this is true. + term bool; + // PC's that can be reached from this flow entry. + jumps []*uint; + // Whether this flow entry has been visited by reachesEnd. + visited bool; +} + +type flowBlock struct { + // If this is a goto, the target label. + target string; + // The inner-most block containing definitions. + block *block; + // The numVars from each block leading to the root of the + // scope, starting at block. + numVars []int; +} + +type flowBuf struct { + cb *codeBuf; + // ents is a map from PC's to flow entries. Any PC missing + // from this map is assumed to reach only PC+1. + ents map[uint] *flowEnt; + // gotos is a map from goto positions to information on the + // block at the point of the goto. + gotos map[*token.Position] *flowBlock; + // labels is a map from label name to information on the block + // at the point of the label. labels are tracked by name, + // since mutliple labels at the same PC can have different + // blocks. + labels map[string] *flowBlock; +} + +func newFlowBuf(cb *codeBuf) *flowBuf { + return &flowBuf{cb, make(map[uint] *flowEnt), make(map[*token.Position] *flowBlock), make(map[string] *flowBlock)}; +} + +// put creates a flow control point for the next PC in the code buffer. +// This should be done before pushing the instruction into the code buffer. +func (f *flowBuf) put(cond bool, term bool, jumps []*uint) { + pc := f.cb.nextPC(); + if ent, ok := f.ents[pc]; ok { + log.Crashf("Flow entry already exists at PC %d: %+v", pc, ent); + } + f.ents[pc] = &flowEnt{cond, term, jumps, false}; +} + +// putTerm creates a flow control point at the next PC that +// unconditionally terminates execution. +func (f *flowBuf) putTerm() { + f.put(false, true, nil); +} + +// put1 creates a flow control point at the next PC that jumps to one +// PC and, if cond is true, can also continue to the PC following the +// next PC. +func (f *flowBuf) put1(cond bool, jumpPC *uint) { + f.put(cond, false, []*uint {jumpPC}); +} + +func newFlowBlock(target string, b *block) *flowBlock { + // Find the inner-most block containing definitions + for b.numVars == 0 && b.outer != nil && b.outer.scope == b.scope { + b = b.outer; + } + + // Count parents leading to the root of the scope + n := 0; + for bp := b; bp.scope == b.scope; bp = bp.outer { + n++; + } + + // Capture numVars from each block to the root of the scope + numVars := make([]int, n); + i := 0; + for bp := b; i < n; bp = bp.outer { + numVars[i] = bp.numVars; + i++; + } + + return &flowBlock{target, b, numVars}; +} + +// putGoto captures the block at a goto statement. This should be +// called in addition to putting a flow control point. +func (f *flowBuf) putGoto(pos token.Position, target string, b *block) { + f.gotos[&pos] = newFlowBlock(target, b); +} + +// putLabel captures the block at a label. +func (f *flowBuf) putLabel(name string, b *block) { + f.labels[name] = newFlowBlock("", b); +} + +// reachesEnd returns true if the end of f's code buffer can be +// reached from the given program counter. Error reporting is the +// caller's responsibility. +func (f *flowBuf) reachesEnd(pc uint) bool { + endPC := f.cb.nextPC(); + if pc > endPC { + log.Crashf("Reached bad PC %d past end PC %d", pc, endPC); + } + + for ; pc < endPC; pc++ { + ent, ok := f.ents[pc]; + if !ok { + continue; + } + + if ent.visited { + return false; + } + ent.visited = true; + + if ent.term { + return false; + } + + // If anything can reach the end, we can reach the end + // from pc. + for _, j := range ent.jumps { + if f.reachesEnd(*j) { + return true; + } + } + // If the jump was conditional, we can reach the next + // PC, so try reaching the end from it. + if ent.cond { + continue; + } + return false; + } + return true; +} + +// gotosObeyScopes returns true if no goto statement causes any +// variables to come into scope that were not in scope at the point of +// the goto. Reports any errors using the given compiler. +func (f *flowBuf) gotosObeyScopes(a *compiler) bool { + for pos, src := range f.gotos { + tgt := f.labels[src.target]; + + // The target block must be a parent of this block + numVars := src.numVars; + b := src.block; + for len(numVars) > 0 && b != tgt.block { + b = b.outer; + numVars = numVars[1:len(numVars)]; + } + if b != tgt.block { + // We jumped into a deeper block + a.diagAt(pos, "goto causes variables to come into scope"); + return false; + } + + // There must be no variables in the target block that + // did not exist at the jump + tgtNumVars := tgt.numVars; + for i := range numVars { + if tgtNumVars[i] > numVars[i] { + a.diagAt(pos, "goto causes variables to come into scope"); + return false; + } + } + } + return true; +} + /* * Statement visitors */ @@ -51,7 +235,36 @@ func (a *stmtCompiler) DoEmptyStmt(s *ast.EmptyStmt) { } func (a *stmtCompiler) DoLabeledStmt(s *ast.LabeledStmt) { - log.Crash("Not implemented"); + bad := false; + + // Define label + l, ok := a.labels[s.Label.Value]; + if ok { + if l.resolved.IsValid() { + a.diag("label %s redefined; previous definition at line %d", s.Label.Value, l.resolved.Line); + bad = true; + } + } else { + pc := badPC; + l = &label{name: s.Label.Value, gotoPC: &pc}; + a.labels[l.name] = l; + } + l.desc = "regular label"; + l.resolved = s.Pos(); + + // Set goto PC + *l.gotoPC = a.nextPC(); + + // Define flow entry so we can check for jumps over declarations. + a.flow.putLabel(l.name, a.block); + + // Compile the statement. Reuse our stmtCompiler for simplicity. + a.pos = s.Stmt.Pos(); + a.stmtLabel = l; + s.Stmt.Visit(a); + if bad { + a.err = true; + } } func (a *stmtCompiler) DoExprStmt(s *ast.ExprStmt) { @@ -73,7 +286,11 @@ func (a *stmtCompiler) DoExprStmt(s *ast.ExprStmt) { } func (a *stmtCompiler) DoIncDecStmt(s *ast.IncDecStmt) { - l := a.compileExpr(a.block, s.X, false); + // Create temporary block for extractEffect + bc := a.enterChild(); + defer bc.exit(); + + l := a.compileExpr(bc.block, s.X, false); if l == nil { return; } @@ -330,8 +547,12 @@ func (a *stmtCompiler) doAssignOp(s *ast.AssignStmt) { return; } - l := a.compileExpr(a.block, s.Lhs[0], false); - r := a.compileExpr(a.block, s.Rhs[0], false); + // Create temporary block for extractEffect + bc := a.enterChild(); + defer bc.exit(); + + l := a.compileExpr(bc.block, s.Lhs[0], false); + r := a.compileExpr(bc.block, s.Rhs[0], false); if l == nil || r == nil { return; } @@ -387,13 +608,10 @@ func (a *stmtCompiler) DoReturnStmt(s *ast.ReturnStmt) { return; } - // Supress return errors even if we fail to compile this - // return statement. - a.returned = true; - if len(s.Results) == 0 && (len(a.fnType.Out) == 0 || a.outVarsNamed) { // Simple case. Simply exit from the function. - a.push(func(v *vm) { v.pc = ^uint(0) }); + a.flow.putTerm(); + a.push(func(v *vm) { v.pc = returnPC }); a.err = false; return; } @@ -429,49 +647,68 @@ func (a *stmtCompiler) DoReturnStmt(s *ast.ReturnStmt) { // Compile start := len(a.fnType.In); nout := len(a.fnType.Out); + a.flow.putTerm(); a.push(func(v *vm) { assign(multiV(v.f.Vars[start:start+nout]), v.f); - v.pc = ^uint(0); + v.pc = returnPC; }); a.err = false; } +func (a *stmtCompiler) findLexicalLabel(name *ast.Ident, pred func(*label) bool, errOp, errCtx string) *label { + bc := a.blockCompiler; + for ; bc != nil; bc = bc.parent { + if bc.label == nil { + continue; + } + l := bc.label; + if name == nil && pred(l) { + return l; + } + if name != nil && l.name == name.Value { + if !pred(l) { + a.diag("cannot %s to %s %s", errOp, l.desc, l.name); + return nil; + } + return l; + } + } + if name == nil { + a.diag("%s outside %s", errOp, errCtx); + } else { + a.diag("%s label %s not defined", errOp, name.Value); + } + return nil; +} + func (a *stmtCompiler) DoBranchStmt(s *ast.BranchStmt) { + var pc *uint; + switch s.Tok { case token.BREAK: - if s.Label != nil { - log.Crash("break with label not implemented"); + l := a.findLexicalLabel(s.Label, func(l *label) bool { return l.breakPC != nil }, "break", "for loop, switch, or select"); + if l == nil { + return; } - - bc := a.blockCompiler; - for ; bc != nil; bc = bc.parent { - if bc.breakPC != nil { - pc := bc.breakPC; - a.push(func(v *vm) { v.pc = *pc }); - a.err = false; - return; - } - } - a.diag("break outside for loop, switch, or select"); + pc = l.breakPC; case token.CONTINUE: - if s.Label != nil { - log.Crash("continue with label not implemented"); + l := a.findLexicalLabel(s.Label, func(l *label) bool { return l.continuePC != nil }, "continue", "for loop"); + if l == nil { + return; } - - bc := a.blockCompiler; - for ; bc != nil; bc = bc.parent { - if bc.continuePC != nil { - pc := bc.continuePC; - a.push(func(v *vm) { v.pc = *pc }); - a.err = false; - return; - } - } - a.diag("continue outside for loop"); + pc = l.continuePC; case token.GOTO: - log.Crash("goto not implemented"); + l, ok := a.labels[s.Label.Value]; + if !ok { + pc := badPC; + l = &label{name: s.Label.Value, desc: "unresolved label", gotoPC: &pc, used: s.Pos()}; + a.labels[l.name] = l; + } + + pc = l.gotoPC; + a.flow.putGoto(s.Pos(), l.name, a.block); case token.FALLTHROUGH: log.Crash("fallthrough not implemented"); @@ -479,6 +716,10 @@ func (a *stmtCompiler) DoBranchStmt(s *ast.BranchStmt) { default: log.Crash("Unexpected branch token %v", s.Tok); } + + a.flow.put1(false, pc); + a.push(func(v *vm) { v.pc = *pc }); + a.err = false; } func (a *stmtCompiler) DoBlockStmt(s *ast.BlockStmt) { @@ -486,7 +727,6 @@ func (a *stmtCompiler) DoBlockStmt(s *ast.BlockStmt) { bc.compileStmts(s); bc.exit(); - a.returned = a.returned || bc.returned; a.err = false; } @@ -509,7 +749,8 @@ func (a *stmtCompiler) DoIfStmt(s *ast.IfStmt) { bc.compileStmt(s.Init); } - var elsePC, endPC uint; + elsePC := badPC; + endPC := badPC; // Compile condition, if any. If there is no condition, we // fall through to the body. @@ -524,6 +765,7 @@ func (a *stmtCompiler) DoIfStmt(s *ast.IfStmt) { bad = true; default: eval := e.asBool(); + a.flow.put1(true, &elsePC); a.push(func(v *vm) { if !eval(v.f) { v.pc = elsePC; @@ -540,15 +782,12 @@ func (a *stmtCompiler) DoIfStmt(s *ast.IfStmt) { // Compile else if s.Else != nil { // Skip over else if we executed the body + a.flow.put1(false, &endPC); a.push(func(v *vm) { v.pc = endPC; }); elsePC = a.nextPC(); bc.compileStmt(s.Else); - - if body.returned && bc.returned { - a.returned = true; - } } else { elsePC = a.nextPC(); } @@ -593,17 +832,27 @@ func (a *stmtCompiler) DoForStmt(s *ast.ForStmt) { bc.compileStmt(s.Init); } - var bodyPC, postPC, checkPC, endPC uint; + bodyPC := badPC; + postPC := badPC; + checkPC := badPC; + endPC := badPC; // Jump to condition check. We generate slightly less code by // placing the condition check after the body. + a.flow.put1(false, &checkPC); a.push(func(v *vm) { v.pc = checkPC }); // Compile body bodyPC = a.nextPC(); body := bc.enterChild(); - body.breakPC = &endPC; - body.continuePC = &postPC; + if a.stmtLabel != nil { + body.label = a.stmtLabel; + } else { + body.label = &label{resolved: s.Pos()}; + } + body.label.desc = "for loop"; + body.label.breakPC = &endPC; + body.label.continuePC = &postPC; body.compileStmts(s.Body); body.exit(); @@ -620,6 +869,7 @@ func (a *stmtCompiler) DoForStmt(s *ast.ForStmt) { checkPC = a.nextPC(); if s.Cond == nil { // If the condition is absent, it is equivalent to true. + a.flow.put1(false, &bodyPC); a.push(func(v *vm) { v.pc = bodyPC }); } else { e := bc.compileExpr(bc.block, s.Cond, false); @@ -631,6 +881,7 @@ func (a *stmtCompiler) DoForStmt(s *ast.ForStmt) { bad = true; default: eval := e.asBool(); + a.flow.put1(true, &bodyPC); a.push(func(v *vm) { if eval(v.f) { v.pc = bodyPC; @@ -658,7 +909,7 @@ func (a *blockCompiler) compileStmt(s ast.Stmt) { if a.block.inner != nil { log.Crash("Child scope still entered"); } - sc := &stmtCompiler{a, s.Pos(), true}; + sc := &stmtCompiler{a, s.Pos(), nil, true}; s.Visit(sc); if a.block.inner != nil { log.Crash("Forgot to exit child scope"); @@ -677,7 +928,6 @@ func (a *blockCompiler) enterChild() *blockCompiler { return &blockCompiler{ funcCompiler: a.funcCompiler, block: block, - returned: false, parent: a, }; } @@ -701,33 +951,36 @@ func (a *compiler) compileFunc(b *block, decl *FuncDecl, body *ast.BlockStmt) (f if decl.InNames[i] != nil { bodyScope.DefineVar(decl.InNames[i].Value, t); } else { - // TODO(austin) Not technically a temp - bodyScope.DefineTemp(t); + bodyScope.DefineSlot(t); } } for i, t := range decl.Type.Out { if decl.OutNames[i] != nil { bodyScope.DefineVar(decl.OutNames[i].Value, t); } else { - bodyScope.DefineTemp(t); + bodyScope.DefineSlot(t); } } // Create block context - fc := &funcCompiler{a, decl.Type, false, newCodeBuf(), false}; - if len(decl.OutNames) > 0 && decl.OutNames[0] != nil { - fc.outVarsNamed = true; - } + cb := newCodeBuf(); + fc := &funcCompiler{ + compiler: a, + fnType: decl.Type, + outVarsNamed: len(decl.OutNames) > 0 && decl.OutNames[0] != nil, + codeBuf: cb, + flow: newFlowBuf(cb), + labels: make(map[string] *label), + err: false, + }; bc := &blockCompiler{ funcCompiler: fc, block: bodyScope.block, - returned: false, }; // Compile body bc.compileStmts(body); - - // TODO(austin) Check that all gotos were linked? + fc.checkLabels(); if fc.err { return nil; @@ -735,7 +988,7 @@ func (a *compiler) compileFunc(b *block, decl *FuncDecl, body *ast.BlockStmt) (f // Check that the body returned if necessary. We only check // this if there were no errors compiling the body. - if len(decl.Type.Out) > 0 && !bc.returned { + if len(decl.Type.Out) > 0 && fc.flow.reachesEnd(0) { // XXX(Spec) Not specified. a.diagAt(&body.Rbrace, "function ends without a return statement"); return nil; @@ -746,6 +999,30 @@ func (a *compiler) compileFunc(b *block, decl *FuncDecl, body *ast.BlockStmt) (f return func(f *Frame) Func { return &evalFunc{f, maxVars, code} }; } +// Checks that labels were resolved and that all jumps obey scoping +// rules. Reports an error and set fc.err if any check fails. +func (a *funcCompiler) checkLabels() { + bad := false; + for _, l := range a.labels { + if !l.resolved.IsValid() { + a.diagAt(&l.used, "label %s not defined", l.name); + bad = true; + } + } + if bad { + a.err = true; + // Don't check scopes if we have unresolved labels + return; + } + + // Executing the "goto" statement must not cause any variables + // to come into scope that were not already in scope at the + // point of the goto. + if !a.flow.gotosObeyScopes(a.compiler) { + a.err = true; + } +} + /* * Testing interface */ @@ -761,16 +1038,25 @@ func (s *Stmt) Exec(f *Frame) { func CompileStmts(scope *Scope, stmts []ast.Stmt) (*Stmt, os.Error) { errors := scanner.NewErrorVector(); cc := &compiler{errors}; - fc := &funcCompiler{cc, nil, false, newCodeBuf(), false}; + cb := newCodeBuf(); + fc := &funcCompiler{ + compiler: cc, + fnType: nil, + outVarsNamed: false, + codeBuf: cb, + flow: newFlowBuf(cb), + labels: make(map[string] *label), + err: false, + }; bc := &blockCompiler{ funcCompiler: fc, block: scope.block, - returned: false }; out := make([]*Stmt, len(stmts)); for i, stmt := range stmts { bc.compileStmt(stmt); } + fc.checkLabels(); if fc.err { return nil, errors.GetError(scanner.Sorted); }