Split Arguments class by functionality

Code changes for https://github.com/icy-arctic-fox/spectator/issues/47 caused a drastic increase in compilation times.
This improves compilation times by splitting concerns for arguments.
In one case, arguments are used for matching.
In the other, arguments are captured for comparison.
The second case has been moved to a FormalArguments class.
Theoretically, this reduces the complexity and combinations the compiler might be iterating.
This commit is contained in:
Michael Miller 2022-11-27 22:26:19 -07:00
parent 015d36ea4c
commit 8efd38fbdd
No known key found for this signature in database
GPG key ID: 32B47AE8F388A1FF
7 changed files with 682 additions and 267 deletions

View file

@ -1,5 +1,13 @@
module Spectator
# Untyped arguments to a method call (message).
abstract class AbstractArguments
# Utility method for comparing two named tuples ignoring order.
private def compare_named_tuples(a : NamedTuple, b : NamedTuple)
a.each do |k, v1|
v2 = b.fetch(k) { return false }
return false unless v1 === v2
end
true
end
end
end

View file

@ -4,123 +4,68 @@ module Spectator
# Arguments used in a method call.
#
# Can also be used to match arguments.
# *Args* must be a `Tuple` or `NamedTuple` type representing the standard arguments.
# *Splat* must be a `Tuple` type representing the extra positional arguments.
# *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments.
class Arguments(Args, Splat, DoubleSplat) < AbstractArguments
# *Args* must be a `Tuple` representing the standard arguments.
# *KWArgs* must be a `NamedTuple` type representing extra keyword arguments.
class Arguments(Args, KWArgs) < AbstractArguments
# Positional arguments.
getter args : Args
# Additional positional arguments.
getter splat : Splat
# Keyword arguments.
getter kwargs : DoubleSplat
# Name of the splat argument, if used.
getter splat_name : Symbol?
getter kwargs : KWArgs
# Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
end
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
def initialize(@args : Args, @kwargs : KWArgs)
{% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %}
{% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %}
end
# Instance of empty arguments.
class_getter none : AbstractArguments = build
class_getter none : AbstractArguments = capture
# Returns unconstrained arguments.
def self.any : AbstractArguments?
nil.as(AbstractArguments?)
end
# Captures arguments passed to a call.
def self.build(args = Tuple.new, kwargs = NamedTuple.new)
new(args, nil, nil, kwargs)
end
# :ditto:
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
new(args, splat_name, splat, kwargs)
end
# Friendlier constructor for capturing arguments.
def self.capture(*args, **kwargs)
new(args, nil, nil, kwargs)
new(args, kwargs)
end
# Returns the positional argument at the specified index.
def [](index : Int)
positional[index]
args[index]
end
# Returns the specified named argument.
def [](arg : Symbol)
{% if Args < NamedTuple %}
return @args[arg] if @args.has_key?(arg)
{% end %}
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
if (splat = @splat)
{% if Args < NamedTuple %}args.values{% else %}args{% end %} + splat
else
{% if Args < NamedTuple %}args.values{% else %}args{% end %}
end
args
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
{% if Args < NamedTuple %}
args.merge(kwargs)
{% else %}
kwargs
{% end %}
kwargs
end
# Constructs a string representation of the arguments.
def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty?
return io << "(no args)" if args.empty? && kwargs.empty?
io << '('
# Add the positional arguments.
{% if Args < NamedTuple %}
# Include argument names.
args.each_with_index do |name, value, i|
io << ", " if i > 0
io << name << ": "
value.inspect(io)
end
{% else %}
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
{% end %}
# Add the splat arguments.
if (splat = @splat) && !splat.empty?
io << ", " unless args.empty?
if splat_name = !args.empty? && @splat_name
io << '*' << splat_name << ": {"
end
splat.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
io << '}' if splat_name
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
# Add the keyword arguments.
offset = args.size
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
kwargs.each_with_index(args.size) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
@ -130,55 +75,35 @@ module Spectator
end
# Checks if this set of arguments and another are equal.
def ==(other : Arguments)
def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs
end
# Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments)
self_args = args
other_args = other.args
case {self_args, other_args}
when {Tuple, Tuple} then compare(positional, other.positional, kwargs, other.kwargs)
when {Tuple, NamedTuple} then compare(kwargs, other.named, positional, other_args, other.splat)
when {NamedTuple, Tuple} then compare(positional, other.positional, kwargs, other.kwargs)
else
self_args === other_args && (!splat || splat === other.splat) && compare_named_tuples(kwargs, other.kwargs)
end
positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
end
private def compare(self_positional : Tuple, other_positional : Tuple, self_kwargs : NamedTuple, other_kwargs : NamedTuple)
self_positional === other_positional && compare_named_tuples(self_kwargs, other_kwargs)
end
private def compare(self_kwargs : NamedTuple, other_named : NamedTuple, self_positional : Tuple, other_args : NamedTuple, other_splat : Tuple?)
return false unless compare_named_tuples(self_kwargs, other_named)
# :ditto:
def ===(other : FormalArguments)
return false unless compare_named_tuples(kwargs, other.named)
i = 0
other_args.each do |k, v2|
next if self_kwargs.has_key?(k) # Covered by named arguments.
other.args.each do |k, v2|
next if kwargs.has_key?(k) # Covered by named arguments.
v1 = self_positional.fetch(i) { return false }
v1 = positional.fetch(i) { return false }
i += 1
return false unless v1 === v2
end
other_splat.try &.each do |v2|
v1 = self_positional.fetch(i) { return false }
other.splat.try &.each do |v2|
v1 = positional.fetch(i) { return false }
i += 1
return false unless v1 === v2
end
i == self_positional.size
end
private def compare_named_tuples(a : NamedTuple, b : NamedTuple)
a.each do |k, v1|
v2 = b.fetch(k) { return false }
return false unless v1 === v2
end
true
i == positional.size
end
end
end

