Skip to content

Commit 1e504dc

Browse files
committed
isc: implement ISC-WF-1 — rulesets, use-with-override, language conditioning
The workflow-extension layer per docs/isc-wf-1.md (maps repo, normative draft 1.0): W1 named rulesets with in-place macro expansion (cycles are load errors), W2 use-with-override merging (child-wins on exact from-equality within one pass), W3 language conditioning (map-declared languages, caller-selected, guard-applicable rules beat unguarded same-from rules, all guards inert otherwise). Grammar: ruleset/use/override/languages/when join the keyword table and the stage/system productions; the .parg port is updated in step. The NodeAdapter expands use sites at load time with per-site deep copies; the Interpreter threads a language through runs and filters parallel passes per selected language (variants memoized per language so the compiled-tree caches stay isolated). Node layer gains Ruleset, Use, LanguagesDecl and Sub#when_languages; the document hash, Lutaml models, YamlBridge and Serializer round-trip the new constructs. Additive-only: the golden-capture gate (11,048 rows over the 404-map corpus) is byte-identical before and after; the full corpus parses 408/408 on both sides. The harness lives in tools/isc_wf1_golden_gate.rb.
1 parent 64f30b4 commit 1e504dc

32 files changed

Lines changed: 1557 additions & 62 deletions

‎lib/interscript.rb‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,22 @@ def load(system_code, maps = {}, compiler: Interscript::Interpreter)
5050
end
5151

5252
# Transliterates the string.
53-
def transliterate(system_code, string, maps = {}, compiler: Interscript::Interpreter)
53+
#
54+
# ISC-WF-1 W3: `language:` selects one of the map's declared
55+
# `languages`, activating the matching `when`-guarded rules. A
56+
# selection outside the declaration (or no selection) leaves every
57+
# guard inert (spec §5.1).
58+
def transliterate(system_code, string, maps = {}, compiler: Interscript::Interpreter, language: nil)
5459
# The current best implementation is Interpreter
55-
load(system_code, maps, compiler: compiler).call(string)
60+
compiled = load(system_code, maps, compiler: compiler)
61+
if language.nil?
62+
compiled.call(string)
63+
elsif compiler.equal?(Interscript::Interpreter)
64+
compiled.call(string, language: language)
65+
else
66+
raise ArgumentError,
67+
"language selection is only supported by Interscript::Interpreter, not #{compiler}"
68+
end
5669
end
5770

5871
# Gives each possible value of the transliteration.

‎lib/interscript/interpreter.rb‎

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ def compile(map, _: nil)
55
self
66
end
77

8-
def call(str, stage = :main, each: false, &block)
8+
def call(str, stage = :main, each: false, language: nil, &block)
99
stage = @map.stages[stage]
1010
if each
1111
e = Enumerator.new do |yielder|
@@ -22,7 +22,7 @@ def call(str, stage = :main, each: false, &block)
2222
# standard:disable Style/GlobalVars (deliberate $DEBUG / -d-flag debug idiom)
2323
$select_nth_string = true
2424
# standard:enable Style/GlobalVars
25-
result = Stage.new(@map, str).execute_rule(stage)
25+
result = Stage.new(@map, str, language: language).execute_rule(stage)
2626
# standard:disable Style/GlobalVars (deliberate $DEBUG / -d-flag debug idiom)
2727
$select_nth_string = false
2828
# standard:enable Style/GlobalVars
@@ -68,20 +68,72 @@ def call(str, stage = :main, each: false, &block)
6868
e
6969
end
7070
else
71-
Stage.new(@map, str).execute_rule(stage)
71+
Stage.new(@map, str, language: language).execute_rule(stage)
7272
end
7373
end
7474

75+
# ISC-WF-1 W3: effective language for a run. A selection outside the
76+
# map's `languages` declaration (or any selection on a map without a
77+
# declaration) means every `when` guard is inert (spec §5.1).
78+
def self.select_language(map, language)
79+
return nil if language.nil?
80+
81+
declared = map.languages
82+
return nil unless declared.is_a?(Interscript::Node::LanguagesDecl) &&
83+
declared.include?(language.to_sym)
84+
85+
language.to_sym
86+
end
87+
7588
class Stage
76-
def initialize(map, str)
89+
attr_reader :language
90+
91+
def initialize(map, str, language: nil)
7792
@str = str
7893
@map = map
94+
@language = Interscript::Interpreter.select_language(map, language)
95+
end
96+
97+
# ISC-WF-1 W3 guard applicability. Unguarded rules always apply; a
98+
# guarded rule applies only under a selected language that its `when`
99+
# list contains. Inert rules are absent from the pass (spec §5.1).
100+
def guard_applies? rule
101+
return true unless Interscript::Node::Rule::Sub === rule && rule.when_languages
102+
return false unless @language
103+
104+
rule.when_languages.include?(@language)
105+
end
106+
107+
# ISC-WF-1 W3: the effective rule set of a parallel pass = unguarded
108+
# rules ∪ guard-applicable rules; where an unguarded rule and a
109+
# guard-applicable rule share the same from-string, the
110+
# guard-applicable rule wins (spec §5.2). Passes without any guard
111+
# return the group untouched — zero change for pre-WF1 maps. Filtered
112+
# variants are memoized per selected language on the group so the
113+
# compiled-tree cache below stays per-language.
114+
def resolve_parallel_guards r
115+
return r unless r.children.any? { |c| Interscript::Node::Rule::Sub === c && c.when_languages }
116+
117+
r.wf1_variants ||= {}
118+
r.wf1_variants[@language] ||= begin
119+
applicable = r.children.select { |c| guard_applies?(c) }
120+
guarded_froms = applicable
121+
.select { |c| Interscript::Node::Rule::Sub === c && c.when_languages }
122+
.map { |c| c.from.inspect }
123+
filtered = Interscript::Node::Group::Parallel.new
124+
filtered.children = applicable.reject do |c|
125+
Interscript::Node::Rule::Sub === c && !c.when_languages &&
126+
guarded_froms.include?(c.from.inspect)
127+
end
128+
filtered
129+
end
79130
end
80131

81132
def execute_rule r
82133
return if r.reverse_run == true
83134
case r
84135
when Interscript::Node::Group::Parallel
136+
r = resolve_parallel_guards(r)
85137
if r.cached_tree
86138
@str = Interscript::Stdlib.parallel_replace_tree(@str, r.cached_tree)
87139
elsif r.subs_regexp && r.subs_replacements
@@ -135,9 +187,12 @@ def execute_rule r
135187
end
136188
when Interscript::Node::Group
137189
r.children.each do |t|
190+
# ISC-WF-1 W3: in sequence, an inert guarded rule is skipped.
191+
next unless guard_applies?(t)
138192
execute_rule(t)
139193
end
140194
when Interscript::Node::Rule::Sub
195+
return @str unless guard_applies?(r)
141196
@str = if r.to == :upcase
142197
@str.gsub(Regexp.new(build_regexp(r)), &:upcase)
143198
elsif r.to == :downcase
@@ -151,10 +206,10 @@ def execute_rule r
151206
if r.stage.map
152207
doc = @map.dep_aliases[r.stage.map].document
153208
stage = doc.imported_stages[r.stage.name]
154-
@str = Stage.new(doc, @str).execute_rule(stage)
209+
@str = Stage.new(doc, @str, language: @language).execute_rule(stage)
155210
else
156211
stage = @map.imported_stages[r.stage.name]
157-
@str = Stage.new(@map, @str).execute_rule(stage)
212+
@str = Stage.new(@map, @str, language: @language).execute_rule(stage)
158213
end
159214
end
160215

‎lib/interscript/isc/document_builder.rb‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def build
3737
tests_arr = []
3838
stages_arr = []
3939
dependencies_arr = []
40+
rulesets_arr = []
41+
languages_arr = []
4042

4143
body.each do |item|
4244
if item[:metadata]
@@ -45,14 +47,20 @@ def build
4547
aliases_arr.concat(extract_aliases(item[:aliases]))
4648
elsif item[:tests]
4749
tests_arr.concat(extract_tests(item[:tests]))
50+
elsif item[:languages]
51+
# ISC-WF-1 W3: map-declared languages.
52+
languages_arr.concat(Array(item[:languages]).map { |l| ident(l) })
53+
elsif item[:ruleset]
54+
# ISC-WF-1 W1: named ruleset declaration.
55+
rulesets_arr << extract_ruleset(item)
4856
elsif item[:stage]
4957
stages_arr << extract_stage(item)
5058
elsif item[:target]
5159
dependencies_arr << extract_dependency(item)
5260
end
5361
end
5462

55-
{
63+
doc = {
5664
schemaVersion: SCHEMA_VERSION,
5765
filename: @filename,
5866
systemCode: code,
@@ -63,6 +71,11 @@ def build
6371
stages: stages_arr,
6472
dependencies: dependencies_arr
6573
}
74+
# ISC-WF-1 keys are present only when the map uses the constructs —
75+
# documents of maps that don't keep their exact pre-WF1 shape.
76+
doc[:languages] = languages_arr if languages_arr.any?
77+
doc[:rulesets] = rulesets_arr if rulesets_arr.any?
78+
doc
6679
end
6780

6881
private
@@ -254,6 +267,15 @@ def extract_stage(item)
254267
{name: name, body: body}
255268
end
256269

270+
# ISC-WF-1 W1: a named ruleset declaration — same body shape as a
271+
# stage, addressed by name through `use` expansion.
272+
def extract_ruleset(item)
273+
node = item[:ruleset]
274+
name = ident(item[:ruleset_name])
275+
body = Array(node).flat_map { |n| extract_stage_items(n) }
276+
{name: name, body: body}
277+
end
278+
257279
def extract_stage_items(n)
258280
return [] unless n.is_a?(Hash)
259281
if n[:sequence]
@@ -279,6 +301,10 @@ def extract_stage_items(n)
279301
[{kind: :run, dependency: nil, stage: ident(n[:run_stage_only][:stage])}]
280302
elsif n[:bare_rule]
281303
[{kind: :bare_rule, rule: extract_rule(n[:bare_rule])}]
304+
elsif n[:use]
305+
# ISC-WF-1 W1/W2: `use <ident>` or `use <alias>.<ident>` with an
306+
# optional override block.
307+
[extract_use(n[:use])]
282308
elsif n[:comment]
283309
[]
284310
elsif n[:noop]
@@ -288,6 +314,19 @@ def extract_stage_items(n)
288314
end
289315
end
290316

317+
# `use <name>` (same-map ruleset) or `use <dep>.<name>` (dependency-
318+
# declared ruleset), plus the optional override rows (W2).
319+
def extract_use(u)
320+
dep = u[:qualified_name] ? ident(u[:use_alias]) : nil
321+
name = ident(u[:qualified_name] || u[:use_alias])
322+
item = {kind: :use, ruleset: name, dependency: dep}
323+
overrides = Array(u[:use_overrides])
324+
.select { |r| r.is_a?(Hash) && r.key?(:from) }
325+
.map { |r| extract_rule(r) }
326+
item[:overrides] = overrides if overrides.any?
327+
item
328+
end
329+
291330
# Filter out noop rules (from: None, to: None) created by empty
292331
# parallel/sequence blocks that contain only comments or whitespace.
293332
def filter_noop(rules)
@@ -302,11 +341,17 @@ def extract_rule(r)
302341
from_val = r.is_a?(Hash) ? r[:from] : nil
303342
to_val = r.is_a?(Hash) ? r[:to] : nil
304343
constraints_val = r.is_a?(Hash) ? r[:constraints] : nil
305-
{
344+
rule = {
306345
from: from_val ? materialize(from_val) : Items::None.new,
307346
to: to_val ? materialize(to_val) : Items::None.new,
308347
constraints: extract_constraints(constraints_val)
309348
}
349+
# ISC-WF-1 W3: a `when` guard folds to the list of language names.
350+
# The rule hash never goes through Transform as a whole (from/to/
351+
# constraints are materialized individually), so the guard fragment
352+
# gets its own apply.
353+
rule[:when_languages] = @transform.apply(when: r[:when]) if r.is_a?(Hash) && r[:when]
354+
rule
310355
end
311356

312357
def extract_constraints(arr)

‎lib/interscript/isc/grammar/concerns/items.rb‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,14 @@ module Items
125125
str("line_end") | str("word_boundary") |
126126
str("downcase") | str("upcase") | str("title_case") |
127127
str("capture") | str("maybe") | str("some") | str("ref") |
128-
str("any_character")
128+
str("any_character") |
129+
# ISC-WF-1: ruleset / use / override / languages / when join
130+
# the keyword table (spec §2). Prefix matching follows the
131+
# existing table behavior; quoted-string forms remain the
132+
# pass-through escape hatch. Verified against the 404-map
133+
# corpus: no bare identifier starts with any of these words.
134+
str("ruleset") | str("use") | str("override") |
135+
str("languages") | str("when")
129136
end
130137

131138
# Concatenation: one or more atoms. The continuation pattern

‎lib/interscript/isc/grammar/concerns/stages.rb‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,44 @@ module Stages
1717
braced(stage_item.repeat(0)).as(:stage)
1818
end
1919

20+
# ISC-WF-1 W1: a named, reusable block of stage items. Declared at
21+
# map top level anywhere a stage may appear; never executes on its
22+
# own (only `use` expansion reaches the runtime).
23+
rule(:ruleset_block) do
24+
str("ruleset") >> whitespace >>
25+
identifier.as(:ruleset_name) >> whitespace? >>
26+
braced(stage_item.repeat(0)).as(:ruleset)
27+
end
28+
29+
# ISC-WF-1 W1/W2: `use <ident>` (same-map ruleset) or
30+
# `use <alias>.<ident>` (dependency-declared ruleset), with an
31+
# optional override block of full replacement rows (child-wins on
32+
# exact from-equality).
33+
rule(:use_directive) do
34+
(str("use") >> whitespace >>
35+
identifier.as(:use_alias) >>
36+
(str(".") >> identifier.as(:qualified_name)).maybe >>
37+
(whitespace >> str("override") >> whitespace? >>
38+
braced(rule_line.repeat(0)).as(:use_overrides)).maybe
39+
).as(:use)
40+
end
41+
42+
# ISC-WF-1 W3: `when <ident> (","|ws <ident>)*` — a language guard
43+
# on a sub rule. Inert unless the caller selects a language the
44+
# map declares (spec §5.1). Identifiers are keyword-guarded so the
45+
# repeat stops at the next rule instead of swallowing it.
46+
rule(:when_guard) do
47+
(whitespace >> str("when") >> whitespace >>
48+
keyword.absent? >> identifier.as(:when_lang) >>
49+
((comma | whitespace) >> (keyword.absent? >> identifier.as(:when_lang))).repeat
50+
).as(:when)
51+
end
52+
2053
rule(:stage_item) do
2154
whitespace? >>
2255
(sequence_block | parallel_block | run_rule | separate_directive |
2356
string_case_directive | compose_directive | rababa_directive |
24-
bare_rule | comment_item) >>
57+
use_directive | bare_rule | comment_item) >>
2558
whitespace?
2659
end
2760

@@ -105,7 +138,8 @@ module Stages
105138
str("sub") >> whitespace >>
106139
item_atom.as(:from) >> whitespace >>
107140
item_atom.as(:to) >>
108-
constraints
141+
constraints >>
142+
when_guard.maybe
109143
end
110144

111145
rule(:block_rule) do
@@ -114,6 +148,7 @@ module Stages
114148
str("from") >> whitespace >> item.as(:from) >> whitespace? >>
115149
str("to") >> whitespace >> item.as(:to) >>
116150
(whitespace >> constraint).repeat.as(:constraints) >>
151+
when_guard.maybe >>
117152
whitespace? >> str("}")
118153
end
119154

‎lib/interscript/isc/grammar/concerns/system.rb‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@ module Concerns
1010
module System
1111
include Parsanol
1212

13+
# ISC-WF-1 W3: the languages a map serves, declared once per map
14+
# inside the system block alongside metadata (spec §5.1). List
15+
# identifiers are keyword-guarded so the repeat stops at the next
16+
# block item (`stage`, `ruleset`, …) instead of swallowing it.
17+
rule(:languages_decl) do
18+
str("languages") >> whitespace >>
19+
(keyword.absent? >> identifier.as(:language) >>
20+
((comma | whitespace) >> (keyword.absent? >> identifier.as(:language))).repeat
21+
).as(:languages)
22+
end
23+
1324
# A block-item is any of the top-level constructs that may appear
1425
# inside `system "<code>" { ... }`.
1526
rule(:block_item) do
1627
whitespace? >>
17-
(metadata_block | aliases_block | tests_block |
18-
stage_block | dependency_decl) >>
28+
(metadata_block | aliases_block | tests_block | languages_decl |
29+
ruleset_block | stage_block | dependency_decl) >>
1930
whitespace?
2031
end
2132

0 commit comments

Comments
 (0)