expressir 2.4.0 → 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.
- checksums.yaml +4 -4
- data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
- data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
- data/TODO.bugs/03-builder-mutable-state.md +43 -0
- data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
- data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
- data/TODO.bugs/06-collection-registry-single-source.md +53 -0
- data/TODO.bugs/07-require-relative-cleanup.md +42 -0
- data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
- data/TODO.bugs/09-parser-split.md +53 -0
- data/TODO.bugs/10-to-s-override.md +42 -0
- data/TODO.bugs/11-parser-class-variables.md +39 -0
- data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
- data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
- data/TODO.bugs/14-model-formatting-leak.md +30 -0
- data/TODO.bugs/15-expression-children-macro.md +27 -0
- data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
- data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
- data/TODO.bugs/18-const-get-private-constants.md +30 -0
- data/TODO.bugs/19-format-methods-public.md +22 -0
- data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
- data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
- data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
- data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
- data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
- data/TODO.bugs/25-debug-puts-in-production.md +21 -0
- data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
- data/TODO.bugs/27-package-build-god-method.md +19 -0
- data/TODO.bugs/28-package-god-class.md +30 -0
- data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
- data/TODO.bugs/30-unicode-map-extraction.md +19 -0
- data/TODO.bugs/README.md +43 -0
- data/TODO.max-perf/01-restore-ci-green.md +29 -0
- data/TODO.max-perf/02-streaming-parse-path.md +31 -0
- data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
- data/TODO.max-perf/04-benchmark-harness.md +28 -0
- data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
- data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
- data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
- data/TODO.max-perf/08-builder-build-perf.md +45 -0
- data/TODO.max-perf/09-grammar-cold-start.md +25 -0
- data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
- data/TODO.max-perf/11-ci-green-closeout.md +25 -0
- data/TODO.max-perf/12-require-boot-profile.md +25 -0
- data/TODO.max-perf/13-key-conversion-specs.md +26 -0
- data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
- data/benchmark/srl_benchmark.rb +76 -17
- data/docs/_tutorials/formatting-schemas.adoc +89 -0
- data/docs/_tutorials/index.adoc +10 -0
- data/docs/lychee.toml +3 -0
- data/expressir.gemspec +4 -2
- data/lib/expressir/cli.rb +3 -0
- data/lib/expressir/commands/changes_import_eengine.rb +0 -6
- data/lib/expressir/commands/changes_validate.rb +0 -2
- data/lib/expressir/commands/clean.rb +1 -1
- data/lib/expressir/commands/coverage.rb +6 -2
- data/lib/expressir/commands/file_violations.rb +70 -0
- data/lib/expressir/commands/format.rb +1 -1
- data/lib/expressir/commands/non_ascii_character.rb +49 -0
- data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
- data/lib/expressir/commands/package.rb +143 -187
- data/lib/expressir/commands/validate_ascii.rb +0 -510
- data/lib/expressir/commands/version.rb +1 -1
- data/lib/expressir/commands.rb +3 -0
- data/lib/expressir/coverage.rb +49 -117
- data/lib/expressir/express/ast_key_converter.rb +114 -0
- data/lib/expressir/express/builder.rb +71 -194
- data/lib/expressir/express/builder_context.rb +22 -0
- data/lib/expressir/express/builders/expression_builder.rb +16 -16
- data/lib/expressir/express/cache.rb +35 -8
- data/lib/expressir/express/error.rb +24 -0
- data/lib/expressir/express/formatter.rb +33 -18
- data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
- data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
- data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
- data/lib/expressir/express/grammar/parser.rb +705 -0
- data/lib/expressir/express/grammar.rb +11 -0
- data/lib/expressir/express/node_position_index.rb +215 -0
- data/lib/expressir/express/parallel_files.rb +229 -0
- data/lib/expressir/express/parser.rb +138 -950
- data/lib/expressir/express/pretty_formatter.rb +23 -5
- data/lib/expressir/express/remark_attacher.rb +497 -581
- data/lib/expressir/express/remark_scanner.rb +84 -19
- data/lib/expressir/express/schema_block_scanner.rb +137 -0
- data/lib/expressir/express/schema_source_formatter.rb +15 -0
- data/lib/expressir/express/scope_resolver.rb +223 -0
- data/lib/expressir/express/source_formatter.rb +15 -0
- data/lib/expressir/express/streaming_builder.rb +147 -176
- data/lib/expressir/express.rb +10 -6
- data/lib/expressir/model/concerns.rb +3 -0
- data/lib/expressir/model/data_types/generic_entity.rb +6 -6
- data/lib/expressir/model/declarations/entity.rb +5 -0
- data/lib/expressir/model/declarations/function.rb +5 -0
- data/lib/expressir/model/declarations/procedure.rb +5 -0
- data/lib/expressir/model/declarations/rule.rb +6 -0
- data/lib/expressir/model/declarations/schema.rb +21 -8
- data/lib/expressir/model/declarations/type.rb +3 -0
- data/lib/expressir/model/exp_file.rb +2 -0
- data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
- data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
- data/lib/expressir/model/expressions/binary_expression.rb +1 -0
- data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
- data/lib/expressir/model/expressions/function_call.rb +1 -0
- data/lib/expressir/model/expressions/interval.rb +1 -0
- data/lib/expressir/model/expressions/query_expression.rb +2 -0
- data/lib/expressir/model/expressions/unary_expression.rb +1 -0
- data/lib/expressir/model/model_element.rb +133 -14
- data/lib/expressir/model/remark_format.rb +17 -0
- data/lib/expressir/model/remark_info.rb +31 -3
- data/lib/expressir/model/remark_placement.rb +29 -0
- data/lib/expressir/model/repository.rb +18 -5
- data/lib/expressir/model/statements/alias.rb +2 -0
- data/lib/expressir/model/statements/assignment.rb +4 -0
- data/lib/expressir/model/statements/case.rb +21 -0
- data/lib/expressir/model/statements/case_action.rb +1 -0
- data/lib/expressir/model/statements/compound.rb +4 -0
- data/lib/expressir/model/statements/escape.rb +3 -0
- data/lib/expressir/model/statements/if.rb +5 -0
- data/lib/expressir/model/statements/null.rb +3 -0
- data/lib/expressir/model/statements/procedure_call.rb +3 -0
- data/lib/expressir/model/statements/repeat.rb +3 -0
- data/lib/expressir/model/statements/return.rb +3 -0
- data/lib/expressir/model/statements/skip.rb +3 -0
- data/lib/expressir/model.rb +12 -3
- data/lib/expressir/package/builder.rb +2 -2
- data/lib/expressir/version.rb +6 -1
- data/lib/expressir.rb +56 -3
- metadata +81 -7
- data/lib/expressir/express/transformer/remark_handling.rb +0 -194
data/lib/expressir/coverage.rb
CHANGED
|
@@ -3,53 +3,39 @@ require "pathname"
|
|
|
3
3
|
module Expressir
|
|
4
4
|
# Coverage module for calculating documentation coverage of EXPRESS entities
|
|
5
5
|
module Coverage
|
|
6
|
-
#
|
|
7
|
-
ENTITY_TYPE_MAP
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"Parameter" => "PARAMETER",
|
|
40
|
-
"Variable" => "VARIABLE",
|
|
41
|
-
"Attribute" => "ATTRIBUTE",
|
|
42
|
-
"DerivedAttribute" => "DERIVED_ATTRIBUTE",
|
|
43
|
-
"InverseAttribute" => "INVERSE_ATTRIBUTE",
|
|
44
|
-
"UniqueRule" => "UNIQUE_RULE",
|
|
45
|
-
"WhereRule" => "WHERE_RULE",
|
|
46
|
-
"EnumerationItem" => "ENUMERATION_ITEM",
|
|
47
|
-
"Interface" => "INTERFACE",
|
|
48
|
-
"InterfaceItem" => "INTERFACE_ITEM",
|
|
49
|
-
"InterfacedItem" => "INTERFACED_ITEM",
|
|
50
|
-
"SchemaVersion" => "SCHEMA_VERSION",
|
|
51
|
-
"SchemaVersionItem" => "SCHEMA_VERSION_ITEM",
|
|
52
|
-
}.freeze
|
|
6
|
+
# Single source of truth: EXPRESS type name → Ruby class name pairs.
|
|
7
|
+
# ENTITY_TYPE_MAP and CLASS_TO_EXPRESS_TYPE_MAP are derived from this
|
|
8
|
+
# so the two never drift out of sync.
|
|
9
|
+
TYPE_PAIRS = [
|
|
10
|
+
["TYPE", "Expressir::Model::Declarations::Type"],
|
|
11
|
+
["ENTITY", "Expressir::Model::Declarations::Entity"],
|
|
12
|
+
["CONSTANT", "Expressir::Model::Declarations::Constant"],
|
|
13
|
+
["FUNCTION", "Expressir::Model::Declarations::Function"],
|
|
14
|
+
["RULE", "Expressir::Model::Declarations::Rule"],
|
|
15
|
+
["PROCEDURE", "Expressir::Model::Declarations::Procedure"],
|
|
16
|
+
["SUBTYPE_CONSTRAINT", "Expressir::Model::Declarations::SubtypeConstraint"],
|
|
17
|
+
["PARAMETER", "Expressir::Model::Declarations::Parameter"],
|
|
18
|
+
["VARIABLE", "Expressir::Model::Declarations::Variable"],
|
|
19
|
+
["ATTRIBUTE", "Expressir::Model::Declarations::Attribute"],
|
|
20
|
+
["DERIVED_ATTRIBUTE", "Expressir::Model::Declarations::DerivedAttribute"],
|
|
21
|
+
["INVERSE_ATTRIBUTE", "Expressir::Model::Declarations::InverseAttribute"],
|
|
22
|
+
["UNIQUE_RULE", "Expressir::Model::Declarations::UniqueRule"],
|
|
23
|
+
["WHERE_RULE", "Expressir::Model::Declarations::WhereRule"],
|
|
24
|
+
["ENUMERATION_ITEM", "Expressir::Model::DataTypes::EnumerationItem"],
|
|
25
|
+
["INTERFACE", "Expressir::Model::Declarations::Interface"],
|
|
26
|
+
["INTERFACE_ITEM", "Expressir::Model::Declarations::InterfaceItem"],
|
|
27
|
+
["INTERFACED_ITEM", "Expressir::Model::Declarations::InterfacedItem"],
|
|
28
|
+
["SCHEMA_VERSION", "Expressir::Model::Declarations::SchemaVersion"],
|
|
29
|
+
["SCHEMA_VERSION_ITEM", "Expressir::Model::Declarations::SchemaVersionItem"],
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# EXPRESS entity type name → Ruby class name.
|
|
33
|
+
ENTITY_TYPE_MAP = TYPE_PAIRS.to_h.freeze
|
|
34
|
+
|
|
35
|
+
# Short Ruby class name → EXPRESS entity type name.
|
|
36
|
+
CLASS_TO_EXPRESS_TYPE_MAP = TYPE_PAIRS.to_h do |express_name, ruby_class|
|
|
37
|
+
[ruby_class.split("::").last, express_name]
|
|
38
|
+
end.freeze
|
|
53
39
|
|
|
54
40
|
# Available TYPE subtypes based on data types
|
|
55
41
|
TYPE_SUBTYPES = %w[
|
|
@@ -401,78 +387,24 @@ module Expressir
|
|
|
401
387
|
entities.concat(container.unique_rules) if container.unique_rules
|
|
402
388
|
entities.concat(container.where_rules) if container.where_rules
|
|
403
389
|
|
|
404
|
-
when Expressir::Model::Declarations::Function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
container.types&.each do |type|
|
|
417
|
-
entities.concat(find_nested_entities(type))
|
|
418
|
-
end
|
|
419
|
-
container.entities&.each do |entity|
|
|
420
|
-
entities.concat(find_nested_entities(entity))
|
|
421
|
-
end
|
|
422
|
-
container.functions&.each do |function|
|
|
423
|
-
entities.concat(find_nested_entities(function))
|
|
424
|
-
end
|
|
425
|
-
container.procedures&.each do |procedure|
|
|
426
|
-
entities.concat(find_nested_entities(procedure))
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
when Expressir::Model::Declarations::Rule
|
|
430
|
-
# Rule nested entities
|
|
431
|
-
entities.concat(container.variables) if container.variables
|
|
432
|
-
entities.concat(container.constants) if container.constants
|
|
433
|
-
entities.concat(container.types) if container.types
|
|
434
|
-
entities.concat(container.entities) if container.entities
|
|
435
|
-
entities.concat(container.functions) if container.functions
|
|
436
|
-
entities.concat(container.procedures) if container.procedures
|
|
437
|
-
entities.concat(container.subtype_constraints) if container.subtype_constraints
|
|
438
|
-
|
|
439
|
-
# Recursively find nested entities in nested containers
|
|
440
|
-
container.types&.each do |type|
|
|
441
|
-
entities.concat(find_nested_entities(type))
|
|
442
|
-
end
|
|
443
|
-
container.entities&.each do |entity|
|
|
444
|
-
entities.concat(find_nested_entities(entity))
|
|
445
|
-
end
|
|
446
|
-
container.functions&.each do |function|
|
|
447
|
-
entities.concat(find_nested_entities(function))
|
|
448
|
-
end
|
|
449
|
-
container.procedures&.each do |procedure|
|
|
450
|
-
entities.concat(find_nested_entities(procedure))
|
|
390
|
+
when Expressir::Model::Declarations::Function,
|
|
391
|
+
Expressir::Model::Declarations::Procedure,
|
|
392
|
+
Expressir::Model::Declarations::Rule
|
|
393
|
+
# All three scope containers follow the same nested-entity pattern.
|
|
394
|
+
# Derive the collection list from the model's own declaration rather
|
|
395
|
+
# than maintaining three separate hand-coded branches.
|
|
396
|
+
nested_attrs = container.class.collection_attributes_list - %i[
|
|
397
|
+
statements remark_items where_rules informal_propositions applies_to
|
|
398
|
+
]
|
|
399
|
+
nested_attrs.each do |attr|
|
|
400
|
+
collection = container.public_send(attr)
|
|
401
|
+
entities.concat(collection) if collection.is_a?(Array)
|
|
451
402
|
end
|
|
452
403
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
entities.concat(container.constants) if container.constants
|
|
458
|
-
entities.concat(container.types) if container.types
|
|
459
|
-
entities.concat(container.entities) if container.entities
|
|
460
|
-
entities.concat(container.functions) if container.functions
|
|
461
|
-
entities.concat(container.procedures) if container.procedures
|
|
462
|
-
entities.concat(container.subtype_constraints) if container.subtype_constraints
|
|
463
|
-
|
|
464
|
-
# Recursively find nested entities in nested containers
|
|
465
|
-
container.types&.each do |type|
|
|
466
|
-
entities.concat(find_nested_entities(type))
|
|
467
|
-
end
|
|
468
|
-
container.entities&.each do |entity|
|
|
469
|
-
entities.concat(find_nested_entities(entity))
|
|
470
|
-
end
|
|
471
|
-
container.functions&.each do |function|
|
|
472
|
-
entities.concat(find_nested_entities(function))
|
|
473
|
-
end
|
|
474
|
-
container.procedures&.each do |procedure|
|
|
475
|
-
entities.concat(find_nested_entities(procedure))
|
|
404
|
+
# Recursively find nested entities in nested scope containers
|
|
405
|
+
%i[types entities functions procedures].each do |attr|
|
|
406
|
+
collection = container.public_send(attr)
|
|
407
|
+
collection&.each { |item| entities.concat(find_nested_entities(item)) }
|
|
476
408
|
end
|
|
477
409
|
|
|
478
410
|
when Expressir::Model::Declarations::Interface
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Converts native-AST CamelCase keys to the snake_case keys the
|
|
6
|
+
# builders consume.
|
|
7
|
+
#
|
|
8
|
+
# build() descends into child nodes whose subtrees the parent already
|
|
9
|
+
# converted, so unchanged containers are marked with an invisible
|
|
10
|
+
# instance variable and skipped on re-visits — without it, every
|
|
11
|
+
# subtree is re-scanned once per ancestor level (~60 convert calls per
|
|
12
|
+
# model node on real schemas). The marker is invisible to equality,
|
|
13
|
+
# hashing, and inspection.
|
|
14
|
+
class AstKeyConverter
|
|
15
|
+
SNAKED_MARKER = :@_expressir_keys_snaked
|
|
16
|
+
UPPERCASE_PATTERN = /[A-Z]/
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Thread-local snake_case conversion cache. Thread-local avoids the
|
|
20
|
+
# mutable-constant anti-pattern while remaining thread-safe. The
|
|
21
|
+
# cache is bounded by the number of unique AST node-type names.
|
|
22
|
+
def snake_case(name)
|
|
23
|
+
cache[name] ||= begin
|
|
24
|
+
str = name.to_s
|
|
25
|
+
if /^[a-z_]+$/.match?(str)
|
|
26
|
+
str.to_sym
|
|
27
|
+
else
|
|
28
|
+
str
|
|
29
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
30
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
31
|
+
.downcase
|
|
32
|
+
.to_sym
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the original object when no conversion is needed; the
|
|
38
|
+
# common no-conversion case allocates nothing and the conversion
|
|
39
|
+
# case walks the keys once.
|
|
40
|
+
def convert(obj)
|
|
41
|
+
case obj
|
|
42
|
+
when Hash
|
|
43
|
+
return obj if obj.empty?
|
|
44
|
+
return obj if obj.instance_variable_defined?(SNAKED_MARKER)
|
|
45
|
+
|
|
46
|
+
keys = obj.keys
|
|
47
|
+
converted_values = nil
|
|
48
|
+
new_keys = nil
|
|
49
|
+
|
|
50
|
+
keys.each_with_index do |k, i|
|
|
51
|
+
val = obj[k]
|
|
52
|
+
case val
|
|
53
|
+
when Hash, Array
|
|
54
|
+
unless val.empty?
|
|
55
|
+
converted_val = convert(val)
|
|
56
|
+
(converted_values ||= {})[k] = converted_val unless converted_val.equal?(val)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if k.match?(UPPERCASE_PATTERN)
|
|
61
|
+
(new_keys ||= keys.dup)[i] = snake_case(k)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return mark_snaked(obj) unless new_keys || converted_values
|
|
66
|
+
|
|
67
|
+
result = {}
|
|
68
|
+
keys.each_with_index do |k, i|
|
|
69
|
+
key = new_keys&.[](i) || k
|
|
70
|
+
result[key] = converted_values&.key?(k) ? converted_values[k] : obj[k]
|
|
71
|
+
end
|
|
72
|
+
mark_snaked(result)
|
|
73
|
+
when Array
|
|
74
|
+
return obj if obj.empty?
|
|
75
|
+
return obj if obj.instance_variable_defined?(SNAKED_MARKER)
|
|
76
|
+
|
|
77
|
+
needs_conversion = false
|
|
78
|
+
result = []
|
|
79
|
+
|
|
80
|
+
obj.each do |item|
|
|
81
|
+
case item
|
|
82
|
+
when Hash, Array
|
|
83
|
+
next if item.empty?
|
|
84
|
+
|
|
85
|
+
converted = convert(item)
|
|
86
|
+
result << converted
|
|
87
|
+
needs_conversion = true unless converted.equal?(item)
|
|
88
|
+
else
|
|
89
|
+
result << item
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
needs_conversion ? mark_snaked(result) : mark_snaked(obj)
|
|
94
|
+
else
|
|
95
|
+
obj
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def cache
|
|
102
|
+
Thread.current[:expressir_snake_case_cache] ||= {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def mark_snaked(obj)
|
|
106
|
+
obj.instance_variable_set(SNAKED_MARKER, true)
|
|
107
|
+
obj
|
|
108
|
+
rescue FrozenError
|
|
109
|
+
obj
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -7,10 +7,26 @@ module Expressir
|
|
|
7
7
|
# This is the ONLY way to build models from AST - no Transformer fallback.
|
|
8
8
|
module Builder
|
|
9
9
|
class << self
|
|
10
|
-
|
|
10
|
+
# Thread-local key under which the current BuilderContext is stored
|
|
11
|
+
# for the duration of a build_with_remarks call. Recursive build
|
|
12
|
+
# calls read source/include_source from here instead of from
|
|
13
|
+
# class-level mutable state.
|
|
14
|
+
CONTEXT_KEY = :expressir_builder_context
|
|
15
|
+
|
|
16
|
+
# Returns the BuilderContext for the current thread, or nil if no
|
|
17
|
+
# build is in progress.
|
|
18
|
+
def current_context
|
|
19
|
+
Thread.current[CONTEXT_KEY]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Convenience accessors — read from the current context.
|
|
23
|
+
def source
|
|
24
|
+
current_context&.source
|
|
25
|
+
end
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
def include_source
|
|
28
|
+
current_context&.include_source
|
|
29
|
+
end
|
|
14
30
|
|
|
15
31
|
# Register a builder for a node type.
|
|
16
32
|
# @param node_type [Symbol] The AST node type
|
|
@@ -23,40 +39,18 @@ module Expressir
|
|
|
23
39
|
|
|
24
40
|
# Build a Model object from AST data.
|
|
25
41
|
# @param ast [Hash] The AST with node type as key
|
|
26
|
-
# @param source [String, nil] The original source code
|
|
27
|
-
# @param include_source [Boolean, nil] Whether to include source
|
|
28
42
|
# @return [Model::ModelElement] The built model object
|
|
29
|
-
|
|
30
|
-
# When these appear as the first key in a multi-key hash, they should be
|
|
31
|
-
# skipped in favor of the content key. This handles grammar patterns like
|
|
32
|
-
# `element >> (op_comma >> element).repeat` which produce
|
|
33
|
-
# {:op_comma => ..., :element => {...}}.
|
|
34
|
-
OPERATOR_TOKENS = Set.new(%i[
|
|
35
|
-
op_comma op_colon op_decl op_delim op_leftparen op_rightparen
|
|
36
|
-
op_leftbracket op_rightbracket op_left_curly_brace op_right_curly_brace
|
|
37
|
-
op_period op_pipe op_double_backslash op_double_pipe op_double_asterisk
|
|
38
|
-
op_asterisk op_slash op_plus op_minus op_less_equal op_greater_equal
|
|
39
|
-
op_less_greater op_less_than op_greater_than op_equals
|
|
40
|
-
op_colon_less_greater_colon op_colon_equals_colon
|
|
41
|
-
op_query_begin op_query_end op_question_mark
|
|
42
|
-
]).freeze
|
|
43
|
-
|
|
44
|
-
def build(ast, source: nil, include_source: nil)
|
|
43
|
+
def build(ast)
|
|
45
44
|
return nil unless ast
|
|
46
45
|
|
|
47
|
-
# Only set instance variables on first call (when they're provided)
|
|
48
|
-
# Recursive calls pass nil which shouldn't override the saved values
|
|
49
|
-
@source = source unless source.nil?
|
|
50
|
-
@include_source = include_source unless include_source.nil?
|
|
51
|
-
|
|
52
46
|
# Optimized: Hash is 90%+ of cases, check it first
|
|
53
47
|
case ast
|
|
54
48
|
when Hash
|
|
55
49
|
node_type = ast.keys.first
|
|
56
50
|
node_data = ast[node_type]
|
|
57
51
|
|
|
58
|
-
handler_key =
|
|
59
|
-
snake_data =
|
|
52
|
+
handler_key = AstKeyConverter.snake_case(node_type)
|
|
53
|
+
snake_data = AstKeyConverter.convert(node_data)
|
|
60
54
|
|
|
61
55
|
builder = @register[handler_key]
|
|
62
56
|
if builder
|
|
@@ -68,26 +62,27 @@ module Expressir
|
|
|
68
62
|
return result
|
|
69
63
|
end
|
|
70
64
|
|
|
71
|
-
# Slow path:
|
|
72
|
-
# Try
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
65
|
+
# Slow path: first-key builder returned nil in a multi-key
|
|
66
|
+
# hash. Try remaining keys for actual content. This handles
|
|
67
|
+
# grammar patterns like `element >> (op_comma >> element).repeat`
|
|
68
|
+
# which produce {:op_comma => ..., :element => {...}} where
|
|
69
|
+
# the first key is an operator separator.
|
|
70
|
+
# Previously gated by a hand-maintained OPERATOR_TOKENS set;
|
|
71
|
+
# now tries all remaining keys unconditionally (TODO.bugs/21).
|
|
72
|
+
ast.each_key do |key|
|
|
73
|
+
next if key == node_type
|
|
74
|
+
|
|
75
|
+
h_key = AstKeyConverter.snake_case(key)
|
|
76
|
+
h_builder = @register[h_key]
|
|
77
|
+
next unless h_builder
|
|
78
|
+
|
|
79
|
+
n_data = ast[key]
|
|
80
|
+
s_data = AstKeyConverter.convert(n_data)
|
|
81
|
+
result = h_builder.call(s_data)
|
|
82
|
+
|
|
83
|
+
unless result.nil?
|
|
84
|
+
attach_source_info(result, n_data)
|
|
85
|
+
return result
|
|
91
86
|
end
|
|
92
87
|
end
|
|
93
88
|
else
|
|
@@ -105,12 +100,23 @@ module Expressir
|
|
|
105
100
|
end
|
|
106
101
|
end
|
|
107
102
|
|
|
108
|
-
# Build with remark attachment
|
|
103
|
+
# Build with remark attachment.
|
|
104
|
+
#
|
|
105
|
+
# Sets up a BuilderContext as a thread-local for the duration of the
|
|
106
|
+
# build so recursive build calls can read source/include_source via
|
|
107
|
+
# `Builder.source` / `Builder.include_source`. The previous context
|
|
108
|
+
# (if any) is restored on exit so nested builds are reentrant.
|
|
109
109
|
def build_with_remarks(ast, source: nil, include_source: nil)
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
# Trigger BuilderRegistry autoload so all AST node type handlers
|
|
111
|
+
# are registered before build() runs. The reference to
|
|
112
|
+
# BuilderRegistry resolves the autoload defined in express.rb.
|
|
113
|
+
BuilderRegistry
|
|
114
|
+
|
|
115
|
+
previous = Thread.current[CONTEXT_KEY]
|
|
116
|
+
Thread.current[CONTEXT_KEY] = BuilderContext.new(
|
|
117
|
+
source: source,
|
|
118
|
+
include_source: include_source,
|
|
119
|
+
)
|
|
114
120
|
|
|
115
121
|
result = build(ast)
|
|
116
122
|
|
|
@@ -122,6 +128,8 @@ module Expressir
|
|
|
122
128
|
end
|
|
123
129
|
|
|
124
130
|
result
|
|
131
|
+
ensure
|
|
132
|
+
Thread.current[CONTEXT_KEY] = previous
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
# Check if a builder is registered for a node type.
|
|
@@ -203,164 +211,33 @@ module Expressir
|
|
|
203
211
|
builder.call(data)
|
|
204
212
|
end
|
|
205
213
|
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# Fast path for factor nodes
|
|
212
|
-
def build_factor(data)
|
|
213
|
-
build_node(:factor, data)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Fast path for simple_factor nodes
|
|
217
|
-
def build_simple_factor(data)
|
|
218
|
-
build_node(:simple_factor, data)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Fast path for primary nodes
|
|
222
|
-
def build_primary(data)
|
|
223
|
-
build_node(:primary, data)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Fast path for expression nodes
|
|
227
|
-
def build_expression(data)
|
|
228
|
-
build_node(:expression, data)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Fast path for simple_expression nodes
|
|
232
|
-
def build_simple_expression(data)
|
|
233
|
-
build_node(:simple_expression, data)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
private
|
|
237
|
-
|
|
238
|
-
# Cached snake_case conversion
|
|
239
|
-
def cached_snake_case(name)
|
|
240
|
-
SNAKE_CASE_CACHE[name] ||= begin
|
|
241
|
-
str = name.to_s
|
|
242
|
-
# Check if already snake_case
|
|
243
|
-
if /^[a-z_]+$/.match?(str)
|
|
244
|
-
str.to_sym
|
|
245
|
-
else
|
|
246
|
-
str
|
|
247
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
248
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
249
|
-
.downcase
|
|
250
|
-
.to_sym
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Optimized key conversion - returns original object when no conversion needed
|
|
256
|
-
# This avoids unnecessary allocations for AST nodes that don't need key conversion
|
|
257
|
-
def fast_convert_keys(obj)
|
|
258
|
-
case obj
|
|
259
|
-
when Hash
|
|
260
|
-
return obj if obj.empty?
|
|
261
|
-
|
|
262
|
-
# First pass: check if any conversion is needed
|
|
263
|
-
keys = obj.keys
|
|
264
|
-
needs_conversion = false
|
|
265
|
-
converted_values = nil
|
|
266
|
-
|
|
267
|
-
keys.each do |k|
|
|
268
|
-
key_str = k.to_s
|
|
269
|
-
# Check if key needs conversion (has uppercase)
|
|
270
|
-
if key_str.match?(/[A-Z]/)
|
|
271
|
-
needs_conversion = true
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Check if value needs conversion
|
|
275
|
-
val = obj[k]
|
|
276
|
-
case val
|
|
277
|
-
when Hash
|
|
278
|
-
next if val.empty?
|
|
279
|
-
|
|
280
|
-
converted_val = fast_convert_keys(val)
|
|
281
|
-
if !converted_val.equal?(val) # Identity check - same object?
|
|
282
|
-
needs_conversion = true
|
|
283
|
-
converted_values ||= {}
|
|
284
|
-
converted_values[k] = converted_val
|
|
285
|
-
end
|
|
286
|
-
when Array
|
|
287
|
-
next if val.empty?
|
|
288
|
-
|
|
289
|
-
converted_val = fast_convert_keys(val)
|
|
290
|
-
if !converted_val.equal?(val)
|
|
291
|
-
needs_conversion = true
|
|
292
|
-
converted_values ||= {}
|
|
293
|
-
converted_values[k] = converted_val
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# Return original if no conversion needed (zero allocation!)
|
|
299
|
-
return obj unless needs_conversion
|
|
300
|
-
|
|
301
|
-
# Build result only when necessary
|
|
302
|
-
result = {}
|
|
303
|
-
keys.each do |k|
|
|
304
|
-
key_str = k.to_s
|
|
305
|
-
new_key = key_str.match?(/[A-Z]/) ? cached_snake_case(k) : k
|
|
306
|
-
new_val = converted_values&.key?(k) ? converted_values[k] : obj[k]
|
|
307
|
-
result[new_key] = new_val
|
|
308
|
-
end
|
|
309
|
-
result
|
|
310
|
-
when Array
|
|
311
|
-
return obj if obj.empty?
|
|
312
|
-
|
|
313
|
-
# Check if any element needs conversion
|
|
314
|
-
needs_conversion = false
|
|
315
|
-
result = []
|
|
316
|
-
|
|
317
|
-
obj.each do |item|
|
|
318
|
-
case item
|
|
319
|
-
when Hash
|
|
320
|
-
next if item.empty?
|
|
321
|
-
|
|
322
|
-
converted = fast_convert_keys(item)
|
|
323
|
-
result << converted
|
|
324
|
-
needs_conversion = true unless converted.equal?(item)
|
|
325
|
-
when Array
|
|
326
|
-
next if item.empty?
|
|
327
|
-
|
|
328
|
-
converted = fast_convert_keys(item)
|
|
329
|
-
result << converted
|
|
330
|
-
needs_conversion = true unless converted.equal?(item)
|
|
331
|
-
else
|
|
332
|
-
result << item
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# Return original if no conversion needed
|
|
337
|
-
needs_conversion ? result : obj
|
|
338
|
-
else
|
|
339
|
-
obj
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
214
|
+
# Keys containing uppercase need snake-casing; testing the key
|
|
215
|
+
# directly avoids allocating `to_s` strings per key per pass.
|
|
216
|
+
# Key conversion lives in AstKeyConverter (MECE: converting AST
|
|
217
|
+
# keys is not building models).
|
|
343
218
|
def extract_source_info(data)
|
|
344
219
|
return nil unless data
|
|
345
|
-
|
|
220
|
+
|
|
221
|
+
src = source
|
|
222
|
+
return nil unless src
|
|
346
223
|
|
|
347
224
|
slice = find_slice(data)
|
|
348
225
|
return nil unless slice
|
|
349
226
|
|
|
350
227
|
{
|
|
351
|
-
text:
|
|
228
|
+
text: src[slice.offset...(slice.offset + slice.length)]&.strip,
|
|
352
229
|
offset: slice.offset,
|
|
353
230
|
}
|
|
354
231
|
end
|
|
355
232
|
|
|
356
233
|
def attach_source_info(result, data)
|
|
357
|
-
return unless
|
|
234
|
+
return unless source && result.is_a?(Expressir::Model::ModelElement)
|
|
358
235
|
|
|
359
236
|
source_info = extract_source_info(data)
|
|
360
237
|
return unless source_info
|
|
361
238
|
|
|
362
239
|
result.source_offset = source_info[:offset]
|
|
363
|
-
result.source = source_info[:text] if
|
|
240
|
+
result.source = source_info[:text] if include_source
|
|
364
241
|
end
|
|
365
242
|
|
|
366
243
|
def find_slice(data, depth = 0)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Value object carrying the per-build context that Builder needs to
|
|
6
|
+
# attach source info to model elements.
|
|
7
|
+
#
|
|
8
|
+
# Stored as a thread-local by Builder.build_with_remarks so that recursive
|
|
9
|
+
# build calls can read `Builder.source` / `Builder.include_source` without
|
|
10
|
+
# the module mutating class-level state. Thread-local means concurrent
|
|
11
|
+
# builds in different threads don't corrupt each other; the save/restore
|
|
12
|
+
# pattern in build_with_remarks means nested builds are reentrant.
|
|
13
|
+
class BuilderContext
|
|
14
|
+
attr_reader :source, :include_source
|
|
15
|
+
|
|
16
|
+
def initialize(source:, include_source:)
|
|
17
|
+
@source = source
|
|
18
|
+
@include_source = include_source
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|