diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 3b37ab19..2005d770 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -47,6 +47,11 @@ type builder struct { children []*bytecode.ISeq parent *builder isBlock bool + // borrowed marks a scope whose locals belong to somebody else's already-built + // frame — the synthetic parent CompileWithLocals puts a Binding's locals in. + // Its slots may be read and written but no new one may be added: the frame + // that will back it has already been sized. + borrowed bool } func newBuilder(name string, params []string) *builder { @@ -236,12 +241,21 @@ func MagicSourceEncoding(src string) string { } name := magicEncodingName(line) switch strings.ToLower(name) { + case "", "utf-8", "utf8": + // UTF-8 is the default: leave literals untagged so they compare equal to + // every other untagged String. + return "" case "binary", "ascii-8bit": return "ASCII-8BIT" case "us-ascii", "ascii": return "US-ASCII" } - return "" + // Any other declared encoding is passed through as written. The compiler has + // no encoding registry — that table lives in the vm — but the vm's lookup is + // case-insensitive over every name and alias, so `# encoding: big5` tags + // literals with a name that resolves to the canonical Big5 object and reports + // itself as "Big5". + return name } // magicEncodingName extracts the value of a `coding:`/`encoding:` field from a @@ -258,19 +272,30 @@ func magicEncodingName(line string) string { } rest := line[i+len("coding"):] rest = strings.TrimLeft(rest, ":= \t") - // The value ends at the first whitespace or a `-*-` terminator. + // The name runs while the bytes can belong to an encoding name — letters, + // digits, `-` and `_` — which is how MRI's set_file_encoding (parse.y + // v3_4_0) reads the value of a `coding` field it finds in a top-of-file + // comment. That is what lets the vim form work, where the value is followed + // by a comma: `# vim: filetype=ruby, fileencoding=big5, tabsize=3`. A `-*-` + // still terminates it even though `-` is a name byte, so the Emacs form + // closes without a space (`# coding:binary-*-`). end := len(rest) - for j, r := range rest { - if r == ' ' || r == '\t' || r == ';' { - end = j - break - } - if strings.HasPrefix(rest[j:], "-*-") { + for j := 0; j < len(rest); j++ { + if strings.HasPrefix(rest[j:], "-*-") || !isEncNameByte(rest[j]) { end = j break } } - return strings.TrimSpace(rest[:end]) + return rest[:end] +} + +// isEncNameByte reports whether b may appear in an encoding name. +func isEncNameByte(b byte) bool { + switch { + case b >= 'a' && b <= 'z', b >= 'A' && b <= 'Z', b >= '0' && b <= '9': + return true + } + return b == '-' || b == '_' } // CompileWithLocals lowers prog for a Binding eval: it compiles in a child scope @@ -287,6 +312,7 @@ func CompileWithLocals(prog *ast.Program, localNames []string) (iseq *bytecode.I c := &Compiler{} parent := newBuilder("", nil) parent.locals = append([]string(nil), localNames...) + parent.borrowed = true c.push(parent) child := newBuilder("(eval)", nil) child.isBlock = true @@ -499,6 +525,10 @@ func (c *Compiler) compileNode(n ast.Node) { b.emit(bytecode.OpSend, b.addName(v.Op), 0) } case *ast.BinaryExpr: + if sc, rhs, ok := scopedConstOrAssign(v); ok { + c.compileScopedConstOrAssign(sc, rhs) + return + } if v.Op == "&&" || v.Op == "||" { c.compileLogical(v) return @@ -549,6 +579,10 @@ func (c *Compiler) compileNode(n ast.Node) { c.compileNode(v.Value) b.emit(bytecode.OpSetConst, b.addName(v.Name), 0) case *ast.ScopedConstAssign: + if sc, be, ok := scopedConstOpAssign(v); ok { + c.compileScopedConstOpAssign(sc, be) + break + } // `Scope::NAME = value`: evaluate the scope, set its constant, yield the // value. Target is the *ScopedConst naming the constant. sc, ok := v.Target.(*ast.ScopedConst) @@ -927,10 +961,19 @@ func (c *Compiler) compileCall(v *ast.Call) { b.emit(bytecode.OpBinding, 0, 0) return } - // __ENCODING__ is the source encoding keyword; rbgo scripts are UTF-8, so it is - // Encoding::UTF_8. + // __ENCODING__ is the source encoding keyword: the encoding a `# encoding:` + // magic comment declared for this file, or Encoding::UTF_8, the default source + // encoding, when it declared none. if v.Recv == nil && v.Block == nil && v.Name == "__ENCODING__" && len(v.Args) == 0 { - c.compileNode(&ast.ScopedConst{Recv: &ast.ConstRef{Name: "Encoding"}, Name: "UTF_8"}) + if c.srcEnc == "" { + c.compileNode(&ast.ScopedConst{Recv: &ast.ConstRef{Name: "Encoding"}, Name: "UTF_8"}) + return + } + c.compileNode(&ast.Call{ + Recv: &ast.ConstRef{Name: "Encoding"}, + Name: "find", + Args: []ast.Node{&ast.StringLit{Value: c.srcEnc}}, + }) return } // A bare eval(str) with no explicit binding evaluates against the caller's @@ -953,6 +996,21 @@ func (c *Compiler) compileCall(v *ast.Call) { return } } + // A compound assignment to an index or an attribute (`a[i] op= v`, + // `a.x op= v`) reaches the compiler as the parser's textual desugaring, whose + // receiver and index nodes are shared between the read and the write. + // Compiled literally that evaluates each of them twice; lower it so each is + // evaluated exactly once (see opassign.go). + if isSetterCall(v) { + if be, idx, ok := indexOpAssign(v); ok { + c.compileIndexOpAssign(v, be, idx) + return + } + if be, ok := attrOpAssign(v); ok { + c.compileAttrOpAssign(v, be) + return + } + } // explicit marks a send written with an explicit receiver other than `self`, // so the VM can enforce private-method visibility (private methods are // callable only through an implicit — or `self.` — receiver). A bare `self.m` @@ -1003,6 +1061,24 @@ func (c *Compiler) compileCall(v *ast.Call) { patchSafe() return } + // A setter written with a splatted argument list (`o[*x] = 1`) still + // evaluates to the assigned value, not to what `[]=` returns. The argument + // count is not known until run time, so the value is recovered as the last + // element of the built argument array — exactly MRI's `dup; putobject -1; + // send :[]` in compile.c's compile_attrasgn (ruby/ruby v3_4_0:10165). + if isSetterCall(v) && len(args) > 0 { + b.emit(bytecode.OpDup, 0, 0) + b.emit(bytecode.OpPushConst, b.addConst(object.IntValue(-1)), 0) + b.emit(bytecode.OpSend, b.addName("[]"), 1) + tmp := b.localSlot("") + b.emit(bytecode.OpSetLocal, tmp, 0) + b.emit(bytecode.OpPop, 0, 0) // the argument array is on top again + sendFlags(b.emit(bytecode.OpSendArray, b.addName(v.Name), 0)) + b.emit(bytecode.OpPop, 0, 0) // discard the setter's return value + b.emit(bytecode.OpGetLocal, tmp, 0) + patchSafe() + return + } at := sendFlags(b.emit(bytecode.OpSendArray, b.addName(v.Name), 0)) if v.Block != nil { b.insns[at].C = c.compileBlock(v.Block) + 1 @@ -1632,14 +1708,14 @@ func (c *Compiler) compileSuper(v *ast.Super) { func (c *Compiler) compileIf(v *ast.If) { b := c.cur() - c.compileNode(v.Cond) + c.compileCondition(v.Cond) thisFalse := b.emit(bytecode.OpBranchUnless, 0, 0) c.compileBody(v.Then) endJumps := []int{b.emit(bytecode.OpJump, 0, 0)} for _, ei := range v.Elsifs { b.patch(thisFalse, b.here()) - c.compileNode(ei.Cond) + c.compileCondition(ei.Cond) thisFalse = b.emit(bytecode.OpBranchUnless, 0, 0) c.compileBody(ei.Body) endJumps = append(endJumps, b.emit(bytecode.OpJump, 0, 0)) @@ -1670,7 +1746,12 @@ type loopCtx struct { kind ctxKind contTarget int // loop: jump here on `next` (re-evaluate the condition) contFixups []int // loop: `next` OpJump placeholders when contTarget is not yet known (a do…end post-loop compiles its condition after the body) - breaks []int // loop: OpJump placeholders patched to the loop exit + breaks []int // loop: OpJump placeholders patched to the loop's break exit + // breakSlot holds a loop `break`'s argument, which is the value of the whole + // loop expression (MRI compile.c compile_loop, v3_4_0:8143: the normal exit + // pushes nil and falls into break_label, where a break arrives with its own + // value instead). Allocated on the first break in this loop, else -1. + breakSlot int } func (c *Compiler) innerCtx() *loopCtx { @@ -1691,9 +1772,32 @@ func (c *Compiler) compileBreak(v *ast.Break) { b.emit(bytecode.OpBreak, 0, 0) return } + // A loop `break` carries its argument out as the loop expression's value. + if ctx.breakSlot < 0 { + ctx.breakSlot = b.localSlot("") + } + c.compileBreakValue(v.Value) + b.emit(bytecode.OpSetLocal, ctx.breakSlot, 0) + b.emit(bytecode.OpPop, 0, 0) ctx.breaks = append(ctx.breaks, b.emit(bytecode.OpJump, 0, 0)) } +// finishLoopValue emits a loop's value once its body is compiled: nil when the +// condition ended it, or the `break` argument when a break did. +func (c *Compiler) finishLoopValue(ctx *loopCtx) { + b := c.cur() + b.emit(bytecode.OpPushNil, 0, 0) + if len(ctx.breaks) == 0 { + return + } + done := b.emit(bytecode.OpJump, 0, 0) + for _, j := range ctx.breaks { + b.patch(j, b.here()) + } + b.emit(bytecode.OpGetLocal, ctx.breakSlot, 0) + b.patch(done, b.here()) +} + func (c *Compiler) compileNext(v *ast.Next) { b := c.cur() ctx := c.innerCtx() @@ -2386,19 +2490,16 @@ func (c *Compiler) compileWhile(v *ast.While) { } b := c.cur() start := b.here() - c.compileNode(v.Cond) + c.compileCondition(v.Cond) exit := b.emit(bytecode.OpBranchUnless, 0, 0) - ctx := &loopCtx{kind: ctxLoop, contTarget: start} + ctx := &loopCtx{kind: ctxLoop, contTarget: start, breakSlot: -1} c.ctxs = append(c.ctxs, ctx) c.compileBody(v.Body) c.ctxs = c.ctxs[:len(c.ctxs)-1] b.emit(bytecode.OpPop, 0, 0) // discard each iteration's value b.emit(bytecode.OpJump, start, 0) b.patch(exit, b.here()) - for _, j := range ctx.breaks { // break lands on the loop's nil value - b.patch(j, b.here()) - } - b.emit(bytecode.OpPushNil, 0, 0) // while evaluates to nil + c.finishLoopValue(ctx) } // compileDoWhile lowers a `begin…end while/until cond` post-loop: the body runs @@ -2410,7 +2511,7 @@ func (c *Compiler) compileWhile(v *ast.While) { func (c *Compiler) compileDoWhile(v *ast.While, body *ast.Begin) { b := c.cur() start := b.here() - ctx := &loopCtx{kind: ctxLoop, contTarget: -1} // condition PC not yet known + ctx := &loopCtx{kind: ctxLoop, contTarget: -1, breakSlot: -1} // condition PC not yet known c.ctxs = append(c.ctxs, ctx) c.compileNode(body) b.emit(bytecode.OpPop, 0, 0) // discard each iteration's value @@ -2419,13 +2520,10 @@ func (c *Compiler) compileDoWhile(v *ast.While, body *ast.Begin) { for _, j := range ctx.contFixups { // `next` lands on the condition test b.patch(j, cont) } - c.compileNode(v.Cond) + c.compileCondition(v.Cond) b.emit(bytecode.OpBranchIf, start, 0) // repeat while the condition holds c.ctxs = c.ctxs[:len(c.ctxs)-1] - for _, j := range ctx.breaks { // break lands on the loop's nil value - b.patch(j, b.here()) - } - b.emit(bytecode.OpPushNil, 0, 0) // the post-loop evaluates to nil + c.finishLoopValue(ctx) } // compileFor lowers `for VARS in ITER ... end` to `ITER.each { ... }`. Unlike a diff --git a/internal/compiler/flipflop.go b/internal/compiler/flipflop.go new file mode 100644 index 00000000..a68ed6d1 --- /dev/null +++ b/internal/compiler/flipflop.go @@ -0,0 +1,143 @@ +// The flip-flop operator: a two-sided range literal written where a condition +// is expected (`x if (i == 4)..(i == 7)`). It is not a Range object but a piece +// of state that turns on when the left condition holds and off after the right +// one does — awk's and sed's range addressing. +// +// MRI's parser marks such a range NODE_FLIP2 (`..`) or NODE_FLIP3 (`...`) by +// its syntactic position, and compile.c's compile_flip_flop (ruby/ruby +// v3_4_0:4607) lowers it against a per-occurrence slot allocated in the +// enclosing *local* iseq — ISEQ_FLIP_CNT_INCREMENT(ISEQ_BODY(iseq)->local_iseq) +// — so a block shares its state with every other invocation of that block, and +// two textually identical flip-flops in different iseqs do not interfere. +// +// The generated sequence is MRI's, with one difference: MRI compiles it +// straight into the surrounding branch's then/else labels, while rbgo's +// conditions are ordinary values that the caller branches on, so this pushes +// true or false instead. rbgo's local slots are already depth-addressed and +// resolution stops at the first non-block scope, which is exactly MRI's +// local_iseq, so a slot allocated there has the lifetime the operator needs. +package compiler + +import ( + "github.com/go-embedded-ruby/ruby/internal/bytecode" + "github.com/go-ruby-parser/parser/ast" +) + +// hasFlipFlop reports whether node is a flip-flop in condition position — a +// two-sided range literal, or a `!` / `&&` / `||` combination containing one. +// A beginless or endless range (`..x`, `x..`) is a Range value, not a +// flip-flop, and is left alone. +func hasFlipFlop(node ast.Node) bool { + switch v := node.(type) { + case *ast.RangeLit: + return v.Lo != nil && v.Hi != nil + case *ast.BinaryExpr: + return (v.Op == "&&" || v.Op == "||") && (hasFlipFlop(v.Left) || hasFlipFlop(v.Right)) + case *ast.UnaryExpr: + return v.Op == "!" && hasFlipFlop(v.Operand) + } + return false +} + +// compileCondition compiles node in condition position, leaving a value for the +// caller to branch on. Without a flip-flop anywhere in it that is exactly +// compileNode; with one, the logical operators around it are re-emitted here so +// each operand is itself compiled in condition position (`a...b or c...d`). +func (c *Compiler) compileCondition(node ast.Node) { + b := c.cur() + switch v := node.(type) { + case *ast.RangeLit: + if v.Lo != nil && v.Hi != nil { + c.compileFlipFlop(v) + return + } + case *ast.BinaryExpr: + // `&&` / `||`, short-circuiting on the left operand's value, as + // compileLogical does for the ordinary case. Only re-emitted here when an + // operand really is a flip-flop; otherwise the ordinary path handles it. + if (v.Op == "&&" || v.Op == "||") && (hasFlipFlop(v.Left) || hasFlipFlop(v.Right)) { + c.compileCondition(v.Left) + b.emit(bytecode.OpDup, 0, 0) + var short int + if v.Op == "&&" { + short = b.emit(bytecode.OpBranchUnless, 0, 0) + } else { + short = b.emit(bytecode.OpBranchIf, 0, 0) + } + b.emit(bytecode.OpPop, 0, 0) + c.compileCondition(v.Right) + b.patch(short, b.here()) + return + } + case *ast.UnaryExpr: // `!cond`, which is also how `unless` and `until` arrive + if v.Op == "!" && hasFlipFlop(v.Operand) { + c.compileCondition(v.Operand) + b.emit(bytecode.OpNot, 0, 0) + return + } + } + c.compileNode(node) +} + +// flipFlopSlot allocates this occurrence's state slot in the nearest enclosing +// non-block scope — MRI's local_iseq — and returns how many block scopes out +// that is, so the emitted GetLocal/SetLocal address it from here. The walk stops +// short of a borrowed scope (a Binding's locals, under eval): that frame is +// already sized, so an eval'd flip-flop keeps its state in the eval's own frame, +// which a proc built there captures and every later call of it shares. +func (c *Compiler) flipFlopSlot() (depth, slot int) { + root, d := c.cur(), 0 + for root.isBlock && root.parent != nil && !root.parent.borrowed { + root, d = root.parent, d+1 + } + // Anonymous, like every other compiler temporary: nothing ever resolves it + // by name, and it must not show up in Binding#local_variables. + return d, root.localSlot("") +} + +// compileFlipFlop emits the operator, leaving true or false on the stack. The +// slot starts nil (falsy), so the first evaluation tests the left condition. +// +// Off, and the left condition false: false, and the right condition is never +// evaluated. Off, and the left condition true: the state turns on, and `..` +// then tests the right condition on this same pass (MRI's `again`) while `...` +// does not — the one difference between the two spellings. On: the right +// condition decides whether this is the last true pass; either way the value is +// true. +func (c *Compiler) compileFlipFlop(v *ast.RangeLit) { + b := c.cur() + depth, slot := c.flipFlopSlot() + + b.emit(bytecode.OpGetLocal, slot, depth) + alreadyOn := b.emit(bytecode.OpBranchIf, 0, 0) + + c.compileCondition(v.Lo) + stayOff := b.emit(bytecode.OpBranchUnless, 0, 0) + b.emit(bytecode.OpPushTrue, 0, 0) + b.emit(bytecode.OpSetLocal, slot, depth) + b.emit(bytecode.OpPop, 0, 0) + turnedOn := -1 + if v.Exclusive { + turnedOn = b.emit(bytecode.OpJump, 0, 0) // `...` skips the right condition on the turn-on pass + } + + b.patch(alreadyOn, b.here()) + c.compileCondition(v.Hi) + stayOn := b.emit(bytecode.OpBranchUnless, 0, 0) + b.emit(bytecode.OpPushFalse, 0, 0) + b.emit(bytecode.OpSetLocal, slot, depth) + b.emit(bytecode.OpPop, 0, 0) + lastPass := b.emit(bytecode.OpJump, 0, 0) + + b.patch(stayOff, b.here()) + b.emit(bytecode.OpPushFalse, 0, 0) + done := b.emit(bytecode.OpJump, 0, 0) + + b.patch(stayOn, b.here()) + if turnedOn >= 0 { + b.patch(turnedOn, b.here()) + } + b.patch(lastPass, b.here()) + b.emit(bytecode.OpPushTrue, 0, 0) + b.patch(done, b.here()) +} diff --git a/internal/compiler/magic_encoding_test.go b/internal/compiler/magic_encoding_test.go index ef015c95..e12fb989 100644 --- a/internal/compiler/magic_encoding_test.go +++ b/internal/compiler/magic_encoding_test.go @@ -16,7 +16,7 @@ func TestMagicSourceEncoding(t *testing.T) { {"# encoding: us-ascii\n", "US-ASCII"}, {"# encoding: ascii\n", "US-ASCII"}, {"# encoding: utf-8\n", ""}, // UTF-8 is the default: no tag - {"# encoding: euc-jp\n", ""}, // unrecognised for tagging: no tag + {"# encoding: euc-jp\n", "euc-jp"}, // any other declared encoding passes through {"x = 1\n", ""}, // no magic comment {"puts 1 # encoding: binary\n", ""}, // must be a comment line, not trailing {"# just a plain comment\n", ""}, // a comment without a coding: field diff --git a/internal/compiler/opassign.go b/internal/compiler/opassign.go new file mode 100644 index 00000000..919bd03d --- /dev/null +++ b/internal/compiler/opassign.go @@ -0,0 +1,317 @@ +// Compound assignment to an index (`a[i] op= v`) and to an attribute +// (`a.x op= v`), lowered so the receiver and the index arguments are evaluated +// exactly once and the expression's value is the assigned value — never what +// the `[]=` / `x=` setter happens to return. +// +// The parser desugars both forms textually: `a[i] op= v` arrives as +// `a.[]=(i, a.[](i) op v)` and `a.x op= v` as `a.x=(a.x op v)`, with the +// receiver and index nodes SHARED (same AST pointers) between the read and the +// write. That shape is correct for value but wrong for effect: compiled +// literally it evaluates the receiver twice and every index argument twice. MRI +// does not: compile.c's compile_op_asgn1 evaluates the receiver and index once +// and reuses them with `dupn` (ruby/ruby v3_4_0 compile.c:9401), and +// compile_op_asgn2 does the same for an attribute with `dup` (compile.c:9535). +// Both then overwrite the reserved result slot with `setn`, so the expression +// yields the right-hand value (or, when `||=` / `&&=` short-circuits, the value +// already there). +// +// rbgo's stack has no `dupn`/`setn`/`topn`, so the same effect is obtained with +// anonymous local slots as temporaries. +package compiler + +import ( + "github.com/go-embedded-ruby/ruby/internal/bytecode" + "github.com/go-ruby-parser/parser/ast" +) + +// indexOpAssign recognises the parser's desugaring of `recv[idx…] op= rhs`, +// returning the operator/right-hand BinaryExpr and the index argument nodes. +// Recognition is by AST pointer identity between the write's receiver/index +// nodes and the read's: the parser builds the read from exactly those nodes, so +// identity is what distinguishes a desugared compound assignment from a +// hand-written `a[i] = (a[i] || v)` (whose two subtrees are distinct nodes and +// which Ruby does evaluate twice). +func indexOpAssign(v *ast.Call) (*ast.BinaryExpr, []ast.Node, bool) { + if v.Name != "[]=" || v.Recv == nil || v.Block != nil || v.Safe || len(v.Args) == 0 { + return nil, nil, false + } + idx := v.Args[:len(v.Args)-1] + be, ok := v.Args[len(v.Args)-1].(*ast.BinaryExpr) + if !ok { + return nil, nil, false + } + read, ok := be.Left.(*ast.Call) + if !ok || read.Name != "[]" || read.Recv != v.Recv || read.Block != nil || len(read.Args) != len(idx) { + return nil, nil, false + } + for i := range idx { + if read.Args[i] != idx[i] { + return nil, nil, false + } + } + return be, idx, true +} + +// attrOpAssign recognises the parser's desugaring of `recv.name op= rhs` +// (`recv.name=(recv.name op rhs)`), returning the operator/right-hand +// BinaryExpr. As for the index form, recognition is by pointer identity of the +// shared receiver node. The caller has already established that v is a setter +// call, so the trailing `=` is an attribute writer and not an operator method. +func attrOpAssign(v *ast.Call) (*ast.BinaryExpr, bool) { + if v.Name == "[]=" || v.Recv == nil || v.Block != nil || v.Safe || len(v.Args) != 1 { + return nil, false + } + base := v.Name[:len(v.Name)-1] + be, ok := v.Args[0].(*ast.BinaryExpr) + if !ok { + return nil, false + } + read, ok := be.Left.(*ast.Call) + if !ok || read.Name != base || read.Recv != v.Recv || read.Block != nil || len(read.Args) != 0 { + return nil, false + } + return be, true +} + +// stash evaluates node and parks its value in a fresh anonymous local slot, +// leaving the stack as it found it. OpSetLocal keeps the value on the stack, so +// the pop is explicit. +func (c *Compiler) stash(node ast.Node) int { + b := c.cur() + c.compileNode(node) + slot := b.localSlot("") + b.emit(bytecode.OpSetLocal, slot, 0) + b.emit(bytecode.OpPop, 0, 0) + return slot +} + +// stashTop parks the value already on top of the stack in a fresh anonymous +// local slot, consuming it. +func (c *Compiler) stashTop() int { + b := c.cur() + slot := b.localSlot("") + b.emit(bytecode.OpSetLocal, slot, 0) + b.emit(bytecode.OpPop, 0, 0) + return slot +} + +// compileIndexOpAssign lowers `recv[idx…] op= rhs` with the receiver and the +// index arguments evaluated exactly once, leaving the assignment's value on the +// stack. Mirrors MRI compile.c compile_op_asgn1 (v3_4_0:9401). +func (c *Compiler) compileIndexOpAssign(v *ast.Call, be *ast.BinaryExpr, idx []ast.Node) { + b := c.cur() + explicit := isExplicitRecv(v.Recv) + recvSlot := c.stash(v.Recv) + + // The index arguments are evaluated once. A `*splat` among them forces the + // dynamic array path (and so a single #to_a call), exactly as MRI's + // VM_CALL_ARGS_SPLAT branch does; otherwise each argument gets its own slot. + splat := hasSplat(idx) || hasTrailingKwSplat(idx) + var argSlots []int + arrSlot := -1 + if splat { + c.compileSplatItems(idx) + arrSlot = c.stashTop() + } else { + argSlots = make([]int, len(idx)) + for i, a := range idx { + argSlots[i] = c.stash(a) + } + } + + // read: recv[idx…] + pushArgs := func() { + b.emit(bytecode.OpGetLocal, recvSlot, 0) + if splat { + b.emit(bytecode.OpGetLocal, arrSlot, 0) + return + } + for _, s := range argSlots { + b.emit(bytecode.OpGetLocal, s, 0) + } + } + pushArgs() + if splat { + c.sendExplicit(b.emit(bytecode.OpSendArray, b.addName("[]"), 0), explicit) + } else { + c.sendExplicit(b.emit(bytecode.OpSend, b.addName("[]"), len(idx)), explicit) + } + + // write emits the `[]=` send for the value held in valSlot, consuming + // nothing from the stack and leaving nothing behind. + write := func(valSlot int) { + pushArgs() + b.emit(bytecode.OpGetLocal, valSlot, 0) + if splat { + // Append the assigned value to a fresh copy of the index array; + // OpConcatArray allocates, so the stashed array is left intact. + b.emit(bytecode.OpNewArray, 1, 0) + b.emit(bytecode.OpConcatArray, 0, 0) + c.sendExplicit(b.emit(bytecode.OpSendArray, b.addName("[]="), 0), explicit) + } else { + c.sendExplicit(b.emit(bytecode.OpSend, b.addName("[]="), len(idx)+1), explicit) + } + b.emit(bytecode.OpPop, 0, 0) // the setter's own return value is discarded + } + c.finishOpAssign(be, write) +} + +// compileAttrOpAssign lowers `recv.name op= rhs` with the receiver evaluated +// exactly once. Mirrors MRI compile.c compile_op_asgn2 (v3_4_0:9535). +func (c *Compiler) compileAttrOpAssign(v *ast.Call, be *ast.BinaryExpr) { + b := c.cur() + explicit := isExplicitRecv(v.Recv) + recvSlot := c.stash(v.Recv) + base := v.Name[:len(v.Name)-1] + + b.emit(bytecode.OpGetLocal, recvSlot, 0) + c.sendExplicit(b.emit(bytecode.OpSend, b.addName(base), 0), explicit) + + write := func(valSlot int) { + b.emit(bytecode.OpGetLocal, recvSlot, 0) + b.emit(bytecode.OpGetLocal, valSlot, 0) + c.sendExplicit(b.emit(bytecode.OpSend, b.addName(v.Name), 1), explicit) + b.emit(bytecode.OpPop, 0, 0) + } + c.finishOpAssign(be, write) +} + +// finishOpAssign completes a compound assignment whose current value is already +// on top of the stack. It computes the new value from be (short-circuiting for +// `||=` / `&&=`), hands it to write, and leaves the expression's value — the +// assigned value, or the untouched current value when the short circuit is +// taken — on the stack. +func (c *Compiler) finishOpAssign(be *ast.BinaryExpr, write func(valSlot int)) { + b := c.cur() + if be.Op == "||" || be.Op == "&&" { + b.emit(bytecode.OpDup, 0, 0) + var short int + if be.Op == "||" { + short = b.emit(bytecode.OpBranchIf, 0, 0) + } else { + short = b.emit(bytecode.OpBranchUnless, 0, 0) + } + b.emit(bytecode.OpPop, 0, 0) // discard the current value; it is being replaced + valSlot := c.stash(be.Right) + write(valSlot) + b.emit(bytecode.OpGetLocal, valSlot, 0) + done := b.emit(bytecode.OpJump, 0, 0) + b.patch(short, b.here()) // short circuit: the current value stays, and is the result + b.patch(done, b.here()) + return + } + // An arithmetic/other operator: combine the current value with the right-hand + // side, store that, and yield it. + c.compileNode(be.Right) + if op, ok := fastBinOp(be.Op); ok { + b.emit(op, 0, 0) + } else { + b.emit(bytecode.OpSend, b.addName(be.Op), 1) + } + valSlot := c.stashTop() + write(valSlot) + b.emit(bytecode.OpGetLocal, valSlot, 0) +} + +// isExplicitRecv reports whether a receiver node counts as an explicit receiver +// for the private-visibility check — every receiver except a literal `self`, +// matching compileCall. +func isExplicitRecv(recv ast.Node) bool { + _, isSelf := recv.(*ast.SelfLit) + return !isSelf +} + +// sendExplicit marks the send at instruction index at as having an explicit +// receiver, when it had one. +func (c *Compiler) sendExplicit(at int, explicit bool) int { + if explicit { + c.cur().insns[at].Flags |= bytecode.FlagSendExplicit + } + return at +} + +// scopedConstOrAssign recognises the parser's desugaring of `Mod::C ||= rhs`, +// which is `(defined?(Mod::C) && Mod::C) || (Mod::C = rhs)` with the SAME +// ScopedConst node in all three positions — so the module part `Mod` would be +// evaluated up to three times. MRI evaluates it once and keeps it on the stack +// (compile.c compile_op_cdecl, ruby/ruby v3_4_0:9657: the `cref` is compiled +// once, then `dup`/`defined`/`getconstant`/`setconstant` all read that one +// copy). Only a qualified constant has a module part to evaluate; a bare +// `C ||= v` has none. +func scopedConstOrAssign(v *ast.BinaryExpr) (*ast.ScopedConst, ast.Node, bool) { + if v.Op != "||" { + return nil, nil, false + } + asg, ok := v.Right.(*ast.ScopedConstAssign) + if !ok { + return nil, nil, false + } + sc, ok := asg.Target.(*ast.ScopedConst) + if !ok || sc.Recv == nil { + return nil, nil, false + } + guard, ok := v.Left.(*ast.BinaryExpr) + if !ok || guard.Op != "&&" || guard.Right != ast.Node(sc) { + return nil, nil, false + } + d, ok := guard.Left.(*ast.Call) + if !ok || d.Name != "defined?" || d.Recv != nil || len(d.Args) != 1 || d.Args[0] != ast.Node(sc) { + return nil, nil, false + } + return sc, asg.Value, true +} + +// scopedConstOpAssign recognises `Mod::C op= rhs` for every operator but `||` +// — the parser writes it as `Mod::C = (Mod::C op rhs)`, again sharing the +// ScopedConst node and so the module part. +func scopedConstOpAssign(v *ast.ScopedConstAssign) (*ast.ScopedConst, *ast.BinaryExpr, bool) { + sc, ok := v.Target.(*ast.ScopedConst) + if !ok || sc.Recv == nil { + return nil, nil, false + } + be, ok := v.Value.(*ast.BinaryExpr) + if !ok || be.Left != ast.Node(sc) { + return nil, nil, false + } + return sc, be, true +} + +// compileScopedConstOrAssign lowers `Mod::C ||= rhs` with the module part +// evaluated exactly once. The constant is read only when it is defined, so an +// undefined one assigns without raising; the right-hand side is evaluated only +// on the assigning path, so a module part that raises never reaches it. +func (c *Compiler) compileScopedConstOrAssign(sc *ast.ScopedConst, rhs ast.Node) { + b := c.cur() + modSlot := c.stash(sc.Recv) + name := b.addName(sc.Name) + b.emit(bytecode.OpGetLocal, modSlot, 0) + b.emit(bytecode.OpDefinedScopedConst, name, 0) + assign := b.emit(bytecode.OpBranchUnless, 0, 0) + b.emit(bytecode.OpGetLocal, modSlot, 0) + b.emit(bytecode.OpGetScopedConst, name, 0) + b.emit(bytecode.OpDup, 0, 0) + keep := b.emit(bytecode.OpBranchIf, 0, 0) // already truthy: that value is the result + b.emit(bytecode.OpPop, 0, 0) + b.patch(assign, b.here()) + valSlot := c.stash(rhs) + b.emit(bytecode.OpGetLocal, modSlot, 0) + b.emit(bytecode.OpGetLocal, valSlot, 0) + b.emit(bytecode.OpSetScopedConst, name, 0) // keeps the value as the result + b.patch(keep, b.here()) +} + +// compileScopedConstOpAssign lowers `Mod::C op= rhs` (including `&&=`) with the +// module part evaluated exactly once. +func (c *Compiler) compileScopedConstOpAssign(sc *ast.ScopedConst, be *ast.BinaryExpr) { + b := c.cur() + modSlot := c.stash(sc.Recv) + name := b.addName(sc.Name) + b.emit(bytecode.OpGetLocal, modSlot, 0) + b.emit(bytecode.OpGetScopedConst, name, 0) + c.finishOpAssign(be, func(valSlot int) { + b.emit(bytecode.OpGetLocal, modSlot, 0) + b.emit(bytecode.OpGetLocal, valSlot, 0) + b.emit(bytecode.OpSetScopedConst, name, 0) + b.emit(bytecode.OpPop, 0, 0) + }) +} diff --git a/internal/compiler/wave24_lang_test.go b/internal/compiler/wave24_lang_test.go new file mode 100644 index 00000000..47b4596c --- /dev/null +++ b/internal/compiler/wave24_lang_test.go @@ -0,0 +1,327 @@ +package compiler + +import ( + "testing" + + "github.com/go-embedded-ruby/ruby/internal/bytecode" + "github.com/go-ruby-parser/parser" + "github.com/go-ruby-parser/parser/ast" +) + +// The op-assign recognisers key on AST pointer identity between the write's +// receiver/index nodes and the read's, which is what the parser's desugaring +// produces. Every other shape must fall through to the ordinary setter path — +// notably a hand-written `a[i] = (a[i] || v)`, which Ruby really does evaluate +// twice. +func TestIndexOpAssignRejectsOtherShapes(t *testing.T) { + recv := &ast.VarRef{Name: "a"} + idx := &ast.IntLit{Value: 1} + read := func(r ast.Node, args ...ast.Node) *ast.Call { + return &ast.Call{Recv: r, Name: "[]", Args: args} + } + cases := []struct { + name string + call *ast.Call + }{ + {"not a setter name", &ast.Call{Recv: recv, Name: "[]", Args: []ast.Node{idx}}}, + {"no receiver", &ast.Call{Name: "[]=", Args: []ast.Node{idx}}}, + {"carries a block", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx}, Block: &ast.Block{}}}, + {"safe navigation", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx}, Safe: true}}, + {"no arguments", &ast.Call{Recv: recv, Name: "[]="}}, + {"value is not a binary expression", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, &ast.IntLit{Value: 2}}}}, + {"value's left side is not a call", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: &ast.IntLit{Value: 2}, Right: &ast.IntLit{Value: 3}}}}}, + {"read is not #[]", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: &ast.Call{Recv: recv, Name: "at", Args: []ast.Node{idx}}, Right: &ast.IntLit{Value: 3}}}}}, + {"read has a different receiver", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: read(&ast.VarRef{Name: "a"}, idx), Right: &ast.IntLit{Value: 3}}}}}, + {"read has a block", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: &ast.Call{Recv: recv, Name: "[]", Args: []ast.Node{idx}, Block: &ast.Block{}}, Right: &ast.IntLit{Value: 3}}}}}, + {"read has a different argument count", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: read(recv), Right: &ast.IntLit{Value: 3}}}}}, + {"read has a different index node", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: read(recv, &ast.IntLit{Value: 1}), Right: &ast.IntLit{Value: 3}}}}}, + } + for _, tc := range cases { + if _, _, ok := indexOpAssign(tc.call); ok { + t.Errorf("indexOpAssign accepted %s", tc.name) + } + } + // The shape the parser really produces is accepted. + good := &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{idx, + &ast.BinaryExpr{Op: "||", Left: read(recv, idx), Right: &ast.IntLit{Value: 3}}}} + be, got, ok := indexOpAssign(good) + if !ok || be.Op != "||" || len(got) != 1 || got[0] != ast.Node(idx) { + t.Fatalf("indexOpAssign rejected the parser's own shape: %v %v %v", be, got, ok) + } +} + +func TestAttrOpAssignRejectsOtherShapes(t *testing.T) { + recv := &ast.VarRef{Name: "o"} + cases := []struct { + name string + call *ast.Call + }{ + {"index setter", &ast.Call{Recv: recv, Name: "[]=", Args: []ast.Node{&ast.IntLit{Value: 1}}}}, + {"no receiver", &ast.Call{Name: "x=", Args: []ast.Node{&ast.IntLit{Value: 1}}}}, + {"carries a block", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{&ast.IntLit{Value: 1}}, Block: &ast.Block{}}}, + {"safe navigation", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{&ast.IntLit{Value: 1}}, Safe: true}}, + {"two arguments", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{&ast.IntLit{Value: 1}, &ast.IntLit{Value: 2}}}}, + {"value is not a binary expression", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{&ast.IntLit{Value: 1}}}}, + {"value's left side is not a call", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{ + &ast.BinaryExpr{Op: "||", Left: &ast.IntLit{Value: 1}, Right: &ast.IntLit{Value: 2}}}}}, + {"read names another method", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{ + &ast.BinaryExpr{Op: "||", Left: &ast.Call{Recv: recv, Name: "y"}, Right: &ast.IntLit{Value: 2}}}}}, + {"read has a different receiver", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{ + &ast.BinaryExpr{Op: "||", Left: &ast.Call{Recv: &ast.VarRef{Name: "o"}, Name: "x"}, Right: &ast.IntLit{Value: 2}}}}}, + {"read has a block", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{ + &ast.BinaryExpr{Op: "||", Left: &ast.Call{Recv: recv, Name: "x", Block: &ast.Block{}}, Right: &ast.IntLit{Value: 2}}}}}, + {"read takes arguments", &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{ + &ast.BinaryExpr{Op: "||", Left: &ast.Call{Recv: recv, Name: "x", Args: []ast.Node{&ast.IntLit{Value: 1}}}, Right: &ast.IntLit{Value: 2}}}}}, + } + for _, tc := range cases { + if _, ok := attrOpAssign(tc.call); ok { + t.Errorf("attrOpAssign accepted %s", tc.name) + } + } + good := &ast.Call{Recv: recv, Name: "x=", Args: []ast.Node{ + &ast.BinaryExpr{Op: "+", Left: &ast.Call{Recv: recv, Name: "x"}, Right: &ast.IntLit{Value: 2}}}} + if be, ok := attrOpAssign(good); !ok || be.Op != "+" { + t.Fatalf("attrOpAssign rejected the parser's own shape: %v %v", be, ok) + } +} + +func TestScopedConstOpAssignRecognisers(t *testing.T) { + sc := &ast.ScopedConst{Recv: &ast.ConstRef{Name: "M"}, Name: "C"} + bare := &ast.ScopedConst{Name: "C"} // leading `::C`: no module part to evaluate + rhs := &ast.IntLit{Value: 1} + guard := func(n ast.Node) *ast.BinaryExpr { + return &ast.BinaryExpr{Op: "&&", Left: &ast.Call{Name: "defined?", Args: []ast.Node{n}}, Right: n} + } + + orCases := []struct { + name string + expr *ast.BinaryExpr + }{ + {"not ||", &ast.BinaryExpr{Op: "&&", Left: guard(sc), Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"right is not a scoped assign", &ast.BinaryExpr{Op: "||", Left: guard(sc), Right: rhs}}, + {"target is not a scoped const", &ast.BinaryExpr{Op: "||", Left: guard(sc), + Right: &ast.ScopedConstAssign{Target: &ast.ConstRef{Name: "C"}, Value: rhs}}}, + {"no module part", &ast.BinaryExpr{Op: "||", Left: guard(bare), + Right: &ast.ScopedConstAssign{Target: bare, Value: rhs}}}, + {"left is not a binary expression", &ast.BinaryExpr{Op: "||", Left: rhs, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"left is not &&", &ast.BinaryExpr{Op: "||", Left: &ast.BinaryExpr{Op: "||", Left: sc, Right: sc}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"guard reads another constant", &ast.BinaryExpr{Op: "||", + Left: &ast.BinaryExpr{Op: "&&", Left: &ast.Call{Name: "defined?", Args: []ast.Node{sc}}, Right: rhs}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"guard is not defined?", &ast.BinaryExpr{Op: "||", + Left: &ast.BinaryExpr{Op: "&&", Left: &ast.Call{Name: "frozen?", Args: []ast.Node{sc}}, Right: sc}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"guard is not a call", &ast.BinaryExpr{Op: "||", + Left: &ast.BinaryExpr{Op: "&&", Left: rhs, Right: sc}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"defined? has a receiver", &ast.BinaryExpr{Op: "||", + Left: &ast.BinaryExpr{Op: "&&", Left: &ast.Call{Recv: sc, Name: "defined?", Args: []ast.Node{sc}}, Right: sc}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"defined? has no argument", &ast.BinaryExpr{Op: "||", + Left: &ast.BinaryExpr{Op: "&&", Left: &ast.Call{Name: "defined?"}, Right: sc}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + {"defined? names another constant", &ast.BinaryExpr{Op: "||", + Left: &ast.BinaryExpr{Op: "&&", Left: &ast.Call{Name: "defined?", Args: []ast.Node{rhs}}, Right: sc}, + Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}}}, + } + for _, tc := range orCases { + if _, _, ok := scopedConstOrAssign(tc.expr); ok { + t.Errorf("scopedConstOrAssign accepted %s", tc.name) + } + } + good := &ast.BinaryExpr{Op: "||", Left: guard(sc), Right: &ast.ScopedConstAssign{Target: sc, Value: rhs}} + if got, val, ok := scopedConstOrAssign(good); !ok || got != sc || val != ast.Node(rhs) { + t.Fatalf("scopedConstOrAssign rejected the parser's own shape: %v %v %v", got, val, ok) + } + + opCases := []struct { + name string + expr *ast.ScopedConstAssign + }{ + {"target is not a scoped const", &ast.ScopedConstAssign{Target: &ast.ConstRef{Name: "C"}, Value: rhs}}, + {"no module part", &ast.ScopedConstAssign{Target: bare, Value: &ast.BinaryExpr{Op: "+", Left: bare, Right: rhs}}}, + {"value is not a binary expression", &ast.ScopedConstAssign{Target: sc, Value: rhs}}, + {"value does not read the target", &ast.ScopedConstAssign{Target: sc, + Value: &ast.BinaryExpr{Op: "+", Left: rhs, Right: rhs}}}, + } + for _, tc := range opCases { + if _, _, ok := scopedConstOpAssign(tc.expr); ok { + t.Errorf("scopedConstOpAssign accepted %s", tc.name) + } + } + goodOp := &ast.ScopedConstAssign{Target: sc, Value: &ast.BinaryExpr{Op: "&&", Left: sc, Right: rhs}} + if got, be, ok := scopedConstOpAssign(goodOp); !ok || got != sc || be.Op != "&&" { + t.Fatalf("scopedConstOpAssign rejected the parser's own shape: %v %v %v", got, be, ok) + } +} + +// A range is a flip-flop only where a condition is expected, and only when it +// has both bounds; hasFlipFlop is the gate that keeps every other expression on +// the untouched compileNode path. +func TestHasFlipFlop(t *testing.T) { + two := &ast.RangeLit{Lo: &ast.IntLit{Value: 1}, Hi: &ast.IntLit{Value: 2}} + cases := []struct { + node ast.Node + want bool + }{ + {two, true}, + {&ast.RangeLit{Hi: &ast.IntLit{Value: 2}}, false}, // beginless: a Range value + {&ast.RangeLit{Lo: &ast.IntLit{Value: 1}}, false}, // endless: a Range value + {&ast.BinaryExpr{Op: "||", Left: two, Right: &ast.BoolLit{}}, true}, // left operand + {&ast.BinaryExpr{Op: "&&", Left: &ast.BoolLit{}, Right: two}, true}, // right operand + {&ast.BinaryExpr{Op: "+", Left: two, Right: two}, false}, // not a logical operator + {&ast.BinaryExpr{Op: "||", Left: &ast.BoolLit{}, Right: &ast.BoolLit{}}, false}, + {&ast.UnaryExpr{Op: "!", Operand: two}, true}, + {&ast.UnaryExpr{Op: "-", Operand: two}, false}, + {&ast.IntLit{Value: 1}, false}, + } + for i, tc := range cases { + if got := hasFlipFlop(tc.node); got != tc.want { + t.Errorf("case %d: hasFlipFlop = %v, want %v", i, got, tc.want) + } + } +} + +// flipFlopSlot stops at a borrowed scope: under CompileWithLocals the parent +// holds a Binding's locals in a frame that is already sized, so the slot must +// land in the eval's own scope at depth 0. +func TestFlipFlopSlotStopsAtBorrowedScope(t *testing.T) { + c := &Compiler{} + parent := newBuilder("", nil) + parent.locals = []string{"a", "b"} + parent.borrowed = true + c.push(parent) + child := newBuilder("(eval)", nil) + child.isBlock = true + child.parent = parent + c.push(child) + depth, slot := c.flipFlopSlot() + if depth != 0 { + t.Fatalf("depth = %d, want 0 (the borrowed parent must not grow)", depth) + } + if slot != 0 || len(parent.locals) != 2 { + t.Fatalf("slot = %d, parent locals = %v; want a fresh slot in the child", slot, parent.locals) + } + // An ordinary block scope, by contrast, is walked through to the method. + c = &Compiler{} + method := newBuilder("m", nil) + c.push(method) + blk := newBlockBuilder("b", nil, method) + c.push(blk) + if depth, _ := c.flipFlopSlot(); depth != 1 { + t.Fatalf("depth = %d, want 1 (the enclosing method scope)", depth) + } +} + +func TestMagicEncodingNameValueTermination(t *testing.T) { + cases := []struct{ line, want string }{ + {"# vim: filetype=ruby, fileencoding=big5, tabsize=3", "big5"}, // comma-separated vim form + {"# coding: big5", "big5"}, + {"# coding:binary-*-", "binary"}, // `-*-` still closes the value + {"# coding: binary;", "binary"}, + {"# -*- coding: euc-jp -*-", "euc-jp"}, + {"# coding: utf_8", "utf_8"}, // `_` is a name byte + {"x = 1", ""}, // not a comment + {"# a plain comment", ""}, // no coding field + } + for _, tc := range cases { + if got := magicEncodingName(tc.line); got != tc.want { + t.Errorf("magicEncodingName(%q) = %q, want %q", tc.line, got, tc.want) + } + } +} + +func TestIsEncNameByte(t *testing.T) { + for _, b := range []byte{'a', 'z', 'A', 'Z', '0', '9', '-', '_'} { + if !isEncNameByte(b) { + t.Errorf("isEncNameByte(%q) = false, want true", b) + } + } + for _, b := range []byte{' ', ',', ';', '*', ':', '"', '\t'} { + if isEncNameByte(b) { + t.Errorf("isEncNameByte(%q) = true, want false", b) + } + } +} + +// mustCompile parses and compiles src, failing the test if either step does. +// Compiling is enough to reach every lowering branch; the vm package's tests +// check what the emitted sequences actually do. +func mustCompile(t *testing.T, src string) *bytecode.ISeq { + t.Helper() + prog, err := parser.Parse(src) + if err != nil { + t.Fatalf("parse %q: %v", src, err) + } + iseq, err := CompileWithEncoding(prog, "") + if err != nil { + t.Fatalf("compile %q: %v", src, err) + } + return iseq +} + +// Every lowering this wave added, compiled. Each line names a branch: the +// splat and non-splat index forms, `||=` / `&&=` / an arithmetic operator, the +// attribute form, the two scoped-constant forms, both flip-flop spellings in +// each condition position, and a loop break with and without a value. +func TestWave24LoweringsCompile(t *testing.T) { + for _, src := range []string{ + // Compound assignment to an index. + "h = {}; h[:a] ||= 1", + "h = {}; h[:a] &&= 1", + "h = {}; h[:a] += 1", + "h = {}; h[:a] **= 2", // an operator with no fast-path opcode dispatches as a send + "h = {}; h[:a, :b] ||= 1", + "h = {}; h[*[:a]] ||= 1", + "h = {}; h[*[:a]] += 1", + "h = {}; self[*[:a]] ||= 1", // an implicit-self receiver takes no explicit-send flag + "h = {}; h[*[:a]] = 1", // a plain splatted setter, not a compound assignment + // Compound assignment to an attribute. + "o = Object.new; o.v ||= 1", + "o = Object.new; o.v &&= 1", + "o = Object.new; o.v += 1", + "o = Object.new; o.v <<= 1", + "self.v ||= 1", + // Compound assignment to a qualified constant. + "module M; end; M::C ||= 1", + "module M; end; M::C &&= 1", + "module M; end; M::C += 1", + "C ||= 1", // bare: no module part + "::C ||= 1", // toplevel-qualified: no module part + // Flip-flops, in each condition position and both spellings. + "10.times { |i| i if (i == 1)..(i == 2) }", + "10.times { |i| i if (i == 1)...(i == 2) }", + "10.times { |i| i unless (i == 1)..(i == 2) }", + "10.times { |i| i if (i == 1)..(i == 2) or (i == 3)..(i == 4) }", + "10.times { |i| i if (i == 1)..(i == 2) and i.odd? }", + "10.times { |i| (i == 1)..(i == 2) ? :a : :b }", + "i = 0; while (i == 1)..(i == 2); i += 1; end", + "i = 0; until (i == 1)..(i == 2); i += 1; end", + "i = 0; begin; i += 1; end while (i == 1)..(i == 2)", + "if false then 1 elsif (1 == 1)..(2 == 2) then 2 else 3 end", + "x = (1..2); x = (1..); x = (..2)", // ordinary Ranges, untouched + "1 if 1 == 1 or 2 == 2", // an ordinary logical condition, untouched + "1 unless 1 == 1", // an ordinary negated condition, untouched + // Loop break, with and without a value. + "while true; break; end", + "while true; break 1; end", + "while true; break *[1, 2]; end", + "until false; break 1; end", + "i = 0; begin; i += 1; break 2; end while true", + "i = 0; while i < 3; i += 1; end", // no break at all + "[1].each { break 1 }", // a block break, not a loop break + "i = 0; while i < 3; i += 1; next; end", + } { + if iseq := mustCompile(t, src); len(iseq.Insns) == 0 { + t.Errorf("compiled %q to nothing", src) + } + } +} diff --git a/internal/vm/prelude_frozen_gen.go b/internal/vm/prelude_frozen_gen.go index b5bf94a8..e1c5ac8c 100644 --- a/internal/vm/prelude_frozen_gen.go +++ b/internal/vm/prelude_frozen_gen.go @@ -1113,7 +1113,7 @@ func embeddedPrelude() *bytecode.ISeq { Children: []*bytecode.ISeq{ &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 52}, {Op: 28, A: 5}, {Op: 11}, {Op: 48, A: 1}, {Op: 26, A: 6}, {Op: 11}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 30, Flags: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 4}, {Op: 12, A: 3}, {Op: 9}, {Op: 1}, {Op: 12, A: 4}, {Op: 9}, {Op: 11, A: 4}, {Op: 11, A: 1, B: 1}, {Op: 30, A: 1, Flags: 1}, {Op: 18}, {Op: 28, A: 50}, {Op: 11, A: 2, B: 1}, {Op: 11, A: 4}, {Op: 30, A: 2, B: 1, Flags: 1}, {Op: 11, A: 2}, {Op: 22}, {Op: 10}, {Op: 28, A: 36}, {Op: 9}, {Op: 11, A: 1, B: 1}, {Op: 11, A: 4}, {Op: 30, A: 2, B: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 30, A: 3, B: 1, Flags: 1}, {Op: 28, A: 42}, {Op: 3}, {Op: 12, A: 3}, {Op: 9}, {Op: 26, A: 50}, {Op: 26, A: 43}, {Op: 2}, {Op: 9}, {Op: 11, A: 4}, {Op: 1, A: 1}, {Op: 13}, {Op: 12, A: 4}, {Op: 9}, {Op: 26, A: 18}, {Op: 2}, {Op: 9}, {Op: 11, A: 3}, {Op: 25}, {Op: 28, A: 67}, {Op: 11, A: 1, B: 1}, {Op: 11, A: 1}, {Op: 30, A: 4, B: 1}, {Op: 9}, {Op: 11, A: 2, B: 1}, {Op: 11, A: 2}, {Op: 30, A: 4, B: 1}, {Op: 9}, {Op: 11, B: 1}, {Op: 11}, {Op: 30, A: 4, B: 1}, {Op: 26, A: 68}, {Op: 2}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 52}, {Op: 28, A: 5}, {Op: 11}, {Op: 48, A: 1}, {Op: 26, A: 6}, {Op: 11}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 30, Flags: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 4}, {Op: 12, A: 3}, {Op: 9}, {Op: 1}, {Op: 12, A: 4}, {Op: 9}, {Op: 11, A: 4}, {Op: 11, A: 1, B: 1}, {Op: 30, A: 1, Flags: 1}, {Op: 18}, {Op: 28, A: 53}, {Op: 11, A: 2, B: 1}, {Op: 11, A: 4}, {Op: 30, A: 2, B: 1, Flags: 1}, {Op: 11, A: 2}, {Op: 22}, {Op: 10}, {Op: 28, A: 36}, {Op: 9}, {Op: 11, A: 1, B: 1}, {Op: 11, A: 4}, {Op: 30, A: 2, B: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 30, A: 3, B: 1, Flags: 1}, {Op: 28, A: 45}, {Op: 3}, {Op: 12, A: 3}, {Op: 9}, {Op: 2}, {Op: 12, A: 5}, {Op: 9}, {Op: 26, A: 55}, {Op: 26, A: 46}, {Op: 2}, {Op: 9}, {Op: 11, A: 4}, {Op: 1, A: 1}, {Op: 13}, {Op: 12, A: 4}, {Op: 9}, {Op: 26, A: 18}, {Op: 2}, {Op: 26, A: 56}, {Op: 11, A: 5}, {Op: 9}, {Op: 11, A: 3}, {Op: 25}, {Op: 28, A: 72}, {Op: 11, A: 1, B: 1}, {Op: 11, A: 1}, {Op: 30, A: 4, B: 1}, {Op: 9}, {Op: 11, A: 2, B: 1}, {Op: 11, A: 2}, {Op: 30, A: 4, B: 1}, {Op: 9}, {Op: 11, B: 1}, {Op: 11}, {Op: 30, A: 4, B: 1}, {Op: 26, A: 73}, {Op: 2}, {Op: 53}}, Consts: []object.Value{ object.IntValue(0), object.IntValue(1), @@ -1124,8 +1124,8 @@ func embeddedPrelude() *bytecode.ISeq { SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 5, - Locals: []string{"x", "key", "h", "seen", "i"}, + NumLocals: 6, + Locals: []string{"x", "key", "h", "seen", "i", ""}, Super: "", }, }, @@ -1504,15 +1504,15 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 11}, {Op: 48, A: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 30, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 13}, {Op: 9}, {Op: 6}, {Op: 12, A: 2}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 11}, {Op: 30, A: 2, B: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11}, {Op: 48, A: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, B: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 11, A: 1}, {Op: 12, A: 3}, {Op: 9}, {Op: 11, A: 2}, {Op: 11, A: 3}, {Op: 30, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 26}, {Op: 9}, {Op: 6}, {Op: 12, A: 4}, {Op: 9}, {Op: 11, A: 2}, {Op: 11, A: 3}, {Op: 11, A: 4}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 4}, {Op: 26, A: 26}, {Op: 11}, {Op: 30, A: 2, B: 1}, {Op: 53}}, Names: []string{"[]", "[]=", "<<"}, Params: []string{"x"}, NumRequired: 1, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 3, - Locals: []string{"x", "k", ""}, + NumLocals: 5, + Locals: []string{"x", "k", "", "", ""}, Super: "", }, }, @@ -3516,15 +3516,15 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 11, B: 1}, {Op: 11}, {Op: 30, B: 1, Flags: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1, B: 1}, {Op: 11, A: 1}, {Op: 11, A: 1, B: 1}, {Op: 11, A: 1}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 16}, {Op: 9}, {Op: 5}, {Op: 30, A: 2}, {Op: 30, A: 3, Flags: 1}, {Op: 12, A: 2}, {Op: 30, A: 4, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 11}, {Op: 30, A: 5, B: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11, B: 1}, {Op: 11}, {Op: 30, B: 1, Flags: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1, B: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 11, A: 1}, {Op: 12, A: 3}, {Op: 9}, {Op: 11, A: 2}, {Op: 11, A: 3}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 29}, {Op: 9}, {Op: 5}, {Op: 30, A: 2}, {Op: 30, A: 3, Flags: 1}, {Op: 12, A: 4}, {Op: 9}, {Op: 11, A: 2}, {Op: 11, A: 3}, {Op: 11, A: 4}, {Op: 30, A: 4, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 4}, {Op: 26, A: 29}, {Op: 11}, {Op: 30, A: 5, B: 1}, {Op: 53}}, Names: []string{"call", "[]", "class", "new", "[]=", "<<"}, Params: []string{"o"}, NumRequired: 1, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 3, - Locals: []string{"o", "k", ""}, + NumLocals: 5, + Locals: []string{"o", "k", "", "", ""}, Super: "", }, }, diff --git a/internal/vm/vm.go b/internal/vm/vm.go index 28a75d81..e56f88a2 100644 --- a/internal/vm/vm.go +++ b/internal/vm/vm.go @@ -1558,6 +1558,7 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, if !ok { raise("TypeError", "%s is not a class/module", recv.Inspect()) } + vm.warnAlreadyInitialized(cls, iseq.Names[in.A]) vm.assignConstIn(cls, iseq.Names[in.A], val) push(val) case bytecode.OpSetConst: @@ -1570,6 +1571,7 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, // class_eval body finds it by the same lexical path (Puppet's file // type defines CREATORS in its newtype block and a nested `validate` // block reads it back this way). + vm.warnAlreadyInitialized(lexCref, iseq.Names[in.A]) vm.assignConst(lexCref, iseq.Names[in.A], stack[len(stack)-1]) case bytecode.OpGetGVar: push(vm.gvar(iseq.Names[in.A])) @@ -2308,6 +2310,26 @@ func (vm *VM) constTable(parent *RClass) map[string]object.Value { return parent.consts } +// warnAlreadyInitialized emits MRI's "already initialized constant" warning when +// a syntactic constant assignment overwrites a value already in scope's own +// table. variable.c's const_tbl_update (ruby/ruby v3_4_0:3658) warns through +// rb_warn — so `$VERBOSE = nil` silences it, which is what ruby/spec's +// suppress_warning relies on — and qualifies the name with the module unless +// that module is Object. (struct.go's warnRedefineConst writes the same text +// through Kernel#warn, which `$VERBOSE = nil` does not silence; the syntactic +// assignment needs rb_warn's behaviour, hence the separate helper. MRI also +// prefixes file:line and follows with "previous definition ... was here"; rbgo +// carries no source map yet, so neither is emitted, as elsewhere in this VM.) +func (vm *VM) warnAlreadyInitialized(scope *RClass, name string) { + if scope == nil { + scope = vm.cObject + } + if _, exists := scope.consts[name]; !exists { + return + } + vm.rbWarn("warning: already initialized constant %s", scopedNameFor(scope, name)) +} + // assignConst sets a bare constant assignment (`NAME = value`) into the current // lexical scope's table. A nil definee (defensive) or Object writes top-level. func (vm *VM) assignConst(definee *RClass, name string, val object.Value) { diff --git a/internal/vm/wave24_lang_test.go b/internal/vm/wave24_lang_test.go new file mode 100644 index 00000000..762fe2de --- /dev/null +++ b/internal/vm/wave24_lang_test.go @@ -0,0 +1,312 @@ +package vm + +import ( + "bytes" + "strings" + "testing" + + "github.com/go-embedded-ruby/ruby/internal/compiler" + "github.com/go-embedded-ruby/ruby/internal/object" + "github.com/go-ruby-parser/parser" +) + +// runSrcEnc runs src through a fresh VM with the source encoding its magic +// comment declares, the way a loaded or required file is compiled. +func runSrcEnc(t *testing.T, src string) string { + t.Helper() + prog, err := parser.Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + iseq, err := compiler.CompileWithEncoding(prog, compiler.MagicSourceEncoding(src)) + if err != nil { + t.Fatalf("compile: %v", err) + } + var buf bytes.Buffer + if _, err := New(&buf).Run(iseq); err != nil { + t.Fatalf("run: %v", err) + } + return strings.TrimRight(buf.String(), "\n") +} + +func checkSrc(t *testing.T, name, src, want string) { + t.Helper() + if got := runSrcEnc(t, src); got != want { + t.Errorf("%s:\n got %q\nwant %q", name, got, want) + } +} + +// A compound assignment to an index evaluates the receiver and every index +// argument exactly once, and yields the assigned value rather than whatever +// `[]=` returns. Every expectation here was taken from MRI 4.0.5. +func TestIndexOpAssignEvaluatesOnce(t *testing.T) { + checkSrc(t, "receiver and index evaluated once", ` +$log = [] +def rv(x); $log << :recv; x; end +def ix(x); $log << :idx; x; end +h = {} +rv(h)[ix(:a)] ||= 1 +p $log +$log = [] +rv(h)[ix(:a)] ||= 2 # the short-circuit path evaluates them once too +p [$log, h] +$log = [] +rv(h)[ix(:a)] += 1 +p [$log, h] +$log = [] +rv(h)[ix(:a)] &&= 9 +p [$log, h] +`, "[:recv, :idx]\n[[:recv, :idx], {a: 1}]\n[[:recv, :idx], {a: 2}]\n[[:recv, :idx], {a: 9}]") + + checkSrc(t, "the setter's return value is discarded", ` +class C + def initialize; @h = {}; end + def []=(*a); @h[a[0...-1]] = a[-1]; :setter_return; end + def [](*a); @h[a]; end +end +c = C.new +p(c[1] = 99) +p(c[*[1]] = 98) +p(c[1, 2] = 97) +p(c[*[1], 2] = 96) +p(c[3] ||= 5) +p(c[3] ||= 6) +p(c[4] += 1) rescue p $!.class +`, "99\n98\n97\n96\n5\n5\nTypeError") + + checkSrc(t, "a splatted index calls #to_a once", ` +k = Object.new +def k.to_a; ($log ||= []) << :to_a; [:k]; end +$log = [] +b = {} +p(b[*k] ||= 20) +p [$log, b] +p(b[*[:x], *[:y]] = 3) +`, "20\n[[:to_a], {k: 20}]\n3") + + checkSrc(t, "an attribute is read through one receiver evaluation", ` +class A; attr_accessor :v; end +$log = [] +def ra(x); $log << :recv; x; end +a = A.new +ra(a).v ||= 10 +p [$log, a.v] +$log = [] +ra(a).v ||= 20 +p [$log, a.v] +$log = [] +p(ra(a).v += 5) +p [$log, a.v] +$log = [] +p(ra(a).v &&= 1) +p [$log, a.v] +`, "[[:recv], 10]\n[[:recv], 10]\n15\n[[:recv], 15]\n1\n[[:recv], 1]") + + // A hand-written read-then-write is NOT a compound assignment: Ruby evaluates + // its two subtrees independently, so the receiver is evaluated twice. The + // pointer-identity gate is what keeps them apart. + checkSrc(t, "a spelled-out read and write still evaluates twice", ` +$log = [] +def rv(x); $log << :recv; x; end +h = {} +rv(h)[:a] = (rv(h)[:a] || 1) +p $log +`, "[:recv, :recv]") +} + +// `Mod::C op= v` evaluates the module part once, and `||=` does not reach the +// right-hand side when the module part raises. +func TestScopedConstantOpAssignEvaluatesModuleOnce(t *testing.T) { + checkSrc(t, "module part evaluated once", ` +$VERBOSE = nil +module M; end +x = 0 +(x += 1; M)::A ||= :assigned +p [x, M::A] +x = 0 +(x += 1; M)::A ||= :again +p [x, M::A] +M::B = nil +x = 0 +(x += 1; M)::B ||= :assigned +p [x, M::B] +M::D = 1 +x = 0 +p((x += 1; M)::D += 2) +p [x, M::D] +x = 0 +p((x += 1; M)::D &&= 9) +p [x, M::D] +M::E = false +x = 0 +p((x += 1; M)::E &&= 9) +p [x, M::E] +`, "[1, :assigned]\n[1, :assigned]\n[1, :assigned]\n3\n[1, 3]\n9\n[1, 9]\nfalse\n[1, false]") + + checkSrc(t, "a raising module part never reaches the right-hand side", ` +module M2; end +x = 0 +y = 0 +begin + (x += 1; raise "boom"; M2)::C ||= (y += 1; :assigned) +rescue => e + p [x, y, e.message] +end +p defined?(M2::C) +`, `[1, 0, "boom"]`+"\nnil") + + // A leading `::C` has no module part, so it keeps the ordinary path. + checkSrc(t, "a toplevel-qualified constant still assigns", ` +$VERBOSE = nil +::TOPC ||= 1 +::TOPC ||= 2 +p ::TOPC +`, "1") +} + +// Reassigning an already-initialized constant warns, qualified by the module +// unless that module is Object, and `$VERBOSE = nil` silences it — MRI's +// rb_warn behaviour. (rbgo leaves $VERBOSE unset, where MRI starts it at false, +// so these set it explicitly to reach the warning at all.) +func TestAlreadyInitializedConstantWarning(t *testing.T) { + checkSrc(t, "toplevel constant", "$VERBOSE = false\nX = 1\nX = 2\np X\n", + "warning: already initialized constant X\n2") + checkSrc(t, "qualified constant", "$VERBOSE = false\nmodule M; end\nM::Y = 1\nM::Y = 2\np M::Y\n", + "warning: already initialized constant M::Y\n2") + checkSrc(t, "inside a class body", "$VERBOSE = false\nclass K; Z = 1; Z = 2; end\np K::Z\n", + "warning: already initialized constant K::Z\n2") + checkSrc(t, "silenced by $VERBOSE = nil", "$VERBOSE = nil\nX = 1\nX = 2\np X\n", "2") + checkSrc(t, "a first assignment does not warn", "$VERBOSE = false\nmodule M; end\nM::Q = 1\np M::Q\n", "1") +} + +// A two-sided range in condition position is the flip-flop operator. Every +// expectation here was taken from MRI 4.0.5. +func TestFlipFlop(t *testing.T) { + checkSrc(t, "inclusive and exclusive ends", ` +$s = []; 10.times { |i| $s << i if (i == 4)..(i == 4) }; p $s +$s = []; 10.times { |i| $s << i if (i == 4)..(i == 7) }; p $s +$s = []; 10.times { |i| $s << i if (i == 4)...(i == 4) }; p $s +$s = []; 10.times { |i| $s << i if (i == 4)...(i == 5) }; p $s +`, "[4]\n[4, 5, 6, 7]\n[4, 5, 6, 7, 8, 9]\n[4, 5]") + + checkSrc(t, "combined with or, and negated by unless", ` +$s = []; 10.times { |i| $s << i if (i == 4)...(i == 5) or (i == 7)...(i == 8) }; p $s +$s = []; 10.times { |i| $s << i unless (i == 4)..(i == 7) }; p $s +$s = []; 10.times { |i| $s << i if (i == 4)..(i == 7) and i.odd? }; p $s +`, "[4, 5, 7, 8]\n[0, 1, 2, 3, 8, 9]\n[5, 7]") + + checkSrc(t, "each side is evaluated lazily", ` +$s = []; c = proc { |i| $s << i }; 10.times { |i| i if c[i]...false }; p $s +$s = []; c = proc { |i| $s << i }; 10.times { |i| i if c[i]..false }; p $s +$s = []; c = proc { |i| $s << i }; 10.times { |i| i if (i == 4)...c[i] }; p $s +$s = []; c = proc { |i| $s << i }; 10.times { |i| i if (i == 4)..c[i] }; p $s +`, "[0]\n[0]\n[5]\n[4]") + + checkSrc(t, "state is per flip-flop and shared across calls of one proc", ` +$s = [] +store_me = proc { |i| $s << i if (i == 4)..(i == 7) } +store_me[1]; store_me[4]; proc { store_me[1] }.call; store_me[7]; store_me[5] +p $s +`, "[4, 1, 7]") + + checkSrc(t, "two identical flip-flops do not interfere", ` +$s = [] +a = eval "proc { |i| $s << i if (i == 4)..(i == 7) }" +b = eval "proc { |i| $s << i if (i == 4)..(i == 7) }" +6.times(&a); 6.times(&b); p $s +`, "[4, 5, 4, 5]") + + checkSrc(t, "in a while condition and in a ternary", ` +$s = []; i = 0; while i < 10; $s << i if (i == 3)..(i == 5); i += 1; end; p $s +$s = []; i = 0; until (i == 3)..(i == 5); $s << i; i += 1; end; p $s +$s = []; 6.times { |i| $s << ((i == 2)..(i == 3) ? :on : :off) }; p $s +`, "[3, 4, 5]\n[0, 1, 2]\n[:off, :off, :on, :on, :off, :off]") + + // A one-sided range in condition position is still a Range value, which is + // truthy — not a flip-flop. + checkSrc(t, "a one-sided range is a Range, not a flip-flop", ` +p((1..5).to_a) +x = 1..5; p x.class +$s = []; 3.times { |i| $s << i if (i..) }; p $s +`, "[1, 2, 3, 4, 5]\nRange\n[0, 1, 2]") +} + +// __ENCODING__ and string literals take the encoding a magic comment declares, +// in every spelling MRI accepts. +func TestMagicCommentSourceEncoding(t *testing.T) { + checkSrc(t, "plain", "# encoding: big5\np __ENCODING__.name\np \"abc\".encoding.name\n", + `"Big5"`+"\n"+`"Big5"`) + checkSrc(t, "case-insensitive", "# CoDiNg: bIg5\np __ENCODING__.name\n", `"Big5"`) + checkSrc(t, "after a shebang", "#!/usr/bin/ruby\n# encoding: big5\np __ENCODING__.name\n", `"Big5"`) + checkSrc(t, "Emacs style", "# -*- encoding: big5 -*-\np __ENCODING__.name\n", `"Big5"`) + checkSrc(t, "vim style", "# vim: filetype=ruby, fileencoding=big5, tabsize=3\np __ENCODING__.name\n", `"Big5"`) + checkSrc(t, "no magic comment", "p __ENCODING__.name\n", `"UTF-8"`) + checkSrc(t, "utf-8 is the default", "# encoding: utf-8\np __ENCODING__.name\n", `"UTF-8"`) + checkSrc(t, "not the first token", "puts 1 # encoding: big5\np __ENCODING__.name\n", "1\n"+`"UTF-8"`) + checkSrc(t, "binary", "# encoding: binary\np __ENCODING__.name\np \"abc\".encoding.name\n", + `"ASCII-8BIT"`+"\n"+`"ASCII-8BIT"`) +} + +// A `break` in a while/until loop carries its argument out as the whole loop +// expression's value; a loop the condition ends is still nil. Expectations +// from MRI 4.0.5. +func TestLoopBreakValue(t *testing.T) { + checkSrc(t, "a value and a splat", ` +p(while true; break; end) +p(while true; break nil; end) +p(while true; break 1; end) +p(while true; break []; end) +p(while true; break [1]; end) +p(while true; break *[1, 2]; end) +p(while true; break *nil; end) +p(while true; break *[]; end) +p(while true; break *[nil]; end) +p(while true; break *[[]]; end) +p(while true; break *1; end) +`, "nil\nnil\n1\n[]\n[1]\n[1, 2]\n[]\n[]\n[nil]\n[[]]\n[1]") + + checkSrc(t, "until, next, and a loop with no break at all", ` +i = 0 +p(until false; i += 1; break i if i == 3; end) +i = 0 +p(while i < 3; i += 1; end) +n = 0 +p(while true; n += 1; next if n == 1; break n; end) +i = 0 +a = begin; i += 1; break i * 10 if i == 3; end while true +p a +i = 0 +a = begin; i += 1; end while i < 5 +p a +`, "3\nnil\n2\nnil\n5") // MRI gives nil for a `begin…end while` too: the modifier form has no value + + // A `break` inside a block still belongs to the block, not to the loop it + // sits in: it makes the *call* return, and the loop keeps its own value. + checkSrc(t, "a block break is not a loop break", ` +r = while true + [1, 2, 3].each { |x| break x * 2 } + break :loop +end +p r +p([1, 2, 3].each { |x| break x * 2 }) +`, ":loop\n2") +} + +// warnAlreadyInitialized falls back to Object when it is asked about a nil +// lexical scope — the defensive case assignConst has always had, reached here +// directly since no compiled program produces a nil cref. +func TestWarnAlreadyInitializedNilScope(t *testing.T) { + var buf bytes.Buffer + vm := New(&buf) + vm.globals["$VERBOSE"] = object.False + vm.cObject.consts["WAIN_TEST_CONST"] = object.IntValue(1) + vm.warnAlreadyInitialized(nil, "WAIN_TEST_CONST") + if got := buf.String(); got != "warning: already initialized constant WAIN_TEST_CONST\n" { + t.Fatalf("nil scope: got %q", got) + } + buf.Reset() + vm.warnAlreadyInitialized(nil, "WAIN_NEVER_SET") + if got := buf.String(); got != "" { + t.Fatalf("an unset constant must not warn: got %q", got) + } +}