View file

@ -0,0 +1,133 @@
require "./abstract_arguments"
module Spectator
# Arguments passed into a method.
#
# *Args* must be a `NamedTuple` type representing the standard arguments.
# *Splat* must be a `Tuple` type representing the extra positional arguments.
# *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments.
class FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments
# Positional arguments.
getter args : Args
# Additional positional arguments.
getter splat : Splat
# Keyword arguments.
getter kwargs : DoubleSplat
# Name of the splat argument, if used.
getter splat_name : Symbol?
# Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
{% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %}
{% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %}
{% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %}
end
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
end
# Captures arguments passed to a call.
def self.build(args = NamedTuple.new, kwargs = NamedTuple.new)
new(args, nil, nil, kwargs)
end
# :ditto:
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
new(args, splat_name, splat, kwargs)
end
# Instance of empty arguments.
class_getter none : AbstractArguments = build
# Returns the positional argument at the specified index.
def [](index : Int)
positional[index]
end
# Returns the specified named argument.
def [](arg : Symbol)
return @args[arg] if @args.has_key?(arg)
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
if (splat = @splat)
args.values + splat
else
args.values
end
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
args.merge(kwargs)
end
# Constructs a string representation of the arguments.
def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty?
io << '('
# Add the positional arguments.
{% if Args < NamedTuple %}
# Include argument names.
args.each_with_index do |name, value, i|
io << ", " if i > 0
io << name << ": "
value.inspect(io)
end
{% else %}
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
{% end %}
# Add the splat arguments.
if (splat = @splat) && !splat.empty?
io << ", " unless args.empty?
if splat_name = !args.empty? && @splat_name
io << '*' << splat_name << ": {"
end
splat.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
io << '}' if splat_name
end
# Add the keyword arguments.
offset = args.size
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
end
io << ')'
end
# Checks if this set of arguments and another are equal.
def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs
end
# Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments)
positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
end
# :ditto:
def ===(other : FormalArguments)
compare_named_tuples(args, other.args) && splat === other.splat && compare_named_tuples(kwargs, other.kwargs)
end
end
end

View file

@ -1,5 +1,6 @@
require "./abstract_arguments"
require "./arguments"
require "./formal_arguments"
module Spectator
# Stores information about a call to a method.
@ -16,7 +17,14 @@ module Spectator
# Creates a method call by splatting its arguments.
def self.capture(method : Symbol, *args, **kwargs)
arguments = Arguments.new(args, kwargs).as(AbstractArguments)
arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments)
new(method, arguments)
end
# Creates a method call from within a method.
# Takes the same arguments as `FormalArguments.build` but with the method name first.
def self.build(method : Symbol, *args, **kwargs)
arguments = FormalArguments.build(*args, **kwargs).as(AbstractArguments)
new(method, arguments)
end

View file

@ -1,5 +1,5 @@
require "../dsl/reserved"
require "./arguments"
require "./formal_arguments"
require "./method_call"
require "./stub"
require "./typed_stub"
@ -140,7 +140,8 @@ module Spectator
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
# Capture information about the call.
%args = ::Spectator::Arguments.build(
%call = ::Spectator::MethodCall.build(
{{method.name.symbolize}},
::NamedTuple.new(
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
),
@ -149,7 +150,6 @@ module Spectator
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
).merge({{method.double_splat}})
)
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments.
@ -242,7 +242,8 @@ module Spectator
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
# Capture information about the call.
%args = ::Spectator::Arguments.build(
%call = ::Spectator::MethodCall.build(
{{method.name.symbolize}},
::NamedTuple.new(
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
),
@ -251,7 +252,6 @@ module Spectator
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
).merge({{method.double_splat}})
)
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments.