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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f786a7123d34be8bada764d132f213c09f229d5f94046509dbdbfc37a0d4279
4
- data.tar.gz: 9b32c54df109ca8e6945ab03d2b7e3697d2b0bce3afbb67476bfe1510e0eb2fe
3
+ metadata.gz: 0b0fe8cde3b24a7a4ce39599fa68b61b02d3f8af45c1a8f5e45ce2607a213922
4
+ data.tar.gz: 869991a150512a7b12fb94f18f97e66bc6954317c311f630b3fac41d55d001b0
5
5
  SHA512:
6
- metadata.gz: 5225e02955f94be94f71e9005330d1c58173ebd9cb5aa97956abfa9b8b8f3f2e964f25648d8074ad8ba41fb678530dad4348570ed8c697c6445224fe77c1514f
7
- data.tar.gz: f97f4c7be51de8b6c5e4e71301555f20d05bdb268b61ef7df2e7be4bd06796af255b1f2cc362e6a25a61867fd62ab0b3610eb25683d57d6b36401c9a338598f5
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
- - [ ] Confirm the rake workflow on main is green
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 = nodes.select do |n|
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 = nodes.select do |n|
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 = nodes.select do |n|
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 = nodes.select do |n|
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
- after = nodes.select { |n| n[:line] && semantic?(n[:node]) }
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
- matching = nodes.select do |n|
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
- (n[:end_line] == remark_line ||
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
- matching.first&.dig(:node) || nodes.find { |n| n[:node].is_a?(node_type) }&.dig(:node)
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, nodes) if inline_remark?(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, nodes)
258
+ closing = closing_region_target(line)
260
259
  return closing if closing.first
261
260
 
262
- enclosing, region, = statement_region_for(line, nodes)
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, nodes)
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 = nodes.find do |n|
355
- n[:node].is_a?(keyword_owner) && n[:line] == opener_line
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 = nodes.select do |n|
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] }
@@ -3,6 +3,6 @@ module Expressir
3
3
  # autoload it (Ruby autoload only works for module/class constants, not
4
4
  # for string constants like `Expressir::VERSION`).
5
5
  module Version
6
- VERSION = "2.4.2".freeze
6
+ VERSION = "2.4.3".freeze
7
7
  end
8
8
  end
data/lib/expressir.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require "lutaml/model"
2
- require "liquid" # To enable Lutaml::Model::Liquefiable
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.2
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-18 00:00:00.000000000 Z
11
+ date: 2026-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64