expressir 2.4.2 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/TODO.max-perf/01-restore-ci-green.md +8 -1
- data/lib/expressir/express/node_position_index.rb +133 -20
- data/lib/expressir/express/remark_attacher.rb +9 -13
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +3 -19
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b0fe8cde3b24a7a4ce39599fa68b61b02d3f8af45c1a8f5e45ce2607a213922
|
|
4
|
+
data.tar.gz: 869991a150512a7b12fb94f18f97e66bc6954317c311f630b3fac41d55d001b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d04f62c906dc98f43a469f88ec56382decf8636407766c94b1248f94fbc21a2667144cc421a021dda592ea5d56987937ca20ea819a4300540589ea935c2767c2
|
|
7
|
+
data.tar.gz: eeaa19435f4e66921136fbe3cee4ac729f5dfd5fa4f5731cc5b88a601639518c6be3ebfa70af4d29ef8f78500e80586a50df7636a88892a03f93787d9bee2182
|
|
@@ -16,7 +16,9 @@ yeptris has since released 0.6.4.1 and 0.6.5.1.
|
|
|
16
16
|
(`package_fixtures_spec`, `package_spec`, `cache_spec`): 84/84 green on
|
|
17
17
|
0.6.5.1
|
|
18
18
|
- [x] Re-run the failed rake workflow on main (resolves 0.6.5.1)
|
|
19
|
-
- [
|
|
19
|
+
- [x] Confirmed: rake green on main after yeptris 0.6.5.3 (mingw FFI
|
|
20
|
+
constants now defined in ffi.rb; new x64-mingw32/x86_64-darwin-24
|
|
21
|
+
platform builds) — verified via the run on this merge
|
|
20
22
|
- [x] 0.6.5.1 does NOT fix mingw (same NameError, all Windows jobs) — filed
|
|
21
23
|
leptris/yeptris#318 with the CI repro
|
|
22
24
|
- [x] expressir-side guard: `Expressir.select_serialization_engines` pins the
|
|
@@ -24,6 +26,11 @@ yeptris has since released 0.6.4.1 and 0.6.5.1.
|
|
|
24
26
|
lutaml-model's autodetection never loads the broken mingw build; Unix
|
|
25
27
|
keeps yeptris. Spec: spec/expressir/engine_selection_spec.rb
|
|
26
28
|
|
|
29
|
+
- [x] Follow-up (2026-09-19): yeptris 0.6.7.4 re-vendors libyeptris (mingw
|
|
30
|
+
gem ships libyeptris.dll + per-Ruby natives; FFI constants defined in
|
|
31
|
+
ffi.rb). Engine loads and parses on darwin via the FFI ladder; this
|
|
32
|
+
PR's matrix run is the Windows verdict with the engine active again.
|
|
33
|
+
|
|
27
34
|
## Acceptance
|
|
28
35
|
|
|
29
36
|
rake workflow green on main across the full platform matrix.
|
|
@@ -13,6 +13,9 @@ module Expressir
|
|
|
13
13
|
# instances; this module turns those spans into a line-keyed index that
|
|
14
14
|
# remark attachment can query.
|
|
15
15
|
class NodePositionIndex
|
|
16
|
+
EMPTY = [].freeze
|
|
17
|
+
private_constant :EMPTY
|
|
18
|
+
|
|
16
19
|
# Single source of truth for "what collections does this node have"
|
|
17
20
|
# lives on the model — each class declares its own via the
|
|
18
21
|
# `collection_attributes` macro. Referenced by both RemarkAttacher
|
|
@@ -21,31 +24,133 @@ module Expressir
|
|
|
21
24
|
|
|
22
25
|
attr_reader :nodes
|
|
23
26
|
|
|
27
|
+
# Semantic entries starting on `line` (node order).
|
|
28
|
+
def starting_at(line)
|
|
29
|
+
semantic_by_line[line] || EMPTY
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Semantic entries ending on `line` (node order).
|
|
33
|
+
def ending_at(line)
|
|
34
|
+
semantic_by_end_line[line] || EMPTY
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Semantic entries whose span covers `line` (node order within the
|
|
38
|
+
# covering band).
|
|
39
|
+
def spanning(line)
|
|
40
|
+
band = line / BAND
|
|
41
|
+
bands = span_bands
|
|
42
|
+
return EMPTY unless bands.key?(band)
|
|
43
|
+
|
|
44
|
+
bands[band].select { |n| line.between?(n[:line], n[:end_line]) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Semantic entries with end_line < line, ascending by end_line.
|
|
48
|
+
def semantic_ending_before(line)
|
|
49
|
+
sorted = semantic_by_end_line_list
|
|
50
|
+
idx = sorted.bsearch_index { |n| n[:end_line] >= line } || sorted.size
|
|
51
|
+
sorted[0...idx]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The first semantic entry (smallest line), for preamble remarks.
|
|
55
|
+
def first_semantic
|
|
56
|
+
first_by_line = semantic_by_line.keys.min
|
|
57
|
+
first_by_line && semantic_by_line[first_by_line]&.first
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Entries owned by `owner` within any of `collections`, in node order.
|
|
61
|
+
# Identity-keyed: ownership is object identity (the tree-walker's
|
|
62
|
+
# `.equal?` semantics), and value-equal model objects must not
|
|
63
|
+
# collide.
|
|
64
|
+
def children_in(owner, collections)
|
|
65
|
+
per_collection = children_index[owner]
|
|
66
|
+
return EMPTY unless per_collection
|
|
67
|
+
|
|
68
|
+
entries = per_collection.values_at(*collections).compact.flatten(1)
|
|
69
|
+
entries.sort_by { |n| node_order(n) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# First node (in node order) that is_a?(type), memoized per type.
|
|
73
|
+
def first_node_of_type(type)
|
|
74
|
+
@first_of_type ||= {}
|
|
75
|
+
@first_of_type[type] ||= @nodes.find { |n| n[:node].is_a?(type) }&.dig(:node)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def semantic_entries
|
|
79
|
+
@semantic_entries ||=
|
|
80
|
+
@nodes.select { |n| n[:line] && semantic?(n[:node]) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Non-vivifying: missed lookups must not materialize keys, or
|
|
84
|
+
# `keys.min`-style queries see phantom lines.
|
|
85
|
+
def semantic_by_line
|
|
86
|
+
@semantic_by_line ||= begin
|
|
87
|
+
h = {}
|
|
88
|
+
semantic_entries.each { |n| (h[n[:line]] ||= []) << n }
|
|
89
|
+
h
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def semantic_by_end_line
|
|
94
|
+
@semantic_by_end_line ||= begin
|
|
95
|
+
h = {}
|
|
96
|
+
semantic_entries.each { |n| (h[n[:end_line]] ||= []) << n if n[:end_line] }
|
|
97
|
+
h
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def semantic_by_end_line_list
|
|
102
|
+
@semantic_by_end_line_list ||=
|
|
103
|
+
semantic_entries.select { |n| n[:end_line] }.sort_by { |n| n[:end_line] }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def children_index
|
|
107
|
+
@children_index ||= begin
|
|
108
|
+
index = {}.compare_by_identity
|
|
109
|
+
@nodes.each do |n|
|
|
110
|
+
next unless n[:owner] && n[:line]
|
|
111
|
+
|
|
112
|
+
per_owner = (index[n[:owner]] ||= {})
|
|
113
|
+
(per_owner[n[:collection]] ||= []) << n
|
|
114
|
+
end
|
|
115
|
+
index
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def span_bands
|
|
120
|
+
@span_bands ||= begin
|
|
121
|
+
bands = Hash.new { |hash, key| hash[key] = [] }
|
|
122
|
+
semantic_entries.each do |n|
|
|
123
|
+
next unless n[:end_line]
|
|
124
|
+
|
|
125
|
+
((n[:line] / BAND)..(n[:end_line] / BAND)).each do |band|
|
|
126
|
+
bands[band] << n
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
bands
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
BAND = 1024
|
|
134
|
+
private_constant :BAND
|
|
135
|
+
|
|
24
136
|
def initialize(model, line_map)
|
|
25
137
|
@model = model
|
|
26
138
|
@line_map = line_map
|
|
27
139
|
@nodes = build_sorted_nodes
|
|
140
|
+
@node_order = nil
|
|
28
141
|
end
|
|
29
142
|
|
|
30
143
|
# Returns the most-specific node whose span contains `remark_line`,
|
|
31
144
|
# preferring same-line starts/ends, then smallest containing span.
|
|
32
145
|
# Excludes Repository and Cache (not semantic scopes for remarks).
|
|
33
146
|
def nearest_node_to(remark_line)
|
|
34
|
-
same_start =
|
|
35
|
-
n[:line] == remark_line && semantic?(n[:node])
|
|
36
|
-
end
|
|
147
|
+
same_start = starting_at(remark_line)
|
|
37
148
|
return same_start.last[:node] if same_start.any?
|
|
38
149
|
|
|
39
|
-
same_end =
|
|
40
|
-
n[:end_line] == remark_line && semantic?(n[:node])
|
|
41
|
-
end
|
|
150
|
+
same_end = ending_at(remark_line)
|
|
42
151
|
return same_end.last[:node] if same_end.any?
|
|
43
152
|
|
|
44
|
-
containing =
|
|
45
|
-
n[:line] && n[:end_line] &&
|
|
46
|
-
n[:line] <= remark_line && n[:end_line] >= remark_line &&
|
|
47
|
-
semantic?(n[:node])
|
|
48
|
-
end
|
|
153
|
+
containing = spanning(remark_line)
|
|
49
154
|
|
|
50
155
|
if containing.any?
|
|
51
156
|
exp_file_node = containing.find { |n| n[:node].is_a?(Model::ExpFile) }
|
|
@@ -62,16 +167,13 @@ module Expressir
|
|
|
62
167
|
candidates = containing if candidates.empty?
|
|
63
168
|
candidates.min_by { |n| n[:end_line] - n[:line] }[:node]
|
|
64
169
|
else
|
|
65
|
-
before =
|
|
66
|
-
n[:end_line] && n[:end_line] < remark_line && semantic?(n[:node])
|
|
67
|
-
end
|
|
170
|
+
before = semantic_ending_before(remark_line)
|
|
68
171
|
if before.any?
|
|
69
172
|
before.max_by { |n| n[:end_line] }[:node]
|
|
70
173
|
else
|
|
71
174
|
# Remark is before all nodes (e.g., preamble comment before SCHEMA).
|
|
72
175
|
# Attach to the first semantic node.
|
|
73
|
-
|
|
74
|
-
after.min_by { |n| n[:line] }[:node] if after.any?
|
|
176
|
+
first_semantic&.dig(:node)
|
|
75
177
|
end
|
|
76
178
|
end
|
|
77
179
|
end
|
|
@@ -90,17 +192,28 @@ module Expressir
|
|
|
90
192
|
end
|
|
91
193
|
return nil unless node_type
|
|
92
194
|
|
|
93
|
-
|
|
195
|
+
candidates = (0..2).flat_map do |back|
|
|
196
|
+
ending_at(remark_line - back)
|
|
197
|
+
end.select do |n|
|
|
94
198
|
n[:node].is_a?(node_type) &&
|
|
95
|
-
|
|
96
|
-
(n[:end_line] && n[:end_line] <= remark_line && n[:end_line] >= remark_line - 2))
|
|
199
|
+
n[:end_line] <= remark_line && n[:end_line] >= remark_line - 2
|
|
97
200
|
end
|
|
98
201
|
|
|
99
|
-
|
|
202
|
+
candidates.min_by { |n| node_order(n) }&.dig(:node) ||
|
|
203
|
+
first_node_of_type(node_type)
|
|
100
204
|
end
|
|
101
205
|
|
|
102
206
|
private
|
|
103
207
|
|
|
208
|
+
def node_order(entry)
|
|
209
|
+
@node_order ||= begin
|
|
210
|
+
map = {}.compare_by_identity
|
|
211
|
+
@nodes.each_with_index { |n, i| map[n] = i }
|
|
212
|
+
map
|
|
213
|
+
end
|
|
214
|
+
@node_order[entry]
|
|
215
|
+
end
|
|
216
|
+
|
|
104
217
|
def semantic?(node)
|
|
105
218
|
!node.is_a?(Model::Repository) && !node.is_a?(Model::Cache)
|
|
106
219
|
end
|
|
@@ -245,21 +245,20 @@ module Expressir
|
|
|
245
245
|
# shares a line with a node or sits outside any statement-bearing node.
|
|
246
246
|
def find_body_comment_target(remark)
|
|
247
247
|
line = remark.line
|
|
248
|
-
nodes = @node_index.nodes
|
|
249
248
|
# An own-line comment shares its line with no node. A node STARTING
|
|
250
249
|
# here means the remark is an inline tail (code; -- note). The
|
|
251
250
|
# end-line check is restricted to statements: container end_lines are
|
|
252
251
|
# child-derived approximations that can collide with comment lines.
|
|
253
|
-
return inline_target(remark,
|
|
252
|
+
return inline_target(remark, @node_index.starting_at(line)) if inline_remark?(remark)
|
|
254
253
|
|
|
255
254
|
# A closing keyword on the next code line is decisive: the comment
|
|
256
255
|
# closes that body. Without this check the comment would instead be
|
|
257
256
|
# read as leading the next statement of an OUTER region, which is
|
|
258
257
|
# where it would wrongly render.
|
|
259
|
-
closing = closing_region_target(line
|
|
258
|
+
closing = closing_region_target(line)
|
|
260
259
|
return closing if closing.first
|
|
261
260
|
|
|
262
|
-
enclosing, region, = statement_region_for(line
|
|
261
|
+
enclosing, region, = statement_region_for(line)
|
|
263
262
|
return [nil, nil, nil] unless region
|
|
264
263
|
|
|
265
264
|
following = region
|
|
@@ -339,7 +338,7 @@ module Expressir
|
|
|
339
338
|
# every span and never reaches statement_region_for. Resolve it from
|
|
340
339
|
# the keyword that follows: it names both the owner type and the body
|
|
341
340
|
# being closed.
|
|
342
|
-
def closing_region_target(line
|
|
341
|
+
def closing_region_target(line)
|
|
343
342
|
keyword_owner, region, keyword_line = closing_keyword_after(line)
|
|
344
343
|
return [nil, nil, nil] unless keyword_owner
|
|
345
344
|
|
|
@@ -351,8 +350,8 @@ module Expressir
|
|
|
351
350
|
opener_line = active_opener_line(keyword_line, keyword_owner)
|
|
352
351
|
return [nil, nil, nil] unless opener_line
|
|
353
352
|
|
|
354
|
-
owner =
|
|
355
|
-
n[:node].is_a?(keyword_owner)
|
|
353
|
+
owner = @node_index.starting_at(opener_line).find do |n|
|
|
354
|
+
n[:node].is_a?(keyword_owner)
|
|
356
355
|
end
|
|
357
356
|
return [nil, nil, nil] unless owner
|
|
358
357
|
|
|
@@ -458,18 +457,15 @@ module Expressir
|
|
|
458
457
|
[nil, nil, nil]
|
|
459
458
|
end
|
|
460
459
|
|
|
461
|
-
def statement_region_for(line, nodes)
|
|
462
|
-
candidates = nodes.select do |n|
|
|
460
|
+
def statement_region_for(line, nodes = nil)
|
|
461
|
+
candidates = (nodes || @node_index.spanning(line)).select do |n|
|
|
463
462
|
n[:line] && n[:end_line] && n[:line] <= line && n[:end_line] >= line &&
|
|
464
463
|
(n[:node].is_a?(Model::Statement) || function_rule_procedure?(n[:node]))
|
|
465
464
|
end
|
|
466
465
|
enclosing = innermost_candidate(candidates)
|
|
467
466
|
return [nil, nil, nil] unless enclosing
|
|
468
467
|
|
|
469
|
-
children =
|
|
470
|
-
n[:owner].equal?(enclosing[:node]) &&
|
|
471
|
-
STATEMENT_REGIONS.include?(n[:collection]) && n[:line]
|
|
472
|
-
end
|
|
468
|
+
children = @node_index.children_in(enclosing[:node], STATEMENT_REGIONS)
|
|
473
469
|
return [enclosing, nil, nil] if children.empty?
|
|
474
470
|
|
|
475
471
|
preceding = children.select { |n| n[:line] < line }.max_by { |n| n[:position] }
|
data/lib/expressir/version.rb
CHANGED
data/lib/expressir.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "lutaml/model"
|
|
2
|
-
|
|
2
|
+
# Liquid is intentionally NOT required here: lutaml-model's Liquefiable
|
|
3
|
+
# lazily loads it on first #to_liquid (lutaml-model#800), keeping ~170ms
|
|
4
|
+
# off the boot of every consumer that never renders Liquid templates.
|
|
3
5
|
|
|
4
6
|
# ..........................................................
|
|
5
7
|
# https://bugs.ruby-lang.org/issues/19319
|
|
@@ -21,24 +23,6 @@ else
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
module Expressir
|
|
24
|
-
# yeptris mingw builds crash once loaded (`uninitialized constant
|
|
25
|
-
# Yeptris::FFI::NODE_SCALAR`, leptris/yeptris#318) and the gem rebinds
|
|
26
|
-
# core YAML on require, so merely having it in the bundle breaks every
|
|
27
|
-
# Hash#to_yaml on Windows. Pinning the portable adapters stops
|
|
28
|
-
# lutaml-model's bundle-based autodetection from loading yeptris there;
|
|
29
|
-
# other platforms keep the native engine. Revisit when yeptris ships a
|
|
30
|
-
# working mingw build.
|
|
31
|
-
def self.select_serialization_engines(windows: Gem.win_platform?)
|
|
32
|
-
return unless windows
|
|
33
|
-
|
|
34
|
-
Lutaml::Model::Config.configure do |config|
|
|
35
|
-
config.yaml_adapter_type = :standard
|
|
36
|
-
config.json_adapter_type = :standard
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
select_serialization_engines
|
|
41
|
-
|
|
42
26
|
# Namespace modules - autoload loads the namespace file which contains
|
|
43
27
|
# autoload definitions for classes within that namespace
|
|
44
28
|
autoload :Config, "#{__dir__}/expressir/config"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expressir
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|