Class: Candor::Signature
- Inherits:
-
Object
- Object
- Candor::Signature
- Defined in:
- lib/candor/signature.rb
Overview
Compiles a method's parameters into a dispatch lambda carrying the same parameter kinds, in the
same order, and therefore the same arity.
Rendering switches on a parameter's kind, never its name. Method#parameters reports names that
cannot be used as parameters (+:_1+), names that are reserved words (+:end+), and entries with no
name at all — [[:req]], for both it and a destructuring parameter. A renderer that echoed names
would die on most real shapes, and on it it would fail silently, emitting arity 0. So every
positional, rest, keyrest and block parameter gets a generated name; only keyword names survive,
because a keyword name is the calling convention.
Those surviving names are the only user input in the generated source, and Ruby lets a parameter
shadow anything the source would otherwise reach. So the lambda closes over nothing it could lose:
the canonical name is a Symbol literal, binding() is called with parentheses a local cannot
shadow, and every generated local is prefixed with a run of underscores no keyword name starts
with. Only the sentinel survives as a free variable, and it is created by the rendered source
itself, under that same collision-free prefix.
The lambda is rendered on one line, so +eval+'s line forges its source_location exactly.
The call site it forwards to is one of two shapes. With via it is via(:name, args…), an
interceptor that receives the canonical name; without, it is name(args…), calling the body
directly. Either way the target is a method name interpolated into +eval+'d source, so
Signature.method_name! gates it first.
Candor::Signature.compile([%i[req a]], name: :greet, via: :__call, source_location: loc)
# => ->(__p0) { __call(:greet, __p0) }
Constant Summary collapse
- KEYWORD_BRANCH_LIMIT =
How many optional keywords still dispatch through call sites rather than a Hash. Each one doubles the sites, so four is where a shape nobody writes stops paying for one everybody does.
2- KINDS =
Every kind
Method#parameterscan emit, and the only ones render accepts. DECLARATIONS.keys.freeze
Class Method Summary collapse
-
.compile(parameters, name:, source_location:, via: nil) ⇒ Proc
+eval+'s file and line forge the lambda's — and therefore the compiled method's —
source_locationonto the body the caller supplied. -
.method_name!(name) ⇒ Symbol
The call site is interpolated into +eval+'d source, so its name is a trust boundary rather than a typo check.
-
.parameters!(parameters) ⇒ Array<Array>
A parameter shape reaching Signature.render from a consumer's override never passed Ruby's parser, so every kind and every keyword name is checked here rather than discovered as a
SyntaxError. -
.render(parameters, name:, via: nil) ⇒ String
Single-line source for a lambda forwarding to the call site.
Instance Method Summary collapse
-
#initialize(parameters, name, via = nil) ⇒ Signature
constructor
A new instance of Signature.
-
#source ⇒ String
The sentinel marking an unpassed optional is a local, not a constant:
Module#const_getpiercesprivate_constant, so a caller could obtain the sentinel and pass it as an argument, silently defeating an optional's default.
Constructor Details
#initialize(parameters, name, via = nil) ⇒ Signature
Returns a new instance of Signature.
174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/candor/signature.rb', line 174 def initialize(parameters, name, via = nil) self.class.parameters!(parameters) @target = self.class.method_name!(via || name) # With an interceptor the canonical name leads the argument list, as a literal rather than a free # variable a keyword could capture. @literal = via ? name.to_sym.inspect : nil @prefix = collision_free_prefix(parameters) @unset = "#{@prefix}u" @hash = "#{@prefix}k" @entries = entries(parameters) @optionals = @entries.select { |entry| OPTIONAL_KINDS.include?(entry.first) } end |
Class Method Details
.compile(parameters, name:, source_location:, via: nil) ⇒ Proc
+eval+'s file and line forge the lambda's — and therefore the compiled method's —
source_location onto the body the caller supplied.
91 92 93 94 95 96 97 98 |
# File 'lib/candor/signature.rb', line 91 def compile(parameters, name:, source_location:, via: nil) eval(render(parameters, name: name, via: via), binding, *source_location) # rubocop:disable Security/Eval rescue SyntaxError => e # {parameters!} sees one entry at a time; only the parser sees the combination — two rests, a # duplicate keyword, a block before a positional. A +SyntaxError+ is a +ScriptError+, which a # consumer's +rescue+ never catches. raise ArgumentError, "malformed parameters: #{parameters.inspect} (#{e..lines.first.strip})" end |
.method_name!(name) ⇒ Symbol
The call site is interpolated into +eval+'d source, so its name is a trust boundary rather than a typo check.
113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/candor/signature.rb', line 113 def method_name!(name) string = name.to_s # +:class+ is callable on a receiver and useless here: the generated source calls the site bare, # and a bare +class(…)+ is a +SyntaxError+. Say that, rather than "not a callable method name". if RESERVED_WORDS.include?(string) raise ArgumentError, "#{name.inspect} is a reserved word: the generated source would call it bare" end raise ArgumentError, "not a callable method name: #{name.inspect}" unless METHOD_NAME.match?(string) string.to_sym end |
.parameters!(parameters) ⇒ Array<Array>
A parameter shape reaching render from a consumer's override never passed Ruby's parser, so
every kind and every keyword name is checked here rather than discovered as a SyntaxError.
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/candor/signature.rb', line 131 def parameters!(parameters) raise ArgumentError, "malformed parameters: #{parameters.inspect}" unless parameters.is_a?(Array) # +index+, not +find+: a +nil+ entry is malformed, and +find+ would hand back the same +nil+ it # returns when every entry is fine. The caller passed the Array; naming the offender saves a bisect. index = parameters.index { |entry| !valid_entry?(entry) } raise ArgumentError, "malformed parameters: entry #{index} is #{parameters[index].inspect}" if index parameters end |
.render(parameters, name:, via: nil) ⇒ String
Returns single-line source for a lambda forwarding to the call site.
105 |
# File 'lib/candor/signature.rb', line 105 def render(parameters, name:, via: nil) = new(parameters, name, via).source |
Instance Method Details
#source ⇒ String
The sentinel marking an unpassed optional is a local, not a constant: Module#const_get pierces
private_constant, so a caller could obtain the sentinel and pass it as an argument, silently
defeating an optional's default.
192 |
# File 'lib/candor/signature.rb', line 192 def source = "#{@unset} = ::Object.new.freeze; ->(#{declarations}) { #{dispatch} }" |