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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
  3. data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
  4. data/TODO.bugs/03-builder-mutable-state.md +43 -0
  5. data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
  6. data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
  7. data/TODO.bugs/06-collection-registry-single-source.md +53 -0
  8. data/TODO.bugs/07-require-relative-cleanup.md +42 -0
  9. data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
  10. data/TODO.bugs/09-parser-split.md +53 -0
  11. data/TODO.bugs/10-to-s-override.md +42 -0
  12. data/TODO.bugs/11-parser-class-variables.md +39 -0
  13. data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
  14. data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
  15. data/TODO.bugs/14-model-formatting-leak.md +30 -0
  16. data/TODO.bugs/15-expression-children-macro.md +27 -0
  17. data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
  18. data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
  19. data/TODO.bugs/18-const-get-private-constants.md +30 -0
  20. data/TODO.bugs/19-format-methods-public.md +22 -0
  21. data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
  22. data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
  23. data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
  24. data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
  25. data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
  26. data/TODO.bugs/25-debug-puts-in-production.md +21 -0
  27. data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
  28. data/TODO.bugs/27-package-build-god-method.md +19 -0
  29. data/TODO.bugs/28-package-god-class.md +30 -0
  30. data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
  31. data/TODO.bugs/30-unicode-map-extraction.md +19 -0
  32. data/TODO.bugs/README.md +43 -0
  33. data/TODO.max-perf/01-restore-ci-green.md +29 -0
  34. data/TODO.max-perf/02-streaming-parse-path.md +31 -0
  35. data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
  36. data/TODO.max-perf/04-benchmark-harness.md +28 -0
  37. data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
  38. data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
  39. data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
  40. data/TODO.max-perf/08-builder-build-perf.md +45 -0
  41. data/TODO.max-perf/09-grammar-cold-start.md +25 -0
  42. data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
  43. data/TODO.max-perf/11-ci-green-closeout.md +25 -0
  44. data/TODO.max-perf/12-require-boot-profile.md +25 -0
  45. data/TODO.max-perf/13-key-conversion-specs.md +26 -0
  46. data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
  47. data/benchmark/srl_benchmark.rb +76 -17
  48. data/docs/_tutorials/formatting-schemas.adoc +89 -0
  49. data/docs/_tutorials/index.adoc +10 -0
  50. data/docs/lychee.toml +3 -0
  51. data/expressir.gemspec +4 -2
  52. data/lib/expressir/cli.rb +3 -0
  53. data/lib/expressir/commands/changes_import_eengine.rb +0 -6
  54. data/lib/expressir/commands/changes_validate.rb +0 -2
  55. data/lib/expressir/commands/clean.rb +1 -1
  56. data/lib/expressir/commands/coverage.rb +6 -2
  57. data/lib/expressir/commands/file_violations.rb +70 -0
  58. data/lib/expressir/commands/format.rb +1 -1
  59. data/lib/expressir/commands/non_ascii_character.rb +49 -0
  60. data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
  61. data/lib/expressir/commands/package.rb +143 -187
  62. data/lib/expressir/commands/validate_ascii.rb +0 -510
  63. data/lib/expressir/commands/version.rb +1 -1
  64. data/lib/expressir/commands.rb +3 -0
  65. data/lib/expressir/coverage.rb +49 -117
  66. data/lib/expressir/express/ast_key_converter.rb +114 -0
  67. data/lib/expressir/express/builder.rb +71 -194
  68. data/lib/expressir/express/builder_context.rb +22 -0
  69. data/lib/expressir/express/builders/expression_builder.rb +16 -16
  70. data/lib/expressir/express/cache.rb +35 -8
  71. data/lib/expressir/express/error.rb +24 -0
  72. data/lib/expressir/express/formatter.rb +33 -18
  73. data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
  74. data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
  75. data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
  76. data/lib/expressir/express/grammar/parser.rb +705 -0
  77. data/lib/expressir/express/grammar.rb +11 -0
  78. data/lib/expressir/express/node_position_index.rb +215 -0
  79. data/lib/expressir/express/parallel_files.rb +229 -0
  80. data/lib/expressir/express/parser.rb +138 -950
  81. data/lib/expressir/express/pretty_formatter.rb +23 -5
  82. data/lib/expressir/express/remark_attacher.rb +497 -581
  83. data/lib/expressir/express/remark_scanner.rb +84 -19
  84. data/lib/expressir/express/schema_block_scanner.rb +137 -0
  85. data/lib/expressir/express/schema_source_formatter.rb +15 -0
  86. data/lib/expressir/express/scope_resolver.rb +223 -0
  87. data/lib/expressir/express/source_formatter.rb +15 -0
  88. data/lib/expressir/express/streaming_builder.rb +147 -176
  89. data/lib/expressir/express.rb +10 -6
  90. data/lib/expressir/model/concerns.rb +3 -0
  91. data/lib/expressir/model/data_types/generic_entity.rb +6 -6
  92. data/lib/expressir/model/declarations/entity.rb +5 -0
  93. data/lib/expressir/model/declarations/function.rb +5 -0
  94. data/lib/expressir/model/declarations/procedure.rb +5 -0
  95. data/lib/expressir/model/declarations/rule.rb +6 -0
  96. data/lib/expressir/model/declarations/schema.rb +21 -8
  97. data/lib/expressir/model/declarations/type.rb +3 -0
  98. data/lib/expressir/model/exp_file.rb +2 -0
  99. data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
  100. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
  101. data/lib/expressir/model/expressions/binary_expression.rb +1 -0
  102. data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
  103. data/lib/expressir/model/expressions/function_call.rb +1 -0
  104. data/lib/expressir/model/expressions/interval.rb +1 -0
  105. data/lib/expressir/model/expressions/query_expression.rb +2 -0
  106. data/lib/expressir/model/expressions/unary_expression.rb +1 -0
  107. data/lib/expressir/model/model_element.rb +133 -14
  108. data/lib/expressir/model/remark_format.rb +17 -0
  109. data/lib/expressir/model/remark_info.rb +31 -3
  110. data/lib/expressir/model/remark_placement.rb +29 -0
  111. data/lib/expressir/model/repository.rb +18 -5
  112. data/lib/expressir/model/statements/alias.rb +2 -0
  113. data/lib/expressir/model/statements/assignment.rb +4 -0
  114. data/lib/expressir/model/statements/case.rb +21 -0
  115. data/lib/expressir/model/statements/case_action.rb +1 -0
  116. data/lib/expressir/model/statements/compound.rb +4 -0
  117. data/lib/expressir/model/statements/escape.rb +3 -0
  118. data/lib/expressir/model/statements/if.rb +5 -0
  119. data/lib/expressir/model/statements/null.rb +3 -0
  120. data/lib/expressir/model/statements/procedure_call.rb +3 -0
  121. data/lib/expressir/model/statements/repeat.rb +3 -0
  122. data/lib/expressir/model/statements/return.rb +3 -0
  123. data/lib/expressir/model/statements/skip.rb +3 -0
  124. data/lib/expressir/model.rb +12 -3
  125. data/lib/expressir/package/builder.rb +2 -2
  126. data/lib/expressir/version.rb +6 -1
  127. data/lib/expressir.rb +56 -3
  128. metadata +81 -7
  129. data/lib/expressir/express/transformer/remark_handling.rb +0 -194
@@ -1,758 +1,131 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "parsanol"
2
4
 
3
5
  module Expressir
4
6
  module Express
7
+ # Public-facing parser facade.
8
+ #
9
+ # Three concerns were split out (TODO.bugs/09):
10
+ # - Grammar rules → {Grammar::Parser} (~250 Parsanol rules)
11
+ # - Schema-block splitting → {SchemaBlockScanner}
12
+ # - I/O orchestration → this class (from_file, from_exp, etc.)
13
+ #
14
+ # The facade delegates grammar work to Grammar::Parser and block
15
+ # extraction to SchemaBlockScanner. All public methods remain
16
+ # backward-compatible.
5
17
  class Parser
6
- class Parser < Parsanol::Parser
7
- root(:syntax)
8
-
9
- # Cache for native parser grammar (class-level)
10
- @@cached_grammar_json = nil
11
- @@cached_grammar_atom_id = nil
12
-
13
- # Cache for parser instance (avoids ~7ms overhead of Parser.new)
14
- @@cached_parser = nil
15
- @@parser_mutex = Mutex.new
16
-
17
- # Cache for schemaDecl grammar JSON (used for streaming parse)
18
- @@cached_schema_grammar_json = nil
19
-
20
- # Threshold for using memory-bounded fresh parse (bytes)
21
- # Files above this use parse_fresh which has no packrat cache
22
- LARGE_FILE_THRESHOLD = 1024 * 1024 # 1 MB
23
-
24
- # Get cached parser instance (thread-safe)
25
- # Reusing the parser avoids the overhead of reinitializing all rule definitions
26
- def self.cached_parser
27
- return @@cached_parser if @@cached_parser
28
-
29
- @@parser_mutex.synchronize do
30
- @@cached_parser ||= new
31
- end
32
- end
33
-
34
- # Clear the cached parser (useful for testing or after grammar changes)
35
- def self.clear_parser_cache
36
- @@parser_mutex.synchronize do
37
- @@cached_parser = nil
38
- end
39
- end
40
-
41
- # Check if native parsing is available
42
- def self.native_available?
43
- return @native_available unless @native_available.nil?
44
-
45
- @native_available = begin
46
- require "parsanol/native"
47
- Parsanol::Native.available?
48
- rescue LoadError
49
- false
50
- end
51
- end
52
-
53
- # Get cached grammar JSON for native parsing
54
- def self.cached_grammar_json
55
- return @@cached_grammar_json if @@cached_grammar_json
56
-
57
- parser = new
58
- atom = parser.syntax
59
-
60
- # Cache by atom object_id
61
- if atom.object_id != @@cached_grammar_atom_id
62
- @@cached_grammar_atom_id = atom.object_id
63
- @@cached_grammar_json = Parsanol::Native.serialize_grammar(atom)
64
- end
65
-
66
- @@cached_grammar_json
67
- end
68
-
69
- # Get cached grammar JSON for schemaDecl (used for streaming parse)
70
- def self.cached_schema_grammar_json
71
- return @@cached_schema_grammar_json if @@cached_schema_grammar_json
72
-
73
- schema_atom = cached_parser.schemaDecl
74
- @@cached_schema_grammar_json = Parsanol::Native.serialize_grammar(schema_atom)
75
- @@cached_schema_grammar_json
76
- end
77
-
78
- # Parse using native engine with Rust-side transformation (fastest)
79
- #
80
- # This method provides ~17x speedup over pure Ruby parsing.
81
- # The transformation happens in Rust using to_parslet_compatible,
82
- # producing Parslet-compatible output that Builder.build can consume directly.
83
- #
84
- # @param source [String] EXPRESS source code to parse
85
- # @return [Hash, Array] Transformed AST in Parslet-compatible format
86
- # @raise [LoadError] If native parser is not available
87
- def self.parse_native(source)
88
- unless native_available?
89
- raise LoadError, "Native parser not available"
90
- end
91
-
92
- grammar_atom = cached_parser.syntax
93
- # Use fresh-parse (no cache) for large files to bound memory
94
- if source.bytesize > LARGE_FILE_THRESHOLD
95
- Parsanol::Native.parse_fresh(grammar_atom, source)
96
- else
97
- Parsanol::Native.parse(grammar_atom, source)
98
- end
99
- end
100
-
101
- def cts(atom)
102
- spaces >> atom
103
- end
104
-
105
- def cstr(atom)
106
- cts(str(atom).as(:str))
107
- end
108
-
109
- KEYWORDS = %i[
110
- ABS ABSTRACT ACOS AGGREGATE ALIAS AND ANDOR ARRAY AS ASIN ATAN BAG BASED_ON
111
- BEGIN BINARY BLENGTH BOOLEAN BY CASE CONSTANT CONST_E COS DERIVE DIV ELSE
112
- END END_ALIAS END_CASE END_CONSTANT END_ENTITY END_FUNCTION END_IF
113
- END_LOCAL END_PROCEDURE END_REPEAT END_RULE END_SCHEMA
114
- END_SUBTYPE_CONSTRAINT END_TYPE ENTITY ENUMERATION ESCAPE EXISTS EXP
115
- EXTENSIBLE FALSE FIXED FOR FORMAT FROM FUNCTION GENERIC GENERIC_ENTITY
116
- HIBOUND HIINDEX IF IN INSERT INTEGER INVERSE LENGTH LIKE LIST LOBOUND LOCAL
117
- LOG LOG10 LOG2 LOGICAL LOINDEX MOD NOT NUMBER NVL ODD OF ONEOF OPTIONAL OR
118
- OTHERWISE PI PROCEDURE QUERY REAL REFERENCE REMOVE RENAMED REPEAT RETURN
119
- ROLESOF RULE SCHEMA SELECT SELF SET SIN SIZEOF SKIP SQRT STRING SUBTYPE
120
- SUBTYPE_CONSTRAINT SUPERTYPE TAN THEN TO TRUE TYPE TYPEOF TOTAL_OVER UNIQUE
121
- UNKNOWN UNTIL USE USEDIN VALUE VALUE_IN VALUE_UNIQUE VAR WITH WHERE WHILE
122
- XOR
123
- ].freeze
124
-
125
- def keyword_rule(str)
126
- key_chars = str.to_s.chars
127
- key_chars
128
- .collect! { |char| match("[#{char}#{char.downcase}]") }
129
- .reduce(:>>) >> match["a-zA-Z0-9_"].absent?
130
- end
18
+ # Threshold for using memory-bounded fresh parse (bytes)
19
+ LARGE_FILE_THRESHOLD = 1024 * 1024 # 1 MB
20
+
21
+ class << self
22
+ # Delegate grammar-level queries to Grammar::Parser.
23
+ def native_available? = Grammar::Parser.native_available?
24
+ def cached_parser = Grammar::Parser.cached_parser
25
+ def cached_grammar_json = Grammar::Parser.cached_grammar_json
26
+ def cached_schema_grammar_json = Grammar::Parser.cached_schema_grammar_json
27
+ def parse_native(source) = Grammar::Parser.parse_native(source)
28
+ def clear_parser_cache = Grammar::Parser.clear_parser_cache
29
+ end
131
30
 
132
- KEYWORDS.each do |keyword|
133
- sym = :"t#{keyword}"
134
- rule(sym) { cts(keyword_rule(keyword).as(:str)).as(sym) }
135
- end
31
+ # UTF-8 BOM byte sequence (EF BB BF). Some editors prepend this
32
+ # to EXPRESS files; the parser fails because SCHEMA isn't at byte 0.
33
+ def self.strip_bom(source)
34
+ bytes = source.b
35
+ return source unless bytes.bytesize >= 3 &&
36
+ bytes.getbyte(0) == 0xEF &&
37
+ bytes.getbyte(1) == 0xBB &&
38
+ bytes.getbyte(2) == 0xBF
136
39
 
137
- rule(:abstractEntityDeclaration) do
138
- tABSTRACT.as(:abstractEntityDeclaration)
139
- end
140
- rule(:abstractSupertypeDeclaration) do
141
- (tABSTRACT >> tSUPERTYPE >> subtypeConstraint.maybe).as(:abstractSupertypeDeclaration)
142
- end
143
- rule(:abstractSupertype) do
144
- (tABSTRACT >> tSUPERTYPE >> op_delim).as(:abstractSupertype)
145
- end
146
- rule(:actualParameterList) do
147
- (op_leftparen >> (parameter >> (op_comma >> parameter).repeat).as(:listOf_parameter).maybe >> op_rightparen)
148
- .as(:actualParameterList)
149
- end
150
- rule(:addLikeOp) { (op_plus | op_minus | tOR | tXOR).as(:addLikeOp) }
151
- rule(:aggregateInitializer) do
152
- (op_leftbracket >> (element >> (op_comma >> element).repeat).as(:listOf_element).maybe >> op_rightbracket)
153
- .as(:aggregateInitializer)
154
- end
155
- rule(:aggregateSource) { simpleExpression.as(:aggregateSource) }
156
- rule(:aggregateType) do
157
- (tAGGREGATE >> (op_colon >> typeLabel).maybe >> tOF >> parameterType).as(:aggregateType)
158
- end
159
- rule(:aggregationTypes) do
160
- (arrayType | bagType | listType | setType).as(:aggregationTypes)
161
- end
162
- rule(:algorithmHead) do
163
- (declaration.repeat.as(:declaration) >> constantDecl.maybe >> localDecl.maybe).as(:algorithmHead)
164
- end
165
- rule(:aliasStmt) do
166
- (tALIAS >> variableId >> tFOR >> generalRef >> qualifier.repeat.as(:qualifier) >>
167
- op_delim >> stmt.repeat(1).as(:stmt) >> tEND_ALIAS >> op_delim.as(:op_delim2)).as(:aliasStmt)
168
- end
169
- rule(:anyKeyword) { KEYWORDS.map { |kw| send("t#{kw}") }.inject(:|) }
170
- rule(:arrayType) do
171
- (tARRAY >> boundSpec >> tOF >> tOPTIONAL.maybe >> tUNIQUE.maybe >> instantiableType).as(:arrayType)
172
- end
173
- rule(:assignmentStmt) do
174
- (generalRef >> qualifier.repeat.as(:qualifier) >> op_decl >> expression >> op_delim).as(:assignmentStmt)
175
- end
176
- rule(:attributeDecl) do
177
- (attributeId | redeclaredAttribute).as(:attributeDecl)
178
- end
179
- rule(:attributeId) { simpleId.as(:attributeId) }
180
- rule(:attributeQualifier) do
181
- (op_period >> attributeRef).as(:attributeQualifier)
182
- end
183
- rule(:attributeRef) { attributeId.as(:attributeRef) }
184
- rule(:bagType) do
185
- (tBAG >> boundSpec.maybe >> tOF >> instantiableType).as(:bagType)
186
- end
187
- rule(:binaryLiteral) do
188
- cts((str("%") >> bit.repeat(1)).as(:str)).as(:binaryLiteral)
189
- end
190
- rule(:binaryType) { (tBINARY >> widthSpec.maybe).as(:binaryType) }
191
- rule(:bit) { match["0-1"] }
192
- rule(:booleanType) { tBOOLEAN.as(:booleanType) }
193
- rule(:bound1) { numericExpression.as(:bound1) }
194
- rule(:bound2) { numericExpression.as(:bound2) }
195
- rule(:boundSpec) do
196
- (op_leftbracket >> bound1 >> op_colon >> bound2 >> op_rightbracket).as(:boundSpec)
197
- end
198
- rule(:builtInConstant) do
199
- (tCONST_E | tPI | tSELF | op_question_mark).as(:builtInConstant)
200
- end
201
- rule(:builtInFunction) do
202
- (tABS | tACOS | tASIN | tATAN | tBLENGTH | tCOS | tEXISTS | tEXP | tFORMAT | tHIBOUND | tHIINDEX | tLENGTH |
203
- tLOBOUND | tLOINDEX | tLOG2 | tLOG10 | tLOG | tNVL | tODD | tROLESOF | tSIN | tSIZEOF | tSQRT | tTAN |
204
- tTYPEOF | tUSEDIN | tVALUE_IN | tVALUE_UNIQUE | tVALUE).as(:builtInFunction)
205
- end
206
- rule(:builtInProcedure) { (tINSERT | tREMOVE).as(:builtInProcedure) }
207
- rule(:caseAction) do
208
- ((caseLabel >> (op_comma >> caseLabel).repeat).as(:listOf_caseLabel) >> op_colon >> stmt).as(:caseAction)
209
- end
210
- rule(:caseLabel) { expression.as(:caseLabel) }
211
- rule(:caseStmt) do
212
- (tCASE >> selector >> tOF >> caseAction.repeat.as(:caseAction) >> (tOTHERWISE >> op_colon >> stmt).maybe >> tEND_CASE >> op_delim).as(:caseStmt)
213
- end
214
- rule(:compoundStmt) do
215
- (tBEGIN >> stmt.repeat(1).as(:stmt) >> tEND >> op_delim).as(:compoundStmt)
216
- end
217
- rule(:concreteTypes) do
218
- (aggregationTypes | simpleTypes | typeRef).as(:concreteTypes)
219
- end
220
- rule(:constantBody) do
221
- (constantId >> op_colon >> instantiableType >> op_decl >> expression >> op_delim).as(:constantBody)
222
- end
223
- rule(:constantDecl) do
224
- (tCONSTANT >> constantBody.repeat(1).as(:constantBody) >> tEND_CONSTANT >> op_delim).as(:constantDecl)
225
- end
226
- rule(:constantFactor) do
227
- (builtInConstant | constantRef).as(:constantFactor)
228
- end
229
- rule(:constantId) { simpleId.as(:constantId) }
230
- rule(:constantRef) { constantId.as(:constantRef) }
231
- rule(:constructedTypes) do
232
- (enumerationType | selectType).as(:constructedTypes)
233
- end
234
- rule(:declaration) do
235
- (entityDecl | functionDecl | procedureDecl | subtypeConstraintDecl | typeDecl).as(:declaration)
236
- end
237
- rule(:delim) { cts(str(";").as(:delim)) }
238
- rule(:deriveClause) do
239
- (tDERIVE >> derivedAttr.repeat(1).as(:derivedAttr)).as(:deriveClause)
240
- end
241
- rule(:derivedAttr) do
242
- (attributeDecl >> op_colon >> parameterType >> op_decl >> expression >> op_delim).as(:derivedAttr)
243
- end
244
- rule(:digit) { match["0-9"] }
245
- rule(:digits) { digit >> digit.repeat }
246
- rule(:domainRule) do
247
- ((ruleLabelId >> op_colon).maybe >> expression).as(:domainRule)
248
- end
249
- rule(:element) do
250
- (expression >> (op_colon >> repetition).maybe).as(:element)
251
- end
252
- rule(:embeddedRemark) do
253
- (str("(*") >> (str("*)").absent? >> (embeddedRemark | any)).repeat >> str("*)")).as(:embeddedRemark)
254
- end
255
- rule(:encodedCharacter) { octet >> octet >> octet >> octet }
256
- rule(:encodedStringLiteral) do
257
- cts((str('"') >> encodedCharacter.repeat(1) >> str('"')).as(:str)).as(:encodedStringLiteral)
258
- end
259
- rule(:entityBody) do
260
- (explicitAttr.repeat.as(:explicitAttr) >> deriveClause.maybe >> inverseClause.maybe >> uniqueClause.maybe >> whereClause.maybe).as(:entityBody)
261
- end
262
- rule(:entityConstructor) do
263
- (entityRef >> op_leftparen >> (expression >> (op_comma >> expression).repeat).as(:listOf_expression).maybe >> op_rightparen).as(:entityConstructor)
264
- end
265
- rule(:entityDecl) do
266
- (entityHead >> entityBody >> tEND_ENTITY >> op_delim).as(:entityDecl)
267
- end
268
- rule(:entityHead) do
269
- (tENTITY >> entityId >> subsuper >> op_delim).as(:entityHead)
270
- end
271
- rule(:entityId) { simpleId.as(:entityId) }
272
- rule(:entityRef) { entityId.as(:entityRef) }
273
- rule(:enumerationExtension) do
274
- (tBASED_ON >> typeRef >> (tWITH >> enumerationItems).maybe).as(:enumerationExtension)
275
- end
276
- rule(:enumerationId) { simpleId.as(:enumerationId) }
277
- rule(:enumerationItem) { enumerationId.as(:enumerationItem) }
278
- rule(:enumerationItems) do
279
- (op_leftparen >> (enumerationItem >> (op_comma >> enumerationItem).repeat).as(:listOf_enumerationItem) >> op_rightparen).as(:enumerationItems)
280
- end
281
- rule(:enumerationRef) { enumerationId.as(:enumerationRef) }
282
- rule(:enumerationReference) do
283
- ((typeRef >> op_period).maybe >> enumerationRef).as(:enumerationReference)
284
- end
285
- rule(:enumerationType) do
286
- (tEXTENSIBLE.maybe >> tENUMERATION >> ((tOF >> enumerationItems) | enumerationExtension).maybe).as(:enumerationType)
287
- end
288
- rule(:escapeStmt) { (tESCAPE >> op_delim).as(:escapeStmt) }
289
- rule(:explicitAttr) do
290
- ((attributeDecl >> (op_comma >> attributeDecl).repeat).as(:listOf_attributeDecl) >> op_colon >>
291
- tOPTIONAL.maybe >> parameterType >> op_delim).as(:explicitAttr)
292
- end
293
- rule(:expression) do
294
- (simpleExpression >> (relOpExtended >> simpleExpression.as(:rhs)).maybe).as(:expression)
295
- end
296
- rule(:factor) do
297
- (simpleFactor >> (op_double_asterisk >> simpleFactor.as(:rhs)).maybe).as(:factor)
298
- end
299
- rule(:formalParameter) do
300
- ((parameterId >> (op_comma >> parameterId).repeat).as(:listOf_parameterId) >> op_colon >> parameterType).as(:formalParameter)
301
- end
302
- rule(:functionCall) do
303
- ((builtInFunction | functionRef) >> actualParameterList.maybe).as(:functionCall)
304
- end
305
- rule(:functionDecl) do
306
- (functionHead >> algorithmHead >> stmt.repeat(1).as(:stmt) >> tEND_FUNCTION >> op_delim).as(:functionDecl)
307
- end
308
- rule(:functionHead) do
309
- (tFUNCTION >> functionId >>
310
- (op_leftparen >> (formalParameter >> (op_delim >> formalParameter).repeat).as(:listOf_formalParameter) >> op_rightparen).maybe >>
311
- op_colon >> parameterType >> op_delim.as(:op_delim2)).as(:functionHead)
312
- end
313
- rule(:functionId) { simpleId.as(:functionId) }
314
- rule(:functionRef) { functionId.as(:functionRef) }
315
- rule(:generalAggregationTypes) do
316
- (generalArrayType | generalBagType | generalListType | generalSetType).as(:generalAggregationTypes)
317
- end
318
- rule(:generalArrayType) do
319
- (tARRAY >> boundSpec.maybe >> tOF >> tOPTIONAL.maybe >> tUNIQUE.maybe >> parameterType).as(:generalArrayType)
320
- end
321
- rule(:generalBagType) do
322
- (tBAG >> boundSpec.maybe >> tOF >> parameterType).as(:generalBagType)
323
- end
324
- rule(:generalizedTypes) do
325
- (aggregateType | generalAggregationTypes | genericEntityType | genericType).as(:generalizedTypes)
326
- end
327
- rule(:generalListType) do
328
- (tLIST >> boundSpec.maybe >> tOF >> tUNIQUE.maybe >> parameterType).as(:generalListType)
329
- end
330
- rule(:generalRef) { (parameterRef | variableId).as(:generalRef) }
331
- rule(:generalSetType) do
332
- (tSET >> boundSpec.maybe >> tOF >> parameterType).as(:generalSetType)
333
- end
334
- rule(:genericEntityType) do
335
- (tGENERIC_ENTITY >> (op_colon >> typeLabel).maybe).as(:genericEntityType)
336
- end
337
- rule(:genericType) do
338
- (tGENERIC >> (op_colon >> typeLabel).maybe).as(:genericType)
339
- end
340
- rule(:groupQualifier) do
341
- (op_double_backslash >> entityRef).as(:groupQualifier)
342
- end
343
- rule(:hexDigit) { match["0-9a-fA-F"] }
344
- rule(:ifStmtElseStatements) do
345
- stmt.repeat(1).as(:stmt).as(:ifStmtElseStatements)
346
- end
347
- rule(:ifStmtStatements) do
348
- stmt.repeat(1).as(:stmt).as(:ifStmtStatements)
349
- end
350
- rule(:ifStmt) do
351
- (tIF >> logicalExpression >> tTHEN >> ifStmtStatements >> (tELSE >> ifStmtElseStatements).maybe >> tEND_IF >> op_delim).as(:ifStmt)
352
- end
353
- rule(:incrementControl) do
354
- (variableId >> op_decl >> bound1 >> tTO >> bound2 >> (tBY >> increment).maybe).as(:incrementControl)
355
- end
356
- rule(:increment) { numericExpression.as(:increment) }
357
- rule(:index1) { index.as(:index1) }
358
- rule(:index2) { index.as(:index2) }
359
- rule(:index) { numericExpression.as(:index) }
360
- rule(:indexQualifier) do
361
- (op_leftbracket >> index1 >> (op_colon >> index2).maybe >> op_rightbracket).as(:indexQualifier)
362
- end
363
- rule(:instantiableType) do
364
- (concreteTypes | entityRef).as(:instantiableType)
365
- end
366
- rule(:integerLiteral) { cts(digits.as(:str)).as(:integerLiteral) }
367
- rule(:integerType) { tINTEGER.as(:integerType) }
368
- rule(:interfaceSpecification) do
369
- (referenceClause | useClause).as(:interfaceSpecification)
370
- end
371
- rule(:intervalHigh) { simpleExpression.as(:intervalHigh) }
372
- rule(:intervalItem) { simpleExpression.as(:intervalItem) }
373
- rule(:intervalLow) { simpleExpression.as(:intervalLow) }
374
- rule(:interval) do
375
- (op_left_curly_brace >> intervalLow >> intervalOp >> intervalItem >> intervalOp.as(:intervalOp2) >>
376
- intervalHigh >> op_right_curly_brace).as(:interval)
377
- end
378
- rule(:intervalOp) { (op_less_equal | op_less_than).as(:intervalOp) }
379
- rule(:inverseAttr) do
380
- (attributeDecl >> op_colon >> inverseAttrType >> tFOR >> (entityRef >> op_period).maybe >> attributeRef >> op_delim).as(:inverseAttr)
381
- end
382
- rule(:inverseAttrType) do
383
- (((tSET | tBAG) >> boundSpec.maybe >> tOF).maybe >> entityRef).as(:inverseAttrType)
384
- end
385
- rule(:inverseClause) do
386
- (tINVERSE >> inverseAttr.repeat(1).as(:inverseAttr)).as(:inverseClause)
387
- end
388
- rule(:letter) { match["a-zA-Z"] }
389
- rule(:listType) do
390
- (tLIST >> boundSpec.maybe >> tOF >> tUNIQUE.maybe >> instantiableType).as(:listType)
391
- end
392
- rule(:literal) do
393
- (binaryLiteral | logicalLiteral | realLiteral | integerLiteral | stringLiteral).as(:literal)
394
- end
395
- rule(:localDecl) do
396
- (tLOCAL >> localVariable.repeat(1).as(:localVariable) >> tEND_LOCAL >> op_delim).as(:localDecl)
397
- end
398
- rule(:localVariable) do
399
- ((variableId >> (op_comma >> variableId).repeat).as(:listOf_variableId) >>
400
- op_colon >> parameterType >>
401
- (op_decl >> expression).maybe >> op_delim).as(:localVariable)
402
- end
403
- rule(:logicalExpression) { expression.as(:logicalExpression) }
404
- rule(:logicalLiteral) do
405
- (tFALSE | tTRUE | tUNKNOWN).as(:logicalLiteral)
406
- end
407
- rule(:logicalType) { tLOGICAL.as(:logicalType) }
408
- rule(:multiplicationLikeOp) do
409
- (op_asterisk | op_slash | tDIV | tMOD | tAND | op_double_pipe).as(:multiplicationLikeOp)
410
- end
411
- rule(:namedTypeOrRename) do
412
- (namedTypes >> (tAS >> (entityId | typeId)).maybe).as(:namedTypeOrRename)
413
- end
414
- rule(:namedTypes) { (entityRef | typeRef).as(:namedTypes) }
415
- rule(:nullStmt) { op_delim.as(:nullStmt) }
416
- rule(:numberType) { tNUMBER.as(:numberType) }
417
- rule(:numericExpression) { simpleExpression.as(:numericExpression) }
418
- rule(:octet) { hexDigit >> hexDigit }
419
- rule(:oneOf) do
420
- (tONEOF >> op_leftparen >> (supertypeExpression >>
421
- (op_comma >> supertypeExpression).repeat).as(:listOf_supertypeExpression) >> op_rightparen).as(:oneOf)
422
- end
423
- rule(:op_asterisk) { cstr("*").as(:op_asterisk) }
424
- rule(:op_colon) { cstr(":").as(:op_colon) }
425
- rule(:op_colon_equals_colon) { cstr(":=:").as(:op_colon_equals_colon) }
426
- rule(:op_colon_less_greater_colon) do
427
- cstr(":<>:").as(:op_colon_less_greater_colon)
428
- end
429
- rule(:op_comma) { cstr(",").as(:op_comma) }
430
- rule(:op_decl) { cstr(":=").as(:op_decl) }
431
- rule(:op_delim) { cstr(";").as(:op_delim) }
432
- rule(:op_double_asterisk) { cstr("**").as(:op_double_asterisk) }
433
- rule(:op_double_backslash) { cstr("\\").as(:op_double_backslash) }
434
- rule(:op_double_pipe) { cstr("||").as(:op_double_pipe) }
435
- rule(:op_equals) { cstr("=").as(:op_equals) }
436
- rule(:op_greater_equal) { cstr(">=").as(:op_greater_equal) }
437
- rule(:op_greater_than) { cstr(">").as(:op_greater_than) }
438
- rule(:op_leftbracket) { cstr("[").as(:op_leftbracket) }
439
- rule(:op_left_curly_brace) { cstr("{").as(:op_left_curly_brace) }
440
- rule(:op_leftparen) { cstr("(").as(:op_leftparen) }
441
- rule(:op_less_equal) { cstr("<=").as(:op_less_equal) }
442
- rule(:op_less_greater) { cstr("<>").as(:op_less_greater) }
443
- rule(:op_less_than) { cstr("<").as(:op_less_than) }
444
- rule(:op_minus) { cstr("-").as(:op_minus) }
445
- rule(:op_period) { cstr(".").as(:op_period) }
446
- rule(:op_pipe) { cstr("|").as(:op_pipe) }
447
- rule(:op_plus) { cstr("+").as(:op_plus) }
448
- rule(:op_query_begin) { cstr("<*").as(:op_query_begin) }
449
- rule(:op_query_end) { cstr("*>").as(:op_query_end) }
450
- rule(:op_question_mark) { cstr("?").as(:op_question_mark) }
451
- rule(:op_rightbracket) { cstr("]").as(:op_rightbracket) }
452
- rule(:op_right_curly_brace) { cstr("}").as(:op_right_curly_brace) }
453
- rule(:op_rightparen) { cstr(")").as(:op_rightparen) }
454
- rule(:op_slash) { cstr("/").as(:op_slash) }
455
- rule(:parameter) { expression.as(:parameter) }
456
- rule(:parameterId) { simpleId.as(:parameterId) }
457
- rule(:parameterRef) { parameterId.as(:parameterRef) }
458
- rule(:parameterType) do
459
- (generalizedTypes | namedTypes | simpleTypes).as(:parameterType)
460
- end
461
- rule(:population) { entityRef.as(:population) }
462
- rule(:precisionSpec) { numericExpression.as(:precisionSpec) }
463
- rule(:primary) do
464
- (literal | (qualifiableFactor >> qualifier.repeat.as(:qualifier))).as(:primary)
465
- end
466
- rule(:procedureCallStmt) do
467
- ((builtInProcedure | procedureRef) >> actualParameterList.maybe >> op_delim).as(:procedureCallStmt)
468
- end
469
- rule(:procedureDecl) do
470
- (procedureHead >> algorithmHead >> stmt.repeat.as(:stmt) >> tEND_PROCEDURE >> op_delim).as(:procedureDecl)
471
- end
472
- rule(:procedureHeadParameter) do
473
- (tVAR.maybe >> formalParameter).as(:procedureHeadParameter)
474
- end
475
- rule(:procedureHead) do
476
- (tPROCEDURE >> procedureId >>
477
- (op_leftparen >> (procedureHeadParameter >>
478
- (op_delim >> procedureHeadParameter).repeat).as(:listOf_procedureHeadParameter) >> op_rightparen).maybe >> op_delim.as(:op_delim2)).as(:procedureHead)
479
- end
480
- rule(:procedureId) { simpleId.as(:procedureId) }
481
- rule(:procedureRef) { procedureId.as(:procedureRef) }
482
- rule(:qualifiableFactor) do
483
- (functionCall | attributeRef | constantFactor | generalRef | population).as(:qualifiableFactor)
484
- end
485
- rule(:qualifiedAttribute) do
486
- (tSELF >> groupQualifier >> attributeQualifier).as(:qualifiedAttribute)
487
- end
488
- rule(:qualifier) do
489
- (attributeQualifier | groupQualifier | indexQualifier).as(:qualifier)
490
- end
491
- rule(:queryExpression) do
492
- (tQUERY >> op_leftparen >> variableId >> op_query_begin >> aggregateSource >> op_pipe >> logicalExpression >> op_rightparen).as(:queryExpression)
493
- end
494
- rule(:realLiteral) do
495
- cts((digits >> str(".") >> digits.maybe >> (match["eE"] >> sign.maybe >> digits).maybe).as(:str)).as(:realLiteral)
496
- end
497
- rule(:realType) do
498
- (tREAL >> (op_leftparen >> precisionSpec >> op_rightparen).maybe).as(:realType)
499
- end
500
- rule(:redeclaredAttribute) do
501
- (qualifiedAttribute >> (tRENAMED >> attributeId).maybe).as(:redeclaredAttribute)
502
- end
503
- rule(:referenceClause) do
504
- (tREFERENCE >> tFROM >> schemaRef >>
505
- (op_leftparen >> (resourceOrRename >> (op_comma >> resourceOrRename).repeat).as(:listOf_resourceOrRename) >> op_rightparen).maybe >> op_delim).as(:referenceClause)
506
- end
507
- rule(:referencedAttribute) do
508
- (attributeRef | qualifiedAttribute).as(:referencedAttribute)
509
- end
510
- rule(:relOpExtended) { (relOp | tIN | tLIKE).as(:relOpExtended) }
511
- rule(:relOp) do
512
- (op_less_equal | op_greater_equal | op_less_greater | op_less_than | op_greater_than | op_equals |
513
- op_colon_less_greater_colon | op_colon_equals_colon).as(:relOp)
514
- end
515
- rule(:renameId) do
516
- (constantId | entityId | functionId | procedureId | typeId).as(:renameId)
517
- end
518
- rule(:repeatControl) do
519
- (incrementControl.maybe >> whileControl.maybe >> untilControl.maybe).as(:repeatControl)
520
- end
521
- rule(:repeatStmt) do
522
- (tREPEAT >> repeatControl >> op_delim >> stmt.repeat(1).as(:stmt) >> tEND_REPEAT >> op_delim.as(:op_delim2)).as(:repeatStmt)
523
- end
524
- rule(:repetition) { numericExpression.as(:repetition) }
525
- rule(:resourceOrRename) do
526
- (resourceRef >> (tAS >> renameId).maybe).as(:resourceOrRename)
527
- end
528
- rule(:resourceRef) do
529
- (constantRef | entityRef | functionRef | procedureRef | typeRef).as(:resourceRef)
530
- end
531
- rule(:returnStmt) do
532
- (tRETURN >> (op_leftparen >> expression >> op_rightparen).maybe >> op_delim).as(:returnStmt)
533
- end
534
- rule(:ruleDecl) do
535
- (ruleHead >> algorithmHead >> stmt.repeat.as(:stmt) >> whereClause >> tEND_RULE >> op_delim).as(:ruleDecl)
536
- end
537
- rule(:ruleHead) do
538
- (tRULE >> ruleId >> tFOR >> op_leftparen >>
539
- (entityRef >> (op_comma >> entityRef).repeat).as(:listOf_entityRef) >> op_rightparen >> op_delim).as(:ruleHead)
540
- end
541
- rule(:ruleId) { simpleId.as(:ruleId) }
542
- rule(:ruleLabelId) { simpleId.as(:ruleLabelId) }
543
- rule(:ruleLabelRef) { ruleLabelId.as(:ruleLabelRef) }
544
- rule(:ruleRef) { ruleId.as(:ruleRef) }
545
- rule(:schemaBodyDeclaration) do
546
- (declaration | ruleDecl).as(:schemaBodyDeclaration)
547
- end
548
- rule(:schemaBody) do
549
- (interfaceSpecification.repeat.as(:interfaceSpecification) >> constantDecl.maybe >>
550
- schemaBodyDeclaration.repeat.as(:schemaBodyDeclaration)).as(:schemaBody)
551
- end
552
- rule(:schemaDecl) do
553
- (tSCHEMA >> schemaId >> schemaVersionId.maybe >> op_delim >> schemaBody >> tEND_SCHEMA >> op_delim.as(:op_delim2)).as(:schemaDecl)
554
- end
555
- rule(:schemaId) { simpleId.as(:schemaId) }
556
- rule(:schemaRef) { schemaId.as(:schemaRef) }
557
- rule(:schemaVersionId) { stringLiteral.as(:schemaVersionId) }
558
- rule(:selectExtension) do
559
- (tBASED_ON >> typeRef >> (tWITH >> selectList).maybe).as(:selectExtension)
560
- end
561
- rule(:selectList) do
562
- (op_leftparen >> (namedTypes >> (op_comma >> namedTypes).repeat).as(:listOf_namedTypes) >> op_rightparen).as(:selectList)
563
- end
564
- rule(:selector) { expression.as(:selector) }
565
- rule(:selectType) do
566
- ((tEXTENSIBLE >> tGENERIC_ENTITY.maybe).maybe >> tSELECT >> (selectList | selectExtension).maybe).as(:selectType)
567
- end
568
- rule(:setType) do
569
- (tSET >> boundSpec.maybe >> tOF >> instantiableType).as(:setType)
570
- end
571
- rule(:sign) { match["+-"] }
572
- rule(:simpleExpression) do
573
- (term >> (addLikeOp.as(:operator) >> term).as(:item).repeat.as(:rhs)).as(:simpleExpression)
574
- end
575
- rule(:simpleFactor) do
576
- (simpleFactorExpression | aggregateInitializer | entityConstructor | interval | queryExpression | stringLiteral |
577
- simpleFactorUnaryExpression | enumerationReference).as(:simpleFactor)
578
- end
579
- rule(:simpleFactorExpression) do
580
- ((op_leftparen >> expression >> op_rightparen) | primary).as(:simpleFactorExpression)
581
- end
582
- rule(:simpleFactorUnaryExpression) do
583
- (unaryOp >> simpleFactorExpression).as(:simpleFactorUnaryExpression)
584
- end
585
- rule(:simpleId) do
586
- anyKeyword.absent? >> cts((match["a-zA-Z_"] >> match["a-zA-Z0-9_"].repeat).as(:str)).as(:simpleId)
587
- end
588
- rule(:simpleStringLiteral) do
589
- cts((str("'") >> (str("'").absent? >> any).repeat >> str("'")).as(:str)).as(:simpleStringLiteral)
590
- end
591
- rule(:simpleTypes) do
592
- (binaryType | booleanType | integerType | logicalType | numberType | realType | stringType).as(:simpleTypes)
593
- end
594
- rule(:skipStmt) { (tSKIP >> op_delim).as(:skipStmt) }
595
- rule(:space) { match[" \r\n\t\f"] | embeddedRemark | tailRemark }
596
- rule(:spaces) { space.repeat.as(:spaces) }
597
- rule(:stmt) do
598
- (aliasStmt | assignmentStmt | caseStmt | compoundStmt | escapeStmt | ifStmt |
599
- nullStmt | procedureCallStmt | repeatStmt | returnStmt | skipStmt).as(:stmt)
600
- end
601
- rule(:stringLiteral) do
602
- (simpleStringLiteral | encodedStringLiteral).as(:stringLiteral)
603
- end
604
- rule(:stringType) { (tSTRING >> widthSpec.maybe).as(:stringType) }
605
- rule(:subsuper) do
606
- (supertypeConstraint.maybe >> subtypeDeclaration.maybe).as(:subsuper)
607
- end
608
- rule(:subtypeConstraintBody) do
609
- (abstractSupertype.maybe >> totalOver.maybe >> (supertypeExpression >> op_delim).maybe).as(:subtypeConstraintBody)
610
- end
611
- rule(:subtypeConstraintDecl) do
612
- (subtypeConstraintHead >> subtypeConstraintBody >> tEND_SUBTYPE_CONSTRAINT >> op_delim).as(:subtypeConstraintDecl)
613
- end
614
- rule(:subtypeConstraintHead) do
615
- (tSUBTYPE_CONSTRAINT >> subtypeConstraintId >> tFOR >> entityRef >> op_delim).as(:subtypeConstraintHead)
616
- end
617
- rule(:subtypeConstraintId) { simpleId.as(:subtypeConstraintId) }
618
- rule(:subtypeConstraintRef) do
619
- subtypeConstraintId.as(:subtypeConstraintRef)
620
- end
621
- rule(:subtypeConstraint) do
622
- (tOF >> op_leftparen >> supertypeExpression >> op_rightparen).as(:subtypeConstraint)
623
- end
624
- rule(:subtypeDeclaration) do
625
- (tSUBTYPE >> tOF >> op_leftparen >> entityRef >> (op_comma >> entityRef).repeat >> op_rightparen).as(:listOf_entityRef).as(:subtypeDeclaration)
626
- end
627
- rule(:supertypeConstraint) do
628
- (abstractSupertypeDeclaration | abstractEntityDeclaration | supertypeRule).as(:supertypeConstraint)
629
- end
630
- rule(:supertypeExpression) do
631
- (supertypeFactor >> (tANDOR.as(:operator) >> supertypeFactor).as(:item).repeat.as(:rhs)).as(:supertypeExpression)
632
- end
633
- rule(:supertypeFactor) do
634
- (supertypeTerm >> (tAND.as(:operator) >> supertypeTerm).as(:item).repeat.as(:rhs)).as(:supertypeFactor)
635
- end
636
- rule(:supertypeRule) do
637
- (tSUPERTYPE >> subtypeConstraint).as(:supertypeRule)
638
- end
639
- rule(:supertypeTerm) do
640
- (entityRef | oneOf | (op_leftparen >> supertypeExpression >> op_rightparen)).as(:supertypeTerm)
641
- end
642
- rule(:syntax) do
643
- (spaces.as(:spaces) >> schemaDecl.repeat(1).as(:schemaDecl) >> spaces.as(:trailer)).as(:syntax)
644
- end
645
- rule(:tailRemark) do
646
- (str("--") >> (str("\n").absent? >> any).repeat >> str("\n").maybe).as(:tailRemark)
647
- end
648
- rule(:term) do
649
- (factor >> (multiplicationLikeOp >> factor).as(:item).repeat.as(:rhs)).as(:term)
650
- end
651
- rule(:totalOver) do
652
- (tTOTAL_OVER >> op_leftparen >> (entityRef >> (op_comma >> entityRef).repeat).as(:listOf_entityRef) >> op_rightparen >> op_delim).as(:totalOver)
653
- end
654
- rule(:typeDecl) do
655
- (tTYPE >> typeId >> op_equals >> underlyingType >> op_delim >> whereClause.maybe >> tEND_TYPE >> op_delim.as(:op_delim2)).as(:typeDecl)
656
- end
657
- rule(:typeId) { simpleId.as(:typeId) }
658
- rule(:typeLabelId) { simpleId.as(:typeLabelId) }
659
- rule(:typeLabelRef) { typeLabelId.as(:typeLabelRef) }
660
- rule(:typeLabel) { (typeLabelId | typeLabelRef).as(:typeLabel) }
661
- rule(:typeRef) { typeId.as(:typeRef) }
662
- rule(:unaryOp) { (op_plus | op_minus | tNOT).as(:unaryOp) }
663
- rule(:underlyingType) do
664
- (concreteTypes | constructedTypes).as(:underlyingType)
665
- end
666
- rule(:uniqueClause) do
667
- (tUNIQUE >> (uniqueRule >> op_delim >> (uniqueRule >> op_delim.as(:op_delim2)).repeat).as(:listOf_uniqueRule)).as(:uniqueClause)
668
- end
669
- rule(:uniqueRule) do
670
- ((ruleLabelId >> op_colon).maybe >> (referencedAttribute >> (op_comma >> referencedAttribute).repeat).as(:listOf_referencedAttribute)).as(:uniqueRule)
671
- end
672
- rule(:untilControl) { (tUNTIL >> logicalExpression).as(:untilControl) }
673
- rule(:useClause) do
674
- (tUSE >> tFROM >> schemaRef >>
675
- (op_leftparen >> (namedTypeOrRename >> (op_comma >>
676
- namedTypeOrRename).repeat).as(:listOf_namedTypeOrRename) >> op_rightparen).maybe >> op_delim).as(:useClause)
677
- end
678
- rule(:variableId) { simpleId.as(:variableId) }
679
- rule(:variableRef) { variableId.as(:variableRef) }
680
- rule(:whereClause) do
681
- (tWHERE >> (domainRule >> op_delim).repeat.as(:listOf_domainRule)).as(:whereClause)
682
- end
683
- rule(:whileControl) { (tWHILE >> logicalExpression).as(:whileControl) }
684
- rule(:width) { numericExpression.as(:width) }
685
- rule(:widthSpec) do
686
- (op_leftparen >> width >> op_rightparen >> tFIXED.maybe).as(:widthSpec)
687
- end
40
+ bytes.byteslice(3..).force_encoding(source.encoding)
688
41
  end
42
+ private_class_method :strip_bom
689
43
 
690
- # Parses Express file into an Express model
691
- # @param [String] file Express file path
692
- # @param [Boolean] skip_references skip resolving references
693
- # @param [Boolean] include_source attach original source code to model elements
694
- # @param [Boolean] use_native use native parser (default: true when available)
44
+ # Parses Express file into an Express model.
45
+ # @param file [String] Express file path
46
+ # @param skip_references [Boolean] skip resolving references
47
+ # @param include_source [Boolean] attach original source code to model elements
48
+ # @param use_native [Boolean] use native parser (default: true when available)
695
49
  # @return [Model::ExpFile] ExpFile containing parsed schemas
696
- # @raise [SchemaParseFailure] if the schema file fails to parse
697
- def self.from_file(file, skip_references: nil, include_source: nil, root_path: nil, use_native: nil) # rubocop:disable Metrics/AbcSize
50
+ # @raise [Error::SchemaParseFailure] if the schema file fails to parse
51
+ def self.from_file(file, skip_references: nil, include_source: nil,
52
+ root_path: nil, use_native: nil) # rubocop:disable Metrics/AbcSize
698
53
  Expressir::Benchmark.measure_file(file) do
699
- source = File.read file
54
+ source = strip_bom(File.read(file))
700
55
 
701
- # remove root path from file path
702
56
  schema_file = root_path ? Pathname.new(file.to_s).relative_path_from(root_path).to_s : file.to_s
703
57
 
704
- use_native = Parser.native_available? if use_native.nil?
58
+ use_native = Grammar::Parser.native_available? if use_native.nil?
705
59
 
706
60
  begin
707
- ast = if use_native && Parser.native_available?
61
+ ast = if use_native && Grammar::Parser.native_available?
708
62
  begin
709
- Parser.parse_native(source)
63
+ Grammar::Parser.parse_native(source)
710
64
  rescue StandardError
711
- # Native parser may fail on non-ASCII or edge cases;
712
- # fall back to Ruby parser
713
- Parser.cached_parser.parse source
65
+ Grammar::Parser.cached_parser.parse source
714
66
  end
715
67
  else
716
- Parser.cached_parser.parse source
68
+ Grammar::Parser.cached_parser.parse source
717
69
  end
718
70
  rescue Parsanol::ParseFailed => e
719
- # Instead of just printing, raise a proper error with file context
720
71
  raise Error::SchemaParseFailure.new(schema_file, e)
721
72
  end
722
73
 
723
- @exp_file = ::Expressir::Express::Builder.build_with_remarks(ast, source: source,
724
- include_source: include_source)
74
+ exp_file = ::Expressir::Express::Builder.build_with_remarks(ast, source: source,
75
+ include_source: include_source)
725
76
 
726
- # Set file path on the ExpFile and propagate to schemas
727
- @exp_file.path = schema_file
728
- @exp_file.schemas.each do |schema|
77
+ transfer_header_to_schema(exp_file, source)
78
+
79
+ exp_file.path = schema_file
80
+ exp_file.schemas.each do |schema|
729
81
  schema.file = schema_file
730
82
  schema.file_basename = File.basename(schema_file, ".exp")
731
83
  end
732
84
 
733
85
  unless skip_references
734
86
  Expressir::Benchmark.measure_references do
735
- @resolve_references_model_visitor = ResolveReferencesModelVisitor.new
736
- @resolve_references_model_visitor.visit(@exp_file)
87
+ ResolveReferencesModelVisitor.new.visit(exp_file)
737
88
  end
738
89
  end
739
90
 
740
- @exp_file
91
+ exp_file
741
92
  end
742
93
  end
743
94
 
744
- # Parses Express files into an Express model
745
- # @param [Array<String>] files Express file paths
746
- # @param [Boolean] skip_references skip resolving references
747
- # @param [Boolean] include_source attach original source code to model elements
748
- # @param [Boolean] use_native use native parser (default: true when available)
749
- # @yield [filename, schemas, error] Optional block called for each file processed
750
- # @yieldparam filename [String] Name of the file being processed
751
- # @yieldparam schemas [Array, nil] Array of parsed schemas (nil if parsing failed)
752
- # @yieldparam error [Exception, nil] Error that occurred (nil if parsing succeeded)
95
+ # Parses Express files into an Express model.
96
+ # @param files [Array<String>] Express file paths
97
+ # @param skip_references [Boolean] skip resolving references
98
+ # @param include_source [Boolean] attach original source code to model elements
99
+ # @param use_native [Boolean] use native parser (default: true when available)
100
+ # @yield [filename, schemas, error] Optional block called for each file
753
101
  # @return [Model::Repository] Repository containing all parsed ExpFiles
754
102
  def self.from_files(files, skip_references: nil, include_source: nil,
755
- root_path: nil, use_native: nil)
103
+ root_path: nil, use_native: nil, max_processes: nil, &progress)
104
+ all_exp_files = if ParallelFiles.sequential?(files, max_processes)
105
+ parse_files_sequentially(
106
+ files, skip_references: skip_references, include_source: include_source,
107
+ root_path: root_path, use_native: use_native
108
+ ) do |file, exp_file, error|
109
+ progress&.call(file, exp_file&.schemas, error)
110
+ end
111
+ else
112
+ ParallelFiles.run(
113
+ files,
114
+ max_processes: max_processes,
115
+ parse: lambda do |file|
116
+ from_file(file, skip_references: true, root_path: root_path,
117
+ use_native: use_native)
118
+ end,
119
+ ) do |file, exp_file, error|
120
+ progress&.call(file, exp_file&.schemas, error)
121
+ end
122
+ end
123
+
124
+ build_repository(all_exp_files, skip_references: skip_references)
125
+ end
126
+
127
+ def self.parse_files_sequentially(files, skip_references: nil,
128
+ include_source: nil, root_path: nil, use_native: nil, &block)
756
129
  all_exp_files = []
757
130
 
758
131
  files.each do |file|
@@ -760,53 +133,56 @@ root_path: nil, use_native: nil)
760
133
  root_path: root_path, use_native: use_native)
761
134
  all_exp_files << exp_file
762
135
 
763
- # Call the progress block if provided
764
- yield(file, exp_file&.schemas, nil) if block_given?
136
+ yield(file, exp_file, nil) if block
765
137
  rescue StandardError => e
766
- # Call the progress block with the error if provided
767
- yield(file, nil, e) if block_given?
768
-
769
- # Re-raise the error if it's not a schema parse failure
770
- # This allows handling of specific schema parse failures while still propagating other errors
138
+ # Nil-pad so results align with files by index, exactly like the
139
+ # parallel path does.
140
+ all_exp_files << nil if e.is_a?(Error::SchemaParseFailure)
141
+ yield(file, nil, e) if block
771
142
  raise unless e.is_a?(Error::SchemaParseFailure)
772
143
  end
773
144
 
774
- @repository = Model::Repository.new(files: all_exp_files)
145
+ all_exp_files
146
+ end
147
+
148
+ def self.build_repository(all_exp_files, skip_references: nil)
149
+ repository = Model::Repository.new(files: all_exp_files)
775
150
 
776
151
  unless skip_references
777
152
  Expressir::Benchmark.measure_references do
778
- @resolve_references_model_visitor = ResolveReferencesModelVisitor.new
779
- @resolve_references_model_visitor.visit(@repository)
153
+ ResolveReferencesModelVisitor.new.visit(repository)
780
154
  end
781
155
  end
782
156
 
783
- @repository
157
+ repository
784
158
  end
785
159
 
786
- # Parses Express content string into an Express model
787
- # @param [String] content Express content as string
788
- # @param [Boolean] skip_references skip resolving references
789
- # @param [Boolean] include_source attach original source code to model elements
790
- # @param [Boolean] use_native use native parser (default: true when available)
791
- # @param [Boolean] use_streaming use streaming builder for maximum performance (default: false)
160
+ # Parses Express content string into an Express model.
161
+ # @param content [String] EXPRESS source code
162
+ # @param skip_references [Boolean] skip resolving references
163
+ # @param include_source [Boolean] attach original source code to model elements
164
+ # @param use_native [Boolean] use native parser (default: true when available)
165
+ # @param use_streaming [Boolean] unsupported on current parsanol;
166
+ # passing true raises {Error::StreamingUnsupportedError}. The
167
+ # streaming paths return when parsanol exposes a stable
168
+ # parse_with_builder (see parsanol-ruby#52 and the TODO.max-perf/02
169
+ # notes).
792
170
  # @return [Model::ExpFile] Parsed ExpFile
793
- # @raise [SchemaParseFailure] if the content fails to parse
171
+ # @raise [Error::SchemaParseFailure] if the content fails to parse
794
172
  def self.from_exp(content, skip_references: nil, include_source: nil,
795
173
  use_native: nil, use_streaming: false)
796
- # Streaming builder mode - uses Parsanol streaming callbacks
797
- if use_streaming && Parser.native_available? && defined?(Parsanol::Native.parse_with_builder)
798
- return from_exp_streaming(content, skip_references: skip_references,
799
- include_source: include_source)
174
+ content = strip_bom(content)
175
+ if use_streaming
176
+ raise Error::StreamingUnsupportedError
800
177
  end
801
178
 
802
- use_native = Parser.native_available? if use_native.nil?
179
+ use_native = Grammar::Parser.native_available? if use_native.nil?
803
180
 
804
181
  begin
805
- # Use cached parser instance for performance (avoids ~7ms Parser.new overhead)
806
- ast = if use_native && Parser.native_available?
807
- Parser.parse_native(content)
182
+ ast = if use_native && Grammar::Parser.native_available?
183
+ Grammar::Parser.parse_native(content)
808
184
  else
809
- Parser.cached_parser.parse(content)
185
+ Grammar::Parser.cached_parser.parse(content)
810
186
  end
811
187
  rescue Parsanol::ParseFailed, StandardError => e
812
188
  raise Error::SchemaParseFailure.new("(from string)", e)
@@ -816,39 +192,7 @@ root_path: nil, use_native: nil)
816
192
  source: content,
817
193
  include_source: include_source)
818
194
 
819
- exp_file.schemas.each do |schema|
820
- schema.file = nil
821
- schema.file_basename = nil
822
- end
823
-
824
- unless skip_references
825
- Expressir::Benchmark.measure_references do
826
- resolve_references_model_visitor = ResolveReferencesModelVisitor.new
827
- resolve_references_model_visitor.visit(exp_file)
828
- end
829
- end
830
-
831
- exp_file
832
- end
833
-
834
- # Parse using streaming builder (construct-by-construct)
835
- # @param [String] content Express content as string
836
- # @param [Boolean] skip_references skip resolving references
837
- # @param [Boolean] include_source attach original source code to model elements
838
- # @return [Model::ExpFile] Parsed ExpFile
839
- # @raise [SchemaParseFailure] if the content fails to parse
840
- def self.from_exp_streaming_builder(content, skip_references: nil,
841
- include_source: nil)
842
- grammar_json = Parser.cached_grammar_json
843
- builder = ::Expressir::Express::StreamingBuilder.new(source: content,
844
- include_source: include_source)
845
-
846
- begin
847
- exp_file = Parsanol::Native.parse_with_builder(grammar_json,
848
- content, builder)
849
- rescue StandardError => e
850
- raise Error::SchemaParseFailure.new("(streaming)", e)
851
- end
195
+ transfer_header_to_schema(exp_file, content)
852
196
 
853
197
  exp_file.schemas.each do |schema|
854
198
  schema.file = nil
@@ -857,192 +201,36 @@ include_source: nil)
857
201
 
858
202
  unless skip_references
859
203
  Expressir::Benchmark.measure_references do
860
- resolve_references_model_visitor = ResolveReferencesModelVisitor.new
861
- resolve_references_model_visitor.visit(exp_file)
204
+ ResolveReferencesModelVisitor.new.visit(exp_file)
862
205
  end
863
206
  end
864
207
 
865
208
  exp_file
866
209
  end
867
210
 
868
- # Parse each schema separately with fresh arena (memory-bounded)
869
- #
870
- # This splits the source into schema blocks and parses each independently.
871
- # Memory is bounded by the largest schema, not the entire file.
872
- #
873
- # @param content [String] EXPRESS source code
874
- # @param skip_references [Boolean] Whether to skip reference resolution
875
- # @param include_source [Boolean] Whether to include source in model
876
- # @return [Expressir::Model::ExpFile] Parsed EXPRESS file
877
- def self.from_exp_streaming(content, skip_references: nil,
878
- include_source: nil)
879
- grammar_json = Parser.cached_schema_grammar_json
880
-
881
- # Extract schema blocks from source
882
- schema_blocks = extract_schema_blocks(content)
211
+ # Transfer file-level untagged remarks that appear before the first
212
+ # SCHEMA keyword to the first schema's +header+ attribute so they are
213
+ # accessible via +schema.header+ and through Liquid drops.
214
+ def self.transfer_header_to_schema(exp_file, source = nil)
215
+ return unless exp_file.untagged_remarks&.any?
216
+ return unless exp_file.schemas&.any?
217
+ return unless source
883
218
 
884
- # Parse each schema with fresh arena
885
- schemas = schema_blocks.map do |block|
886
- ast = Parsanol::Native.parse_fresh(grammar_json, block[:source])
887
- schema_model = Builder.build(ast)
888
- schema_model.source = block[:source]
889
- schema_model
890
- rescue StandardError => e
891
- raise Error::SchemaParseFailure.new(
892
- "(schema #{block[:name] || 'unknown'})", e
893
- )
894
- end
895
-
896
- # Build the file model
897
- exp_file = Expressir::Model::ExpFile.new
898
- exp_file.schemas = schemas
899
-
900
- exp_file.schemas.each do |schema|
901
- schema.file = nil
902
- schema.file_basename = nil
903
- end
219
+ schema_pos = source.b.index(/\bSCHEMA\b/)
220
+ return unless schema_pos
904
221
 
905
- unless skip_references
906
- Expressir::Benchmark.measure_references do
907
- resolve_references_model_visitor = ResolveReferencesModelVisitor.new
908
- resolve_references_model_visitor.visit(exp_file)
909
- end
222
+ header_remarks = exp_file.untagged_remarks.select do |r|
223
+ r.source_offset && r.source_offset < schema_pos
910
224
  end
225
+ return unless header_remarks.any?
911
226
 
912
- exp_file
227
+ header_text = header_remarks.map(&:text).join("\n")
228
+ exp_file.schemas.first.header = header_text
229
+ exp_file.untagged_remarks -= header_remarks
913
230
  end
914
-
915
- # Extract individual schema blocks from EXPRESS source
916
- #
917
- # This uses a state machine to properly handle nested comments and strings.
918
- #
919
- # @param source [String] EXPRESS source
920
- # @return [Array<Hash>] Array of {name: String, source: String} for each schema
921
- def self.extract_schema_blocks(source)
922
- blocks = []
923
- pos = 0
924
- len = source.length
925
-
926
- while pos < len
927
- # Skip whitespace and find SCHEMA keyword
928
- skip_ws_and_comments(source, pos)
929
- pos = skip_ws_and_comments(source, pos)
930
-
931
- break if pos >= len
932
-
933
- # Check for SCHEMA
934
- if source[pos..].start_with?("SCHEMA")
935
- result = parse_schema_block(source, pos)
936
- if result
937
- blocks << result
938
- pos = result[:end_pos]
939
- next
940
- end
941
- end
942
-
943
- pos += 1
944
- end
945
-
946
- blocks
947
- end
948
-
949
- private_class_method :extract_schema_blocks
950
-
951
- def self.parse_schema_block(source, start_pos)
952
- # Must be at SCHEMA keyword
953
- return nil unless source[start_pos..].start_with?("SCHEMA")
954
-
955
- pos = start_pos + "SCHEMA".length
956
- pos = skip_spaces(source, pos)
957
-
958
- # Extract schema name (identifier)
959
- name_start = pos
960
- while pos < source.length && (source[pos] =~ /[a-zA-Z0-9_]/ || source[pos] == "_")
961
- pos += 1
962
- end
963
- schema_name = source[name_start...pos]
964
-
965
- return nil if schema_name.empty?
966
-
967
- # Skip to END_SCHEMA
968
- depth = 1
969
- search_pos = pos
970
- end_pos = nil
971
-
972
- while search_pos < source.length
973
- if source[search_pos] == '"'
974
- # String literal - skip to end
975
- search_pos += 1
976
- while search_pos < source.length && source[search_pos] != '"'
977
- search_pos += 1
978
- end
979
- search_pos += 1
980
- elsif source[search_pos] == "(" && source[search_pos + 1] == "*"
981
- # Comment - skip to end
982
- search_pos += 2
983
- while search_pos < source.length && !(source[search_pos] == "*" && source[search_pos + 1] == ")")
984
- search_pos += 1
985
- end
986
- search_pos += 2
987
- elsif source[search_pos..].start_with?("END_SCHEMA")
988
- depth -= 1
989
- if depth.zero?
990
- end_pos = search_pos + "END_SCHEMA".length
991
- # Skip trailing whitespace and semicolon
992
- pos = end_pos
993
- pos = skip_spaces(source, pos)
994
- pos += 1 if source[pos] == ";" # semicolon
995
- break
996
- end
997
- search_pos += "END_SCHEMA".length
998
- else
999
- search_pos += 1
1000
- end
1001
- end
1002
-
1003
- return nil unless end_pos
1004
-
1005
- schema_source = source[start_pos...end_pos]
1006
- {
1007
- name: schema_name,
1008
- source: schema_source,
1009
- start_pos: start_pos,
1010
- end_pos: pos,
1011
- }
1012
- end
1013
- private_class_method :parse_schema_block
1014
-
1015
- def self.skip_spaces(source, pos)
1016
- while pos < source.length && [" ", "\t", "\n",
1017
- "\r"].include?(source[pos])
1018
- pos += 1
1019
- end
1020
- pos
1021
- end
1022
- private_class_method :skip_spaces
1023
-
1024
- def self.skip_ws_and_comments(source, pos)
1025
- len = source.length
1026
- while pos < len
1027
- c = source[pos]
1028
- if [" ", "\t", "\n", "\r"].include?(c)
1029
- pos += 1
1030
- elsif c == "(" && source[pos + 1] == "*"
1031
- # Comment - skip to end
1032
- pos += 2
1033
- while pos < len - 1 && !(source[pos] == "*" && source[pos + 1] == ")")
1034
- pos += 1
1035
- end
1036
- pos += 2
1037
- else
1038
- break
1039
- end
1040
- end
1041
- pos
1042
- end
1043
- private_class_method :skip_ws_and_comments
1044
-
1045
- private_class_method :from_exp_streaming
231
+ private_class_method :transfer_header_to_schema
232
+ private_class_method :parse_files_sequentially
233
+ private_class_method :build_repository
1046
234
  end
1047
235
  end
1048
236
  end