interscript 3.2.1 → 3.4.0

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.
@@ -26,10 +26,16 @@ module Interscript
26
26
  @local_raw = Array(isc_doc[:aliases]).to_h { |a| [a[:name].to_sym, a[:value]] }
27
27
  @charset_cache = {}
28
28
  @resolving = []
29
+ # ISC-WF-1 W1: memoized fully-expanded rulesets, keyed
30
+ # [dependency alias or nil, ruleset name]; @resolving_rulesets
31
+ # guards `use` cycles (a load error, spec §6.4).
32
+ @ruleset_memo = {}
33
+ @resolving_rulesets = []
29
34
  end
30
35
 
31
36
  def build
32
37
  Interscript::Node::Document.new.tap do |doc|
38
+ @doc = doc
33
39
  doc.metadata = build_metadata
34
40
  doc.tests = build_tests
35
41
  # Maps are addressed by their file-derived name (locate() and
@@ -55,6 +61,10 @@ module Interscript
55
61
  # (grek_latn_any = any([grek_any, …])) resolve too.
56
62
  @imported = doc.imported_aliases
57
63
  doc.aliases = build_aliases(doc.name)
64
+ # ISC-WF-1: declarations before stage building — `use` expansion
65
+ # resolves against the document's rulesets and its dependencies'.
66
+ doc.languages = build_languages
67
+ doc.rulesets = build_rulesets(doc.name)
58
68
  build_stages(doc.name).each { |name, stage| doc.stages[name] = stage }
59
69
  end
60
70
  end
@@ -126,39 +136,189 @@ module Interscript
126
136
  end
127
137
  end
128
138
 
139
+ # ISC-WF-1 W1: build + memoize every declared ruleset, fully
140
+ # expanded (nested `use` resolved, cycles rejected).
141
+ def build_rulesets(doc_name)
142
+ Array(@isc_doc[:rulesets]).each_with_object({}) do |rs, h|
143
+ h[rs[:name].to_sym] = resolve_ruleset(nil, rs[:name], doc_name: doc_name)
144
+ end
145
+ end
146
+
147
+ def build_languages
148
+ return nil unless @isc_doc[:languages]&.any?
149
+
150
+ Interscript::Node::LanguagesDecl.new(@isc_doc[:languages].map(&:to_sym))
151
+ end
152
+
129
153
  def build_stage(stage_def)
130
154
  stage = Interscript::Node::Stage.new(stage_def[:name].to_sym)
