Skip to content

Commit ec1390f

Browse files
authored
Merge pull request #2957 from tk0miya/claude/memoize-hash-and-resolver-cache
Flyweight TypeName / Namespace cached in a shared trie
2 parents 177b1d8 + a35ba9f commit ec1390f

11 files changed

Lines changed: 304 additions & 69 deletions

File tree

ext/rbs_extension/ast_translation.c

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,30 @@ VALUE rbs_type_param_variance_to_ruby(enum rbs_type_param_variance value) {
177177
rb_class_new_instance(argc, argv, receiver)
178178
#endif
179179

180+
// Route Namespace / TypeName construction through the Ruby-side
181+
// flyweight cache (`RBS::Namespace.[]` / `RBS::TypeName.[]`) so that
182+
// structurally equal values produced by the parser share canonical
183+
// instances. An in-C trie walk was tried but did not beat
184+
// `rb_funcallv` on Ruby 4.0+, where method dispatch is well optimized.
185+
static ID id_intern_brackets;
186+
187+
static inline ID intern_brackets(void) {
188+
if (!id_intern_brackets) id_intern_brackets = rb_intern("[]");
189+
return id_intern_brackets;
190+
}
191+
192+
static VALUE rbs_intern_namespace(rbs_translation_context_t ctx, rbs_namespace_t *node) {
193+
VALUE args[2];
194+
args[0] = rbs_node_list_to_ruby_array(ctx, node->path);
195+
args[1] = node->absolute ? Qtrue : Qfalse;
196+
return rb_funcallv(RBS_Namespace, intern_brackets(), 2, args);
197+
}
198+
199+
static VALUE rbs_intern_type_name(VALUE namespace, VALUE name) {
200+
VALUE args[2] = { namespace, name };
201+
return rb_funcallv(RBS_TypeName, intern_brackets(), 2, args);
202+
}
203+
180204
VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instance) {
181205
if (instance == NULL) return Qnil;
182206

@@ -1378,19 +1402,7 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan
13781402
return CLASS_NEW_INSTANCE(RBS_MethodType, 1, &h);
13791403
}
13801404
case RBS_NAMESPACE: {
1381-
rbs_namespace_t *node = (rbs_namespace_t *) instance;
1382-
1383-
// Compute child VALUEs into locals variables first, before any recursion into `rbs_struct_to_ruby_value()`.
1384-
VALUE arg_path = rbs_node_list_to_ruby_array(ctx, node->path);
1385-
VALUE arg_absolute = node->absolute ? Qtrue : Qfalse;
1386-
1387-
// Claim the shared kwargs hash, clear it, fill it, and hand it to `.new`.
1388-
// Must not recurse between `rb_hash_clear()` and `CLASS_NEW_INSTANCE()`.
1389-
VALUE h = ctx.reusable_kwargs_hash;
1390-
rb_hash_clear(h);
1391-
rb_hash_aset(h, ID2SYM(rb_intern("path")), arg_path);
1392-
rb_hash_aset(h, ID2SYM(rb_intern("absolute")), arg_absolute);
1393-
return CLASS_NEW_INSTANCE(RBS_Namespace, 1, &h);
1405+
return rbs_intern_namespace(ctx, (rbs_namespace_t *) instance);
13941406
}
13951407
case RBS_SIGNATURE: {
13961408
rbs_signature_t *signature = (rbs_signature_t *) instance;
@@ -1402,18 +1414,9 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan
14021414
}
14031415
case RBS_TYPE_NAME: {
14041416
rbs_type_name_t *node = (rbs_type_name_t *) instance;
1405-
1406-
// Compute child VALUEs into locals variables first, before any recursion into `rbs_struct_to_ruby_value()`.
1407-
VALUE arg_namespace = rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->rbs_namespace); // rbs_namespace
1408-
VALUE arg_name = rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name); // rbs_ast_symbol
1409-
1410-
// Claim the shared kwargs hash, clear it, fill it, and hand it to `.new`.
1411-
// Must not recurse between `rb_hash_clear()` and `CLASS_NEW_INSTANCE()`.
1412-
VALUE h = ctx.reusable_kwargs_hash;
1413-
rb_hash_clear(h);
1414-
rb_hash_aset(h, ID2SYM(rb_intern("namespace")), arg_namespace);
1415-
rb_hash_aset(h, ID2SYM(rb_intern("name")), arg_name);
1416-
return CLASS_NEW_INSTANCE(RBS_TypeName, 1, &h);
1417+
VALUE ns = rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->rbs_namespace);
1418+
VALUE name = rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name);
1419+
return rbs_intern_type_name(ns, name);
14171420
}
14181421
case RBS_TYPES_ALIAS: {
14191422
rbs_types_alias_t *node = (rbs_types_alias_t *) instance;

lib/rbs/environment.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def constant_entry(type_name, normalized: false)
173173
unless type_name.namespace.empty?
174174
parent = type_name.namespace.to_type_name
175175
normalized_parent = normalize_module_name?(parent) or return
176-
constant_name = TypeName.new(name: type_name.name, namespace: normalized_parent.to_namespace)
176+
constant_name = TypeName[normalized_parent.to_namespace, type_name.name]
177177
constant_decls.fetch(constant_name, nil)
178178
end
179179
end
@@ -193,7 +193,7 @@ def normalize_type_name?(name)
193193
parent = normalize_module_name?(parent)
194194
return parent unless parent
195195

196-
TypeName.new(namespace: parent.to_namespace, name: name.name)
196+
TypeName[parent.to_namespace, name.name]
197197
else
198198
name
199199
end

lib/rbs/namespace.rb

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,65 @@ def initialize(path:, absolute:)
99
@absolute = absolute ? true : false
1010
end
1111

12+
# Process-wide flyweight cache. Two tries (one per `absolute` flag)
13+
# keyed on path Symbols, with the cached Namespace stored under the
14+
# `INTERN_LEAF` sentinel at each path's terminal node.
15+
@intern_mutex = Mutex.new
16+
@intern_trie_absolute = {}
17+
@intern_trie_relative = {}
18+
INTERN_LEAF = Module.new
19+
20+
# Returns a canonical `Namespace` instance for the given `path` /
21+
# `absolute` pair. Repeated calls with structurally equal arguments
22+
# return the same object, so callers can rely on `equal?` for fast
23+
# equality. The path Array is duplicated and frozen on insert.
24+
def self.[](path, absolute)
25+
absolute = absolute ? true : false
26+
27+
# Lock-free fast path.
28+
node = absolute ? @intern_trie_absolute : @intern_trie_relative
29+
path.each do |sym|
30+
node = node[sym]
31+
break unless node
32+
end
33+
if node && (cached = node[INTERN_LEAF])
34+
return cached
35+
end
36+
37+
@intern_mutex.synchronize do
38+
node = absolute ? @intern_trie_absolute : @intern_trie_relative
39+
path.each { |sym| node = (node[sym] ||= {}) }
40+
node[INTERN_LEAF] ||= begin
41+
frozen_path = path.frozen? ? path : path.dup.freeze
42+
new(path: frozen_path, absolute: absolute)
43+
end
44+
end
45+
end
46+
1247
def self.empty
13-
@empty ||= new(path: [], absolute: false)
48+
@empty ||= self[[], false]
1449
end
1550

1651
def self.root
17-
@root ||= new(path: [], absolute: true)
52+
@root ||= self[[], true]
1853
end
1954

2055
def +(other)
2156
if other.absolute?
2257
other
2358
else
24-
self.class.new(path: path + other.path, absolute: absolute?)
59+
Namespace[path + other.path, absolute?]
2560
end
2661
end
2762

2863
def append(component)
29-
self.class.new(path: path + [component], absolute: absolute?)
64+
Namespace[path + [component], absolute?]
3065
end
3166

3267
def parent
3368
@parent ||= begin
3469
raise "Parent with empty namespace" if empty?
35-
self.class.new(path: path.take(path.size - 1), absolute: absolute?)
70+
Namespace[path.take(path.size - 1), absolute?]
3671
end
3772
end
3873

@@ -45,25 +80,26 @@ def relative?
4580
end
4681

4782
def absolute!
48-
self.class.new(path: path, absolute: true)
83+
Namespace[path, true]
4984
end
5085

5186
def relative!
52-
self.class.new(path: path, absolute: false)
87+
Namespace[path, false]
5388
end
5489

5590
def empty?
5691
path.empty?
5792
end
5893

5994
def ==(other)
95+
return true if equal?(other)
6096
other.is_a?(Namespace) && other.path == path && other.absolute? == absolute?
6197
end
6298

6399
alias eql? ==
64100

65101
def hash
66-
path.hash ^ absolute?.hash
102+
@hash ||= path.hash ^ absolute?.hash
67103
end
68104

69105
def split
@@ -87,14 +123,14 @@ def to_type_name
87123
raise unless name
88124
raise unless parent
89125

90-
TypeName.new(name: name, namespace: parent)
126+
TypeName[parent, name]
91127
end
92128

93129
def self.parse(string)
94130
if string.start_with?("::")
95-
new(path: string.split("::").drop(1).map(&:to_sym), absolute: true)
131+
self[string.split("::").drop(1).map(&:to_sym), true]
96132
else
97-
new(path: string.split("::").map(&:to_sym), absolute: false)
133+
self[string.split("::").map(&:to_sym), false]
98134
end
99135
end
100136

lib/rbs/resolver/type_name_resolver.rb

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,17 @@ def self.build(env)
2929
new(all_names, aliases)
3030
end
3131

32-
def try_cache(query)
33-
cache.fetch(query) do
34-
result = yield
35-
cache[query] = result
36-
end
32+
def try_cache(type_name, context)
33+
inner = cache[context] ||= {}
34+
inner.fetch(type_name) { inner[type_name] = yield }
3735
end
3836

3937
def resolve(type_name, context:)
4038
if type_name.absolute? && has_type_name?(type_name)
4139
return type_name
4240
end
4341

44-
try_cache([type_name, context]) do
42+
try_cache(type_name, context) do
4543
if type_name.class?
4644
resolve_namespace0(type_name, context, Set.new) || nil
4745
else
@@ -51,7 +49,7 @@ def resolve(type_name, context:)
5149
resolve_type_name(type_name.name, context)
5250
else
5351
if namespace = resolve_namespace0(namespace.to_type_name, context, Set.new)
54-
type_name = TypeName.new(name: type_name.name, namespace: namespace.to_namespace)
52+
type_name = TypeName[namespace.to_namespace, type_name.name]
5553
has_type_name?(type_name)
5654
end
5755
end
@@ -68,7 +66,7 @@ def resolve_namespace(type_name, context:)
6866
raise "Type name must be a class name: #{type_name}"
6967
end
7068

71-
try_cache([type_name, context]) do
69+
try_cache(type_name, context) do
7270
ns = resolve_namespace0(type_name, context, Set.new) or return ns
7371
end
7472
end
@@ -93,10 +91,10 @@ def resolve_type_name(type_name, context)
9391
resolve_type_name(type_name, outer)
9492
else
9593
has_type_name?(inner) or raise "Context must be normalized: #{inner.inspect}"
96-
has_type_name?(TypeName.new(name: type_name, namespace: inner.to_namespace)) || resolve_type_name(type_name, outer)
94+
has_type_name?(TypeName[inner.to_namespace, type_name]) || resolve_type_name(type_name, outer)
9795
end
9896
else
99-
type_name = TypeName.new(name: type_name, namespace: Namespace.root)
97+
type_name = TypeName[Namespace.root, type_name]
10098
has_type_name?(type_name)
10199
end
102100
end
@@ -109,11 +107,11 @@ def resolve_head_namespace(head, context)
109107
resolve_head_namespace(head, outer)
110108
when TypeName
111109
has_type_name?(inner) or raise "Context must be normalized: #{inner.inspect}"
112-
type_name = TypeName.new(name: head, namespace: inner.to_namespace)
110+
type_name = TypeName[inner.to_namespace, head]
113111
has_type_name?(type_name) || aliased_name?(type_name) || resolve_head_namespace(head, outer)
114112
end
115113
else
116-
type_name = TypeName.new(name: head, namespace: Namespace.root)
114+
type_name = TypeName[Namespace.root, head]
117115
has_type_name?(type_name) || aliased_name?(type_name)
118116
end
119117
end
@@ -140,7 +138,7 @@ def resolve_namespace0(type_name, context, visited)
140138

141139
head =
142140
if type_name.absolute?
143-
root_name = TypeName.new(name: head, namespace: Namespace.root)
141+
root_name = TypeName[Namespace.root, head]
144142
has_type_name?(root_name) || aliased_name?(root_name)
145143
else
146144
resolve_head_namespace(head, context)
@@ -152,7 +150,7 @@ def resolve_namespace0(type_name, context, visited)
152150
end
153151

154152
tail.inject(head) do |namespace, name|
155-
type_name = TypeName.new(name: name, namespace: namespace.to_namespace)
153+
type_name = TypeName[namespace.to_namespace, name]
156154
case
157155
when has_type_name?(type_name)
158156
type_name

lib/rbs/type_name.rb

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,40 @@ def initialize(namespace:, name:)
2222
end
2323
end
2424

25+
# Process-wide flyweight cache. Two-level Hash keyed by canonical
26+
# Namespace identity (outer uses `compare_by_identity`) and name
27+
# Symbol.
28+
@intern_mutex = Mutex.new
29+
@intern_cache = {} #: Hash[Namespace, Hash[Symbol, TypeName]]
30+
@intern_cache.compare_by_identity
31+
32+
# Returns a canonical `TypeName` instance for the given `namespace` /
33+
# `name` pair. The namespace is canonicalized through `Namespace.[]`
34+
# so identity-based lookup works regardless of the caller passing a
35+
# fresh `Namespace.new` or an already-interned instance.
36+
def self.[](namespace, name)
37+
ns = Namespace[namespace.path, namespace.absolute?]
38+
39+
inner = @intern_cache[ns]
40+
if inner && (cached = inner[name])
41+
return cached
42+
end
43+
44+
@intern_mutex.synchronize do
45+
inner = (@intern_cache[ns] ||= {})
46+
inner[name] ||= new(namespace: ns, name: name)
47+
end
48+
end
49+
2550
def ==(other)
51+
return true if equal?(other)
2652
other.is_a?(self.class) && other.namespace == namespace && other.name == name
2753
end
2854

2955
alias eql? ==
3056

3157
def hash
32-
namespace.hash ^ name.hash
58+
@hash ||= namespace.hash ^ name.hash
3359
end
3460

3561
def to_s
@@ -53,23 +79,23 @@ def alias?
5379
end
5480

5581
def absolute!
56-
self.class.new(namespace: namespace.absolute!, name: name)
82+
TypeName[namespace.absolute!, name]
5783
end
5884

5985
def absolute?
6086
namespace.absolute?
6187
end
6288

6389
def relative!
64-
self.class.new(namespace: namespace.relative!, name: name)
90+
TypeName[namespace.relative!, name]
6591
end
6692

6793
def interface?
6894
kind == :interface
6995
end
7096

7197
def with_prefix(namespace)
72-
self.class.new(namespace: namespace + self.namespace, name: name)
98+
TypeName[namespace + self.namespace, name]
7399
end
74100

75101
def split
@@ -80,23 +106,17 @@ def +(other)
80106
if other.absolute?
81107
other
82108
else
83-
TypeName.new(
84-
namespace: self.to_namespace + other.namespace,
85-
name: other.name
86-
)
109+
TypeName[self.to_namespace + other.namespace, other.name]
87110
end
88111
end
89-
112+
90113
def self.parse(string)
91114
absolute = string.start_with?("::")
92115

93116
*path, name = string.delete_prefix("::").split("::").map(&:to_sym)
94117
raise unless name
95118

96-
TypeName.new(
97-
name: name,
98-
namespace: RBS::Namespace.new(path: path, absolute: absolute)
99-
)
119+
TypeName[Namespace[path, absolute], name]
100120
end
101121
end
102122
end

0 commit comments

Comments
 (0)