expressir 2.4.1 → 2.4.2

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.max-perf/01-restore-ci-green.md +29 -0
  3. data/TODO.max-perf/02-streaming-parse-path.md +31 -0
  4. data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
  5. data/TODO.max-perf/04-benchmark-harness.md +28 -0
  6. data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
  7. data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
  8. data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
  9. data/TODO.max-perf/08-builder-build-perf.md +45 -0
  10. data/TODO.max-perf/09-grammar-cold-start.md +25 -0
  11. data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
  12. data/TODO.max-perf/11-ci-green-closeout.md +25 -0
  13. data/TODO.max-perf/12-require-boot-profile.md +25 -0
  14. data/TODO.max-perf/13-key-conversion-specs.md +26 -0
  15. data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
  16. data/benchmark/srl_benchmark.rb +76 -17
  17. data/expressir.gemspec +1 -1
  18. data/lib/expressir/cli.rb +3 -0
  19. data/lib/expressir/commands/coverage.rb +6 -2
  20. data/lib/expressir/commands/package.rb +4 -1
  21. data/lib/expressir/express/ast_key_converter.rb +114 -0
  22. data/lib/expressir/express/builder.rb +8 -119
  23. data/lib/expressir/express/error.rb +17 -0
  24. data/lib/expressir/express/parallel_files.rb +229 -0
  25. data/lib/expressir/express/parser.rb +44 -86
  26. data/lib/expressir/express/remark_attacher.rb +25 -7
  27. data/lib/expressir/express/schema_block_scanner.rb +3 -2
  28. data/lib/expressir/express/scope_resolver.rb +34 -5
  29. data/lib/expressir/express.rb +2 -0
  30. data/lib/expressir/model/model_element.rb +6 -1
  31. data/lib/expressir/model/repository.rb +18 -5
  32. data/lib/expressir/version.rb +1 -1
  33. data/lib/expressir.rb +18 -0
  34. metadata +22 -6
@@ -35,6 +35,10 @@ module Expressir
35
35
  # via `child_attributes :foo, :bar, ...`. See TODO.bugs/15.
36
36
  EXPRESSION_CHILDREN = Model::ModelElement.child_attributes_registry
37
37
 
38
+ # `WHERE <label> :` clause headers; the captured label maps to the
39
+ # 1-based line number(s) it appears on.
40
+ WHERE_CLAUSE_PATTERN = /\A\s*WHERE\s+(\w+)\s*:/i
41
+
38
42
  def initialize(source)
39
43
  @source = source
40
44
  @attached_spans = Set.new
@@ -71,6 +75,8 @@ module Expressir
71
75
  @line_map = nil
72
76
  @owner_map = nil
73
77
  @active_scope_map = nil
78
+ @source_lines_for_where_clause = nil
79
+ @where_clause_line_index = nil
74
80
  end
75
81
 
76
82
  private
@@ -658,27 +664,39 @@ module Expressir
658
664
  where_rules = get_collection(scope, :where_rules)
659
665
  return nil unless where_rules&.any?
660
666
 
661
- lines = source_lines_for_where_clause
667
+ where_clause_lines = where_clause_line_index
662
668
 
663
669
  where_rules.each do |wr|
664
670
  next unless wr.id
665
671
 
666
- lines.each_with_index do |line, idx|
667
- line_num = idx + 1
672
+ where_clause_lines.fetch(wr.id, []).each do |line_num|
668
673
  next unless line_num < remark_line
669
674
 
670
- if (line =~ /^\s*WHERE\s+#{Regexp.escape(wr.id)}\s*:/i) && remark_line.between?(line_num, line_num + 5)
671
- return create_remark_item(wr, tag)
672
- end
675
+ return create_remark_item(wr, tag) if remark_line.between?(line_num, line_num + 5)
673
676
  end
674
677
  end
675
678
 
676
679
  nil
677
680
  end
678
681
 
682
+ # Single scan over the source lines mapping each `WHERE <id>:` label to
683
+ # its 1-based line number, so per-remark lookups stop re-testing every
684
+ # line against every WHERE rule's regex.
685
+ def where_clause_line_index
686
+ @where_clause_line_index ||= begin
687
+ index = Hash.new { |h, k| h[k] = [] }
688
+ source_lines_for_where_clause.each_with_index do |line, idx|
689
+ if (match = line.match(WHERE_CLAUSE_PATTERN))
690
+ index[match[1]] << (idx + 1)
691
+ end
692
+ end
693
+ index
694
+ end
695
+ end
696
+
679
697
  def source_lines_for_where_clause
680
698
  # @source is set for the duration of `attach`; freed at the end.
681
- @source.lines
699
+ @source_lines_for_where_clause ||= @source.lines
682
700
  end
683
701
 
684
702
  def find_node_in_statement(stmt, tag)
@@ -6,8 +6,9 @@ module Expressir
6
6
  #
7
7
  # A hand-rolled state machine that skips comments (`(* ... *)`) and
8
8
  # string literals, tracks SCHEMA/END_SCHEMA depth, and returns one
9
- # block per schema declaration. Used by `Parser.from_exp_streaming`
10
- # to parse each schema independently with a fresh memory arena.
9
+ # block per schema declaration. Retained for the future streaming
10
+ # parse path (see TODO.max-perf/02) that parses each schema
11
+ # independently with a fresh memory arena.
11
12
  #
12
13
  # Extracted from the outer Parser class (TODO.bugs/09) so block-
13
14
  # scanning logic lives in one focused module behind one interface.
@@ -50,8 +50,13 @@ module Expressir
50
50
  @model = model
51
51
  @nodes_with_positions = nodes_with_positions
52
52
  @scope_map = nil
53
+ @source_lines = nil
54
+ @position_buckets = nil
53
55
  end
54
56
 
57
+ # Line-band width for the position fallback index.
58
+ BUCKET_LINES = 1024
59
+
55
60
  # Returns the innermost Model::ScopeContainer whose source span contains
56
61
  # the given 1-based remark line, or nil if none is found.
57
62
  def containing_scope_for(remark_line)
@@ -69,7 +74,7 @@ module Expressir
69
74
  type_state = { line: nil, name: nil }
70
75
  rule_state = { line: nil, name: nil }
71
76
 
72
- @source.lines.each_with_index do |line, idx|
77
+ source_lines.each_with_index do |line, idx|
73
78
  line_num = idx + 1
74
79
 
75
80
  case line
@@ -131,8 +136,15 @@ module Expressir
131
136
  @scope_map ||= build_scope_map
132
137
  end
133
138
 
139
+ # Source split into lines, computed once per resolver: both the
140
+ # scope-map build and find_by_source_text need it, and re-splitting the
141
+ # whole source per lookup dominates remark attachment on large files.
142
+ def source_lines
143
+ @source_lines ||= @source.lines
144
+ end
145
+
134
146
  def build_scope_map
135
- lines = @source.lines
147
+ lines = source_lines
136
148
  map = {}
137
149
  return map if lines.empty?
138
150
 
@@ -160,10 +172,27 @@ module Expressir
160
172
 
161
173
  # --- Strategy 2: position-based fallback against the node index ---
162
174
 
175
+ # Nodes bucketed by 1024-line bands: a node spanning [line, end_line]
176
+ # is registered in every band it overlaps, so a remark-line lookup only
177
+ # scans nodes that can possibly contain it. Bucket order preserves the
178
+ # original index order, so select+reverse_each semantics are unchanged.
179
+ def position_buckets
180
+ @position_buckets ||= begin
181
+ buckets = Hash.new { |h, k| h[k] = [] }
182
+ @nodes_with_positions.each do |n|
183
+ next unless n[:line] && n[:end_line]
184
+
185
+ ((n[:line] / BUCKET_LINES)..(n[:end_line] / BUCKET_LINES)).each do |b|
186
+ buckets[b] << n
187
+ end
188
+ end
189
+ buckets
190
+ end
191
+ end
192
+
163
193
  def find_by_position(remark_line)
164
- containing = @nodes_with_positions.select do |n|
165
- n[:line] && n[:end_line] &&
166
- remark_line >= n[:line] && remark_line <= n[:end_line] &&
194
+ containing = position_buckets[remark_line / BUCKET_LINES].select do |n|
195
+ remark_line.between?(n[:line], n[:end_line]) &&
167
196
  !n[:node].is_a?(Model::Repository) && !n[:node].is_a?(Model::Cache)
168
197
  end
169
198
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Expressir
4
4
  module Express
5
+ autoload :AstKeyConverter, "#{__dir__}/express/ast_key_converter"
5
6
  autoload :Builder, "#{__dir__}/express/builder"
6
7
  autoload :BuilderContext, "#{__dir__}/express/builder_context"
7
8
  autoload :BuilderRegistry, "#{__dir__}/express/builder_registry"
@@ -17,6 +18,7 @@ module Expressir
17
18
  autoload :LineMap, "#{__dir__}/express/line_map"
18
19
  autoload :ModelVisitor, "#{__dir__}/express/model_visitor"
19
20
  autoload :NodePositionIndex, "#{__dir__}/express/node_position_index"
21
+ autoload :ParallelFiles, "#{__dir__}/express/parallel_files"
20
22
  autoload :Parser, "#{__dir__}/express/parser"
21
23
  autoload :PrettyFormatter, "#{__dir__}/express/pretty_formatter"
22
24
  autoload :RemarkAttacher, "#{__dir__}/express/remark_attacher"
@@ -155,7 +155,12 @@ module Expressir
155
155
  end
156
156
 
157
157
  def source
158
- Expressir::Express::SourceFormatter.format(self)
158
+ # Formatting is not free and callers probe `source` repeatedly while
159
+ # walking the tree (position index, remark attachment), so the text is
160
+ # computed once per node. Deliberately NOT @source: that ivar holds
161
+ # the raw source span lutaml-model's `source=` stores when
162
+ # include_source is on, and readers must keep seeing formatted text.
163
+ @formatted_source ||= Expressir::Express::SourceFormatter.format(self) # rubocop:disable Naming/MemoizedInstanceVariableName
159
164
  end
160
165
 
161
166
  # @param [Hash] options
@@ -291,14 +291,27 @@ reference_index: nil)
291
291
  # Build repository from list of schema files