131
- stage_def[:body].each do |item|
132
- case item[:kind]
133
- when :parallel
134
- group = Interscript::Node::Group::Parallel.new
135
- item[:rules].each { |r| group.children << build_rule(r) }
136
- stage.children << group
137
- when :sequence
138
- item[:rules].each { |r| stage.children << build_rule(r) }
139
- when :bare_rule
140
- stage.children << build_rule(item[:rule])
141
- when :run
142
- stage.children << build_run_rule(item)
143
- when :separate
144
- kwargs = {}
145
- kwargs[:separator] = item[:separator].value if item[:separator]
146
- stage.children << Interscript::Node::Rule::Funcall.new(:separate, **kwargs)
147
- when :string_case
148
- stage.children << Interscript::Node::Rule::Funcall.new(item[:op].to_sym,
149
- **(item[:kwargs] || {}).transform_keys(&:to_sym))
150
- when :compose
151
- stage.children << Interscript::Node::Rule::Funcall.new(:compose)
152
- when :decompose
153
- stage.children << Interscript::Node::Rule::Funcall.new(:decompose)
154
- when :funcall
155
- stage.children << Interscript::Node::Rule::Funcall.new(
156
- item[:name].to_sym,
157
- **item[:kwargs].transform_keys(&:to_sym)
158
- )
155
+ expand_items(stage_def[:body]).each { |node| stage.children << node }
156
+ stage
157
+ end
158
+
159
+ # Build the Nodes for one ISC stage-item def (no `use` — that goes
160
+ # through expand_use). Returns a list; sequence blocks contribute
161
+ # their rules individually, parallel blocks one group.
162
+ def build_stage_item_nodes(item)
163
+ case item[:kind]
164
+ when :parallel
165
+ group = Interscript::Node::Group::Parallel.new
166
+ item[:rules].each { |r| group.children << build_rule(r) }
167
+ [group]
168
+ when :sequence
169
+ item[:rules].map { |r| build_rule(r) }
170
+ when :bare_rule
171
+ [build_rule(item[:rule])]
172
+ when :run
173
+ [build_run_rule(item)]
174
+ when :separate
175
+ kwargs = {}
176
+ kwargs[:separator] = item[:separator].value if item[:separator]
177
+ [Interscript::Node::Rule::Funcall.new(:separate, **kwargs)]
178
+ when :string_case
179
+ [Interscript::Node::Rule::Funcall.new(item[:op].to_sym,
180
+ **(item[:kwargs] || {}).transform_keys(&:to_sym))]
181
+ when :compose
182
+ [Interscript::Node::Rule::Funcall.new(:compose)]
183
+ when :decompose
184
+ [Interscript::Node::Rule::Funcall.new(:decompose)]
185
+ when :funcall
186
+ [Interscript::Node::Rule::Funcall.new(
187
+ item[:name].to_sym,
188
+ **item[:kwargs].transform_keys(&:to_sym)
189
+ )]
190
+ else
191
+ []
192
+ end
193
+ end
194
+
195
+ # -- ISC-WF-1 W1/W2: ruleset expansion --------------------------------
196
+
197
+ # Expand a list of ISC stage-item defs into Node children, resolving
198
+ # `use` directives recursively. Used for both stage bodies and
199
+ # ruleset bodies (a ruleset body is a stage body, spec §3.1).
200
+ def expand_items(items)
201
+ Array(items).flat_map do |item|
202
+ if item.is_a?(Hash) && item[:kind] == :use
203
+ expand_use(item)
204
+ else
205
+ build_stage_item_nodes(item)
159
206
  end
160
207
  end
