diff --git a/internal/bytecode/bytecode.go b/internal/bytecode/bytecode.go index ecaee3bc..4639ae31 100644 --- a/internal/bytecode/bytecode.go +++ b/internal/bytecode/bytecode.go @@ -120,8 +120,44 @@ const ( OpGetConstTop // A = Names index; leading `::Name` — top-level (Object) constant only, ignoring lexical nesting OpDefinedConstTop // A = Names index; "constant" if the top-level `::Name` exists, else nil OpRegexpOnce // A = target pc past the interpolation build; guards a /o literal: if the occurrence's Regexp is already memoised, push it and jump to A, else fall through to (re)build once + OpDefinedSuper // "super" if the method `super` would reach from the current frame exists, else nil; never evaluates the super arguments ) +// Defined tag Strings. +// +// MRI builds every `defined?` answer with rb_iseq_defined_string (iseq.c +// v3_4_0:3692), which returns rb_fstring_cstr(...) — a FROZEN, deduplicated +// String. ruby/spec pins that: language/defined_spec.rb asserts +// `defined?(nil).frozen?.should == true` for every tag it names. +// +// The tags live here, in the package both halves of the implementation import, +// because both halves must agree: the compiler bakes a tag into the literal +// pool for the syntactically-decided cases (nil/self/expression/…), while the +// VM builds one at run time for the cases only it can decide +// (constant/method/yield/…). One table, one constructor, so the two cannot +// drift apart and leave defined? half-frozen. +const ( + DefinedNil = "nil" + DefinedIvar = "instance-variable" + DefinedLvar = "local-variable" + DefinedGvar = "global-variable" + DefinedCvar = "class variable" + DefinedConst = "constant" + DefinedMethod = "method" + DefinedYield = "yield" + DefinedZSuper = "super" + DefinedSelf = "self" + DefinedTrue = "true" + DefinedFalse = "false" + DefinedAsgn = "assignment" + DefinedExprTag = "expression" +) + +// DefinedTag returns the frozen String a `defined?` answer is made of. It +// mirrors rb_iseq_defined_string: the result is frozen, so a caller may share +// it freely, and `defined?(nil).frozen?` is true as in MRI. +func DefinedTag(tag string) *object.String { return object.NewFrozenStringView(tag) } + var opNames = map[Op]string{ OpNop: "nop", OpPushConst: "push_const", OpPushNil: "push_nil", OpPushTrue: "push_true", OpPushFalse: "push_false", OpPushSelf: "push_self", @@ -159,6 +195,7 @@ var opNames = map[Op]string{ OpDefinedGuard: "defined_guard", OpGetConstTop: "get_const_top", OpDefinedConstTop: "defined_const_top", + OpDefinedSuper: "defined_super", } func (o Op) String() string { diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 0ab0e600..506fad9f 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -199,6 +199,10 @@ type Compiler struct { // (and index arguments) were evaluated into before the right-hand side ran. // See preEvalMasgnTarget. masgnPre map[ast.Node]*masgnPreEval + // patCache is the local slot holding the current `case/in` statement's + // deconstructed-subject cache, or -1 outside one (the zero value is fixed up + // in compilePattern, which never sees slot 0 as a cache). See compileCaseIn. + patCache int } // masgnPreEval holds the temporaries into which one multiple-assignment target's @@ -232,7 +236,7 @@ func CompileWithEncoding(prog *ast.Program, srcEnc string) (iseq *bytecode.ISeq, iseq, err = nil, r.(compileError) } }() - c := &Compiler{srcEnc: srcEnc} + c := &Compiler{srcEnc: srcEnc, patCache: -1} c.push(newBuilder("
", nil)) c.compileBody(prog.Body) c.cur().emit(bytecode.OpReturn, 0, 0) @@ -344,7 +348,7 @@ func CompileWithLocals(prog *ast.Program, localNames []string) (iseq *bytecode.I iseq, err = nil, r.(compileError) } }() - c := &Compiler{} + c := &Compiler{patCache: -1} parent := newBuilder("", nil) parent.locals = append([]string(nil), localNames...) parent.borrowed = true @@ -380,6 +384,9 @@ func (c *Compiler) compileBody(body []ast.Node) { return } for i, n := range body { + if i < len(body)-1 && c.compileDiscarded(n) { + continue + } c.compileNode(n) if i < len(body)-1 { c.cur().emit(bytecode.OpPop, 0, 0) @@ -387,6 +394,25 @@ func (c *Compiler) compileBody(body []ast.Node) { } } +// compileDiscarded emits n in a position where its value is thrown away, and +// reports whether it handled n (the caller emits it normally otherwise). +// +// Only `defined?` is special here. MRI compiles NODE_DEFINED to NOTHING when +// its result is popped — `case NODE_DEFINED: if (!popped) …` (compile.c +// v3_4_0:11332-11336) — and ruby/spec pins the consequence: a bare +// `defined?(DefinedSpecs.side_effects / 2)` statement must not run the +// receiver, even though the value-returning form does. The operand's LOCALS are +// still declared, because MRI declares them in the parser, which walks the +// operand whatever the context does with the answer. +func (c *Compiler) compileDiscarded(n ast.Node) bool { + v, ok := n.(*ast.Call) + if !ok || v.Recv != nil || v.Block != nil || v.Name != "defined?" || len(v.Args) != 1 { + return false + } + c.declareDefinedLocals(v.Args[0]) + return true +} + func (c *Compiler) compileNode(n ast.Node) { b := c.cur() switch v := n.(type) { @@ -1923,9 +1949,26 @@ func (c *Compiler) compileCaseIn(v *ast.CaseIn) { b.emit(bytecode.OpSetLocal, subj, 0) b.emit(bytecode.OpPop, 0, 0) + // One deconstructed-subject cache for the whole statement. MRI keeps it on + // the stack beside the subject and threads `use_deconstructed_cache` into the + // TOP-LEVEL patterns only (iseq_compile_array_deconstruct, compile.c + // v3_4_0:7055+); the clauses of one `case` share a subject, so the first + // #deconstruct answers for all of them, and ruby/spec pins the single call. + // + // Three states, as in MRI: nil = not tried, false = the subject does not + // answer #deconstruct, an Array = the deconstruction. It is reset here rather + // than relying on frame-entry zeroing, because a `case` inside a loop runs + // again in the same frame with a different subject. + cache := b.localSlot("") + b.emit(bytecode.OpPushNil, 0, 0) + b.emit(bytecode.OpSetLocal, cache, 0) + b.emit(bytecode.OpPop, 0, 0) + var endJumps []int for _, clause := range v.Clauses { + c.patCache = cache c.compilePattern(clause.Pattern, subj) + c.patCache = -1 if clause.Guard != nil { // Pattern match AND guard: only evaluate the guard when the pattern // matched (the pattern's bindings are visible to the guard). @@ -1969,6 +2012,15 @@ func (c *Compiler) compileCaseIn(v *ast.CaseIn) { // bindings as a side effect. func (c *Compiler) compilePattern(pat ast.Pattern, subj int) { b := c.cur() + // The deconstructed-subject cache reaches a TOP-LEVEL array/find pattern and + // its alternatives, and nothing nested inside one: a nested pattern is + // matched against an ELEMENT, not the subject. MRI expresses the same by + // passing use_deconstructed_cache = false down every recursive call but the + // alternative arms'. + cache := c.patCache + if _, isAlt := pat.(*ast.AltPattern); !isAlt { + c.patCache = -1 + } switch p := pat.(type) { case *ast.BindPattern: // Bind the whole subject; always matches. @@ -1983,10 +2035,13 @@ func (c *Compiler) compilePattern(pat ast.Pattern, subj int) { b.emit(bytecode.OpSend, b.addName("==="), 1) b.emit(bytecode.OpTruthy, 0, 0) case *ast.ConstPattern: - // subject.is_a?(Const) - b.emit(bytecode.OpGetLocal, subj, 0) + // Const === subject, not subject.is_a?(Const): a bare constant pattern is + // compiled like a value pattern, `checkmatch VM_CHECKMATCH_TYPE_CASE` + // (compile.c v3_4_0, iseq_compile_pattern_each's default arm), so a + // redefined — or refined — #=== decides the match. c.compileNode(p.Const) - b.emit(bytecode.OpSend, b.addName("is_a?"), 1) + b.emit(bytecode.OpGetLocal, subj, 0) + b.emit(bytecode.OpSend, b.addName("==="), 1) b.emit(bytecode.OpTruthy, 0, 0) case *ast.BindingPattern: // Match the sub-pattern, then bind the subject to name on success. @@ -2001,11 +2056,11 @@ func (c *Compiler) compilePattern(pat ast.Pattern, subj int) { b.emit(bytecode.OpPushFalse, 0, 0) b.patch(end, b.here()) case *ast.ArrayPattern: - c.compileArrayPattern(p, subj) + c.compileArrayPattern(p, subj, cache) case *ast.HashPattern: c.compileHashPattern(p, subj) case *ast.FindPattern: - c.compileFindPattern(p, subj) + c.compileFindPattern(p, subj, cache) case *ast.AltPattern: // Alternative: true if any branch matches (short-circuit on the first). var hits []int @@ -2028,29 +2083,15 @@ func (c *Compiler) compilePattern(pat ast.Pattern, subj int) { // compileArrayPattern emits the deconstruct-protocol match for an array pattern. // It checks the optional constant, that the subject responds to :deconstruct, // the resulting Array's length, then each element against its sub-pattern. -func (c *Compiler) compileArrayPattern(p *ast.ArrayPattern, subj int) { +func (c *Compiler) compileArrayPattern(p *ast.ArrayPattern, subj, cache int) { b := c.cur() arr := b.localSlot("") // the deconstructed Array // Result accumulator: start true, AND each test, short-circuiting via jumps // to a shared failure label. var fails []int - // Optional constant guard: subject.is_a?(Const). - if p.Const != nil { - b.emit(bytecode.OpGetLocal, subj, 0) - c.compileNode(p.Const) - b.emit(bytecode.OpSend, b.addName("is_a?"), 1) - fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) - } - // respond_to?(:deconstruct) - b.emit(bytecode.OpGetLocal, subj, 0) - b.emit(bytecode.OpPushConst, b.addConst(object.Symbol("deconstruct")), 0) - b.emit(bytecode.OpSend, b.addName("respond_to?"), 1) - fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) - // arr = subject.deconstruct - b.emit(bytecode.OpGetLocal, subj, 0) - b.emit(bytecode.OpSend, b.addName("deconstruct"), 0) - b.emit(bytecode.OpSetLocal, arr, 0) - b.emit(bytecode.OpPop, 0, 0) + // Optional constant guard, FIRST and before any protocol call. + fails = append(fails, c.patternConstantGuard(p.Const, subj)...) + fails = append(fails, c.deconstructInto(arr, subj, cache)...) // Length check: == (pre+post) without splat, >= (pre+post) with. b.emit(bytecode.OpGetLocal, arr, 0) b.emit(bytecode.OpSend, b.addName("length"), 0) @@ -2119,25 +2160,12 @@ func (c *Compiler) compileArrayPattern(p *ast.ArrayPattern, subj int) { // compileFindPattern emits the array find pattern `[*pre, mid…, *post]`: it // deconstructs the subject, then scans for the first window where every Mid // matches, binding pre/post. It leaves a boolean (matched) on the stack. -func (c *Compiler) compileFindPattern(p *ast.FindPattern, subj int) { +func (c *Compiler) compileFindPattern(p *ast.FindPattern, subj, cache int) { b := c.cur() arr := b.localSlot("") var fails []int - if p.Const != nil { - b.emit(bytecode.OpGetLocal, subj, 0) - c.compileNode(p.Const) - b.emit(bytecode.OpSend, b.addName("is_a?"), 1) - fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) - } - // respond_to?(:deconstruct), then arr = subject.deconstruct. - b.emit(bytecode.OpGetLocal, subj, 0) - b.emit(bytecode.OpPushConst, b.addConst(object.Symbol("deconstruct")), 0) - b.emit(bytecode.OpSend, b.addName("respond_to?"), 1) - fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) - b.emit(bytecode.OpGetLocal, subj, 0) - b.emit(bytecode.OpSend, b.addName("deconstruct"), 0) - b.emit(bytecode.OpSetLocal, arr, 0) - b.emit(bytecode.OpPop, 0, 0) + fails = append(fails, c.patternConstantGuard(p.Const, subj)...) + fails = append(fails, c.deconstructInto(arr, subj, cache)...) // n = arr.length n := b.localSlot("") b.emit(bytecode.OpGetLocal, arr, 0) @@ -2229,22 +2257,134 @@ func (c *Compiler) setLocalInt(slot int, v int64) { b.emit(bytecode.OpPop, 0, 0) } +// deconstructInto emits the #deconstruct protocol for an array-shaped pattern, +// leaving the deconstructed Array in slot arr. It returns the branch sites to +// patch to the pattern's failure label. +// +// It mirrors iseq_compile_array_deconstruct (compile.c v3_4_0:7055+): +// respond_to?(:deconstruct), the call, and `unless Array === d` -> TypeError, +// because a #deconstruct answering with something else is an error, not a +// failed match. When cache >= 0 the result is memoised in that slot for the +// rest of the `case`, with MRI's three states — nil not tried, false does not +// answer #deconstruct, else the Array. +func (c *Compiler) deconstructInto(arr, subj, cache int) []int { + b := c.cur() + var fails []int + reuse := -1 + if cache >= 0 { + b.emit(bytecode.OpGetLocal, cache, 0) + fresh := b.emit(bytecode.OpBranchNil, 0, 0) // nil: not tried yet + b.emit(bytecode.OpGetLocal, cache, 0) + fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) // false: no #deconstruct + b.emit(bytecode.OpGetLocal, cache, 0) + b.emit(bytecode.OpSetLocal, arr, 0) + b.emit(bytecode.OpPop, 0, 0) + reuse = b.emit(bytecode.OpJump, 0, 0) + b.patch(fresh, b.here()) + } + // respond_to?(:deconstruct) + b.emit(bytecode.OpGetLocal, subj, 0) + b.emit(bytecode.OpPushConst, b.addConst(object.Symbol("deconstruct")), 0) + b.emit(bytecode.OpSend, b.addName("respond_to?"), 1) + if cache >= 0 { + // Remember the answer. A false stays; a true is overwritten by the Array + // below. OpSetLocal leaves the value, which the branch then consumes. + b.emit(bytecode.OpSetLocal, cache, 0) + } + fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) + // arr = subject.deconstruct + b.emit(bytecode.OpGetLocal, subj, 0) + b.emit(bytecode.OpSend, b.addName("deconstruct"), 0) + b.emit(bytecode.OpSetLocal, arr, 0) + b.emit(bytecode.OpPop, 0, 0) + c.patternTypeCheck(arr, "Array", "deconstruct must return Array") + if cache >= 0 { + b.emit(bytecode.OpGetLocal, arr, 0) + b.emit(bytecode.OpSetLocal, cache, 0) + b.emit(bytecode.OpPop, 0, 0) + b.patch(reuse, b.here()) // the cached path rejoins past the type check + } + return fails +} + +// patternConstantGuard emits the optional constant test a container pattern +// carries (`in Point[x, y]`, `in Point(x:, y:)`). It returns the branch sites to +// patch to the pattern's failure label. +// +// The test is `Const === subject`, not `subject.is_a?(Const)`: MRI compiles it +// to `checkmatch VM_CHECKMATCH_TYPE_CASE` over the constant +// (iseq_compile_pattern_constant, compile.c v3_4_0:7054+), which is Module#=== +// — so a constant whose #=== is redefined decides the match, and ruby/spec pins +// that ("does not match object if Constant === object returns false"). +// +// It is emitted BEFORE the deconstruct protocol, which is what +// "checks Constant === object before calling #deconstruct_keys" pins. +func (c *Compiler) patternConstantGuard(konst ast.Node, subj int) []int { + if konst == nil { + return nil + } + b := c.cur() + c.compileNode(konst) + b.emit(bytecode.OpGetLocal, subj, 0) + b.emit(bytecode.OpSend, b.addName("==="), 1) + return []int{b.emit(bytecode.OpBranchUnless, 0, 0)} +} + +// patternTypeCheck emits MRI's type_error arm: the value a deconstruct protocol +// method returned must be of the expected class, and a wrong one is a TypeError +// (compile.c v3_4_0:7224 and 7587), not a failed match that would let the next +// `in` clause run. rbgo has no raw type-check opcode, so the test is +// `value.is_a?(Class)` where MRI uses `checktype`; the two differ only for an +// object that redefines #is_a?. +func (c *Compiler) patternTypeCheck(slot int, className, msg string) { + b := c.cur() + b.emit(bytecode.OpGetLocal, slot, 0) + b.emit(bytecode.OpGetConst, b.addName(className), 0) + b.emit(bytecode.OpSend, b.addName("is_a?"), 1) + ok := b.emit(bytecode.OpBranchIf, 0, 0) + b.emit(bytecode.OpPushSelf, 0, 0) + b.emit(bytecode.OpGetConst, b.addName("TypeError"), 0) + b.emit(bytecode.OpPushConst, b.addConst(object.NewString(msg)), 0) + b.emit(bytecode.OpSend, b.addName("raise"), 2) + b.emit(bytecode.OpPop, 0, 0) + b.patch(ok, b.here()) +} + func (c *Compiler) compileHashPattern(p *ast.HashPattern, subj int) { b := c.cur() h := b.localSlot("") // the deconstructed Hash var fails []int + // Optional constant guard (`in Point(x:, y:)`), FIRST: MRI screens the + // constant before it asks anything of the subject, so a subject the constant + // rejects is never sent #deconstruct_keys at all. + fails = append(fails, c.patternConstantGuard(p.Const, subj)...) // respond_to?(:deconstruct_keys) b.emit(bytecode.OpGetLocal, subj, 0) b.emit(bytecode.OpPushConst, b.addConst(object.Symbol("deconstruct_keys")), 0) b.emit(bytecode.OpSend, b.addName("respond_to?"), 1) fails = append(fails, b.emit(bytecode.OpBranchUnless, 0, 0)) - // h = subject.deconstruct_keys(nil) — we pass nil (full hash) for simplicity, - // which is always a valid request under the protocol. + // h = subject.deconstruct_keys(keys) + // + // The argument tells the subject which keys the pattern will ask for, so it + // may build only those. It is the pattern's key list, EXCEPT when the pattern + // has a named `**rest` or a `**nil` — then the whole hash is wanted and MRI + // passes nil. An anonymous `**` still gets the key list. Witnessed against + // ruby 4.0.5 for all four shapes, and pinned by ruby/spec's three + // "passes keys …" examples. b.emit(bytecode.OpGetLocal, subj, 0) - b.emit(bytecode.OpPushNil, 0, 0) + if p.RestNil || p.RestName != "" { + b.emit(bytecode.OpPushNil, 0, 0) + } else { + for _, key := range p.Keys { + b.emit(bytecode.OpPushConst, b.addConst(object.Symbol(key)), 0) + } + b.emit(bytecode.OpNewArray, len(p.Keys), 0) + } b.emit(bytecode.OpSend, b.addName("deconstruct_keys"), 1) b.emit(bytecode.OpSetLocal, h, 0) b.emit(bytecode.OpPop, 0, 0) + // `unless Hash === d: goto type_error`. + c.patternTypeCheck(h, "Hash", "deconstruct_keys must return Hash") // `**nil`, or the empty pattern `{}`: the hash must have exactly the named // keys and no extras (an empty `{}` thus matches only an empty hash). if p.RestNil || (len(p.Keys) == 0 && !p.HasRest) { diff --git a/internal/compiler/defined.go b/internal/compiler/defined.go index c230b5bd..5b7a9d02 100644 --- a/internal/compiler/defined.go +++ b/internal/compiler/defined.go @@ -2,7 +2,6 @@ package compiler import ( "github.com/go-embedded-ruby/ruby/internal/bytecode" - "github.com/go-embedded-ruby/ruby/internal/object" "github.com/go-ruby-parser/parser/ast" ) @@ -17,15 +16,36 @@ func (c *Compiler) compileDefined(operand ast.Node) { c.declareDefinedLocals(operand) switch v := operand.(type) { case *ast.NilLit: - c.pushDefinedTag("nil") + c.pushDefinedTag(bytecode.DefinedNil) case *ast.BoolLit: if v.Value { - c.pushDefinedTag("true") + c.pushDefinedTag(bytecode.DefinedTrue) } else { - c.pushDefinedTag("false") + c.pushDefinedTag(bytecode.DefinedFalse) } case *ast.SelfLit: - c.pushDefinedTag("self") + c.pushDefinedTag(bytecode.DefinedSelf) + case *ast.Super: + // NODE_SUPER and NODE_ZSUPER are both DEFINED_ZSUPER (compile.c + // v3_4_0:6139-6143): a `putnil` then `defined ZSUPER`, which means the + // super ARGUMENTS are never evaluated — only the existence of the method + // `super` would reach is tested. The VM half answers it (vm_defined's + // DEFINED_ZSUPER, vm_insnhelper.c v3_4_0:5505-5518). + b.emit(bytecode.OpDefinedSuper, 0, 0) + case *ast.ArrayLit: + c.compileDefinedElements(v.Elems) + case *ast.HashLit: + // NODE_HASH walks its nd_head, a flat LIST of key, value, key, value… + // (compile.c v3_4_0:5975-5993), so keys are probed like values. + pairs := make([]ast.Node, 0, len(v.Keys)+len(v.Values)) + for i, k := range v.Keys { + pairs = append(pairs, k, v.Values[i]) + } + c.compileDefinedElements(pairs) + case *ast.SplatArg: + // NODE_SPLAT probes the splatted expression, then reports "expression" + // (compile.c v3_4_0:6011-6019). + c.compileDefinedElements([]ast.Node{v.Value}) case *ast.Yield: b.emit(bytecode.OpDefinedYield, 0, 0) case *ast.IvarRef: @@ -38,12 +58,12 @@ func (c *Compiler) compileDefined(operand ast.Node) { b.emit(bytecode.OpDefinedConst, b.addName(v.Name), 0) case *ast.VarRef: // The parser already classified this as a known local read. - c.pushDefinedTag("local-variable") + c.pushDefinedTag(bytecode.DefinedLvar) case *ast.ScopedConst: c.compileDefinedScopedConst(v) case *ast.Assign, *ast.OpAssign, *ast.MultiAssign, *ast.ConstAssign, *ast.ScopedConstAssign, *ast.IvarAssign, *ast.CVarAssign, *ast.GVarAssign: - c.pushDefinedTag("assignment") + c.pushDefinedTag(bytecode.DefinedAsgn) case *ast.Call: // A compound assignment to an attribute or an index (`a.b += 1`, // `a[:b] ||= 1`) reaches the compiler as the parser's textual desugaring @@ -57,11 +77,11 @@ func (c *Compiler) compileDefined(operand ast.Node) { // the two apart; see opassign.go for why identity is the test. if isSetterCall(v) { if _, _, ok := indexOpAssign(v); ok { - c.pushDefinedTag("assignment") + c.pushDefinedTag(bytecode.DefinedAsgn) return } if _, ok := attrOpAssign(v); ok { - c.pushDefinedTag("assignment") + c.pushDefinedTag(bytecode.DefinedAsgn) return } } @@ -73,10 +93,54 @@ func (c *Compiler) compileDefined(operand ast.Node) { // all of them "method" once the operand is defined. c.compileDefinedReceiverMethod(v.Operand, unaryMethodName(v.Op)) default: - // Literals (numbers, strings, arrays, ranges, regexps, hashes, …) and any - // other expression are "expression". - c.pushDefinedTag("expression") + // Literals (numbers, strings, ranges, regexps, …) and any other expression + // are "expression" — compile.c's `default:` arm (v3_4_0:6003-6008). + c.pushDefinedTag(bytecode.DefinedExprTag) + } +} + +// isSourceKeyword reports whether name is one of the three source-position +// pseudo-variables the parser hands over as a bare call. They are keywords in +// MRI's grammar (parse.y v3_4_0: `keyword__FILE__`, `keyword__LINE__`, +// `keyword__ENCODING__` in `var_ref`), never method calls, so defined? must not +// probe self for a method of that name. +func isSourceKeyword(name string) bool { + switch name { + case "__FILE__", "__LINE__", "__ENCODING__": + return true + } + return false +} + +// compileDefinedElements lowers the container forms — an array literal, a hash +// literal, a splat — whose answer is "expression" but only once EVERY part is +// itself defined. MRI probes each element with defined_expr0 and branches to the +// whole expression's nil label on the first undefined one (compile.c +// v3_4_0:5975-6019); `defined?([NonExistentConstant, Array])` is nil, not +// "expression". An empty container has nothing to probe and is plain +// "expression" (NODE_ZLIST). +// +// Each probe is a full defined? of the element, so nothing is evaluated for its +// value and a raising sub-expression is already guarded by the element's own +// lowering. +func (c *Compiler) compileDefinedElements(elems []ast.Node) { + if len(elems) == 0 { + c.pushDefinedTag(bytecode.DefinedExprTag) + return } + b := c.cur() + undef := make([]int, 0, len(elems)) + for _, e := range elems { + c.compileDefined(e) + undef = append(undef, b.emit(bytecode.OpBranchNil, 0, 0)) + } + c.pushDefinedTag(bytecode.DefinedExprTag) + done := b.emit(bytecode.OpJump, 0, 0) + for _, at := range undef { + b.patch(at, b.here()) + } + b.emit(bytecode.OpPushNil, 0, 0) + b.patch(done, b.here()) } // unaryMethodName maps a unary operator to the method name MRI checks for. Only @@ -88,10 +152,14 @@ func unaryMethodName(op string) string { return op } -// pushDefinedTag pushes a constant tag String. +// pushDefinedTag pushes a constant tag String. The tag comes from +// bytecode.DefinedTag, the one constructor the VM half uses too, so it is +// FROZEN: MRI answers defined? with rb_iseq_defined_string, an fstring +// (iseq.c v3_4_0:3692), and language/defined_spec.rb asserts `.frozen?` on +// every tag it names. func (c *Compiler) pushDefinedTag(tag string) { b := c.cur() - b.emit(bytecode.OpPushConst, b.addConst(object.NewString(tag)), 0) + b.emit(bytecode.OpPushConst, b.addConst(bytecode.DefinedTag(tag)), 0) } // compileDefinedScopedConst handles `defined?(A::B)` and `defined?(::B)`. @@ -115,9 +183,27 @@ func (c *Compiler) compileDefinedScopedConst(v *ast.ScopedConst) { // implicit) receiver, with the receiver and arguments evaluated. func (c *Compiler) compileDefinedCall(v *ast.Call) { b := c.cur() + // A nested `defined?` is not a method call on self: MRI parses it to + // NODE_DEFINED, which is not among the cases defined_expr0 names and so + // falls through to its `default:` arm — "expression" (compile.c + // v3_4_0:6003-6008). The operand's locals are still declared, as compiling it + // normally would do. + if v.Recv == nil && v.Block == nil && v.Name == "defined?" && len(v.Args) == 1 { + c.declareDefinedLocals(v.Args[0]) + c.pushDefinedTag(bytecode.DefinedExprTag) + return + } if v.Recv == nil && v.Block == nil && len(v.Args) == 0 { if _, _, ok := b.resolve(v.Name); ok { - c.pushDefinedTag("local-variable") + c.pushDefinedTag(bytecode.DefinedLvar) + return + } + // __FILE__, __LINE__ and __ENCODING__ are keywords, not methods: the + // parser hands them over as bare calls, but MRI parses them to + // NODE_FILE / NODE_LINE / NODE_ENCODING, which are listed with the + // literals that fall through to DEFINED_EXPR (compile.c v3_4_0:5996-6008). + if isSourceKeyword(v.Name) { + c.pushDefinedTag(bytecode.DefinedExprTag) return } } @@ -135,10 +221,10 @@ func (c *Compiler) compileDefinedBinary(v *ast.BinaryExpr) { // (prism_compile.c v3_4_0:4059 and 4063; compile.c's NODE_OP_CDECL at // v3_4_0:6162 says the same) — and ruby/spec pins it. if constOrAssign(v) { - c.pushDefinedTag("assignment") + c.pushDefinedTag(bytecode.DefinedAsgn) return } - c.pushDefinedTag("expression") + c.pushDefinedTag(bytecode.DefinedExprTag) return } c.compileDefinedReceiverMethod(v.Left, v.Op) @@ -184,6 +270,9 @@ func constOrAssign(v *ast.BinaryExpr) bool { func (c *Compiler) compileDefinedReceiverMethod(recv ast.Node, name string) { c.guarded(func(gb *builder) { if recv == nil { + // No receiver written: MRI's DEFINED_FUNC, which tests the response at + // ANY visibility (rb_ec_obj_respond_to with the include-private flag), + // so a private method called bare is still "method". B = 0 says so. gb.emit(bytecode.OpPushSelf, 0, 0) gb.emit(bytecode.OpDefinedMethod, gb.addName(name), 0) return @@ -192,7 +281,11 @@ func (c *Compiler) compileDefinedReceiverMethod(recv ast.Node, name string) { c.compileDefined(recv) recvUndef := gb.emit(bytecode.OpBranchNil, 0, 0) c.compileNode(recv) - gb.emit(bytecode.OpDefinedMethod, gb.addName(name), 0) + // A receiver was written: MRI's DEFINED_METHOD, which screens the method + // entry's visibility — private is not defined?, protected only for a kin + // self (compile.c v3_4_0:6099-6111 picks the tag; vm_insnhelper.c + // v3_4_0:5476-5498 applies the rule). B = 1 says so. + gb.emit(bytecode.OpDefinedMethod, gb.addName(name), 1) done := gb.emit(bytecode.OpJump, 0, 0) gb.patch(recvUndef, gb.here()) gb.emit(bytecode.OpPushNil, 0, 0) @@ -269,6 +362,17 @@ func (c *Compiler) declareDefinedLocals(n ast.Node) { c.declareDefinedLocals(v.Right) case *ast.UnaryExpr: c.declareDefinedLocals(v.Operand) + case *ast.ArrayLit: + for _, e := range v.Elems { + c.declareDefinedLocals(e) + } + case *ast.HashLit: + for i, k := range v.Keys { + c.declareDefinedLocals(k) + c.declareDefinedLocals(v.Values[i]) + } + case *ast.SplatArg: + c.declareDefinedLocals(v.Value) } } diff --git a/internal/compiler/wave27_defined_test.go b/internal/compiler/wave27_defined_test.go new file mode 100644 index 00000000..6295f7d4 --- /dev/null +++ b/internal/compiler/wave27_defined_test.go @@ -0,0 +1,212 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package compiler + +import ( + "testing" + + "github.com/go-embedded-ruby/ruby/internal/bytecode" + "github.com/go-embedded-ruby/ruby/internal/object" +) + +// countOp reports how many times op appears in iseq's instructions. +func countOp(iseq *bytecode.ISeq, op bytecode.Op) int { + n := 0 + for _, in := range iseq.Insns { + if in.Op == op { + n++ + } + } + return n +} + +// definedTagConsts collects the String constants of an ISeq's pool, which for a +// `defined?` lowering are exactly the tags the compiler decided statically. +func definedTagConsts(iseq *bytecode.ISeq) []*object.String { + var out []*object.String + for _, c := range iseq.Consts { + if s, ok := c.(*object.String); ok { + out = append(out, s) + } + } + return out +} + +// TestDefinedTagsAreFrozenConstants covers pushDefinedTag: every tag the +// compiler bakes into the literal pool is built by bytecode.DefinedTag and is +// therefore FROZEN, as MRI's rb_iseq_defined_string (an fstring) is. +func TestDefinedTagsAreFrozenConstants(t *testing.T) { + t.Parallel() + for _, src := range []string{ + `defined?(nil)`, `defined?(true)`, `defined?(false)`, `defined?(self)`, + `defined?(1)`, `defined?(x = 1)`, `x = 1; defined?(x)`, + `defined?(__FILE__)`, `defined?(__LINE__)`, `defined?(__ENCODING__)`, + `defined?(defined?(y))`, `defined?([])`, `defined?([1, 2])`, + `defined?({1 => 2})`, + } { + iseq := compileSrc(t, src) + tags := definedTagConsts(iseq) + if len(tags) == 0 { + t.Fatalf("%s: no String constant in the pool, expected a defined? tag", src) + } + for _, s := range tags { + if !s.Frozen { + t.Errorf("%s: tag %q is not frozen", src, s.Str()) + } + } + } +} + +// TestDefinedSuperEmitsItsOwnOpcode covers the *ast.Super arm of compileDefined: +// `defined?(super)` is DEFINED_ZSUPER, an opcode of its own, and it must NOT +// evaluate the super arguments — so no send for them is emitted either. +func TestDefinedSuperEmitsItsOwnOpcode(t *testing.T) { + t.Parallel() + for _, src := range []string{ + `def m; defined?(super); end`, + `def m(a); defined?(super(a)); end`, + `def m(a); defined?(super(raise("never"))); end`, + } { + iseq := compileSrc(t, src) + if len(iseq.Children) != 1 { + t.Fatalf("%s: want one child ISeq, got %d", src, len(iseq.Children)) + } + body := iseq.Children[0] + if got := countOp(body, bytecode.OpDefinedSuper); got != 1 { + t.Errorf("%s: OpDefinedSuper count = %d, want 1", src, got) + } + if got := countOp(body, bytecode.OpSend); got != 0 { + t.Errorf("%s: emitted %d sends, want 0 (the super arguments are never evaluated)", src, got) + } + } +} + +// TestDefinedContainerProbesEachElement covers compileDefinedElements: a +// non-empty container probes every element and branches away on the first +// undefined one, while an EMPTY one has nothing to probe and is a bare tag push. +func TestDefinedContainerProbesEachElement(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + src string + wantBranch int + }{ + {`defined?([])`, 0}, + {`defined?({})`, 0}, + {`defined?([A])`, 1}, + {`defined?([A, B, C])`, 3}, + {`defined?({A => B})`, 2}, // a hash probes keys AND values + // The array probes its one element, and that element — a splat — probes + // the splatted expression in turn: two nested probes. + {`defined?([*A])`, 2}, + } { + iseq := compileSrc(t, tc.src) + if got := countOp(iseq, bytecode.OpBranchNil); got != tc.wantBranch { + t.Errorf("%s: OpBranchNil count = %d, want %d", tc.src, got, tc.wantBranch) + } + } +} + +// TestDefinedSourceKeywordsAreNotMethodProbes covers isSourceKeyword and its +// call site: the three source pseudo-variables answer "expression" with a plain +// constant push, while a bare name that is neither a local nor one of them is a +// method probe (a guard child ISeq). +func TestDefinedSourceKeywordsAreNotMethodProbes(t *testing.T) { + t.Parallel() + for _, name := range []string{"__FILE__", "__LINE__", "__ENCODING__"} { + iseq := compileSrc(t, `defined?(`+name+`)`) + if got := countOp(iseq, bytecode.OpDefinedGuard); got != 0 { + t.Errorf("defined?(%s): emitted a method-probe guard", name) + } + } + // A near-miss name (not one of the three) still probes for a method. + iseq := compileSrc(t, `defined?(__method__)`) + if got := countOp(iseq, bytecode.OpDefinedGuard); got != 1 { + t.Errorf("defined?(__method__): OpDefinedGuard count = %d, want 1", got) + } + if isSourceKeyword("__method__") { + t.Error(`isSourceKeyword("__method__") = true, want false`) + } +} + +// TestDefinedMethodProbeRecordsTheReceiverKind covers the B operand of +// OpDefinedMethod: 0 for an implicit receiver (MRI's DEFINED_FUNC, any +// visibility) and 1 for a written one (DEFINED_METHOD, visibility screened). +func TestDefinedMethodProbeRecordsTheReceiverKind(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + src string + want int + }{ + {`defined?(puts)`, 0}, + {`defined?(nil.foo)`, 1}, + {`defined?(self.foo)`, 1}, + } { + iseq := compileSrc(t, tc.src) + if len(iseq.Children) != 1 { + t.Fatalf("%s: want one guard child, got %d", tc.src, len(iseq.Children)) + } + found := false + for _, in := range iseq.Children[0].Insns { + if in.Op != bytecode.OpDefinedMethod { + continue + } + found = true + if in.B != tc.want { + t.Errorf("%s: OpDefinedMethod B = %d, want %d", tc.src, in.B, tc.want) + } + } + if !found { + t.Errorf("%s: no OpDefinedMethod emitted", tc.src) + } + } +} + +// TestDefinedInVoidContextEmitsNothing covers compileDiscarded: MRI compiles +// NODE_DEFINED to nothing when its value is popped, so the operand's receiver is +// never run — but the locals it assigns are still declared. +func TestDefinedInVoidContextEmitsNothing(t *testing.T) { + t.Parallel() + kept := compileSrc(t, `defined?(foo.bar); 1`) + if got := countOp(kept, bytecode.OpDefinedGuard); got != 0 { + t.Errorf("a discarded defined? emitted %d guards, want 0", got) + } + if len(kept.Children) != 0 { + t.Errorf("a discarded defined? built %d child ISeqs, want 0", len(kept.Children)) + } + // The value-returning form in the same position still compiles. + used := compileSrc(t, `x = defined?(foo.bar); 1`) + if got := countOp(used, bytecode.OpDefinedGuard); got != 1 { + t.Errorf("a used defined? emitted %d guards, want 1", got) + } + // A discarded defined? still declares the operand's locals. + decl := compileSrc(t, "defined?(zz = 1)\np 2") + if !hasLocal(decl, "zz") { + t.Error("a discarded defined?(zz = 1) did not declare zz") + } + // A non-defined? statement in a discarded position is untouched. + other := compileSrc(t, `foo; 1`) + if countOp(other, bytecode.OpSend) == 0 { + t.Error("a discarded ordinary call was elided") + } +} + +// TestDefinedDeclaresLocalsInsideContainers covers the container arms of +// declareDefinedLocals: `defined?` never evaluates its operand, but the +// assignments inside one still take a local slot, as they do in MRI's parser. +func TestDefinedDeclaresLocalsInsideContainers(t *testing.T) { + t.Parallel() + for _, tc := range []struct{ src, name string }{ + {"defined?([a = 1])\np 0", "a"}, + {"defined?({(b = 1) => 2})\np 0", "b"}, + {"defined?({1 => (c = 2)})\np 0", "c"}, + {"defined?([*(d = [1])])\np 0", "d"}, + {"defined?(defined?(e = 1))\np 0", "e"}, + } { + iseq := compileSrc(t, tc.src) + if !hasLocal(iseq, tc.name) { + t.Errorf("%s: local %q not declared (locals %v)", tc.src, tc.name, iseq.Locals) + } + } +} diff --git a/internal/vm/defined.go b/internal/vm/defined.go index a9b154a2..1fa6a765 100644 --- a/internal/vm/defined.go +++ b/internal/vm/defined.go @@ -5,10 +5,12 @@ import ( "github.com/go-embedded-ruby/ruby/internal/object" ) -// definedTag returns the canonical (frozen) String for a `defined?` tag. The -// String is freshly built each call to match MRI, which returns a new String -// object from defined?; callers never mutate it. -func definedTag(tag string) object.Value { return object.NewString(tag) } +// definedTag returns the canonical String for a `defined?` tag, through the one +// constructor the compiler half uses too (bytecode.DefinedTag). It is FROZEN: +// MRI builds every defined? answer with rb_iseq_defined_string, which returns +// rb_fstring_cstr(...) (iseq.c v3_4_0:3692), and language/defined_spec.rb +// asserts `.frozen?` on the tags it names. +func definedTag(tag string) object.Value { return bytecode.DefinedTag(tag) } // hasScopedConst reports whether cls or its ancestors define name — the // non-raising form of scopedConst (used by defined?(A::B)). A pending autoload on @@ -110,6 +112,70 @@ func (vm *VM) respondsTo(recv object.Value, name string) bool { return false } +// definedResponds answers the two `defined?` method probes, which MRI keeps +// apart (vm_insnhelper.c v3_4_0:5472-5498): +// +// - DEFINED_FUNC, an implicit receiver (`defined?(foo)`), is +// rb_ec_obj_respond_to(..., TRUE): ANY visibility counts, so a private +// method called without a receiver is "method". +// - DEFINED_METHOD, an explicit receiver (`defined?(obj.foo)`), inspects the +// method entry's visibility: private is NOT defined?, and protected only +// when the calling self is a kind of the method's owner. That is the same +// rule an explicit-receiver SEND enforces, so it is asked of the same +// helper (visBlockedKind) rather than restated here. +// +// Both fall back to respond_to_missing? when there is no method entry at all +// (check_respond_to_missing in the DEFINED_METHOD arm, and rb_obj_respond_to's +// own fallback in the DEFINED_FUNC one), which is how a proxy that answers +// through method_missing reports as defined. +// +// caller is the self of the frame the defined? was written in. +func (vm *VM) definedResponds(caller, recv object.Value, name string, explicit bool) bool { + if m := vm.findMethod(recv, name); m != nil { + if !explicit { + return true + } + return vm.visBlockedKind(recv, name, m, caller) == visPublic + } + // The arithmetic operators are a compiler fast path in rbgo, not method-table + // entries, so a receiver that HAS them answers nothing above. They are still + // methods in MRI, hence the fallback — but only for a receiver the fast path + // actually computes on: `nil / 2` raises NoMethodError here exactly as in + // MRI, so `defined?(nil / 2)` must be nil rather than "method". + if _, ok := operatorOpcode(name); ok && hasInlineArith(recv) { + return true + } + return vm.respondToMissing(recv, name) +} + +// hasInlineArith reports whether the arithmetic fast path would compute for +// recv rather than raise NoMethodError. It names the receivers it EXCLUDES, +// because the fast path's own fallback (arith.go's binary) covers the built-in +// value types generically: nil, true/false and a Symbol have no arithmetic in +// any Ruby, and a user object with no built-in backing that reached here has no +// operator method either, so it belongs to the respond_to_missing? path — which +// is where MRI sends it too. +func hasInlineArith(recv object.Value) bool { + switch o := recv.(type) { + case object.Nil, object.Bool, object.Symbol: + return false + case *RObject: + return !object.IsNil(o.builtin) + } + return true +} + +// respondToMissing runs MRI's check_respond_to_missing: it asks recv's +// respond_to_missing?(name, true), which is how an object that serves a method +// through method_missing reports it as defined. A receiver with no override +// answers false through Object#respond_to_missing?. +func (vm *VM) respondToMissing(recv object.Value, name string) bool { + if vm.findMethod(recv, "respond_to_missing?") == nil { + return false + } + return vm.send(recv, "respond_to_missing?", []object.Value{object.Symbol(name), object.True}, nil).Truthy() +} + // runDefinedGuard executes a `defined?` guard child ISeq sharing the enclosing // frame's scope (parentEnv), self, definee and block, mapping any raise inside // to nil. The child always leaves exactly one value via OpReturn. diff --git a/internal/vm/object_model.go b/internal/vm/object_model.go index a82aadeb..7025c0d2 100644 --- a/internal/vm/object_model.go +++ b/internal/vm/object_model.go @@ -2171,9 +2171,16 @@ func (vm *VM) bindBlockArgs(p *Proc, args []object.Value) []object.Value { func (vm *VM) bindBlockPositionals(p *Proc, args []object.Value) []object.Value { np := len(p.iseq.Params) if np > 1 && len(args) == 1 { - if arr, ok := args[0].(*object.Array); ok { - args = arr.Elems - } + // The auto-splat is not a type test on the lone argument: MRI runs it + // through rb_check_array_type (args_check_block_arg0, vm_args.c + // v3_4_0:97-119), which converts a non-Array with #to_ary, consulting an + // OVERRIDDEN #respond_to? first, raising TypeError when #to_ary answers + // with a non-Array, and declining — leaving the object whole — when there + // is no #to_ary or it returns nil. masgnExpandOperand is that same + // rb_check_array_type, already written for expandarray: it yields the + // elements on a successful conversion and the one-element list [v] on a + // decline, which is exactly "do not splat". + args = vm.masgnExpandOperand(args[0]) } if p.iseq.SplatIndex >= 0 { // A *rest block param has variable arity: pad up to the required diff --git a/internal/vm/puppet_batch2_test.go b/internal/vm/puppet_batch2_test.go index a25c0f38..08289565 100644 --- a/internal/vm/puppet_batch2_test.go +++ b/internal/vm/puppet_batch2_test.go @@ -116,12 +116,20 @@ func TestEmptyKwSplatDrop(t *testing.T) { } } -// TestDefinedOperatorMethod covers defined?(recv op arg) routing an operator name -// with no method-table entry (a plain object) through operatorOpcode, including %. +// TestDefinedOperatorMethod covers defined?(recv op arg) for an operator name +// with no method-table entry, including %. +// +// A PLAIN object answers nil, not "method". The two cases below asserted +// "method" while defined? took rbgo's operator fast path for every receiver; +// MRI is the witness that this was wrong — `class Plain; end; +// p defined?(Plain.new + 1)` prints nil under ruby 4.0.5, because +// DEFINED_METHOD finds no method entry and Object#respond_to_missing? answers +// false (vm_insnhelper.c v3_4_0:5476-5498). `!=` differs: it is a real +// BasicObject method, so it is found and reported. func TestDefinedOperatorMethod(t *testing.T) { cases := []struct{ src, want string }{ - {`class Plain; end; p defined?(Plain.new % 1)`, "\"method\"\n"}, - {`class Plain; end; p defined?(Plain.new + 1)`, "\"method\"\n"}, + {`class Plain; end; p defined?(Plain.new % 1)`, "nil\n"}, + {`class Plain; end; p defined?(Plain.new + 1)`, "nil\n"}, {`class Plain; end; p defined?(Plain.new != 1)`, "\"method\"\n"}, // Unary ~/+@/-@ resolve as real numeric methods, so defined? reports them. {`p defined?(5.~)`, "\"method\"\n"}, diff --git a/internal/vm/vm.go b/internal/vm/vm.go index 9a1b8343..b0a787e3 100644 --- a/internal/vm/vm.go +++ b/internal/vm/vm.go @@ -1448,7 +1448,16 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, // A string literal evaluates to a fresh mutable object each time // (Ruby semantics), so clone string constants on push; every other // constant is immutable and can be shared. - if s, ok := iseq.Consts[in.A].(*object.String); ok { + // + // A FROZEN String constant is shared, not cloned. It cannot be + // mutated, so nothing can tell the sharing apart — and cloning would + // hand back an unfrozen copy (String.Dup drops Frozen), which is + // exactly what MRI does not do: it pushes a frozen literal with + // `putobject` over the shared fstring, and only a mutable one with + // `putstring`, which duplicates. `defined?` tags are built that way + // (rb_iseq_defined_string, iseq.c v3_4_0:3692), so a clone here made + // `defined?(nil).frozen?` false. + if s, ok := iseq.Consts[in.A].(*object.String); ok && !s.Frozen { push(s.Dup()) } else { push(iseq.Consts[in.A]) @@ -1971,13 +1980,13 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, // reports "constant" without requiring the file. name := iseq.Names[in.A] if _, ok := vm.resolveConstNoAutoload(lexCref, name); ok || vm.autoloadPending(lexCref, name) { - push(definedTag("constant")) + push(definedTag(bytecode.DefinedConst)) } else { push(object.NilV) } case bytecode.OpDefinedConstTop: if _, ok := vm.cObject.consts[iseq.Names[in.A]]; ok { - push(definedTag("constant")) + push(definedTag(bytecode.DefinedConst)) } else { push(object.NilV) } @@ -1990,7 +1999,7 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, // (vm_insnhelper.c, ruby/ruby v3_4_0:1139-1141), so a // private_constant is not `defined?` by its qualified path. if ok && vm.hasScopedConst(cls, name) && !vm.scopedConstIsPrivate(cls, name) { - push(definedTag("constant")) + push(definedTag(bytecode.DefinedConst)) } else { push(object.NilV) } @@ -1998,7 +2007,7 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, name := iseq.Names[in.A] if t := ivarTable(self); t != nil { if _, ok := t[name]; ok { - push(definedTag("instance-variable")) + push(definedTag(bytecode.DefinedIvar)) break } } @@ -2006,26 +2015,41 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, case bytecode.OpDefinedCVar: name := iseq.Names[in.A] if definee != vm.cObject && cvarOwner(definee, name) != nil { - push(definedTag("class variable")) + push(definedTag(bytecode.DefinedCvar)) } else { push(object.NilV) } case bytecode.OpDefinedGVar: if vm.gvarDefined(iseq.Names[in.A]) { - push(definedTag("global-variable")) + push(definedTag(bytecode.DefinedGvar)) } else { push(object.NilV) } case bytecode.OpDefinedMethod: + // B == 1 is MRI's DEFINED_METHOD (an explicit receiver, so visibility + // applies); B == 0 is DEFINED_FUNC (an implicit self receiver, where + // every visibility counts). See definedResponds. recv := pop() - if vm.respondsTo(recv, iseq.Names[in.A]) { - push(definedTag("method")) + if vm.definedResponds(self, recv, iseq.Names[in.A], in.B == 1) { + push(definedTag(bytecode.DefinedMethod)) } else { push(object.NilV) } case bytecode.OpDefinedYield: if block != nil { - push(definedTag("yield")) + push(definedTag(bytecode.DefinedYield)) + } else { + push(object.NilV) + } + case bytecode.OpDefinedSuper: + // DEFINED_ZSUPER: MRI takes the frame's method entry, finds the normal + // superclass of its defined_class and asks rb_method_boundp for the + // method's ORIGINAL id (vm_insnhelper.c v3_4_0:5505-5518). A frame with + // no method entry — the top level, a class body — answers nil. + // homeSuperName/homeSuperDefinee are the same pair the super CALL uses, + // so a block and a define_method body answer for their home method. + if vm.superMethod(self, homeSuperDefinee, homeSuperName) != nil { + push(definedTag(bytecode.DefinedZSuper)) } else { push(object.NilV) } @@ -2143,7 +2167,13 @@ func (vm *VM) exec(iseq *bytecode.ISeq, self object.Value, args []object.Value, continue } case bytecode.OpXStr: - push(object.NewString(vm.runShellCommand(iseq.Names[in.A]))) + // A backtick / %x{} literal is not a builtin operation: MRI compiles + // it to a CALL of the method `` ` `` on self with the command String + // (NODE_XSTR -> putself, putobject, send :`), so a redefined + // Kernel#` — or a singleton `` ` `` on the object the literal is + // evaluated against — receives it. The String is an fstring, hence + // frozen; language/execution_spec.rb asserts both. + push(vm.send(self, "`", []object.Value{object.NewFrozenStringView(iseq.Names[in.A])}, nil)) case bytecode.OpSplatToArray: push(vm.splatToArray(pop())) case bytecode.OpExpandArray: @@ -2575,14 +2605,50 @@ func (vm *VM) invokeSuper(self object.Value, definee *RClass, methodName string, if methodName == "" { raise("RuntimeError", "super called outside of method") } + if found := vm.superMethod(self, definee, methodName); found != nil { + return vm.invoke(found, self, args, blk) + } + // No callable superclass method. MRI routes a failed super through + // method_missing: a user-defined one handles it, while the default (on + // BasicObject) reports the super-specific message below. Dispatch only a + // genuine override so the default path keeps that exact message. + // + // A refinement method's super has no method_missing fallback in MRI — it is + // resolved in the refined class alone — so it reports straight away. + if !definee.isRefinement { + if mm := lookupMethod(vm.classOf(self), "method_missing"); mm != nil && !mm.undefined && mm.owner != vm.cBasicObject { + mmArgs := append([]object.Value{object.SymVal(methodName)}, args...) + return vm.invoke(mm, self, mmArgs, blk) + } + } + raise("NoMethodError", "super: no superclass method '%s'", methodName) + return object.NilV +} + +// superMethod resolves the method a `super` written in methodName (defined in +// definee, running on receiver self) would call, WITHOUT calling it. It returns +// nil when no callable definition exists above definee. +// +// It is the single resolution shared by the super CALL (OpInvokeSuper) and by +// `defined?(super)` (OpDefinedSuper): MRI answers DEFINED_ZSUPER with +// rb_method_boundp over vm_search_normal_superclass of the frame's +// defined_class (vm_insnhelper.c v3_4_0:5505-5518), i.e. the same search the +// call performs, so the two must not drift apart. +func (vm *VM) superMethod(self object.Value, definee *RClass, methodName string) *Method { + // A frame with no method entry — the top level, a class body — has no super + // to find. MRI's DEFINED_ZSUPER asks the same question of + // rb_vm_frame_method_entry before searching (vm_insnhelper.c v3_4_0:5507). + if methodName == "" { + return nil + } // super inside a refinement method resolves in the refined class only — its // own method and normal ancestor chain — skipping every other active // refinement (MRI: "looks only in the refined class"). if definee.isRefinement { if m := lookupMethod(definee.refinedClass, methodName); m != nil && !m.undefined { - return vm.invoke(m, self, args, blk) + return m } - raise("NoMethodError", "super: no superclass method '%s'", methodName) + return nil } // super resolves to the next definition of methodName after the current // method's owner (definee) in the receiver's ancestor chain — so it walks @@ -2634,16 +2700,7 @@ func (vm *VM) invokeSuper(self object.Value, definee *RClass, methodName string, found = lookupSMethod(definee.super, methodName) } if found != nil && !found.undefined { - return vm.invoke(found, self, args, blk) + return found } - // No callable superclass method. MRI routes a failed super through - // method_missing: a user-defined one handles it, while the default (on - // BasicObject) reports the super-specific message below. Dispatch only a - // genuine override so the default path keeps that exact message. - if mm := lookupMethod(vm.classOf(self), "method_missing"); mm != nil && !mm.undefined && mm.owner != vm.cBasicObject { - mmArgs := append([]object.Value{object.SymVal(methodName)}, args...) - return vm.invoke(mm, self, mmArgs, blk) - } - raise("NoMethodError", "super: no superclass method '%s'", methodName) - return object.NilV + return nil } diff --git a/internal/vm/wave27_lang_test.go b/internal/vm/wave27_lang_test.go new file mode 100644 index 00000000..945c8f89 --- /dev/null +++ b/internal/vm/wave27_lang_test.go @@ -0,0 +1,305 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm + +import "testing" + +// TestDefinedSuper covers OpDefinedSuper and superMethod: DEFINED_ZSUPER asks +// whether the method `super` would reach exists, without calling it, and answers +// nil when there is none — or when the frame has no method at all. +func TestDefinedSuper(t *testing.T) { + for _, tc := range []struct{ src, want string }{ + // A superclass method exists. + {`class A; def m; end; end + class B < A; def m; p defined?(super); end; end + B.new.m`, "\"super\"\n"}, + // None does. + {`class C; def m; p defined?(super); end; end + C.new.m`, "nil\n"}, + // From a block inside the method: a block is transparent to super. + {`class A2; def m; end; end + class B2 < A2; def m; [1].each { p defined?(super) }; end; end + B2.new.m`, "\"super\"\n"}, + // From a define_method body. + {`class A3; def m; end; end + class B3 < A3; define_method(:m) { p defined?(super) }; end + B3.new.m`, "\"super\"\n"}, + // Through an included module's method, into the including hierarchy. + {`class D; def m; end; end + module M; def m; p defined?(super); end; end + class E < D; include M; end + E.new.m`, "\"super\"\n"}, + // An undef'ed superclass method is a blocker, not a definition. + {`class F; def m; end; end + class G < F; undef_method :m; end + class H < G; def m; p defined?(super); end; end + H.new.m`, "nil\n"}, + // Outside any method there is no method entry to ask about. + {`p defined?(super)`, "nil\n"}, + // The super ARGUMENTS are not evaluated. + {`class A4; def m(x); end; end + class B4 < A4; def m(x); p defined?(super(raise("never"))); end; end + B4.new.m(1)`, "\"super\"\n"}, + // A class-method super. + {`class A5; def self.m; end; end + class B5 < A5; def self.m; p defined?(super); end; end + B5.m`, "\"super\"\n"}, + } { + if got := eval(t, tc.src); got != tc.want { + t.Errorf("src=%q got=%q want=%q", tc.src, got, tc.want) + } + } +} + +// TestDefinedSuperInRefinement covers superMethod's refinement arm: a `super` +// written in a refinement method resolves in the REFINED class alone. +func TestDefinedSuperInRefinement(t *testing.T) { + src := `class RBase; def m; "base"; end; end + module RMod + refine RBase do + def m; p defined?(super); super; end + end + end + using RMod + p RBase.new.m` + if got, want := eval(t, src), "\"super\"\n\"base\"\n"; got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestDefinedTagsAreFrozen covers the runtime half of the tag change: the tags +// the VM decides (constant/method/yield/…) are frozen too, as MRI's +// rb_iseq_defined_string fstrings are, and a frozen String CONSTANT is shared by +// OpPushConst rather than duplicated. +func TestDefinedTagsAreFrozen(t *testing.T) { + src := `p defined?(nil).frozen? + p defined?(self).frozen? + p defined?(String).frozen? + p defined?(puts).frozen? + p defined?(@x = 1).frozen? + $g = 1; p defined?($g).frozen? + @iv = 1; p defined?(@iv).frozen? + class FT; @@cv = 1; def m; defined?(@@cv); end; end + p FT.new.m.frozen? + def fy; defined?(yield); end + p fy { }.frozen? + p defined?(::String).frozen? + p defined?(Kernel::Integer).frozen? + p "plain".frozen?` + want := "true\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\nfalse\n" + if got := eval(t, src); got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestDefinedMethodVisibility covers definedResponds: DEFINED_FUNC (no receiver +// written) counts any visibility, DEFINED_METHOD (a receiver written) screens +// it, and a protected method answers only for a kin self. +func TestDefinedMethodVisibility(t *testing.T) { + src := `class V + def pub; end + private; def priv; end + protected; def prot; end + public + def probe; [defined?(priv), defined?(self.priv), defined?(prot), defined?(self.prot), defined?(self.pub)]; end + end + p V.new.probe + p defined?(V.new.priv) + p defined?(V.new.prot) + class W < V; def probe2; defined?(V.new.prot); end; end + p W.new.probe2` + want := "[\"method\", nil, \"method\", \"method\", \"method\"]\nnil\nnil\n\"method\"\n" + if got := eval(t, src); got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestDefinedRespondToMissing covers the respond_to_missing? fallback of both +// probes, and the branch where the receiver has no such hook to ask. +func TestDefinedRespondToMissing(t *testing.T) { + src := `o = Object.new + def o.respond_to_missing?(n, p) = n == :yes + p defined?(o.yes) + p defined?(o.no) + p defined?(Object.new.whatever) + b = BasicObject.new + p defined?(b.whatever)` + want := "\"method\"\nnil\nnil\nnil\n" + if got := eval(t, src); got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestDefinedOperatorReceiverKind covers hasInlineArith: the arithmetic +// operators have no method-table entry in rbgo, so defined? falls back to the +// fast path — but only for a receiver that path computes on. nil, true/false and +// a Symbol have no arithmetic at all, and a user object with no built-in backing +// belongs to the respond_to_missing? path. +func TestDefinedOperatorReceiverKind(t *testing.T) { + src := `p defined?(1 / 2) + p defined?("a" % 1) + p defined?([1] + [2]) + p defined?(nil / 2) + p defined?(true + 1) + p defined?(:a + 1) + p defined?(Object.new + 1) + class SubStr < String; end + p defined?(SubStr.new("a") + "b")` + want := "\"method\"\n\"method\"\n\"method\"\nnil\nnil\nnil\nnil\n\"method\"\n" + if got := eval(t, src); got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestRespondsToOperatorFastPath covers the operator arm of respondsTo, the +// VM-internal predicate (distinct from defined?'s) that Object#try and the +// library bindings use to ask whether a receiver answers a name. +func TestRespondsToOperatorFastPath(t *testing.T) { + if got, want := eval(t, `p 1.try(:+, 2)`), "3\n"; got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestDefinedInVoidContextDoesNotRunTheReceiver covers the runtime consequence +// of compileDiscarded. +func TestDefinedInVoidContextDoesNotRunTheReceiver(t *testing.T) { + src := `$ran = false + def se; $ran = true; 1; end + defined?(se / 2) + p $ran + p defined?(se / 2) + p $ran` + want := "false\n\"method\"\ntrue\n" + if got := eval(t, src); got != want { + t.Errorf("got=%q want=%q", got, want) + } +} + +// TestBlockAutoSplatUsesToAry covers the auto-splat conversion: a lone argument +// handed to a multi-parameter block goes through rb_check_array_type, so #to_ary +// converts, a non-Array answer is a TypeError, and an object that declines stays +// whole. +func TestBlockAutoSplatUsesToAry(t *testing.T) { + for _, tc := range []struct{ src, want string }{ + {`o = Object.new; def o.to_ary = [1, 2] + [o].each { |a, b| p [a, b] }`, "[1, 2]\n"}, + {`o = Object.new + [o].each { |a, b| p [a.class, b] }`, "[Object, nil]\n"}, + {`o = Object.new; def o.to_ary = nil + [o].each { |a, b| p [a.class, b] }`, "[Object, nil]\n"}, + {`o = Object.new; def o.to_ary = 1 + begin; [o].each { |a, b| }; rescue TypeError => e; p e.class; end`, "TypeError\n"}, + {`o = Object.new; def o.to_ary = raise("boom") + begin; [o].each { |a, b| }; rescue RuntimeError => e; p e.message; end`, "\"boom\"\n"}, + {`[[1, 2]].each { |a, b| p [a, b] }`, "[1, 2]\n"}, + {`[[1, 2]].each { |a| p a }`, "[1, 2]\n"}, + } { + if got := eval(t, tc.src); got != tc.want { + t.Errorf("src=%q got=%q want=%q", tc.src, got, tc.want) + } + } +} + +// TestPatternMatchingDeconstructContract covers the pattern-matching changes: +// the constant is tested with ===, before the protocol call; #deconstruct_keys +// is told which keys the pattern wants; a wrong return type is a TypeError; and +// #deconstruct is called once per `case`. +func TestPatternMatchingDeconstructContract(t *testing.T) { + for _, tc := range []struct{ src, want string }{ + // The constant decides through ===, and is asked FIRST. + {`class NoK; def self.===(o) = false; end + o = Object.new; def o.deconstruct_keys(k) = (p :asked; {a: 1}) + case o + in NoK(a: 1) then p :matched + else p :else + end`, ":else\n"}, + {`class YesK; def self.===(o) = true; end + o = Object.new; def o.deconstruct_keys(k) = {a: 1} + case o + in YesK(a: 1) then p :matched + end`, ":matched\n"}, + // The key list reaches #deconstruct_keys. + {`o = Object.new; def o.deconstruct_keys(k) = (p k; {a: 1, b: 2}) + case o + in {a:, b:} then p :ok + end`, "[:a, :b]\n:ok\n"}, + {`o = Object.new; def o.deconstruct_keys(k) = (p k; {a: 1, b: 2}) + case o + in {a:, **rest} then p :ok + end`, "nil\n:ok\n"}, + {`o = Object.new; def o.deconstruct_keys(k) = (p k; {a: 1}) + case o + in {a:, **nil} then p :ok + end`, "nil\n:ok\n"}, + // A wrong return type is a TypeError, not a failed match. + {`class BadD; def deconstruct = 1; end + begin; case BadD.new; in [1]; end; rescue TypeError => e; p e.message; end`, + "\"deconstruct must return Array\"\n"}, + {`class BadK2; def deconstruct_keys(k) = 1; end + begin; case BadK2.new; in {a: 1}; end; rescue TypeError => e; p e.message; end`, + "\"deconstruct_keys must return Hash\"\n"}, + // #deconstruct is asked once for the whole case, across clauses… + {`$n = 0 + o = Object.new; def o.deconstruct = ($n += 1; [0, 1]) + case o + in [1, 2] then p false + in [0, 1] then p true + end + p $n`, "true\n1\n"}, + // …and its ABSENCE is remembered too, so respond_to? is asked once. + {`$n = 0 + o = Object.new + def o.respond_to?(m, p = false) = ($n += 1; false) + case o + in [1] then p :a + in [2] then p :b + else p :else + end + p $n`, ":else\n1\n"}, + // A nested pattern matches an ELEMENT, so it must not read the cache. + {`case [[1, 2], [3, 4]] + in [[1, 2], [3, 4]] then p :nested + end`, ":nested\n"}, + // A case in a loop re-deconstructs each new subject. + {`2.times do |i| + o = Object.new + o.define_singleton_method(:deconstruct) { [i] } + case o + in [0] then p :zero + in [1] then p :one + end + end`, ":zero\n:one\n"}, + // A find pattern takes the same protocol. + {`o = Object.new; def o.deconstruct = [1, 2, 3] + case o + in [*, 2, *] then p :found + end`, ":found\n"}, + // A bare constant pattern goes through === as well. + {`class OnlyEq; def self.===(o) = o == 7; end + case 7 + in OnlyEq then p :seven + end`, ":seven\n"}, + } { + if got := eval(t, tc.src); got != tc.want { + t.Errorf("src=%q got=%q want=%q", tc.src, got, tc.want) + } + } +} + +// TestBacktickLiteralCallsTheMethod covers OpXStr: the literal is a CALL of +// “ ` “ on self, with a frozen String, so a redefinition receives it. +func TestBacktickLiteralCallsTheMethod(t *testing.T) { + src := `runner = Object.new + runner.singleton_class.define_method(:` + "`" + `) do |str| + p [str, str.frozen?] + :from_backtick + end + runner.instance_exec { p ` + "`test command`" + ` } + runner.instance_exec { p %x(other command) }` + want := "[\"test command\", true]\n:from_backtick\n[\"other command\", true]\n:from_backtick\n" + if got := eval(t, src); got != want { + t.Errorf("got=%q want=%q", got, want) + } +}