expressir 2.4.3 → 2.4.4

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.github/workflows/rust-ext.yml +41 -0
  4. data/.github/workflows/verify_remarks.yml +33 -0
  5. data/.gitignore +6 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_todo.yml +1 -0
  8. data/Cargo.toml +7 -0
  9. data/README.adoc +65 -0
  10. data/Rakefile +12 -0
  11. data/TODO.parity-ee/00-overview.md +58 -0
  12. data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
  13. data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
  14. data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
  15. data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
  16. data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
  17. data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
  18. data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
  19. data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
  20. data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
  21. data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
  22. data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
  23. data/TODO.parity-ee/parity-matrix.md +108 -0
  24. data/exe/expressir +6 -0
  25. data/ext/expressir_core/Cargo.toml +24 -0
  26. data/ext/expressir_core/extconf.rb +30 -0
  27. data/ext/expressir_core/src/lib.rs +446 -0
  28. data/lib/expressir/cli.rb +49 -4
  29. data/lib/expressir/commands/check.rb +56 -0
  30. data/lib/expressir/commands/expand.rb +26 -0
  31. data/lib/expressir/commands/fix.rb +33 -0
  32. data/lib/expressir/commands/flatten.rb +29 -0
  33. data/lib/expressir/commands/parity_inputs.rb +85 -0
  34. data/lib/expressir/commands/validate.rb +22 -0
  35. data/lib/expressir/commands.rb +5 -0
  36. data/lib/expressir/express/builder_registry.rb +11 -2
  37. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -7
  38. data/lib/expressir/express/builders/subtype_constraint_builder.rb +3 -1
  39. data/lib/expressir/express/builders/type_builder.rb +8 -0
  40. data/lib/expressir/express/builders/unique_rule_builder.rb +8 -1
  41. data/lib/expressir/express/checker.rb +458 -0
  42. data/lib/expressir/express/concatenator.rb +79 -0
  43. data/lib/expressir/express/core.rb +66 -0
  44. data/lib/expressir/express/formatter.rb +25 -3
  45. data/lib/expressir/express/formatters/data_types_formatter.rb +2 -2
  46. data/lib/expressir/express/formatters/declarations_formatter.rb +1 -0
  47. data/lib/expressir/express/formatters/remark_formatter.rb +30 -6
  48. data/lib/expressir/express/formatters/statements_formatter.rb +7 -1
  49. data/lib/expressir/express/interface_dot.rb +105 -0
  50. data/lib/expressir/express/listing.rb +174 -0
  51. data/lib/expressir/express/model_traversal.rb +42 -0
  52. data/lib/expressir/express/node_position_index.rb +18 -4
  53. data/lib/expressir/express/parser.rb +242 -4
  54. data/lib/expressir/express/pretty_gate.rb +61 -0
  55. data/lib/expressir/express/refs_overlay.rb +47 -0
  56. data/lib/expressir/express/remark_attacher.rb +122 -10
  57. data/lib/expressir/express/remark_overlay.rb +86 -0
  58. data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
  59. data/lib/expressir/express/schema_source_formatter.rb +5 -5
  60. data/lib/expressir/express/self_schema_reference.rb +126 -0
  61. data/lib/expressir/express/shtolo.rb +522 -0
  62. data/lib/expressir/express.rb +13 -0
  63. data/lib/expressir/model/concerns.rb +15 -1
  64. data/lib/expressir/model/declarations/entity.rb +6 -1
  65. data/lib/expressir/model/declarations/interface.rb +13 -0
  66. data/lib/expressir/model/declarations/schema.rb +74 -23
  67. data/lib/expressir/model/indexes/item_graph.rb +135 -0
  68. data/lib/expressir/model/indexes.rb +1 -0
  69. data/lib/expressir/model/model_element.rb +30 -1
  70. data/lib/expressir/model/remark_info.rb +20 -1
  71. data/lib/expressir/model/remark_placement.rb +15 -2
  72. data/lib/expressir/model/repository.rb +8 -2
  73. data/lib/expressir/model.rb +2 -1
  74. data/lib/expressir/version.rb +1 -1
  75. data/lib/tasks/verify_remarks.rake +9 -0
  76. data/rakelib/verify_remarks.rake +16 -0
  77. metadata +42 -2
@@ -125,7 +125,7 @@ module Expressir
125
125
 
126
126
  # For scope containers: exclude last remark ONLY if it's a tail remark (END_* remark)
127
127
  # Keep all embedded remarks
128
- if is_scope_container && preamble_remarks.length > 1
128
+ if is_scope_container && preamble_remarks.length >= 1
129
129
  last_remark = preamble_remarks.last
130
130
  # Only exclude if it's a tail remark
131
131
  preamble_remarks = preamble_remarks[0..-2] if last_remark.tail?
@@ -154,15 +154,39 @@ module Expressir
154
154
  end
155
155
  end
156
156
 
157
- # Remarks written after their statement on the same line. Attachment
158
- # only assigns these to single-line statements, so appending keeps
159
- # them on that statement's line.
157
+ # Remarks written after their statement on the same line, which for a
158
+ # single-line statement is where appending puts them back. Opener
159
+ # remarks are excluded: those belong on the first line rather than
160
+ # after the closing keyword, and {#format_opener_remarks} emits them.
161
+ # Mid-keyword remarks (ELSE / OTHERWISE regions) are excluded too:
162
+ # {#format_mid_keyword_remarks} emits them after their keyword.
160
163
  def format_inline_statement_remarks(node)
164
+ formatted_inline_remarks(node) { |remark| remark.region.nil? }
165
+ end
166
+
167
+ # Remarks that trailed the opening line of a statement spanning
168
+ # several lines, as in `IF x THEN -- why`. Where they are written back
169
+ # is Formatter#format's business, not this method's.
170
+ def format_opener_remarks(node)
171
+ formatted_inline_remarks(node, &:opener?)
172
+ end
173
+
174
+ # Remarks that trailed a mid-construct keyword of `node` — the ELSE
175
+ # line of an IF, the OTHERWISE line of a CASE — emitted after that
176
+ # keyword, where they were written.
177
+ def format_mid_keyword_remarks(node, region)
178
+ formatted_inline_remarks(node) { |remark| remark.inline_region?(region) }
179
+ end
180
+
181
+ # The inline remarks of a node that the given block accepts, formatted
182
+ # and joined. The callers partition the same collection, so every
183
+ # inline remark is emitted by exactly one of them.
184
+ def formatted_inline_remarks(node, &)
161
185
  return "" if @no_remarks
162
- return "" unless node.is_a?(Model::Statement)
186
+ return "" unless node.is_a?(Model::TakesInlineRemark)
163
187
 
164
188
  Array(node.untagged_remarks).filter_map do |remark|
165
- next unless remark.inline?
189
+ next unless remark.inline? && yield(remark)
166
190
 
167
191
  formatted = format_untagged_remark(remark)
168
192
  formatted unless formatted.empty?
@@ -98,6 +98,8 @@ module Expressir
98
98
  "OTHERWISE",
99
99
  " ",
100
100
  ":",
101
+ format_mid_keyword_remarks(node,
102
+ Model::RemarkPlacement::OTHERWISE_REGION),
101
103
  ].join,
102
104
  indent(format(node.otherwise_statement)),
103
105
  ]
@@ -167,7 +169,11 @@ module Expressir
167
169
  *(then_trailing if has_else),
168
170
  *if has_else
169
171
  [
170
- "ELSE",
172
+ [
173
+ "ELSE",
174
+ format_mid_keyword_remarks(node,
175
+ Model::RemarkPlacement::ELSE_REGION),
176
+ ].join,
171
177
  indent(node.else_statements.map { |x| format(x) }.join("\n")),
172
178
  ].join("\n")
173
179
  end,
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # Interface dependency GraphViz emitter — port of eeng
6
+ # kernel/dot-graph.lisp `:interface` graph (parity-ee stage 10).
7
+ #
8
+ # Nodes are schema names (lowercase); edges are USE FROM (default
9
+ # blue) and REFERENCE FROM (default green). An edge with an item
10
+ # list uses arrowhead=curve; a bare interface uses arrowhead=normal.
11
+ class InterfaceDot
12
+ def initialize(root_schema, repository,
13
+ iface: :both, uf_color: :blue, rf_color: :green,
14
+ depth: nil, prune: [])
15
+ @root = root_schema
16
+ @repository = repository
17
+ @iface = iface
18
+ @uf_color = uf_color
19
+ @rf_color = rf_color
20
+ @depth = depth
21
+ @prune = Array(prune).map { |n| n.to_s.downcase }
22
+ @by_name = repository.schemas.compact.to_h { |s| [s.id.safe_downcase, s] }
23
+ end
24
+
25
+ def write(io = nil)
26
+ owned = io.nil?
27
+ io ||= +""
28
+ schemas, edges = collect
29
+ io << "// -*- Mode: Dot -*-\n\n"
30
+ io << "// Dot File Written by Expressir\n"
31
+ io << "// #{Expressir::VERSION}\n"
32
+ io << "\ndigraph interfaces {\n"
33
+ io << " node [shape=none];\n"
34
+ if %i[both use].include?(@iface)
35
+ io << " edge [color=#{@uf_color}];\n"
36
+ edges.select { |e| e[:kind] == :use }.each do |e|
37
+ head = e[:items] ? "curve" : "normal"
38
+ io << " #{e[:from]} -> #{e[:to]} [arrowhead=#{head}];\n"
39
+ end
40
+ end
41
+ if %i[both ref].include?(@iface)
42
+ io << " edge [color=#{@rf_color}];\n"
43
+ edges.select { |e| e[:kind] == :ref }.each do |e|
44
+ head = e[:items] ? "curve" : "normal"
45
+ io << " #{e[:from]} -> #{e[:to]} [arrowhead=#{head}];\n"
46
+ end
47
+ end
48
+ # Ensure isolated schemas still appear as nodes.
49
+ named = edges.flat_map { |e| [e[:from], e[:to]] }.uniq
50
+ (schemas.map { |s| s.id.downcase } - named).each do |n|
51
+ io << " #{n};\n"
52
+ end
53
+ io << "}\n"
54
+ owned ? io : nil
55
+ end
56
+
57
+ private
58
+
59
+ def collect
60
+ visited = {}
61
+ edges = []
62
+ queue = [[@root, 0]]
63
+ until queue.empty?
64
+ schema, level = queue.shift
65
+ key = schema.id.safe_downcase
66
+ next if visited.key?(key)
67
+ next if @prune.include?(key)
68
+
69
+ visited[key] = schema
70
+
71
+ Array(schema.interfaces).each do |iface|
72
+ target_name = iface_schema_name(iface)
73
+ next unless target_name
74
+
75
+ tkey = target_name.safe_downcase
76
+ next if @prune.include?(tkey)
77
+ next if tkey == key
78
+
79
+ kind = iface.kind == Model::Declarations::Interface::REFERENCE ? :ref : :use
80
+ next if @iface == :use && kind != :use
81
+ next if @iface == :ref && kind != :ref
82
+
83
+ edges << {
84
+ kind: kind,
85
+ items: Array(iface.items).any?,
86
+ from: key,
87
+ to: tkey,
88
+ }
89
+
90
+ target = @by_name[tkey]
91
+ next unless target
92
+ next if @depth && level + 1 >= @depth
93
+
94
+ queue << [target, level + 1]
95
+ end
96
+ end
97
+ [visited.values, edges.uniq]
98
+ end
99
+
100
+ def iface_schema_name(iface)
101
+ iface.schema.is_a?(String) ? iface.schema : iface.schema&.id
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Expressir
6
+ module Express
7
+ # Declaration listing and SMRL index writers — ports of eeng
8
+ # plugins/p11/wo-list.lisp and wo-smrl-xml.lisp (parity-ee stage 09).
9
+ #
10
+ # Two encodings share the same schema walk and declaration ordering
11
+ # (TYPE, ENTITY, SUBTYPE_CONSTRAINT, FUNCTION, RULE, PROCEDURE —
12
+ # each sorted by name):
13
+ # :list — human-readable SCHEMA.name; inventory
14
+ # :smrl_xml — <concatenated_express_file_content_list> index
15
+ class Listing
16
+ DECL_ORDER = %i[types entities subtype_constraints functions rules procedures].freeze
17
+ DECL_LABEL = {
18
+ types: "TYPE",
19
+ entities: "ENTITY",
20
+ subtype_constraints: "SUBTYPE_CONSTRAINT",
21
+ functions: "FUNCTION",
22
+ rules: "RULE",
23
+ procedures: "PROCEDURE",
24
+ }.freeze
25
+ XML_TAG = {
26
+ types: "type",
27
+ entities: "entity",
28
+ subtype_constraints: "subtype_constraint",
29
+ functions: "function",
30
+ rules: "rule",
31
+ procedures: "procedure",
32
+ }.freeze
33
+
34
+ def self.list(schemas, io = nil)
35
+ new(Array(schemas)).write(:list, io)
36
+ end
37
+
38
+ def self.smrl_xml(schemas, io = nil, source: nil)
39
+ new(Array(schemas)).write(:smrl_xml, io, source: source)
40
+ end
41
+
42
+ def initialize(schemas)
43
+ @schemas = schemas.compact.sort_by { |s| s.id.to_s.downcase }
44
+ end
45
+
46
+ def write(encoding, io = nil, source: nil)
47
+ owned = io.nil?
48
+ io ||= +""
49
+ case encoding
50
+ when :list then write_list(io)
51
+ when :smrl_xml then write_smrl_xml(io, source: source)
52
+ else
53
+ raise ArgumentError, "unknown listing encoding #{encoding.inspect}"
54
+ end
55
+ owned ? io : nil
56
+ end
57
+
58
+ private
59
+
60
+ def write_list(io)
61
+ @schemas.each_with_index do |schema, i|
62
+ io << "\n" if i.positive?
63
+ write_schema_list(schema, io)
64
+ end
65
+ end
66
+
67
+ def write_schema_list(schema, io)
68
+ version = schema.version ? " #{schema.version}" : ""
69
+ io << "SCHEMA #{schema.id.downcase}#{version};\n\n"
70
+ counts = decl_counts(schema)
71
+ DECL_ORDER.each do |key|
72
+ io << format("(* %-20s %4d *)\n", DECL_LABEL[key], counts[key])
73
+ end
74
+ ifaces = Array(schema.interfaces)
75
+ if ifaces.any?
76
+ io << "\n"
77
+ ifaces.each do |iface|
78
+ kind = iface.kind == Model::Declarations::Interface::REFERENCE ? "REFERENCE FROM" : "USE FROM"
79
+ target = iface_name(iface)
80
+ suffix = Array(iface.items).any? ? "(...)" : ""
81
+ io << " #{kind} #{target}#{suffix};\n"
82
+ end
83
+ end
84
+ constants = Array(schema.constants).sort_by { |c| c.id.to_s.downcase }
85
+ if constants.any?
86
+ io << "\n"
87
+ constants.each { |c| io << " CONSTANT #{c.id.downcase};\n" }
88
+ end
89
+ decls = ordered_decls(schema)
90
+ if decls.any?
91
+ io << "\n"
92
+ decls.each do |key, name|
93
+ io << " #{DECL_LABEL[key]} #{schema.id}.#{name.downcase};\n"
94
+ end
95
+ end
96
+ end
97
+
98
+ def write_smrl_xml(io, source: nil)
99
+ io << "<!-- Written by Expressir -->\n"
100
+ io << "<!-- #{Expressir::VERSION} -->\n"
101
+ io << "<!-- File: #{source} -->\n" if source
102
+ io << "<concatenated_express_file_content_list>\n\n"
103
+ names = @schemas.map { |s| schema_name_string(s.id) }
104
+ io << "<!-- #{@schemas.size} Schema#{'ta' unless @schemas.size == 1}:\n"
105
+ io << " #{names.join(', ')}\n"
106
+ io << "-->\n"
107
+ @schemas.each do |schema|
108
+ io << "\n\n"
109
+ write_schema_xml(schema, io)
110
+ end
111
+ io << "\n</concatenated_express_file_content_list>\n"
112
+ end
113
+
114
+ def write_schema_xml(schema, io)
115
+ io << "<schema>#{schema_name_string(schema.id)}</schema>\n"
116
+ if schema.version
117
+ io << " <schema_version>#{schema.version}</schema_version>\n"
118
+ end
119
+ counts = decl_counts(schema)
120
+ DECL_ORDER.each do |key|
121
+ io << format(" <!-- %-20s %4d -->\n", DECL_LABEL[key], counts[key])
122
+ end
123
+ Array(schema.interfaces).each do |iface|
124
+ tag = iface.kind == Model::Declarations::Interface::REFERENCE ? "reference-from" : "use-from"
125
+ target = iface_name(iface)
126
+ suffix = Array(iface.items).any? ? "(...)" : ""
127
+ io << " <#{tag}>#{target}#{suffix}</#{tag}>\n"
128
+ end
129
+ Array(schema.constants).sort_by { |c| c.id.to_s.downcase }.each do |c|
130
+ io << " <constant>#{c.id.downcase}</constant>\n"
131
+ end
132
+ ordered_decls(schema).each do |key, name|
133
+ tag = XML_TAG[key] || key.to_s
134
+ cased = if key == :entities && arm_mode?(schema)
135
+ name.split("_").map(&:capitalize).join("_")
136
+ else
137
+ name.downcase
138
+ end
139
+ io << " <#{tag}>#{schema.id}.#{cased}</#{tag}>\n"
140
+ end
141
+ end
142
+
143
+ def decl_counts(schema)
144
+ DECL_ORDER.to_h { |k| [k, Array(schema.public_send(k)).size] }
145
+ end
146
+
147
+ def ordered_decls(schema)
148
+ DECL_ORDER.flat_map do |key|
149
+ Array(schema.public_send(key))
150
+ .sort_by { |d| d.id.to_s.downcase }
151
+ .map { |d| [key, d.id] }
152
+ end
153
+ end
154
+
155
+ def iface_name(iface)
156
+ name = iface.schema.is_a?(String) ? iface.schema : iface.schema&.id
157
+ schema_name_string(name)
158
+ end
159
+
160
+ # eeng schema-name-string: arm/mim/arm_lf/mim_lf tails stay lowercase;
161
+ # everything else downcased.
162
+ def schema_name_string(name)
163
+ return "" unless name
164
+
165
+ name.to_s.downcase
166
+ end
167
+
168
+ def arm_mode?(schema)
169
+ id = schema.id.to_s.downcase
170
+ id.end_with?("arm", "arm_lf")
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # Structural traversal over model trees, mirroring {ModelVisitor}:
6
+ # every typed attribute, statements included. Nodes are keyed by
7
+ # attribute path plus array index — stable across a compiled-set
8
+ # round trip because the wire is byte-identical — which is what the
9
+ # remark and reference overlays key on ({Model::ModelElement#path}
10
+ # is empty for id-less nodes).
11
+ module ModelTraversal
12
+ module_function
13
+
14
+ def each_node(node, key, &block)
15
+ yield node, key
16
+ return unless node.is_a?(Model::ModelElement)
17
+
18
+ node.class.attributes.each_key do |attr|
19
+ next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr) || attr == :parent
20
+
21
+ value = node.public_send(attr)
22
+ case value
23
+ when Array
24
+ value.each_with_index do |item, index|
25
+ next unless item.is_a?(Model::ModelElement)
26
+
27
+ each_node(item, "#{key}/#{attr}[#{index}]", &block)
28
+ end
29
+ when Model::ModelElement
30
+ each_node(value, "#{key}/#{attr}", &block)
31
+ end
32
+ end
33
+ end
34
+
35
+ def each_model(models, &block)
36
+ models.each_with_index do |model, index|
37
+ each_node(model, "model[#{index}]", &block)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -142,15 +142,15 @@ module Expressir
142
142
 
143
143
  # Returns the most-specific node whose span contains `remark_line`,
144
144
  # preferring same-line starts/ends, then smallest containing span.
145
- # Excludes Repository and Cache (not semantic scopes for remarks).
145
+ # Skips nodes that cannot own such a remark; see {#remark_scope?}.
146
146
  def nearest_node_to(remark_line)
147
- same_start = starting_at(remark_line)
147
+ same_start = starting_at(remark_line).select { |n| remark_scope?(n[:node]) }
148
148
  return same_start.last[:node] if same_start.any?
149
149
 
150
- same_end = ending_at(remark_line)
150
+ same_end = ending_at(remark_line).select { |n| remark_scope?(n[:node]) }
151
151
  return same_end.last[:node] if same_end.any?
152
152
 
153
- containing = spanning(remark_line)
153
+ containing = spanning(remark_line).select { |n| remark_scope?(n[:node]) }
154
154
 
155
155
  if containing.any?
156
156
  exp_file_node = containing.find { |n| n[:node].is_a?(Model::ExpFile) }
@@ -218,6 +218,20 @@ module Expressir
218
218
  !node.is_a?(Model::Repository) && !node.is_a?(Model::Cache)
219
219
  end
220
220
 
221
+ # Whether a node can own a remark sitting on a line of its own.
222
+ # Distinct from Model::TakesInlineRemark, which asks whether a node can
223
+ # own a remark written AFTER it on the same line: a statement answers
224
+ # yes to both; an interface clause only to that one.
225
+ #
226
+ # An interface clause is indexed (so a remark trailing it can find it
227
+ # via the same-line lookup), but it encloses nothing: an own-line
228
+ # remark below `REFERENCE FROM x;` introduces whatever comes next, and
229
+ # giving it to the clause would both misplace it and lose it, since
230
+ # nothing renders remarks there.
231
+ def remark_scope?(node)
232
+ semantic?(node) && !node.is_a?(Model::Declarations::Interface)
233
+ end
234
+
221
235
  def build_sorted_nodes
222
236
  result = []
223
237
  collect_nodes(@model, result, Set.new)