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
|
@@ -22,7 +22,21 @@ module Expressir
|
|
|
22
22
|
# Marker for types supporting where rules
|
|
23
23
|
module HasWhereRules; end
|
|
24
24
|
|
|
25
|
+
# Marker for types that can own a remark written after them on the same
|
|
26
|
+
# line, as in `x := 1; -- why`. Includers are ModelElements, so they all
|
|
27
|
+
# carry the `untagged_remarks` collection the remark is stored in.
|
|
28
|
+
#
|
|
29
|
+
# Attachment and formatting must agree on this exactly: a type the
|
|
30
|
+
# attacher marks INLINE but the formatter declines to emit loses the
|
|
31
|
+
# remark with nothing to show for it. Both ask this one question.
|
|
32
|
+
#
|
|
33
|
+
# Not the same question as NodePositionIndex#remark_scope?, which decides
|
|
34
|
+
# who owns a remark on a line of its OWN.
|
|
35
|
+
module TakesInlineRemark; end
|
|
36
|
+
|
|
25
37
|
# Marker for executable statements (ISO 10303-11 section 13)
|
|
26
|
-
module Statement
|
|
38
|
+
module Statement
|
|
39
|
+
include TakesInlineRemark
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
@@ -17,7 +17,12 @@ module Expressir
|
|
|
17
17
|
attribute :abstract, :boolean
|
|
18
18
|
attribute :supertype_expression, ModelElement
|
|
19
19
|
attribute :subtype_of, ModelElement, collection: true
|
|
20
|
-
|
|
20
|
+
# Heterogeneous by design: Attribute, DerivedAttribute, and
|
|
21
|
+
# InverseAttribute all live here (the builders concatenate the
|
|
22
|
+
# three clauses in declaration order). The ModelElement base
|
|
23
|
+
# keeps from_hash polymorphic via the _class marker, exactly
|
|
24
|
+
# like the Ruby-built model.
|
|
25
|
+
attribute :attributes, ModelElement, collection: true
|
|
21
26
|
attribute :derived_attributes, DerivedAttribute, collection: true
|
|
22
27
|
attribute :inverse_attributes, InverseAttribute, collection: true
|
|
23
28
|
attribute :unique_rules, UniqueRule, collection: true
|
|
@@ -4,6 +4,9 @@ module Expressir
|
|
|
4
4
|
# Specified in ISO 10303-11:2004
|
|
5
5
|
# - section 11 Interface specification
|
|
6
6
|
class Interface < ModelElement
|
|
7
|
+
# A remark may trail the clause: `REFERENCE FROM x; -- why`.
|
|
8
|
+
include TakesInlineRemark
|
|
9
|
+
|
|
7
10
|
USE = "USE".freeze
|
|
8
11
|
REFERENCE = "REFERENCE".freeze
|
|
9
12
|
|
|
@@ -12,11 +15,21 @@ module Expressir
|
|
|
12
15
|
attribute :items, InterfaceItem, collection: true
|
|
13
16
|
attribute :_class, :string, default: -> { self.class.name }
|
|
14
17
|
|
|
18
|
+
# Registers :items with the tree-walker registry: the position index
|
|
19
|
+
# needs the item list to size the clause's span, so an opener-line
|
|
20
|
+
# remark gets the OPENER_REGION and renders back on the clause's
|
|
21
|
+
# first line instead of after the closing semicolon.
|
|
22
|
+
collection_attributes :items
|
|
23
|
+
|
|
15
24
|
key_value do
|
|
16
25
|
map "_class", to: :_class, render_default: true
|
|
17
26
|
map "kind", to: :kind
|
|
18
27
|
map "schema", to: :schema
|
|
19
28
|
map "items", to: :items
|
|
29
|
+
# Without this the clause's remark survives a format but not a
|
|
30
|
+
# cache round trip: it would be dropped on serialization and never
|
|
31
|
+
# come back.
|
|
32
|
+
map "untagged_remarks", to: :untagged_remarks
|
|
20
33
|
end
|
|
21
34
|
end
|
|
22
35
|
end
|
|
@@ -9,7 +9,7 @@ module Expressir
|
|
|
9
9
|
|
|
10
10
|
# Tree-walker collection attributes (TODO.bugs/12 — single source
|
|
11
11
|
# of truth; NodePositionIndex derives COLLECTION_REGISTRY from this).
|
|
12
|
-
collection_attributes :constants, :types, :entities,
|
|
12
|
+
collection_attributes :interfaces, :constants, :types, :entities,
|
|
13
13
|
:subtype_constraints, :functions, :rules,
|
|
14
14
|
:procedures, :remark_items
|
|
15
15
|
|
|
@@ -60,6 +60,7 @@ module Expressir
|
|
|
60
60
|
map :header, to: :header
|
|
61
61
|
map :formatted, to: :formatted
|
|
62
62
|
map :source, to: :source
|
|
63
|
+
map :source_hyperlinked, to: :source_hyperlinked
|
|
63
64
|
map :full_source, to: :full_source
|
|
64
65
|
map :children, to: :children
|
|
65
66
|
map :safe_children, to: :safe_children
|
|
@@ -96,8 +97,18 @@ module Expressir
|
|
|
96
97
|
@formatted ||= format(no_remarks: false)
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
# Schema head text without hyperlinks (#255) — the `source` face
|
|
101
|
+
# templates call for the verbatim schema declaration block.
|
|
102
|
+
# Deliberately NOT @source: that ivar holds the raw source span
|
|
103
|
+
# lutaml-model's `source=` stores when include_source is on.
|
|
99
104
|
def source
|
|
100
|
-
@
|
|
105
|
+
@plain_source ||= Expressir::Express::SchemaPlainSourceFormatter.format(self) # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Schema head text with cross-references rendered as hyperlinks
|
|
109
|
+
# (#255) — the hyperlinked counterpart of #source.
|
|
110
|
+
def source_hyperlinked
|
|
111
|
+
Expressir::Express::SchemaSourceFormatter.format(self)
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
private
|
|
@@ -118,30 +129,70 @@ module Expressir
|
|
|
118
129
|
def interfaced_items
|
|
119
130
|
return [] unless parent
|
|
120
131
|
|
|
132
|
+
visited_schemas = {}
|
|
121
133
|
interfaces.flat_map do |interface|
|
|
122
|
-
|
|
123
|
-
next [] unless schema
|
|
124
|
-
|
|
125
|
-
safe_children = schema.safe_children
|
|
126
|
-
children_by_id = safe_children.each_with_object({}) do |child, hash|
|
|
127
|
-
hash[child.id.safe_downcase] = child if child.id
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
if interface.items.empty?
|
|
131
|
-
safe_children.map do |base_item|
|
|
132
|
-
create_interfaced_item(base_item.id, base_item)
|
|
133
|
-
end
|
|
134
|
-
else
|
|
135
|
-
interface.items.filter_map do |item|
|
|
136
|
-
base_item = children_by_id[item.ref.id.safe_downcase]
|
|
137
|
-
if base_item
|
|
138
|
-
create_interfaced_item(item.id || base_item.id,
|
|
139
|
-
base_item)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
134
|
+
items_for_interface(interface, visited_schemas)
|
|
143
135
|
end.compact
|
|
144
136
|
end
|
|
137
|
+
|
|
138
|
+
# Direct items of one interface plus the items visible through
|
|
139
|
+
# the foreign schema's own USE interfaces (transitive USE
|
|
140
|
+
# visibility, eeng :use-only → :use-from recursion).
|
|
141
|
+
def items_for_interface(interface, visited_schemas)
|
|
142
|
+
schema = foreign_schema(interface.schema.id.safe_downcase)
|
|
143
|
+
return [] unless schema
|
|
144
|
+
# Visited means this schema's contribution is already in the
|
|
145
|
+
# caller's result: re-entering — mutual USE FROM included —
|
|
146
|
+
# recursed forever when the interface carried an item list
|
|
147
|
+
# (the old guard only fired for the list-less form).
|
|
148
|
+
return [] if visited_schemas.key?(schema.id.safe_downcase)
|
|
149
|
+
|
|
150
|
+
visited_schemas[schema.id.safe_downcase] = true
|
|
151
|
+
|
|
152
|
+
safe_children = schema.safe_children
|
|
153
|
+
children_by_id = safe_children.each_with_object({}) do |child, hash|
|
|
154
|
+
hash[child.id.safe_downcase] = child if child.id
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
own = if interface.items.empty?
|
|
158
|
+
safe_children.map do |base_item|
|
|
159
|
+
create_interfaced_item(base_item.id, base_item)
|
|
160
|
+
end
|
|
161
|
+
else
|
|
162
|
+
interface.items.filter_map do |item|
|
|
163
|
+
base_item = children_by_id[item.ref.id.safe_downcase]
|
|
164
|
+
if base_item
|
|
165
|
+
create_interfaced_item(item.id || base_item.id,
|
|
166
|
+
base_item)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Transitive items are not restricted by this interface's
|
|
172
|
+
# item list: implicit interfacing through USE is unfiltered
|
|
173
|
+
# (eeng :use-only → :use-from recursion finds any id in the
|
|
174
|
+
# foreign schema's use scope).
|
|
175
|
+
own + Array(schema.interfaces).filter_map do |foreign_iface|
|
|
176
|
+
next nil unless foreign_iface.kind == Interface::USE
|
|
177
|
+
|
|
178
|
+
foreign_items = items_for_interface(foreign_iface,
|
|
179
|
+
visited_schemas)
|
|
180
|
+
foreign_items.empty? ? nil : foreign_items
|
|
181
|
+
end.flatten
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Find a foreign schema by name: same file first, then the
|
|
185
|
+
# enclosing repository (expressir's compiled/batch repository).
|
|
186
|
+
def foreign_schema(name)
|
|
187
|
+
current = parent
|
|
188
|
+
while current
|
|
189
|
+
return current.children_by_id[name] if current.respond_to?(:children_by_id) &&
|
|
190
|
+
current.children_by_id[name]
|
|
191
|
+
|
|
192
|
+
current = current.is_a?(ModelElement) ? current.parent : nil
|
|
193
|
+
end
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
145
196
|
end
|
|
146
197
|
end
|
|
147
198
|
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Model
|
|
5
|
+
module Indexes
|
|
6
|
+
# Item-level graph over a resolved repository: nodes are schema
|
|
7
|
+
# items (canonical "schema.item" paths), edges are inheritance,
|
|
8
|
+
# interface imports, and attribute-type references.
|
|
9
|
+
#
|
|
10
|
+
# {Model::ModelElement#path} is file-qualified and empty for
|
|
11
|
+
# id-less nodes, so node identity is the last two path segments —
|
|
12
|
+
# unique because schema names are unique within a repository.
|
|
13
|
+
class ItemGraph
|
|
14
|
+
attr_reader :nodes, :subtype_of, :dependencies
|
|
15
|
+
|
|
16
|
+
# @param repository [Repository] a resolved repository
|
|
17
|
+
def initialize(repository)
|
|
18
|
+
@nodes = {}
|
|
19
|
+
@subtype_of = Hash.new { |h, k| h[k] = [] }
|
|
20
|
+
@dependencies = Hash.new { |h, k| h[k] = [] }
|
|
21
|
+
build(repository)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Plain tables for artifact persistence — node keys only, no
|
|
25
|
+
# model references. `ItemGraph.from_tables` is the zero-model
|
|
26
|
+
# counterpart used on warm loads.
|
|
27
|
+
def tables
|
|
28
|
+
{
|
|
29
|
+
"nodes" => @nodes.keys,
|
|
30
|
+
"subtype_of" => @subtype_of.reject { |_, parents| parents.empty? },
|
|
31
|
+
"dependencies" => @dependencies.reject { |_, deps| deps.empty? },
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.from_tables(tables)
|
|
36
|
+
graph = allocate
|
|
37
|
+
graph.instance_variable_set(:@nodes, tables["nodes"].to_h { |k| [k, nil] })
|
|
38
|
+
graph.instance_variable_set(
|
|
39
|
+
:@subtype_of,
|
|
40
|
+
Hash.new { |h, k| h[k] = [] }.update(tables["subtype_of"] || {}),
|
|
41
|
+
)
|
|
42
|
+
graph.instance_variable_set(
|
|
43
|
+
:@dependencies,
|
|
44
|
+
Hash.new { |h, k| h[k] = [] }.update(tables["dependencies"] || {}),
|
|
45
|
+
)
|
|
46
|
+
graph
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def include?(path)
|
|
50
|
+
@nodes.key?(canonical(path))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Transitive supertype closure of an entity, breadth-first.
|
|
54
|
+
def supertypes(path)
|
|
55
|
+
key = canonical(path)
|
|
56
|
+
return [] unless @nodes.key?(key)
|
|
57
|
+
|
|
58
|
+
seen = {}
|
|
59
|
+
result = []
|
|
60
|
+
queue = @subtype_of[key].dup
|
|
61
|
+
until queue.empty?
|
|
62
|
+
current = queue.shift
|
|
63
|
+
next if seen[current]
|
|
64
|
+
|
|
65
|
+
seen[current] = true
|
|
66
|
+
result << current
|
|
67
|
+
queue.concat(@subtype_of[current])
|
|
68
|
+
end
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Schemas whose items `schema` imports (USE FROM / REFERENCE
|
|
73
|
+
# FROM), direct only. Accepts a schema object or its id.
|
|
74
|
+
def dependencies_of(schema)
|
|
75
|
+
schema_id = schema.respond_to?(:id) ? schema.id : schema.to_s
|
|
76
|
+
@dependencies[schema_id.safe_downcase].dup
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def subtype_edges
|
|
80
|
+
@subtype_of.flat_map { |child, parents| parents.map { |p| [child, p] } }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cross_schema_subtype_edges
|
|
84
|
+
subtype_edges.count { |child, parent| child.split(".")[0] != parent.split(".")[0] }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def canonical(path)
|
|
90
|
+
return nil unless path
|
|
91
|
+
|
|
92
|
+
parts = path.to_s.split(".")
|
|
93
|
+
return nil if parts.size < 2
|
|
94
|
+
|
|
95
|
+
parts[-2, 2].join(".").safe_downcase
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build(repository)
|
|
99
|
+
# Nodes register before their subtype edges: subtype references
|
|
100
|
+
# may be unresolved (the declaring schema is not in the
|
|
101
|
+
# repository), and an edge to a node that does not exist would
|
|
102
|
+
# surface as a phantom path in supertype closures.
|
|
103
|
+
edges = []
|
|
104
|
+
repository.schemas.each do |schema|
|
|
105
|
+
schema_id = schema.id.safe_downcase
|
|
106
|
+
schema.entities.to_a.each do |entity|
|
|
107
|
+
key = node_key(schema_id, entity)
|
|
108
|
+
@nodes[key] = entity
|
|
109
|
+
edges << [key, schema_id, entity]
|
|
110
|
+
end
|
|
111
|
+
schema.types.to_a.each do |type|
|
|
112
|
+
@nodes[node_key(schema_id, type)] = type
|
|
113
|
+
end
|
|
114
|
+
schema.interfaces.to_a.each do |interface|
|
|
115
|
+
target = interface.schema.is_a?(String) ? interface.schema : interface.schema&.id
|
|
116
|
+
next unless target
|
|
117
|
+
|
|
118
|
+
@dependencies[schema_id] << target.safe_downcase
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
edges.each do |key, schema_id, entity|
|
|
122
|
+
entity.subtype_of.to_a.each do |ref|
|
|
123
|
+
parent = ref.base_path ? canonical(ref.base_path) : "#{schema_id}.#{ref.id.safe_downcase}"
|
|
124
|
+
@subtype_of[key] << parent if parent && @nodes.key?(parent)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def node_key(schema_id, element)
|
|
130
|
+
canonical(element.path) || "#{schema_id}.#{element.id.safe_downcase}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -6,6 +6,7 @@ module Expressir
|
|
|
6
6
|
autoload :EntityIndex, "#{__dir__}/indexes/entity_index"
|
|
7
7
|
autoload :TypeIndex, "#{__dir__}/indexes/type_index"
|
|
8
8
|
autoload :ReferenceIndex, "#{__dir__}/indexes/reference_index"
|
|
9
|
+
autoload :ItemGraph, "#{__dir__}/indexes/item_graph"
|
|
9
10
|
end
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -61,7 +61,11 @@ module Expressir
|
|
|
61
61
|
|
|
62
62
|
# ---- End child-attributes macro ----
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
# :source is skipped deliberately: its reader formats the node,
|
|
65
|
+
# and reading it during construction (attach_parent_to_children on
|
|
66
|
+
# a from_hash-built object whose attributes are not yet applied)
|
|
67
|
+
# would memoize an incomplete rendering forever.
|
|
68
|
+
SKIP_ATTRIBUTES = %i[parent _class source].freeze
|
|
65
69
|
# :parent is a special attribute that is used to store the parent of the current element
|
|
66
70
|
# It is not a real attribute
|
|
67
71
|
# attribute :parent, ModelElement
|
|
@@ -101,6 +105,7 @@ module Expressir
|
|
|
101
105
|
"Expressir::Model::Declarations::Entity" => "Expressir::Model::Declarations::Entity",
|
|
102
106
|
"Expressir::Model::Declarations::Function" => "Expressir::Model::Declarations::Function",
|
|
103
107
|
"Expressir::Model::Declarations::InterfaceItem" => "Expressir::Model::Declarations::InterfaceItem",
|
|
108
|
+
"Expressir::Model::Declarations::InverseAttribute" => "Expressir::Model::Declarations::InverseAttribute",
|
|
104
109
|
"Expressir::Model::Declarations::Interface" => "Expressir::Model::Declarations::Interface",
|
|
105
110
|
"Expressir::Model::Declarations::InterfacedItem" => "Expressir::Model::Declarations::InterfacedItem",
|
|
106
111
|
"Expressir::Model::Declarations::Parameter" => "Expressir::Model::Declarations::Parameter",
|
|
@@ -152,6 +157,10 @@ module Expressir
|
|
|
152
157
|
key_value do
|
|
153
158
|
map "_class", to: :_class, render_default: true,
|
|
154
159
|
polymorphic_map: POLYMORPHIC_CLASS_MAP
|
|
160
|
+
# Hydrate-only: byte positions ride the wire (Rust core path)
|
|
161
|
+
# without ever appearing in to_hash — the serialized shape stays
|
|
162
|
+
# identical to the Ruby-built model.
|
|
163
|
+
map "source_offset", to: :source_offset, serialize: false
|
|
155
164
|
end
|
|
156
165
|
|
|
157
166
|
def source
|
|
@@ -278,6 +287,24 @@ module Expressir
|
|
|
278
287
|
end
|
|
279
288
|
end
|
|
280
289
|
|
|
290
|
+
# Wire the parent pointers of this node's entire subtree. Needed
|
|
291
|
+
# for models materialized outside the builders (e.g. hydrated
|
|
292
|
+
# from the Rust core path), whose parents start unset.
|
|
293
|
+
def wire_parents(parent = nil)
|
|
294
|
+
self.parent = parent
|
|
295
|
+
self.class.attributes.each_key do |attr|
|
|
296
|
+
next if SKIP_ATTRIBUTES.include?(attr) || attr == :parent
|
|
297
|
+
|
|
298
|
+
value = public_send(attr)
|
|
299
|
+
case value
|
|
300
|
+
when Array
|
|
301
|
+
value.each { |v| v.wire_parents(self) if v.is_a?(ModelElement) }
|
|
302
|
+
when ModelElement
|
|
303
|
+
value.wire_parents(self)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
281
308
|
# @return [Array<Declaration>]
|
|
282
309
|
def children
|
|
283
310
|
[]
|
|
@@ -327,6 +354,8 @@ module Expressir
|
|
|
327
354
|
# @return [nil]
|
|
328
355
|
def attach_parent_to_children
|
|
329
356
|
self.class.attributes.each_pair do |symbol, _lutaml_attr|
|
|
357
|
+
next if SKIP_ATTRIBUTES.include?(symbol)
|
|
358
|
+
|
|
330
359
|
value = public_send(symbol)
|
|
331
360
|
|
|
332
361
|
case value
|
|
@@ -9,7 +9,10 @@ module Expressir
|
|
|
9
9
|
attribute :tag, :string # optional remark tag like "entity.attr"
|
|
10
10
|
# See RemarkPlacement. nil = legacy placement.
|
|
11
11
|
attribute :placement, :string
|
|
12
|
-
# For TRAILING
|
|
12
|
+
# Qualifies the placement. For TRAILING, which body of the owner the
|
|
13
|
+
# remark closes, named by that body's attribute. For INLINE,
|
|
14
|
+
# RemarkPlacement::OPENER_REGION when it trailed a compound statement's
|
|
15
|
+
# opening line rather than a whole statement.
|
|
13
16
|
attribute :region, :string
|
|
14
17
|
attribute :source_offset, :integer # byte offset in source text
|
|
15
18
|
|
|
@@ -43,6 +46,22 @@ module Expressir
|
|
|
43
46
|
placement == RemarkPlacement::INLINE
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
# Check if this remark trailed the opening line of a statement spanning
|
|
50
|
+
# several lines, as in `IF x THEN -- why`
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def opener?
|
|
53
|
+
inline? && region == RemarkPlacement::OPENER_REGION
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if this INLINE remark belongs after a mid-construct keyword of
|
|
57
|
+
# its owning construct — `ELSE -- why`, `OTHERWISE : -- why` — rather
|
|
58
|
+
# than after the statement's closing keyword.
|
|
59
|
+
# @param name [Symbol, String] a RemarkPlacement region constant
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def inline_region?(name)
|
|
62
|
+
inline? && region == name.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
46
65
|
# Check if this remark closes the given body of its owning node
|
|
47
66
|
# @param name [Symbol, String] region attribute name
|
|
48
67
|
# @return [Boolean]
|
|
@@ -14,8 +14,11 @@ module Expressir
|
|
|
14
14
|
# keyword. The owner is the body's owner, and RemarkInfo#region names
|
|
15
15
|
# which body: an IF owns two, and they close at different keywords.
|
|
16
16
|
# - INLINE — on the same line as the owning statement, after its text.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# Emitted after the whole statement, which for a single-line one is the
|
|
18
|
+
# line it was written on. RemarkInfo#region separates the other case:
|
|
19
|
+
# OPENER_REGION means the remark trailed the opening line of a statement
|
|
20
|
+
# spanning several lines, as in `IF x THEN -- why`, and belongs back on
|
|
21
|
+
# that opener rather than after the closing keyword.
|
|
19
22
|
#
|
|
20
23
|
# `nil` means the remark was attached by a legacy path, or predates
|
|
21
24
|
# placement tracking. Those keep their historical emission and must not
|
|
@@ -24,6 +27,16 @@ module Expressir
|
|
|
24
27
|
LEADING = "leading"
|
|
25
28
|
TRAILING = "trailing"
|
|
26
29
|
INLINE = "inline"
|
|
30
|
+
|
|
31
|
+
# Region of an INLINE remark that trailed a compound statement's opener.
|
|
32
|
+
OPENER_REGION = "opener"
|
|
33
|
+
|
|
34
|
+
# Regions of INLINE remarks that trailed a mid-construct keyword line —
|
|
35
|
+
# `ELSE -- why` or `OTHERWISE : -- why`. The owning construct is the
|
|
36
|
+
# enclosing IF / CASE, and the remark belongs back after that keyword,
|
|
37
|
+
# not after the construct's closing END_.
|
|
38
|
+
ELSE_REGION = "else"
|
|
39
|
+
OTHERWISE_REGION = "otherwise"
|
|
27
40
|
end
|
|
28
41
|
end
|
|
29
42
|
end
|
|
@@ -24,6 +24,10 @@ module Expressir
|
|
|
24
24
|
# Index instances (lazy-loaded)
|
|
25
25
|
attr_reader :entity_index, :type_index, :reference_index
|
|
26
26
|
|
|
27
|
+
# Item-level graph; loaded from the compiled-set artifact on
|
|
28
|
+
# warm starts, built on demand otherwise.
|
|
29
|
+
attr_accessor :item_graph
|
|
30
|
+
|
|
27
31
|
# Restore deserialized indexes (used by Package::Reader)
|
|
28
32
|
def restore_indexes(entity_index: nil, type_index: nil,
|
|
29
33
|
reference_index: nil)
|
|
@@ -56,10 +60,12 @@ reference_index: nil)
|
|
|
56
60
|
schemas&.each { |schema| @_schemas << schema }
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
# Get all schemas (from both files and direct storage)
|
|
63
|
+
# Get all schemas (from both files and direct storage).
|
|
64
|
+
# Files that failed to parse are nil in `files` — a parse failure must
|
|
65
|
+
# not crash schema enumeration with a NoMethodError from in here.
|
|
60
66
|
# @return [Array<Declarations::Schema>]
|
|
61
67
|
def schemas
|
|
62
|
-
file_schemas = files
|
|
68
|
+
file_schemas = files.compact.flat_map(&:schemas).compact
|
|
63
69
|
file_schemas + @_schemas
|
|
64
70
|
end
|
|
65
71
|
|
data/lib/expressir/model.rb
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module Expressir
|
|
4
4
|
module Model
|
|
5
5
|
# Marker modules — autoload the same concerns.rb file from each name.
|
|
6
|
-
# The first reference triggers the load; the file defines all
|
|
6
|
+
# The first reference triggers the load; the file defines all eight markers.
|
|
7
7
|
autoload :HasId, "#{__dir__}/model/concerns"
|
|
8
8
|
autoload :HasRemarkItems, "#{__dir__}/model/concerns"
|
|
9
9
|
autoload :HasRemarks, "#{__dir__}/model/concerns"
|
|
10
10
|
autoload :ScopeContainer, "#{__dir__}/model/concerns"
|
|
11
11
|
autoload :HasInformalPropositions, "#{__dir__}/model/concerns"
|
|
12
12
|
autoload :HasWhereRules, "#{__dir__}/model/concerns"
|
|
13
|
+
autoload :TakesInlineRemark, "#{__dir__}/model/concerns"
|
|
13
14
|
autoload :Statement, "#{__dir__}/model/concerns"
|
|
14
15
|
|
|
15
16
|
autoload :ModelElement, "#{__dir__}/model/model_element"
|
data/lib/expressir/version.rb
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# `rake verify:remarks` — the remark conservation gate. Runs only the
|
|
4
|
+
# production-scale checks (`:production_scale` tag; see spec_helper's
|
|
5
|
+
# filter), which the default suite excludes: they parse a full-size
|
|
6
|
+
# schema and cost about a minute.
|
|
7
|
+
RSpec::Core::RakeTask.new(:"verify:remarks") do |task|
|
|
8
|
+
task.rspec_opts = "--tag production_scale"
|
|
9
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :verify do
|
|
4
|
+
desc "Check remarks against the recorded round-trip gaps, production included"
|
|
5
|
+
task :remarks do
|
|
6
|
+
specs = %w[
|
|
7
|
+
spec/expressir/remark_conservation_spec.rb
|
|
8
|
+
spec/expressir/remark_ownership_spec.rb
|
|
9
|
+
spec/expressir/express/formatter_remark_conservation_spec.rb
|
|
10
|
+
spec/expressir/express/formatter_remark_ownership_spec.rb
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
sh({ "EXPRESSIR_PRODUCTION_SCALE" => "1" },
|
|
14
|
+
"bundle", "exec", "rspec", *specs)
|
|
15
|
+
end
|
|
16
|
+
end
|