161
- stage
208
+ end
209
+
210
+ # Resolve + deep-copy + merge-override one `use` site (spec §3.2, §4).
211
+ def expand_use(item)
212
+ rs = resolve_ruleset(item[:dependency], item[:ruleset])
213
+ apply_overrides(deep_copy_children(rs.children), item[:overrides])
214
+ end
215
+
216
+ # The fully-expanded Node::Ruleset for a reference, memoized.
217
+ # Same-map rulesets build here; dependency rulesets arrive already
218
+ # built through the recursive adapter of the dependency document.
219
+ def resolve_ruleset(dep_alias, name, doc_name: nil)
220
+ key = [dep_alias&.to_sym, name.to_sym]
221
+ return @ruleset_memo[key] if @ruleset_memo&.key?(key)
222
+ raise cycle_error(name) if @resolving_rulesets.include?(key)
223
+
224
+ @resolving_rulesets << key
225
+ node =
226
+ if dep_alias
227
+ dep = @doc.dep_aliases[dep_alias.to_sym] ||
228
+ raise(Interscript::MapLogicError,
229
+ "use #{dep_alias}.#{name}: no declared dependency aliased #{dep_alias}")
230
+ dep.document&.rulesets&.[](name.to_sym) ||
231
+ raise(Interscript::MapLogicError,
232
+ "use #{dep_alias}.#{name}: ruleset not found in dependency #{dep.full_name}")
233
+ else
234
+ rs_def = Array(@isc_doc[:rulesets]).find { |r| r[:name] == name } ||
235
+ raise(Interscript::MapLogicError, "use #{name}: ruleset not found")
236
+ Interscript::Node::Ruleset.new(rs_def[:name].to_sym).tap do |rs|
237
+ rs.doc_name = doc_name
238
+ expand_items(rs_def[:body]).each { |child| rs.children << child }
239
+ end
240
+ end
241
+ @ruleset_memo[key] = node
242
+ node
243
+ ensure
244
+ @resolving_rulesets.delete(key)
245
+ end
246
+
247
+ def cycle_error(name)
248
+ Interscript::MapLogicError.new(
249
+ "use cycle: ruleset #{name} revisited through #{@resolving_rulesets.map { |_, n| n }.join(" -> ")}"
250
+ )
251
+ end
252
+
253
+ # Per-site deep copy: expansions must not share Group nodes (the
254
+ # Interpreter caches compiled pass trees on them) nor be mutated by
255
+ # another site's override merge. Rule/Item leaves are read-only at
256
+ # execution time and may be shared.
257
+ def deep_copy_children(children)
258
+ children.map { |node| deep_copy_node(node) }
259
+ end
260
+
261
+ def deep_copy_node(node)
262
+ case node
263
+ when Interscript::Node::Group
264
+ copy = node.class.new(reverse_run: node.reverse_run)
265
+ if node.is_a?(Interscript::Node::Stage)
266
+ copy.name = node.name
267
+ copy.doc_name = node.doc_name
268
+ copy.dont_reverse = node.dont_reverse
269
+ end
270
+ copy.children = node.children.map { |c| deep_copy_node(c) }
271
+ copy
272
+ else
273
+ node.dup
274
+ end
275
+ end
276
+
277
+ # ISC-WF-1 W2 merge (spec §4.2). R = the fully expanded ruleset rows
278
+ # (document order), O = the override rows.
279
+ # 1. A row whose from is byte-exact equal to an override from is
280
+ # REPLACED — removed from wherever it sits; all same-from
281
+ # duplicates go (the ruleset's internal last-wins winner is
282
+ # superseded too). From-equality is computed on the node's
283
+ # structural inspect — for plain strings this is exactly the
284
+ # byte-exact string of the corpus convention.
285
+ # 2. Kept rows keep their order and block structure (§4.2.2 with
286
+ # the §3.2 pass-structure inheritance).
287
+ # 3. The override rows land at the expansion tail: appended INTO
288
+ # the final parallel block when the expansion ends with one, so
289
+ # the child rows join that block's single longest-match pass
290
+ # (§4.2.5, §4.3 — replacement on the source-script pass, not
291
+ # post-pass re-substitution); otherwise appended as trailing
292
+ # rows. Unmatched overrides are simply added (§4.2.4).
293
+ def apply_overrides(children, overrides)
294
+ return children if overrides.nil? || overrides.empty?
295
+
296
+ o_rules = overrides.map { |r| build_rule(r) }
297
+ o_keys = o_rules.map { |r| from_key(r) }
298
+
299
+ kept = children.filter_map do |node|
300
+ case node
301
+ when Interscript::Node::Group::Parallel
302
+ node.children.reject! { |c| c.is_a?(Interscript::Node::Rule::Sub) && o_keys.include?(from_key(c)) }
303
+ node
304
+ when Interscript::Node::Rule::Sub
305
+ o_keys.include?(from_key(node)) ? nil : node
306
+ else
307
+ node
308
+ end
309
+ end
310
+
311
+ tail = kept.last
312
+ if tail.is_a?(Interscript::Node::Group::Parallel)
313
+ tail.children.push(*o_rules)
314
+ else
315
+ kept.push(*o_rules)
316
+ end
317
+ kept
318
+ end
319
+
320
+ def from_key(rule)
321
+ rule.from.inspect
162
322
  end
163
323
 
164
324
  def build_rule(rule_def)
@@ -170,6 +330,9 @@ module Interscript
170
330
  constraint = rule_def[:constraints].find { |c| c[:kind] == k }
171
331
  opts[k] = convert_item(constraint[:item], :class)
172
332
  end
333
+ # ISC-WF-1 W3: the language guard rides on the rule; inert unless
334
+ # the interpreter selects a declared language.
335
+ opts[:when_languages] = rule_def[:when_languages] if rule_def[:when_languages]&.any?
173
336
  Interscript::Node::Rule::Sub.new(from, to, **opts)
174
337
  end
175
338
 
@@ -69,6 +69,16 @@ module Interscript
69
69
  {tests: merge_tests(items)}
70
70
  elsif block.key?(:stage)
71
71
  {stage_name: scalar(block[:stage_name]), stage: normalize_stage_items(block[:stage])}
72
+ elsif block.key?(:ruleset)
73
+ # ISC-WF-1 W1: same shape as a stage, under the ruleset key.
74
+ {ruleset_name: scalar(block[:ruleset_name]), ruleset: normalize_stage_items(block[:ruleset])}
75
+ elsif block.key?(:languages)
76
+ # ISC-WF-1 W3: `languages a, b` folds to a plain list of names.
77
+ # One language captures as a single {language:} hash, several as
78
+ # a list.
79
+ langs = block[:languages]
80
+ list = langs.is_a?(Hash) ? [langs] : Array(langs)
81
+ {languages: list.map { |l| l.is_a?(Hash) ? scalar(l[:language]) : scalar(l) }}
72
82
  elsif block.key?(:target)
73
83
  {target: scalar(block[:target]), alias: scalar(block[:alias])}.compact
74
84
  else
@@ -85,6 +95,17 @@ module Interscript
85
95
  %i[parallel sequence].each do |k|
86
96
  item = {k => []} if item.key?(k) && !item[k].is_a?(Array)
87
97
  end
98
+ # ISC-WF-1 W1/W2: `use` items get their reference identifiers
99
+ # folded to strings; the override rules keep their parslet
100
+ # shape (DocumentBuilder materializes them through Transform).
101
+ if item.key?(:use) && item[:use].is_a?(Hash)
102
+ use = item[:use]
103
+ item = {use: {
104
+ use_alias: use[:use_alias] && scalar(use[:use_alias]),
105
+ qualified_name: use[:qualified_name] && scalar(use[:qualified_name]),
106
+ use_overrides: use[:use_overrides]
107
+ }.compact}
108
+ end
88
109
  end
89
110
  item
90
111
  end
@@ -21,10 +21,12 @@ module Interscript
21
21
 
22
22
  def serialize
23
23
  emit_system_open
24
+ emit_languages if @doc[:languages]&.any?
24
25
  emit_metadata if @doc[:metadata]&.any?
25
26
  emit_tests if @doc[:tests]&.any?
26
27
  emit_aliases if @doc[:aliases]&.any?
27
28
  emit_dependencies if @doc[:dependencies]&.any?
29
+ emit_rulesets
28
30
  emit_stages
29
31
  emit_system_close
30
32
  @out
@@ -40,6 +42,21 @@ module Interscript
40
42
  @out << "}\n"
41
43
  end
42
44
 
45
+ # ISC-WF-1 W3: the map's declared languages.
46
+ def emit_languages
47
+ @out << "\nlanguages #{@doc[:languages].join(" ")}\n"
48
+ end
49
+
50
+ # ISC-WF-1 W1: named rulesets, emitted ahead of the stages that
51
+ # reference them.
52
+ def emit_rulesets
53
+ @doc[:rulesets]&.each do |rs|
54
+ @out << "\nruleset #{rs[:name]} {\n"
55
+ rs[:body].each { |item| emit_stage_item(item) }
56
+ @out << "}\n"
57
+ end
58
+ end
59
+
43
60
  def emit_blank
44
61
  @out << "\n"
45
62
  end
@@ -157,6 +174,19 @@ module Interscript
157
174
  @out << " }\n"
158
175
  when :bare_rule
159
176
  emit_rule(item[:rule], 2)
177
+ when :use
178
+ # ISC-WF-1 W1/W2: `use <dep>.<name> [override { rows }]` — the
179
+ # override block is part of the use directive (one syntax, one
180
+ # site, spec §4.1).
181
+ ref = item[:dependency] ? "#{item[:dependency]}.#{item[:ruleset]}" : item[:ruleset]
182
+ overrides = item[:overrides]
183
+ if overrides&.any?
184
+ @out << " use #{ref} override {\n"
185
+ overrides.each { |r| emit_rule(r, 4) }
186
+ @out << " }\n"
187
+ else
188
+ @out << " use #{ref}\n"
189
+ end
160
190
  when :run
