Repository navigation
vm: Ruby Hash key semantics, catch/throw stack restore, and Pops-parse primitives (Puppet boots) - #32
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (runningthe 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, andPuppet.initialize_settingsprogresses to the exact same point as MRI (a
LoadErroron the unstagedfactergem — an environment limit, not an engine gap).
Root causes (each with a minimal rbgo-vs-MRI repro, all fixed)
object.Hashkeyed every non-String key by Go pointer identity. A freshkey that is
#eql?to a stored one (same#hash) missed; Array/Hash keys alsofailed by-value. Puppet's loader registry keys a Hash by
Pops::Loader::TypedName(custom
#hash/#eql?), soloader.load(:type, "annotation")returnednil→reference to unresolved type 'Annotation'. Now content-addresses Array/Hash/Bignum keys and routes user objects through a VM
CustomKeyHook(Ruby#hashbucket +
#eql?collision lists), matching MRI replace/delete/collisionsemantics. A non-Integer
#hashnow raisesTypeErroras MRI does.Kernel#catchdid not restore the per-frame tracking stacks on a matchingthrow. The unwound block frames leaked theirfileStack/requireDirspushes. racc's
catch(:racc_end_parse)+throwdrives the Pops parser, soevery type parse leaked ~6
fileStackentries — corrupting__FILE__andbreaking a later
require_relative 'parser/compiler/catalog_validator'.StringScanner#[]was missing. The Pops lexer readsscn[1]after a scan;only
@matched(the whole match) was tracked. Now stores theMatchDataandadds
#[](whole match / group / named group).UnboundMethod#bindrejected a singleton-method receiver. Trollop'scloaker_trick takesinstance_methodoffclass << selfand binds it back;classOf(recv)never reaches the singleton in its super chain. Now also walksrecv's own singleton class.
Regexp#===did not record$~.case s; when /re/leftRegexp.last_match/$1nil in the taken branch (Trollop derives an option's:longthis way). Now records the match on success and clears it on anon-string operand / no match, like
=~. Also addedRegexp.last_match,Hash#clear,Hash#member?.Tests & coverage
New
puppet_pcore_loader_test.goand 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[], singletonbind_call, incompatible-bind rejection). Cross-package coverageis 100.0%; full suite green; gofmt +
go vetclean; frozen preluderegenerated (drift test passes).
🤖 Generated with Claude Code