Skip to content

vm: Ruby Hash key semantics, catch/throw stack restore, and Pops-parse primitives (Puppet boots) - #32

Merged
tannevaled merged 3 commits into
mainfrom
puppet-pcore-loader
Jun 28, 2026
Merged

tannevaled merged 3 commits into
mainfrom
puppet-pcore-loader

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

Five small VM/prelude primitives diverged from MRI and, together, blocked
require "puppet". Tracing the Pops/pcore load path against MRI 4.0.5 (running
the same instrumented checkout under both interpreters and diffing the first
divergence) reduced each "large" blocker to a focused fix. With all five,
require "puppet" now fully loads in rbgo, and Puppet.initialize_settings
progresses to the exact same point as MRI (a LoadError on the unstaged facter
gem — an environment limit, not an engine gap).

Root causes (each with a minimal rbgo-vs-MRI repro, all fixed)

  1. object.Hash keyed every non-String key by Go pointer identity. A fresh
    key that is #eql? to a stored one (same #hash) missed; Array/Hash keys also
    failed by-value. Puppet's loader registry keys a Hash by Pops::Loader::TypedName
    (custom #hash/#eql?), so loader.load(:type, "annotation") returned nil →
    reference to unresolved type 'Annotation'. Now content-addresses Array/Hash/
    Bignum keys and routes user objects through a VM CustomKeyHook (Ruby #hash
    bucket + #eql? collision lists), matching MRI replace/delete/collision
    semantics. A non-Integer #hash now raises TypeError as MRI does.

  2. Kernel#catch did not restore the per-frame tracking stacks on a matching
    throw.
    The unwound block frames leaked their fileStack/requireDirs
    pushes. racc's catch(:racc_end_parse) + throw drives the Pops parser, so
    every type parse leaked ~6 fileStack entries — corrupting __FILE__ and
    breaking a later require_relative 'parser/compiler/catalog_validator'.

  3. StringScanner#[] was missing. The Pops lexer reads scn[1] after a scan;
    only @matched (the whole match) was tracked. Now stores the MatchData and
    adds #[] (whole match / group / named group).

  4. UnboundMethod#bind rejected a singleton-method receiver. Trollop's
    cloaker_ trick takes instance_method off class << self and binds it back;
    classOf(recv) never reaches the singleton in its super chain. Now also walks
    recv's own singleton class.

  5. Regexp#=== did not record $~. case s; when /re/ left
    Regexp.last_match/$1 nil in the taken branch (Trollop derives an option's
    :long this way). Now records the match on success and clears it on a
    non-string operand / no match, like =~. Also added Regexp.last_match,
    Hash#clear, Hash#member?.

Tests & coverage

New puppet_pcore_loader_test.go and object-package cases cover every branch
(object keys, collisions, delete/clear, non-integer-hash TypeError, KeyUnwrapper
subclass keys, catch/throw stack restore, ===/last_match $~, StringScanner
[], singleton bind_call, incompatible-bind rejection). Cross-package coverage
is 100.0%; full suite green; gofmt + go vet clean; frozen prelude
regenerated (drift test passes).

🤖 Generated with Claude Code

tannevaled and others added 3 commits June 28, 2026 17:38
…stacks on throw

Two VM primitives diverged from MRI and together blocked `require "puppet"`
(full Pops/pcore boot):

1. object.Hash keyed every non-String key by Go pointer identity, so a fresh
   key that is #eql? to a stored one (and same #hash) missed — Array/Hash keys
   and user objects that override #hash/#eql? all failed by-value lookup. Puppet's
   loader registry keys a Hash by Pops::Loader::TypedName (custom #hash/#eql?),
   so `loader.load(:type, "annotation")` returned nil → "reference to unresolved
   type 'Annotation'". Now content-addresses Array/Hash recursively and routes a
   user object through CustomKeyHook (Ruby #hash bucket + #eql? collision lists),
   matching MRI for replace/delete/collision semantics.

2. Kernel#catch recovered a matching throw without restoring fileStack /
   frameNames / frameFiles / requireDirs to the catch's depth, so the unwound
   block frames leaked their pushes. racc's catch(:racc_end_parse)+throw drives
   the Pops parser, so every type parse leaked ~6 fileStack entries, corrupting
   __FILE__ and breaking a later `require_relative 'parser/compiler/...'`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Pops lexer (slurp_support) reads scn[1] after a scan to recover an
interpolation terminator, but StringScanner only tracked @matched (the whole
match) and had no #[], so parsing any manifest raised
NoMethodError: undefined method '[]' for StringScanner. Store the MatchData from
scan/scan_until and add #[] (0 = whole match, n = nth group, name = named group;
nil for a no-match or out-of-range group), with getch/terminate/reset clearing it
to match MRI. Frozen prelude regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…$~, Hash#clear/#member?, Regexp.last_match

Further small MRI divergences uncovered driving Puppet.initialize_settings past
the pcore boot (each verified vs MRI 4.0.5 and fully tested):

- checkBindable: an UnboundMethod taken from a per-object singleton class
  (Trollop's cloaker_ trick) could not bind back to that object — classOf(recv)
  never reaches the singleton in its super chain. Now also walks recv's own
  singleton class.
- Regexp#=== matched without recording $~, so `case s; when /re/` left
  Regexp.last_match / $1 nil in the taken branch (Trollop derives :long this way).
  Now records the MatchData on success and clears it on a non-string operand or
  no match, like =~.
- Added Regexp.last_match (with optional group arg), Hash#clear, Hash#member?
  (alias of key?). A key whose #hash returns a non-Integer now raises TypeError
  as MRI does, rather than silently falling back to identity.

require "puppet" now fully loads in rbgo; Puppet.initialize_settings reaches the
same point as MRI (LoadError on the unstaged `facter` gem). Cross-package coverage
100.0%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit f00b039 into main Jun 28, 2026
9 checks passed
@tannevaled
tannevaled deleted the puppet-pcore-loader branch June 28, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant