Help us shape the future of Go! Take the 2026 Go Developer Survey

bytecode

package
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

Documentation

Overview

Package bytecode defines the instruction set and the compiled unit (ISeq).

Phase 0 is a stack VM in the YARV lineage (plan-rbgo.md §6), kept minimal. Arithmetic and comparison have dedicated opcodes as a fast path; Phase 1 generalizes them to OpSend over the real object model so that monkey-patching Integer#+ works. Until then there is no method dispatch on receivers.

Index

Constants

View Source
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"
)

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.

View Source
const (
	// FlagSendExplicit marks a send whose receiver was written explicitly
	// (`obj.foo`), as opposed to an implicit-receiver call (`foo`). A bare
	// `self.foo` is treated as implicit for the private-visibility check (MRI
	// permits private calls through an explicit self), so the compiler does NOT
	// set this flag when the receiver is `self`.
	FlagSendExplicit = 1 << iota

	// FlagSendNoKW marks a send whose LAST argument is syntactically a POSITIONAL
	// value, so a Hash arriving there must NOT be turned into keyword arguments.
	//
	// MRI decides this at the call site, not in the callee: setup_parameters_complex
	// (vm_args.c v3_4_0:591) only ever peels a trailing hash when the call info
	// carries VM_CALL_KWARG (literal `k: v` arguments) or VM_CALL_KW_SPLAT (`**h`).
	// With neither flag, `keyword_hash` stays Qnil and the hash counts as an
	// ordinary positional argument — which is why, since Ruby 3.0,
	//
	//	def foo(a, b, c, **hsh); end
	//	h = {key: 42}
	//	foo(1, 2, 3, h)     # ArgumentError (given 4, expected 3)
	//	foo(1, 2, 3, **h)   # hsh == {key: 42}
	//
	// rbgo materialises keyword arguments into a trailing Hash in the compiler, so
	// by the time the VM binds parameters the two forms look identical. This flag
	// carries the call site's answer through. It is NEGATIVE on purpose: it is set
	// only where the compiler can PROVE the last argument is positional, so every
	// path that cannot say (a native re-dispatch such as Object#send, Proc#call, or
	// a call site whose last argument is a Hash LITERAL — see below) keeps the
	// older "peel a trailing hash" behaviour rather than silently losing keywords.
	//
	// A braced hash literal IS decided, since go-ruby-parser v0.9.0 carries
	// ast.HashLit.Braced — MRI's own nd_brace bit (parse.y-ruby_4_0:4415-4419),
	// which rb_node_hash_new leaves 0 for a hash the grammar assembled from bare
	// `k: v` pairs (parse.y-ruby_4_0:11784-11791). So `foo(1, 2, 3, {key: 42})` is
	// flagged positional and `foo(1, 2, 3, key: 42)` is not, exactly as MRI reads
	// the same bit in aryset_check (parse.y-ruby_4_0:13669).
	FlagSendNoKW

	// FlagSendKWSplat marks an array-dispatch send (OpSendArray, OpSendArrayBlockArg,
	// OpInvokeBlockArray, OpInvokeSuperArray) whose LAST written argument is a
	// keyword splat — `**h`, or a keyword list containing one — so the last element
	// of the argument Array is that keyword Hash and nothing else is.
	//
	// It is MRI's VM_CALL_KW_SPLAT, and it exists because the hash may have to
	// disappear: ignore_keyword_hash_p (vm_args.c v3_4_0:506) drops an EMPTY
	// keyword splat from the argument list entirely, so `f(**{})` passes no
	// argument at all and `f(x, **{})` passes exactly one — a POSITIONAL x, even
	// when x is itself a Hash. rbgo used to make that decision in the compiler, by
	// emitting an `empty?` test that skipped the concatenation; but a call site
	// that drops its own last argument can no longer say whether what is now last
	// was written as keywords, which is what FlagSendNoKW has to answer. So the
	// hash is always appended and the VM drops it, exactly where MRI does.
	FlagSendKWSplat

	// FlagSendVCall marks a VCALL: a BARE identifier written with no receiver, no
	// parentheses, no arguments and no block, which at compile time did not
	// resolve to a local variable. MRI's parser makes it a different NODE from a
	// call — gettable's "method call without arguments" branch returns NEW_VCALL
	// (parse.y-ruby_4_0:13086) where an explicit call builds NODE_FCALL — and the
	// difference survives all the way to the error:
	//
	//	nope          # NameError:     undefined local variable or method 'nope' for main
	//	nope()        # NoMethodError: undefined method 'nope' for main
	//	nope {}       # NoMethodError
	//	self.nope     # NoMethodError
	//
	// The call info carries VM_CALL_VCALL (scope_to_ci, vm_eval.c:435); the miss
	// turns it into MISSING_VCALL, and raise_method_missing (vm_eval.c:943) is the
	// single place that reads it: MISSING_VCALL alone selects the format string
	// "undefined local variable or method '%1$s' for %3$s%4$s" AND swaps the
	// exception class to rb_eNameError, which rb_make_no_method_exception
	// (vm_eval.c:927) then builds with rb_name_err_new instead of
	// rb_nomethod_err_new. #name and #receiver are set either way.
	//
	// Since go-ruby-parser v0.9.0 the compiler decides this EXACTLY, and the flag
	// is the whole rule rather than an approximation of it. ast.Call.Paren
	// separates `foo` from `foo()` — MRI's `fcall: operation` feeding
	// `method_call: fcall paren_args` (parse.y-ruby_4_0:3572-3577 and 5242-5248)
	// builds NEW_FCALL where a bare name reaching gettable builds NEW_VCALL — and
	// the compiler also tests the shape of the NAME, because `operation` admits
	// tFID: `foo!` and `foo?` are parenthesis-less FCALLs
	// (parse.y-ruby_4_0:4370-4374 and 6696-6698). See compiler.vcallName. The VM
	// therefore reads this flag alone; it holds no second copy of the rule.
	FlagSendVCall
)
View Source
const ErrinfoCell = "#$!"

ErrinfoCell is the reserved global-variable name the compiler uses to read and write the interpreter's errinfo cell — the value Ruby's `$!` reports — when it lowers a begin/rescue. It is deliberately UNSPELLABLE in Ruby source (`#` opens a comment), so no program can name it, and it is the name MRI gives the same hidden slot: the rescue ISeq's local table calls it `#$!`, which is why errinfo_place can read `$!` straight out of a frame (eval.c ruby_4_0:2009-2029).

It exists because `$!` itself is READ-ONLY to Ruby (rb_define_virtual_variable with a 0 setter, eval.c ruby_4_0:2225 — `$! = x` raises NameError on ruby 4.0.5 and in rbgo), so the lowering cannot go through the user-visible name.

Variables

This section is empty.

Functions

func DefinedTag

func DefinedTag(tag string) *object.String

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.

Types

type ISeq

type ISeq struct {
	Name string
	// File is the source-file path this ISeq (and its nested children) was loaded
	// from, for Kernel#__FILE__ / #__dir__. doRequire propagates it to Children so
	// a method defined in one file reports that file even when called from another.
	// Empty for compiled-in code (the prelude).
	File        string
	Insns       []Instr
	Consts      []object.Value // literal pool: integers, floats, strings
	Names       []string       // method-call and definition names
	Params      []string       // parameter names
	NumRequired int            // count of required (non-defaulted) leading params
	SplatIndex  int            // index of the *splat param, or -1

	// PostCount is the number of trailing REQUIRED positional parameters when
	// there is NO *splat — the post parameters of `def m(a=1, b)` or
	// `{ |a=5, b, c, d| }`. It is 0 whenever SplatIndex >= 0, where the post
	// parameters are already derivable as len(Params)-SplatIndex-1, and 0 for the
	// ordinary shape where every required parameter leads.
	//
	// MRI keeps post_num beside lead_num and opt_num in every iseq
	// (rb_iseq_param, vm_core.h), independent of has_rest, because the two counts
	// answer different questions: min_argc is lead_num + post_num, and post
	// parameters bind from the TAIL before the optionals are filled from what is
	// left (setup_parameters_complex, vm_args.c v3_4_0:878-892). With only
	// NumRequired and SplatIndex the no-splat case cannot be said at all: rbgo
	// read `def m(a=1, b)` as two optionals and bound b from the front.
	//
	// NumRequired stays the LEADING required count, so a reader that predates this
	// field keeps the meaning it had.
	PostCount  int
	KwNames    []string // keyword-param names; slots follow the positionals
	KwRequired []bool   // parallel to KwNames; true = required (no default)
	KwRestSlot int      // slot of the **rest keyword-splat param, or -1
	BlockSlot  int      // slot of the &block param, or -1
	NumLocals  int      // total local slots (params first, then assigns)
	Locals     []string // local-variable names by slot (for Binding); "" for anonymous slots
	Children   []*ISeq  // nested ISeqs (method bodies / class bodies defined here)
	Super      string   // for a class body: the superclass name ("" → Object)

	// Lines is the source map: which source line each instruction came from,
	// COMPRESSED to one entry per line CHANGE and kept sorted by PC.
	//
	// That compression is MRI's, not a shortcut. MRI records a line for every
	// instruction while compiling and then encodes the result as an
	// insns_info[] of distinct entries beside a positions[] of the instruction
	// index each one starts at (iseq.c v3_4_0:673), because a line spans a run
	// of instructions and storing it per instruction would multiply every ISeq
	// for nothing. Reading it back is a search for the last entry at or before
	// the pc — get_insn_info_binary_search (iseq.c v3_4_0:2160), which is the
	// implementation MRI ships as VM_INSN_INFO_TABLE_IMPL == 1. LineAt is that
	// search.
	//
	// Empty for an ISeq compiled from a source with no position information
	// (the AOT-frozen prelude of an older build); LineAt then answers 0, exactly
	// as rb_iseq_line_no does when get_insn_info finds no entry
	// (iseq.c v3_4_0:2314).
	Lines []LineEntry

	// FirstLine is the line the ISeq's defining construct sits on — MRI's
	// body->location.first_lineno. rb_vm_get_sourceline (vm_backtrace.c
	// v3_4_0:105) falls back to it when the per-instruction lookup yields 0, so
	// a frame that has not reached a mapped instruction still reports where it
	// was defined rather than line 0.
	FirstLine int

	// Ruby2Keywords is MRI's ISEQ_BODY(iseq)->param.flags.ruby2_keywords, set by
	// Module#ruby2_keywords / Proc#ruby2_keywords (vm_method.c
	// rb_mod_ruby2_keywords:3114, proc.c proc_ruby2_keywords:4133 at tag
	// ruby_4_0). It can only be set on an ISeq that has a *rest and neither
	// keyword parameters nor post positionals, which is what makes it
	// unambiguous: a trailing keyword Hash bound into that *rest is FLAGGED
	// (RHASH_PASS_AS_KEYWORDS) so a later `f(*rest)` re-splats it as keywords.
	//
	// It lives on the ISeq, not the Method, because MRI marks the instruction
	// sequence: an alias created either before or after the call shares it, which
	// is the "applies across aliasing" contract, and a Proc duplicated with #dup
	// or #clone shares it too.
	Ruby2Keywords bool

	// Caches backs the per-call-site inline method caches, one slot per
	// instruction (only OpSend slots are ever used). It is opaque to this package
	// — the vm package allocates it and gives it meaning — so the field is typed
	// `any` to keep bytecode free of a vm import (vm imports bytecode, not the
	// reverse). The vm fills it lazily on first execution of the ISeq.
	Caches any
	// contains filtered or unexported fields
}

ISeq is a compiled instruction sequence: a method body, or the program top level. Catch tables and full arity arrive with the later phases.

func (*ISeq) HandlerState

func (s *ISeq) HandlerState() uint8

HandlerState reports the memoised rescue-handler flag (0/1/2; see the field).

func (*ISeq) HasLine

func (s *ISeq) HasLine(line int) bool

HasLine reports whether any instruction in this ISeq is mapped to the given 1-based source line. It is what decides MRI's "can not enable any hooks" ArgumentError for a `target_line:` that names no line of the target's code (rb_tracepoint_enable_for_target, vm_trace.c v3_4_0:1234, whose count n stays 0 when rb_iseq_add_local_tracepoint_recursively matches nothing).

func (*ISeq) IsLineStart

func (s *ISeq) IsLineStart(pc int) bool

IsLineStart reports whether pc is the FIRST instruction of a line run — the place a `line` trace event belongs.

MRI does not answer this at run time at all: its compiler emits a distinct `trace` instruction wherever a new line begins (iseq_compile_each keeps last_line and only adds one when the line changes), and rb_iseq_trace_set (iseq.c v3_4_0:3981) flips those encodings on when a hook wants ISEQ_TRACE_EVENTS, so a program with no TracePoint executes no trace instruction and pays nothing. rbgo has no trace instruction to flip, so the same question is asked of the line map instead — and the map already holds exactly the same information, because Lines carries one entry per line CHANGE (see the field comment), which is the set of positions MRI would have marked.

The search is LineAt's, narrowed to an exact hit: the entry found must start at pc, not merely cover it. Callers gate this on tracing being enabled, so the binary search never runs for an untraced program.

func (*ISeq) LineAt

func (s *ISeq) LineAt(pc int) int

LineAt returns the 1-based source line the instruction at pc came from, or 0 when this ISeq carries no line covering it.

It is MRI's get_insn_info_binary_search (iseq.c v3_4_0:2160) over the pair (positions, insns_info): find the last entry whose position is at or before pc. A pc before the first entry has no line — MRI's search likewise starts at index 1 and can only return an entry it has passed — and answers 0, which is what rb_iseq_line_no returns for an unplaceable pc (iseq.c v3_4_0:2314).

func (*ISeq) SetHandlerState

func (s *ISeq) SetHandlerState(v uint8)

SetHandlerState records the rescue-handler flag computed by the vm. Kept off the exported field set so the AOT freeze emitter (which writes only the source-level fields) is unaffected.

type Instr

type Instr struct {
	Op      Op
	A, B, C int
	Flags   int
}

Instr is one instruction. A, B and C are operands whose meaning depends on Op. For OpSend, C is the block: 0 means none, otherwise Children[C-1] is the literal block compiled for the call.

Flags is a bitfield of per-instruction flags (see the FlagSend* constants). For the send opcodes it records whether the call had an explicit receiver, so the VM can enforce private/protected method visibility (a private method is callable only with an implicit — or `self.` — receiver).

type LineEntry

type LineEntry struct {
	PC   int // first instruction index this line covers
	Line int // 1-based source line
}

LineEntry maps the first instruction of a run to the source line that run came from. It is one half of MRI's pair: insns_info[i].line_no beside positions[i], the instruction index the entry starts at (iseq.c v3_4_0:673, rb_iseq_insns_info_encode_positions).

type Op

type Op uint8

Op is a single opcode.