161
191
  dep = item[:dependency]
162
192
  stage = item[:stage]
@@ -179,6 +209,8 @@ module Interscript
179
209
  pad = " " * indent
180
210
  from_str = emit_item(rule[:from])
181
211
  to_str = emit_item(rule[:to])
212
+ # ISC-WF-1 W3: the language guard trails the constraints.
213
+ when_str = rule[:when_languages]&.any? ? "when #{rule[:when_languages].join(" ")}" : nil
182
214
 
183
215
  needs_block = rule[:from].is_a?(Items::Concat) || rule[:to].is_a?(Items::Concat)
184
216
 
@@ -188,11 +220,13 @@ module Interscript
188
220
  @out << "#{inner}from #{from_str}\n"
189
221
  @out << "#{inner}to #{to_str}\n"
190
222
  rule[:constraints]&.each { |c| @out << "#{inner}#{emit_constraint(c)}\n" }
223
+ @out << "#{inner}#{when_str}\n" if when_str
191
224
  @out << "#{pad}}\n"
192
225
  else
193
226
  constraints_str = rule[:constraints]&.map { |c| emit_constraint(c) }&.join(" ") || ""
194
227
  @out << "#{pad}sub #{from_str} #{to_str}"
195
228
  @out << " #{constraints_str}" unless constraints_str.empty?
229
+ @out << " #{when_str}" if when_str
196
230
  @out << "\n"
197
231
  end
198
232
  end
@@ -133,6 +133,21 @@ module Interscript
133
133
  rule(not_before: subtree(:x)) { {kind: :not_before, item: x} }
134
134
  rule(not_after: subtree(:x)) { {kind: :not_after, item: x} }
135
135
  rule(constraints: sequence(:c)) { c }
136
+
137
+ # ISC-WF-1 W3: a `when` guard folds to the plain list of language
138
+ # names. A single language captures as one {when_lang:} hash,
139
+ # several as a list; identifier leaves may still be raw slices at
140
+ # this point, so decode them here.
141
+ rule(when: subtree(:langs)) do
142
+ list = langs.is_a?(Hash) ? [langs] : Array(langs)
143
+ list.map do |l|
144
+ lang = l.is_a?(Hash) ? l[:when_lang] : l
145
+ case lang
146
+ when Hash then lang[:identifier].to_s
147
+ else lang.to_s
148
+ end
149
+ end.compact
150
+ end
136
151
  end
137
152
  end
138
153
  end
@@ -31,6 +31,8 @@ module Interscript
31
31
  tests: (doc_hash[:tests] || []).map { |t| Model::Test.new(t.transform_keys(&:to_s)) },
32
32
  aliases: (doc_hash[:aliases] || []).map { |a| build_alias_model(a) },
33
33
  stages: (doc_hash[:stages] || []).map { |s| build_stage_model(s) },
34
+ rulesets: (doc_hash[:rulesets] || []).map { |r| build_ruleset_model(r) },
35
+ languages: doc_hash[:languages],
34
36
  dependencies: (doc_hash[:dependencies] || []).map { |d| build_dependency_model(d) }
35
37
  )
36
38
  end
@@ -63,6 +65,13 @@ module Interscript
63
65
  )
64
66
  end
65
67
 
68
+ def build_ruleset_model(ruleset_hash)
69
+ Model::Ruleset.new(
70
+ name: ruleset_hash[:name],
71
+ body: (ruleset_hash[:body] || []).map { |item| stage_item_to_model(item) }
72
+ )
73
+ end
74
+
66
75
  def build_dependency_model(dep_hash)
67
76
  Model::Dependency.new(
68
77
  target: dep_hash[:target],
@@ -80,6 +89,11 @@ module Interscript
80
89
  when :run
81
90
  attrs[:dependency] = item[:dependency]
82
91
  attrs[:stage] = item[:stage]
92
+ when :use
93
+ # ISC-WF-1 W1/W2
94
+ attrs[:dependency] = item[:dependency]
95
+ attrs[:ruleset] = item[:ruleset]
96
+ attrs[:overrides] = (item[:overrides] || []).map { |r| rule_to_model(r) }
83
97
  when :separate
84
98
  attrs[:separator] = item[:separator] ? item_to_model(item[:separator]) : nil
85
99
  when :string_case
@@ -92,7 +106,8 @@ module Interscript
92
106
  Model::Rule.new(
93
107
  from: item_to_model(rule_hash[:from]),
94
108
  to: item_to_model(rule_hash[:to]),
95
- constraints: (rule_hash[:constraints] || []).map { |c| constraint_to_model(c) }
109
+ constraints: (rule_hash[:constraints] || []).map { |c| constraint_to_model(c) },
110
+ when_languages: rule_hash[:when_languages]
96
111
  )
97
112
  end
98
113
 
@@ -140,7 +155,7 @@ module Interscript
140
155
  # --- Reverse direction: model → document hash ---
141
156
 
142
157
  def model_to_hash(model)
143
- {
158
+ hash = {
144
159
  schemaVersion: 1,
145
160
  systemCode: model.system_code,
146
161
  metadata: model_metadata_to_hash(model.metadata),
@@ -149,6 +164,11 @@ module Interscript
149
164
  stages: (model.stages || []).map { |s| stage_model_to_hash(s) },
150
165
  dependencies: (model.dependencies || []).map { |d| {target: d.target, alias: d.alias_name}.compact }
151
166
  }
167
+ # ISC-WF-1 keys round-trip only when present — pre-WF1 documents
168
+ # keep their exact hash shape.
169
+ hash[:languages] = model.languages if model.languages&.any?
170
+ hash[:rulesets] = (model.rulesets || []).map { |r| ruleset_model_to_hash(r) } if model.rulesets&.any?
171
+ hash
152
172
  end
153
173
 
154
174
  def model_metadata_to_hash(meta)
@@ -171,6 +191,11 @@ module Interscript
171
191
  {kind: :bare_rule, rule: rule_model_to_hash(item.rule)}
172
192
  when "run"
173
193
  {kind: :run, dependency: item.dependency, stage: item.stage}
194
+ when "use"
195
+ # ISC-WF-1 W1/W2
196
+ hash = {kind: :use, ruleset: item.ruleset, dependency: item.dependency}
197
+ hash[:overrides] = (item.overrides || []).map { |r| rule_model_to_hash(r) } if item.overrides&.any?
198
+ hash.compact
174
199
  when "separate"
175
200
  {kind: :separate, separator: item.separator ? model_to_item(item.separator) : nil}
176
201
  when "compose"
@@ -182,12 +207,21 @@ module Interscript
182
207
  end
183
208
  end
184
209
 
185
- def rule_model_to_hash(rule_model)
210
+ def ruleset_model_to_hash(ruleset_model)
186
211
  {
212
+ name: ruleset_model.name,
213
+ body: ruleset_model.body.map { |item| stage_item_model_to_hash(item) }
214
+ }
215
+ end
216
+
217
+ def rule_model_to_hash(rule_model)
218
+ hash = {
187
219
  from: model_to_item(rule_model.from),
188
220
  to: model_to_item(rule_model.to),
189
221
  constraints: (rule_model.constraints || []).map { |c| constraint_model_to_hash(c) }
190
222
  }
223
+ hash[:when_languages] = rule_model.when_languages if rule_model.when_languages&.any?
224
+ hash
191
225
  end
192
226
 
193
227
  def constraint_model_to_hash(constraint_model)
@@ -2,6 +2,9 @@ class Interscript::Node::Document
2
2
  attr_accessor :metadata, :tests, :name
3
3
  attr_accessor :dependencies, :aliases, :stages, :dep_aliases
4
4
  attr_accessor :reversed_from
5
+ # ISC-WF-1: map-declared languages (LanguagesDecl or nil) and the named
6
+ # rulesets available to `use` expansion (name => Node::Ruleset).
7
+ attr_accessor :languages, :rulesets
5
8
 
6
9
  def initialize
7
10
  puts "Interscript::Node::Document.new " if $DEBUG
@@ -11,6 +14,8 @@ class Interscript::Node::Document
11
14
  @dep_aliases = {}
12
15
  @aliases = {}
13
16
  @stages = {}
17
+ @languages = nil
18
+ @rulesets = {}
14
19
  end
15
20
 
16
21
  def imported_aliases
@@ -69,11 +74,18 @@ class Interscript::Node::Document
69
74
  end
70
75
 
71
76
  def to_hash
72
- {class: self.class.to_s, metadata: @metadata&.to_hash,
73
- tests: @tests&.to_hash,
74
- dependencies: @dependencies.map { |x| x.to_hash },
75
- dep_aliases: @dep_aliases.transform_values(&:to_hash),
76
- aliases: @aliases.transform_values(&:to_hash),
77
- stages: @stages.transform_values(&:to_hash)}
77
+ hash = {
78
+ class: self.class.to_s, metadata: @metadata&.to_hash,
79
+ tests: @tests&.to_hash,
80
+ dependencies: @dependencies.map { |x| x.to_hash },
81
+ dep_aliases: @dep_aliases.transform_values(&:to_hash),
82
+ aliases: @aliases.transform_values(&:to_hash),
83
+ stages: @stages.transform_values(&:to_hash)
84
+ }
85
+ # ISC-WF-1 keys appear only on maps that use the constructs — the IR
86
+ # of every pre-WF1 map stays byte-identical.
87
+ hash[:languages] = @languages.to_hash if @languages
88
+ hash[:rulesets] = @rulesets.transform_values(&:to_hash) if @rulesets&.any?
89
+ hash
78
90
  end
79
91
  end
@@ -2,6 +2,10 @@ class Interscript::Node::Group::Parallel < Interscript::Node::Group
2
2
  # A place for Interpreter to store a compiled form of the tree
3
3
  attr_accessor :cached_tree
4
4
  attr_accessor :subs_regexp, :subs_replacements, :subs_array
5
+ # ISC-WF-1 W3: per-selected-language filtered variants of this pass
6
+ # (selected language => parallel group). Only populated for passes
7
+ # that contain `when`-guarded rules.
8
+ attr_accessor :wf1_variants
5
9
 
6
10
  def inspect
7
11
  "parallel {\n#{super}\n}"
@@ -0,0 +1,26 @@
1
+ class Interscript::Node::LanguagesDecl < Interscript::Node
2
+ # ISC-WF-1 W3: the languages a map serves (spec §5.1). Attached to
3
+ # Node::Document#languages; nil means "no declaration" — every `when`
4
+ # guard is inert regardless of caller selection.
5
+ attr_accessor :languages
6
+
7
+ def initialize(languages = [])
8
+ self.languages = languages
9
+ end
10
+
11
+ def include?(language)
12
+ languages.include?(language)
13
+ end
14
+
15
+ def to_hash
16
+ {class: self.class.to_s, languages: languages}
17
+ end
18
+
19
+ def ==(other)
20
+ super && languages == other.languages
21
+ end
22
+
23
+ def inspect
24
+ "languages #{languages.join(" ")}"
25
+ end
26
+ end
@@ -4,11 +4,14 @@ class Interscript::Node::Rule::Sub < Interscript::Node::Rule
4
4
  attr_accessor :reverse_before, :reverse_not_before, :reverse_after, :reverse_not_after
5
5
  attr_accessor :reverse_run
6
6
  attr_accessor :priority
7
+ # ISC-WF-1 W3: the language guard (`when kur, chv`). nil = unguarded.
8
+ attr_accessor :when_languages
7
9
 
8
10
  def initialize(from, to,
9
11
  before: nil, not_before: nil,
10
12
  after: nil, not_after: nil,
11
- priority: nil, reverse_run: nil)
13
+ priority: nil, reverse_run: nil,
14
+ when_languages: nil)
12
15
  self.from = Interscript::Node::Item.try_convert from
13
16
  self.to = if to == :upcase
14
17
  :upcase
@@ -19,6 +22,7 @@ class Interscript::Node::Rule::Sub < Interscript::Node::Rule
19
22
  end
20
23
 
21
24
  self.priority = priority
25
+ self.when_languages = when_languages&.map(&:to_sym)
22
26
 
23
27
  # raise TypeError, "Can't supply both before and not_before" if before && not_before
24
28
  # raise TypeError, "Can't supply both after and not_after" if after && not_after
@@ -59,6 +63,7 @@ class Interscript::Node::Rule::Sub < Interscript::Node::Rule
59
63
  hash[:after] = after&.to_hash if after
60
64
  hash[:not_after] = not_after&.to_hash if not_after
61
65
  hash[:priority] = priority if priority
66
+ hash[:when_languages] = when_languages if when_languages
62
67
 
63
68
  hash
64
69
  end
@@ -91,7 +96,8 @@ class Interscript::Node::Rule::Sub < Interscript::Node::Rule
91
96
 
92
97
  reverse_run: rrun,
93
98
 
94
- priority: priority ? -priority : nil)
99
+ priority: priority ? -priority : nil,
100
+ when_languages: when_languages)
95
101
  end
96
102
 
97
103
  def has_assertions?
@@ -202,7 +208,8 @@ class Interscript::Node::Rule::Sub < Interscript::Node::Rule
202
208
  after == other.after &&
203
209
  not_before == other.not_before &&
204
210
  not_after == other.not_after &&
205
- priority == other.priority
211
+ priority == other.priority &&
212
+ when_languages == other.when_languages
206
213
  end
207
214
 
208
215
  def inspect
@@ -222,6 +229,7 @@ class Interscript::Node::Rule::Sub < Interscript::Node::Rule
222
229
  params << "not_after: #{@not_after.inspect}" if @not_after
223
230
 
224
231
  params << "priority: #{@priority.inspect}" if @priority
232
+ params << "when: #{@when_languages.join(", ")}" if @when_languages
225
233
  out << params.join(", ")
226
234
  end
227
235
  # standard:disable Layout/TrailingEmptyLines (deliberate)
@@ -0,0 +1,31 @@
1
+ class Interscript::Node::Rule::Use < Interscript::Node::Rule
2
+ # ISC-WF-1 W1/W2: a `use` directive before expansion. The NodeAdapter
3
+ # expands use directives at build time (macro expansion, spec §3.2), so
4
+ # this node only appears on documents built without expansion; it names
5
+ # the ruleset and, when present, the dependency alias and the per-site
6
+ # override rows.
7
+ attr_accessor :ruleset, :map, :overrides
8
+
9
+ def initialize(ruleset, map: nil, overrides: [])
10
+ self.ruleset = ruleset
11
+ self.map = map
12
+ self.overrides = overrides
13
+ end
14
+
15
+ def to_hash
16
+ {class: self.class.to_s,
17
+ ruleset: ruleset,
18
+ map: map,
19
+ overrides: overrides.map(&:to_hash)}
20
+ end
21
+
22
+ def ==(other)
23
+ super && ruleset == other.ruleset && map == other.map && overrides == other.overrides
24
+ end
25
+
26
+ def inspect
27
+ out = "use #{[map, ruleset].compact.map { |i| "@#{i}" }.join(".")}"
28
+ out += " override { #{overrides.map(&:inspect).join(", ")} }" unless overrides.empty?
29
+ out
30
+ end
31
+ end
@@ -3,6 +3,7 @@ class Interscript::Node::Rule < Interscript::Node
3
3
  autoload :Sub, "interscript/node/rule/sub"
4
4
  autoload :Run, "interscript/node/rule/run"
5
5
  autoload :Funcall, "interscript/node/rule/funcall"
6
+ autoload :Use, "interscript/node/rule/use"
6
7
 
7
8
  def ==(other)
8
9
  super && reverse_run == other.reverse_run