292
292
  # @param file_paths [Array<String>] Schema file paths
293
293
  # @param base_dir [String, nil] Base directory for path resolution
294
+ # @param max_processes [Integer, nil] parse in a fork worker pool when
295
+ # set above 1 on a fork-capable platform (advisory; degrades to
296
+ # sequential otherwise). Unlike Express::Parser.from_files, a file
297
+ # that fails to parse always raises.
294
298
  # @return [Repository] Built repository with all schemas
295
- def self.from_files(file_paths, base_dir: nil)
299
+ def self.from_files(file_paths, base_dir: nil, max_processes: nil)
296
300
  repo = new(base_dir: base_dir)
297
301
 
298
- file_paths.each do |path|
299
- parsed = Expressir::Express::Parser.from_file(path)
300
- next unless parsed
301
-
302
+ pool = Expressir::Express::ParallelFiles
303
+ parsed_files = if pool.sequential?(file_paths, max_processes)
304
+ file_paths.map do |path|
305
+ Expressir::Express::Parser.from_file(path)
306
+ end
307
+ else
308
+ pool.run(file_paths,
309
+ parse: ->(path) { Expressir::Express::Parser.from_file(path) },
310
+ max_processes: max_processes,
311
+ strict: true)
312
+ end
313
+
314
+ parsed_files.each do |parsed|
302
315
  repo.files << parsed if parsed.is_a?(ExpFile)
303
316
  end
304
317
 
@@ -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.1".freeze
6
+ VERSION = "2.4.2".freeze
7
7
  end
8
8
  end
data/lib/expressir.rb CHANGED
@@ -21,6 +21,24 @@ else
21
21
  end
22
22
 
23
23
  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
+
24
42
  # Namespace modules - autoload loads the namespace file which contains
25
43
  # autoload definitions for classes within that namespace
26
44
  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.1
4
+ version: 2.4.2
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-17 00:00:00.000000000 Z
11
+ date: 2026-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -174,16 +174,16 @@ dependencies:
174
174
  name: rubyzip
175
175
  requirement: !ruby/object:Gem::Requirement
176
176
  requirements:
177
- - - "~>"
177
+ - - ">="
178
178
  - !ruby/object:Gem::Version
179
- version: '3.4'
179
+ version: '0'
180
180
  type: :runtime
181
181
  prerelease: false
182
182
  version_requirements: !ruby/object:Gem::Requirement
183
183
  requirements:
184
- - - "~>"
184
+ - - ">="
185
185
  - !ruby/object:Gem::Version
186
- version: '3.4'
186
+ version: '0'
187
187
  - !ruby/object:Gem::Dependency
188
188
  name: table_tennis
189
189
  requirement: !ruby/object:Gem::Requirement
@@ -282,6 +282,20 @@ files:
282
282
  - TODO.bugs/29-validate-ascii-god-class.md
283
283
  - TODO.bugs/30-unicode-map-extraction.md
284
284
  - TODO.bugs/README.md
285
+ - TODO.max-perf/01-restore-ci-green.md
286
+ - TODO.max-perf/02-streaming-parse-path.md
287
+ - TODO.max-perf/03-cli-parallel-opt-in.md
288
+ - TODO.max-perf/04-benchmark-harness.md
289
+ - TODO.max-perf/05-parallel-fidelity-specs.md
290
+ - TODO.max-perf/06-builder-cpu-audit.md
291
+ - TODO.max-perf/07-upstream-parsanol-roadmap.md
292
+ - TODO.max-perf/08-builder-build-perf.md
293
+ - TODO.max-perf/09-grammar-cold-start.md
294
+ - TODO.max-perf/10-parser-facade-hygiene.md
295
+ - TODO.max-perf/11-ci-green-closeout.md
296
+ - TODO.max-perf/12-require-boot-profile.md
297
+ - TODO.max-perf/13-key-conversion-specs.md
298
+ - TODO.max-perf/14-builder-call-handler-audit.md
285
299
  - benchmark/srl_benchmark.rb
286
300
  - benchmark/srl_native_benchmark.rb
287
301
  - benchmark/srl_ruby_benchmark.rb
@@ -397,6 +411,7 @@ files:
397
411
  - lib/expressir/eengine/modified_object.rb
398
412
  - lib/expressir/errors.rb
399
413
  - lib/expressir/express.rb
414
+ - lib/expressir/express/ast_key_converter.rb
400
415
  - lib/expressir/express/builder.rb
401
416
  - lib/expressir/express/builder_context.rb
402
417
  - lib/expressir/express/builder_registry.rb
@@ -453,6 +468,7 @@ files:
453
468
  - lib/expressir/express/line_map.rb
454
469
  - lib/expressir/express/model_visitor.rb
455
470
  - lib/expressir/express/node_position_index.rb
471
+ - lib/expressir/express/parallel_files.rb
456
472
  - lib/expressir/express/parser.rb
457
473
  - lib/expressir/express/pretty_formatter.rb
458
474
  - lib/expressir/express/remark_attacher.rb