Module: Axn::Internal::NativeMethods
- Defined in:
- lib/axn/internal/native_methods.rb
Overview
Which of a caller's object's answers are RUBY'S OWN — read from the method table, without running a line the object's class wrote.
Three layers need this, for one reason. Cooperating with a caller's object means running its code: storing a
declared inclusion: container copies it, renaming a contract failure runs the exception's #exception, and
deciding what JSON property a declared name means renders it. Verifying that the code BEHAVED produced a new
counterexample every round, because the body is arbitrary — a duplication hook that drops only a derived
lookup index leaves every element it holds answering include? correctly and the aliases it no longer
indexes answering wrongly, an #exception that succeeds on its first call and raises on its second is not
excluded by the object having been raised once, and a to_s agreeing with a name's bytes when the rules ask
says nothing about what it answers the encoder. No behavioural probe terminates against an arbitrary body,
so each fix was defeated by the next case.
OWNERSHIP terminates. It is a fact about the method table rather than a prediction about behaviour: where
Ruby's own implementation is what answers, the operation is bounded; where it is not, axn does not perform
the operation at all and takes an honest fallback instead (see Internal::ShapeGraph.detached_option_array and
Axn::Tools._named_invalid_contract). This deliberately over-rejects — a faithful include? is
indistinguishable from a lying one without running it — and bounded-and-slightly-strict is the trade every
unbounded verification here lost.
But ownership of WHICH methods, and looked up WHERE, is where a bounded rule can still be wrong, and both
halves were. A copy is faithful only when the state dup copies faithfully determines every answer, so
owning the duplication hooks was too narrow: dup shares the instance variables and drops the singleton
class, and a container answering membership from either diverges from its copy with entirely native
duplication. Hence own_array_methods, which asks for the whole of what a container answers with rather
than for a set of names, and asks the OBJECT (through its singleton class) rather than only its class.
The exception pair keeps a named set and an object lookup for its own reason: neither is copying anything,
each is deciding what raising will dispatch — clone copies the singleton class, and raise asks the object
it is handed. They are two predicates rather than one because they gate two different operations: RENAMING an
exception clones it and hands the clone to raise, while re-raising it hands raise the object itself, so
the set of methods reached differs and the narrower question must not inherit the wider one's refusals.
native_name_rendering? asks about one method for a third reason again: the
question is not whether axn can copy or dispatch the name safely but whether the name HAS a single property
to be, since a String carries bytes as well as a rendering and three separate readers pick between them.
Class Method Summary collapse
-
.absent_value?(value) ⇒ Boolean
Does this caller-supplied value mean "nothing was supplied" —
nil,false, an all-whitespace (or empty) String, or the empty Symbol?. -
.ascii_compatible_name?(value) ⇒ Boolean
Whether a name is written in an encoding whose ASCII range is ASCII — which every declared name must be, because a name that is not can neither be ASKED the questions a declaration asks of it nor SERVE as a wire key afterwards.
- .frozen?(value) ⇒ Boolean
-
.method_owner(value, name) ⇒ Object
Which class or module OWNS the method a value would dispatch for
name— resolved out of the value's method table, so the answer comes from the table rather than from the value. -
.name_encoding(value) ⇒ Object
The ENCODING a name's bytes are in, read from the bound base implementation rather than asked of the name — a String subclass can override
encodingas readily asto_s, and this decides a guard. -
.native_exception_reporting?(error) ⇒ Boolean
Whether reporting this exception AS ITSELF can run none of the exception's own code.
-
.native_exception_reraise?(error) ⇒ Boolean
Whether handing this exception BACK to
raiseunchanged re-raises that same object. -
.native_name_rendering?(name) ⇒ Boolean
Whether this NAME renders through Ruby's own code, which is the condition for "the property a rule judged is the property every consumer reads".
-
.own_array_methods(value) ⇒ Object
Every method name this Array answers with CODE OF ITS OWN, read from the method table.
-
.public_instance_method?(mod, name) ⇒ Boolean
Whether a MODULE defines a public instance method — read out of its method table, not asked of it.
Class Method Details
.absent_value?(value) ⇒ Boolean
Does this caller-supplied value mean "nothing was supplied" — nil, false, an all-whitespace (or
empty) String, or the empty Symbol?
Two questions of that shape, both about a value axn was handed and neither able to ask the value: what a
declared name means (on:, as:, expose_return_as:) and whether a fail! reason was given at all
(Axn::Failure#supplied_reason).
present?/blank? cannot answer it: they are ActiveSupport methods on Object, so a String subclass
overrides them, and a value that answered "blank" here and "present" to a later reader skipped
canonicalization and was stored raw — the exact guard/consumer split canonicalizing exists to close.
The reason case fails harder still, because it is read while a failure is already being reported: an
override that raises replaces the failure with its own exception rather than merely disagreeing.
But the SET has to stay what blank? meant, because this decides whether an option was supplied at all
and every spelling of "not supplied" a caller could reasonably write was one: expose_return_as: false
and on: false are "no exposure"/"no route", and a whitespace-only String names nothing. Answering
only nil/empty here handed false to to_sym (NoMethodError) and declared an exposure named :" ".
Whitespace is read off the value's BYTES with axn's own frozen Regexp — Regexp#match? takes a String
operand as-is in C (no to_str, no =~) — which is a bound read, not a question put to the value.
String#empty?/#encoding are bound for the same reason they would be overridden. Symbol#empty? is
NOT bound and does not need to be: a Symbol subclass can be DECLARED but never instantiated (new is
undefined and allocate raises TypeError), so no value is ever an instance of one.
Anything that is neither nil/false nor a String nor a Symbol is "present" here, which leaves it to the
caller's to_sym exactly as before — on: 123 still raises NoMethodError rather than being silently
treated as no route. That is deliberately narrower than blank?, which called every empty container
blank (it dispatches empty? on anything that answers it): [] names nothing and is not a spelling of
"no option", so it earns the same NoMethodError as 123 rather than being silently ignored. A reason
gets the same treatment from the other direction — fail!([]) carries [] as its reason rather than
falling back to the default message, which is the honest reading of a caller passing a container.
332 333 334 335 336 337 338 339 |
# File 'lib/axn/internal/native_methods.rb', line 332 def self.absent_value?(value) case value when nil, false then true when ::Symbol then value.empty? when ::String then STRING_EMPTY.bind_call(value) || _blank_string?(value) else false end end |
.ascii_compatible_name?(value) ⇒ Boolean
Whether a name is written in an encoding whose ASCII range is ASCII — which every declared name must be, because a name that is not can neither be ASKED the questions a declaration asks of it nor SERVE as a wire key afterwards.
Not a stand-in for "is UTF-8": a Latin-N name is ASCII-compatible, compares against axn's own ASCII
patterns, and works end to end (it canonicalizes to its UTF-8 rendering for every property it names).
What is excluded is the wide encodings — UTF-16, UTF-32 — where "a.b".include?(".") raises
Encoding::CompatibilityError rather than answering, and where the Symbol the name interns to is a
DISTINCT object from its UTF-8 twin ("ab".encode("UTF-16LE").to_sym != :ab), so the property the
schema advertises can never be the key a caller supplies.
294 295 296 297 298 299 |
# File 'lib/axn/internal/native_methods.rb', line 294 def self.ascii_compatible_name?(value) encoding = name_encoding(value) return true if encoding.nil? encoding.ascii_compatible? end |
.frozen?(value) ⇒ Boolean
167 |
# File 'lib/axn/internal/native_methods.rb', line 167 def self.frozen?(value) = KERNEL_FROZEN.bind_call(value) |
.method_owner(value, name) ⇒ Object
Which class or module OWNS the method a value would dispatch for name — resolved out of the value's
method table, so the answer comes from the table rather than from the value. nil when the value has no
such method at all.
This is what decides whether CALLING that method runs Ruby's own code or the caller's, which a walk needs
before it may run one at all: a container subclass that INHERITS empty? answers with the built-in's
implementation, while one that overrides it — or carries a singleton, which sits ahead of its class — is
arbitrary code that a verdict must not enter.
Resolved through the value's SINGLETON CLASS, on the same terms and for the same reason as
own_array_methods: that one module's ancestry is the whole of what the value would dispatch — its
singleton methods, a module EXTENDED onto it, and its class's own methods with anything mixed in or
prepended — so one lookup against it is the complete question. Module#instance_method resolves it,
rather than a hand-rolled walk over MODULE_ANCESTORS, because it is the same C-level resolver
Object#method uses over that same ancestry: it finds private and protected definitions, honours a
PREPENDED module's position, and treats a name undef_method removed as absent — three things a walk
comparing name lists gets wrong in the unsafe direction, since an UNDEF'd to_s would otherwise resolve
to the superclass implementation that no longer answers.
Asking the singleton class rather than the value is the whole point: Object#method is a question put to
the VALUE, and Ruby consults the value's respond_to_missing? whenever the name is ABSENT — so on a
value that defines that hook, an ownership lookup ran the caller's code, and one that raised outside
NameError left through the predicate as the verdict. Absence is not a corner here (facade_inspector
asks for a to_fs that exists in no process without ActiveSupport's conversions), and the exception path is the one
place a predicate must not become the failure: an exception that removes its own #exception while
answering is asked about a method that is by then gone.
The cost of asking the complete question is that reading the singleton class materializes an empty one,
exactly as in own_array_methods, and nothing observes the difference.
A method_missing-backed method is reported ABSENT here, where Object#method reports the class that
would dispatch it. That is the answer this module's question wants: method_missing is by definition
the caller's own code, so it is never Ruby's own implementation that answers, and every caller compares
the owner against a specific built-in — so nil and "the value's own class" take the identical branch.
213 214 215 216 217 |
# File 'lib/axn/internal/native_methods.rb', line 213 def self.method_owner(value, name) MODULE_INSTANCE_METHOD.bind_call(method_table(value), name).owner rescue ::NameError nil end |
.name_encoding(value) ⇒ Object
The ENCODING a name's bytes are in, read from the bound base implementation rather than asked of the
name — a String subclass can override encoding as readily as to_s, and this decides a guard. Nil
for anything that is neither, which has no bytes to judge and is refused by the type rule instead.
277 278 279 280 281 282 |
# File 'lib/axn/internal/native_methods.rb', line 277 def self.name_encoding(value) case value when ::Symbol then SYMBOL_ENCODING.bind_call(value) when ::String then STRING_ENCODING.bind_call(value) end end |
.native_exception_reporting?(error) ⇒ Boolean
Whether reporting this exception AS ITSELF can run none of the exception's own code. Renaming it
clones it (Exception#exception(message)) and then hands the clone to raise, which dispatches
the 0-arg #exception on it — so the reachable code is the three duplication hooks plus
#exception, looked up on the OBJECT because clone copies the singleton class onto the copy.
A frozen exception fails the same test for a different reason: Exception#exception(message)
stores the new message on the clone, clone preserves frozen state, and the store then raises
FrozenError from inside the reporting path.
150 151 152 |
# File 'lib/axn/internal/native_methods.rb', line 150 def self.native_exception_reporting?(error) !frozen?(error) && _object_owns_none?(error, EXCEPTION_REPORTING) end |
.native_exception_reraise?(error) ⇒ Boolean
Whether handing this exception BACK to raise unchanged re-raises that same object. raise error
dispatches the 0-arg #exception on it, and Ruby has no re-raise that skips that dispatch (a bare
raise re-raising $! included), so a class owning #exception can answer with a different object
or raise something else entirely — which is how a guard that re-raises what it caught came to emit a
third exception. Looked up on the OBJECT, because raise asks the object it is handed.
Deliberately NARROWER than native_exception_reporting?, which additionally refuses a frozen
exception and the duplication hooks. Both of those are about the CLONE that renaming makes; a bare
re-raise makes no clone, so raise hands back a frozen exception, and one owning only
initialize_copy, exactly as it received them (verified). Refusing them here would substitute axn's
own error for an original that could have been re-raised faithfully.
165 |
# File 'lib/axn/internal/native_methods.rb', line 165 def self.native_exception_reraise?(error) = _object_owns_none?(error, EXCEPTION_DISPATCH) |
.native_name_rendering?(name) ⇒ Boolean
Whether this NAME renders through Ruby's own code, which is the condition for "the property a rule judged is the property every consumer reads".
One property name is read by three separate readers: the property-name rules canonicalize it, the emitter
writes it into required through its to_s, and JSON.generate renders a Hash key through that same
to_s. Where the rendering is Ruby's own, those three are one fact. Where it is not, there is no single
fact to be had — and the failure is the one the rules exist to prevent. A String SUBCLASS that defines
to_s has BOTH bytes and a rendering, and only its author knows which one names the property (a subclass
holding "other" and rendering "dup" passed the collision rules beside a :dup field and then emitted
the property "dup" twice). Anything that is neither a String nor a Symbol has one rendering but produces
it per call, so a to_s that answers differently answers the verdict one property and the encoder another.
Both are refused rather than verified, for the reason this module exists.
A Symbol needs no lookup and cannot be a false negative: it can carry no override at all — Symbol takes
no instance of a subclass (new is undefined, allocate raises TypeError) and :x.singleton_class raises
TypeError — so :x.to_s is always Symbol's own. A String subclass that does NOT define to_s inherits
String's, which renders the receiver's own bytes, so it is as native here as a plain String.
The lookup asks the OBJECT rather than its class, because what will be dispatched is the whole question and a
singleton to_s is as much a name's rendering as its class's. That answer is complete rather than reachable
from every caller: a PLAIN String carrying one is never handed to this by the property-name rules, since the
emitted property they read is the frozen copy Ruby makes of a plain String Hash key — which is why the
emitter reads one name once as well (Reflection::Schema.required_key), the two together being what keeps
every artifact naming one property.
The lookup goes through method_owner, the one place this module resolves an owner and the one place it
decides what ABSENCE means. A String that has UNDEF'd to_s resolves to no method at all, so it renders
through whatever method_missing serves — emphatically not String's own — and a nil owner is never
equal? to STRING_TO_S, so that is the answer without a rescue wrapping the whole method body.
266 267 268 269 270 271 272 |
# File 'lib/axn/internal/native_methods.rb', line 266 def self.native_name_rendering?(name) case name when ::Symbol then true when ::String then STRING_TO_S.equal?(method_owner(name, :to_s)) else false end end |
.own_array_methods(value) ⇒ Object
Every method name this Array answers with CODE OF ITS OWN, read from the method table. Empty means every
answer anything can get out of it is Ruby's own Array code — which is the whole condition for
Kernel#dup of it being FAITHFUL, and it is a stronger condition than owning no duplication hook.
dup copies the elements, SHARES the instance variables and drops the singleton class. So a native
duplication only guarantees a faithful copy when the copied state determines the answers — and the state
dup copies faithfully is the elements alone. A container whose own include? reads self (identity is
not copied), or an ivar (shared, and still the caller's to mutate), or that lives on the singleton class
(not carried at all) answers one way as the caller declared it and another way in the copy axn stores.
Each of those was a counterexample to "the hooks are native, so the copy is faithful"; the condition that
holds is that the container contributes no code at all.
Which is why this is not a list of the predicates one consumer dispatches. inclusion:/exclusion: are
answered by ActiveModel with include? (or cover? for a numeric/time Range) after routing through the
container's respond_to?/is_a?/call/to_sym, while the SAME copy path stores a type:/of: list
that axn reads with Array(…)/any?/join — so an enumerated predicate list is a prediction about
consumers, wrong the moment a consumer or an ActiveModel version dispatches something else. "Owns
nothing" needs no such prediction.
ONE walk covers the three places own code can live, because they are one method table: the object's
singleton methods, a module EXTENDED onto it, and its class's own methods (with any module mixed in).
Private ones count — Ruby dispatches initialize_dup, method_missing and respond_to_missing? itself,
and a private singleton respond_to_missing? answering to :call is enough to route ActiveModel's
membership check through the container's own code on the original and not on the copy.
Reading the singleton class materializes an empty one for a plain Array. That is deliberate and is the
cost of asking the complete question: Ruby creates singleton classes lazily and nothing observes the
difference (class, dup, clone, Marshal.dump and singleton_methods all answer identically), and
the alternative is per-name owner lookups over a list of names a consumer might dispatch.
What ::Array itself answers with is the BASELINE, read from ::Array.ancestors rather than assumed to be
Array alone, so a module PREPENDED to Array (which sits ahead of it in every Array's ancestry) is
Ruby's own here rather than every declared container's undoing. The bound: a monkeypatch on ::Array's own
table is indistinguishable from Ruby's implementation by any question about owners, so this says "nothing
below ::Array adds code", and an app that redefines Array#include? globally has changed what every
Array means, copy and original alike.
129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/axn/internal/native_methods.rb', line 129 def self.own_array_methods(value) native = MODULE_ANCESTORS.bind_call(::Array) names = [] MODULE_ANCESTORS.bind_call(KERNEL_SINGLETON_CLASS.bind_call(value)).each do |mod| break if ::Array.equal?(mod) next if native.include?(mod) names.concat(MODULE_INSTANCE_METHODS.bind_call(mod, false)) names.concat(MODULE_PRIVATE_INSTANCE_METHODS.bind_call(mod, false)) end names end |
.public_instance_method?(mod, name) ⇒ Boolean
Whether a MODULE defines a public instance method — read out of its method table, not asked of it. A
declared type is a caller's class or module, and public_method_defined? is as overridable as anything
else: one that answers wrongly inverts the verdict a declaration guard reaches, turning a declaration
error into a runtime failure or refusing a type that is perfectly capable.
The caller must have established that mod IS a Module first, through a case/when (Module#=== is
a C-level check that runs none of the object's code): binding this to anything else is a TypeError, which
would be exactly the replaced-verdict failure the bound read exists to prevent.
177 |
# File 'lib/axn/internal/native_methods.rb', line 177 def self.public_instance_method?(mod, name) = MODULE_PUBLIC_METHOD_DEFINED.bind_call(mod, name) |