const (
	OpNop Op = iota

	OpPushConst // A = index into Consts
	OpPushNil
	OpPushTrue
	OpPushFalse
	OpPushSelf
	OpNewArray // A = element count; pops that many values into a new array
	OpNewHash  // A = pair count; pops 2*A values (k0,v0,…) into a new hash
	OpNewRange // A = 1 if exclusive; pops Hi then Lo into a new range

	OpPop
	OpDup

	OpGetLocal // A = local slot
	OpSetLocal // A = local slot (leaves the value on the stack)

	// Arithmetic / comparison fast paths (binary: pop b, pop a, push a OP b).
	OpAdd
	OpSub
	OpMul
	OpDiv
	OpMod
	OpLt
	OpGt
	OpLe
	OpGe
	OpEq
	OpNeq

	// Unary (pop a, push OP a).
	OpNeg
	OpNot

	OpJump         // A = target pc
	OpBranchIf     // A = target pc; pops, jumps if truthy
	OpBranchUnless // A = target pc; pops, jumps unless truthy
	OpBranchNil    // A = target pc; pops, jumps if the value is nil (safe nav)

	OpSend                  // A = Names index (selector), B = argc; stack: recv, args… → result
	OpGetIvar               // A = Names index; pushes @name from self (nil if unset)
	OpSetIvar               // A = Names index; sets @name on self, leaves the value
	OpGetConst              // A = Names index; pushes the named constant
	OpGetScopedConst        // A = Names index; pops a module/class, pushes its named constant
	OpSetScopedConst        // A = Names index; stack: module/class, value → value (sets recv::name)
	OpGetGVar               // A = Names index; pushes the named global (match-data specials + user globals)
	OpSetGVar               // A = Names index; sets the named global to top of stack (kept)
	OpGetCVar               // A = Names index; pushes the class variable @@name (NameError if unset)
	OpGetCVarQuiet          // A = Names index; like OpGetCVar but pushes nil (no NameError) if unset — for @@name ||= …
	OpSetCVar               // A = Names index; sets the class variable @@name to top of stack (kept)
	OpSetConst              // A = Names index; sets the named constant to top of stack (kept)
	OpDefineClass           // A = Names index, B = Children index; defines/reopens a class
	OpDefineModule          // A = Names index, B = Children index; defines/reopens a module
	OpDefineMethod          // A = Names index, B = Children index; defines on the current class
	OpDefineSMethod         // A = Names index, B = Children index; defines a singleton (class) method
	OpDefineSingletonMethod // A = Names index, B = Children index; pops a receiver, defines a singleton method on it (def recv.foo)
	OpInvokeSuper           // A = argc, B = 1 to forward the frame's args (bare super) else 0; C = 1 when an explicit block value sits on top of the stack (overriding the frame block)
	OpInvokeBlock           // A = argc; yields to the block passed to the current method
	OpInvokeBlockArray      // pops an Array of args; yields it to the block passed to the current method (yield(*args))
	OpExcMatchAny           // pops an Array of classes then an exception; pushes true if the exception is_a? any of them (rescue *classes)
	OpCaseMatchAny          // pops a subject then an Array of candidates; pushes true if any candidate === subject (case … when *array)
	OpBlockGiven            // pushes true if a block was passed to the current method
	OpReturn                // returns top of stack from the current ISeq
	OpArgGiven              // A = param index; pushes true if that argument was supplied
	OpBreak                 // unwinds a block `break`: pops the value, signals the call site
	OpPushHandler           // A = rescue handler pc; pushes a begin/rescue handler
	OpPopHandler            // pops the innermost handler (begin body completed normally)
	OpReThrow               // re-raises the exception object on top of the stack
	OpExpandArray           // A=pre, B=post, C=hasSplat; pops an Array, pushes the multi-assign target values in reverse order
	OpSplatToArray          // pops a value; pushes it if an Array else wrapped in a 1-array
	OpConcatArray           // pops two Arrays b,a; pushes a concatenated with b
	OpSendArray             // like OpSend but args come from an Array: stack recv, argsArray
	OpKwGiven               // A = keyword-param index; pushes true if that keyword was supplied
	OpHashSetPair           // stack acc,k,v → acc with k→v set (incremental hash build)
	OpHashMerge             // stack acc,other → acc with other (a Hash) merged in (** splat)
	OpSendBlockArg          // like OpSend but a &block-pass value sits on top of the args
	OpSendArrayBlockArg     // like OpSendArray with a &block-pass value on top
	OpRegexp                // A = Names index (source), B = Names index (flags); pushes a compiled Regexp (memoised per occurrence, frozen)
	OpRegexpDyn             // A = 0, or (for /o) 1+the cache-slot pc to store the once-compiled Regexp into; B = Names index (flags); pops the interpolated source String, pushes a compiled Regexp
	OpTruthy                // pops a value, pushes true if it is truthy else false (normalize a === / is_a? result)
	OpRaiseNoMatch          // pops the subject value, raises NoMatchingPatternError naming it (case/in fell through)
	OpBinding               // pushes a Binding capturing the current frame (locals, self, definee)
	OpDefineClassScoped     // A = Names index (trailing name), B = Children index, C = flags (1=parent on stack, 2=super-expr on stack); defines/reopens a class at a `::` path / with a `::`-expression superclass
	OpDefineModuleScoped    // A = Names index (trailing name), B = Children index; pops a parent module/class, defines/reopens the module there
	OpXStr                  // A = Names index (command); runs the shell command and pushes its stdout as a String (`%x{…}` / backticks)
	OpInvokeSuperArray      // super(*a, **k, &b): stack is argsArray (then a &block-pass value when C==1); C>1 means a literal block from child C-2; dispatches super with the array's elements
	OpOpenSingletonClass    // A = Children index; pops a target, runs the child ISeq with the target's singleton (meta) class as the definee (`class << target`)
	OpAlias                 // A = Names index (new name), B = Names index (old name); aliases an existing method (or global variable) on the current definee
	OpUndef                 // A = Names index; undefines the named method on the current definee

	// defined? support. Each pushes the matching tag String, or nil when the
	// operand is not defined, and never raises. The compiler routes each
	// `defined?` operand to the right one by its syntactic kind; method/receiver
	// cases run under OpDefinedGuard so an undefined sub-expression yields nil.
	OpDefinedConst       // A = Names index; "constant" if the top-level constant exists, else nil
	OpDefinedScopedConst // A = Names index; pops a module/class, "constant" if it has that constant, else nil
	OpDefinedIvar        // A = Names index; "instance-variable" if self has @name set, else nil
	OpDefinedCVar        // A = Names index; "class variable" if @@name is visible in the definee chain, else nil
	OpDefinedGVar        // A = Names index; "global-variable" if $name is set, else nil
	OpDefinedMethod      // A = Names index; pops a receiver, "method" if it responds to name, else nil
	OpDefinedYield       // "yield" if a block was passed to the current method, else nil
	OpDefinedGuard       // A = Children index; runs the child ISeq; any raise inside maps to nil

	// Added after the original opcode block to preserve existing opcode numbers
	// (and the frozen prelude bytecode).
	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
	OpConcatStrings   // A = part count; pops A values and concatenates their string forms (MRI's concatstrings). It does NOT dispatch String#+, which is what makes an interpolated literal independent of a redefined operator
	OpObjToString     // pops a value; pushes it unchanged when it is a String (subclasses included), else its #to_s -- or MRI's rb_any_to_s of the value when that #to_s did not answer with a String. MRI's objtostring + anytostring in one

	// OpLast is one past the highest opcode. It is not an instruction; it sizes
	// per-opcode tables (see the VM's basic-operator cache). Keep it last.
	OpLast
)

func (Op) String

func (o Op) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL