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.
- checksums.yaml +4 -4
- data/.cargo/config.toml +3 -0
- data/.github/workflows/rust-ext.yml +41 -0
- data/.github/workflows/verify_remarks.yml +33 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +1 -0
- data/Cargo.toml +7 -0
- data/README.adoc +65 -0
- data/Rakefile +12 -0
- data/TODO.parity-ee/00-overview.md +58 -0
- data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
- data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
- data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
- data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
- data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
- data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
- data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
- data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
- data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
- data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
- data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
- data/TODO.parity-ee/parity-matrix.md +108 -0
- data/exe/expressir +6 -0
- data/ext/expressir_core/Cargo.toml +24 -0
- data/ext/expressir_core/extconf.rb +30 -0
- data/ext/expressir_core/src/lib.rs +446 -0
- data/lib/expressir/cli.rb +49 -4
- data/lib/expressir/commands/check.rb +56 -0
- data/lib/expressir/commands/expand.rb +26 -0
- data/lib/expressir/commands/fix.rb +33 -0
- data/lib/expressir/commands/flatten.rb +29 -0
- data/lib/expressir/commands/parity_inputs.rb +85 -0
- data/lib/expressir/commands/validate.rb +22 -0
- data/lib/expressir/commands.rb +5 -0
- data/lib/expressir/express/builder_registry.rb +11 -2
- data/lib/expressir/express/builders/entity_decl_builder.rb +17 -7
- data/lib/expressir/express/builders/subtype_constraint_builder.rb +3 -1
- data/lib/expressir/express/builders/type_builder.rb +8 -0
- data/lib/expressir/express/builders/unique_rule_builder.rb +8 -1
- data/lib/expressir/express/checker.rb +458 -0
- data/lib/expressir/express/concatenator.rb +79 -0
- data/lib/expressir/express/core.rb +66 -0
- data/lib/expressir/express/formatter.rb +25 -3
- data/lib/expressir/express/formatters/data_types_formatter.rb +2 -2
- data/lib/expressir/express/formatters/declarations_formatter.rb +1 -0
- data/lib/expressir/express/formatters/remark_formatter.rb +30 -6
- data/lib/expressir/express/formatters/statements_formatter.rb +7 -1
- data/lib/expressir/express/interface_dot.rb +105 -0
- data/lib/expressir/express/listing.rb +174 -0
- data/lib/expressir/express/model_traversal.rb +42 -0
- data/lib/expressir/express/node_position_index.rb +18 -4
- data/lib/expressir/express/parser.rb +242 -4
- data/lib/expressir/express/pretty_gate.rb +61 -0
- data/lib/expressir/express/refs_overlay.rb +47 -0
- data/lib/expressir/express/remark_attacher.rb +122 -10
- data/lib/expressir/express/remark_overlay.rb +86 -0
- data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
- data/lib/expressir/express/schema_source_formatter.rb +5 -5
- data/lib/expressir/express/self_schema_reference.rb +126 -0
- data/lib/expressir/express/shtolo.rb +522 -0
- data/lib/expressir/express.rb +13 -0
- data/lib/expressir/model/concerns.rb +15 -1
- data/lib/expressir/model/declarations/entity.rb +6 -1
- data/lib/expressir/model/declarations/interface.rb +13 -0
- data/lib/expressir/model/declarations/schema.rb +74 -23
- data/lib/expressir/model/indexes/item_graph.rb +135 -0
- data/lib/expressir/model/indexes.rb +1 -0
- data/lib/expressir/model/model_element.rb +30 -1
- data/lib/expressir/model/remark_info.rb +20 -1
- data/lib/expressir/model/remark_placement.rb +15 -2
- data/lib/expressir/model/repository.rb +8 -2
- data/lib/expressir/model.rb +2 -1
- data/lib/expressir/version.rb +1 -1
- data/lib/tasks/verify_remarks.rake +9 -0
- data/rakelib/verify_remarks.rake +16 -0
- 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
|
|
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
|
|
158
|
-
#
|
|
159
|
-
#
|
|
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::
|
|
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
|
-
|
|
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
|
-
#
|
|
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)
|