interscript 2.4.5 → 3.0.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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -0
  3. data/docs/demo/20191118-interscript-demo-cast.gif +0 -0
  4. data/exe/codemod-imp-to-isc +5 -0
  5. data/exe/diagnose_parse_failures +47 -0
  6. data/exe/interscript +2 -1
  7. data/exe/verify_isc_deep +211 -0
  8. data/exe/verify_isc_equivalence +128 -0
  9. data/interscript.gemspec +27 -20
  10. data/lib/interscript/command.rb +21 -12
  11. data/lib/interscript/compiler/javascript.rb +34 -35
  12. data/lib/interscript/compiler/json_ir.rb +214 -0
  13. data/lib/interscript/compiler/python.rb +354 -0
  14. data/lib/interscript/compiler/ruby.rb +23 -22
  15. data/lib/interscript/compiler.rb +20 -3
  16. data/lib/interscript/detector.rb +14 -7
  17. data/lib/interscript/dsl/aliases.rb +1 -1
  18. data/lib/interscript/dsl/document.rb +6 -3
  19. data/lib/interscript/dsl/group/parallel.rb +1 -1
  20. data/lib/interscript/dsl/group.rb +18 -9
  21. data/lib/interscript/dsl/items.rb +28 -16
  22. data/lib/interscript/dsl/metadata.rb +19 -18
  23. data/lib/interscript/dsl/stage.rb +1 -1
  24. data/lib/interscript/dsl/symbol_mm.rb +4 -2
  25. data/lib/interscript/dsl/tests.rb +1 -1
  26. data/lib/interscript/dsl.rb +21 -13
  27. data/lib/interscript/interpreter.rb +32 -21
  28. data/lib/interscript/isc/codemod.rb +791 -0
  29. data/lib/interscript/isc/document_builder.rb +354 -0
  30. data/lib/interscript/isc/generator.rb +191 -0
  31. data/lib/interscript/isc/grammar/concerns/aliases.rb +29 -0
  32. data/lib/interscript/isc/grammar/concerns/dependencies.rb +23 -0
  33. data/lib/interscript/isc/grammar/concerns/items.rb +176 -0
  34. data/lib/interscript/isc/grammar/concerns/metadata.rb +142 -0
  35. data/lib/interscript/isc/grammar/concerns/primitives.rb +112 -0
  36. data/lib/interscript/isc/grammar/concerns/stages.rb +129 -0
  37. data/lib/interscript/isc/grammar/concerns/system.rb +37 -0
  38. data/lib/interscript/isc/grammar/concerns/tests.rb +31 -0
  39. data/lib/interscript/isc/grammar/concerns.rb +18 -0
  40. data/lib/interscript/isc/grammar/core.rb +23 -0
  41. data/lib/interscript/isc/grammar/isc.artifact.json +1 -0
  42. data/lib/interscript/isc/grammar/isc.parg +196 -0
  43. data/lib/interscript/isc/grammar.rb +12 -0
  44. data/lib/interscript/isc/items.rb +185 -0
  45. data/lib/interscript/isc/model/alias.rb +20 -0
  46. data/lib/interscript/isc/model/constraint.rb +20 -0
  47. data/lib/interscript/isc/model/dependency.rb +19 -0
  48. data/lib/interscript/isc/model/document.rb +31 -0
  49. data/lib/interscript/isc/model/item.rb +50 -0
  50. data/lib/interscript/isc/model/rule.rb +23 -0
  51. data/lib/interscript/isc/model/stage.rb +20 -0
  52. data/lib/interscript/isc/model/stage_item.rb +33 -0
  53. data/lib/interscript/isc/model/test.rb +21 -0
  54. data/lib/interscript/isc/model.rb +19 -0
  55. data/lib/interscript/isc/node_adapter.rb +310 -0
  56. data/lib/interscript/isc/normalizer.rb +127 -0
  57. data/lib/interscript/isc/parser.rb +78 -0
  58. data/lib/interscript/isc/serializer.rb +248 -0
  59. data/lib/interscript/isc/transform.rb +138 -0
  60. data/lib/interscript/isc/yaml_bridge.rb +236 -0
  61. data/lib/interscript/isc.rb +30 -0
  62. data/lib/interscript/ml/byt5_onnx.rb +102 -0
  63. data/lib/interscript/ml/imf.rb +182 -0
  64. data/lib/interscript/ml/model.rb +52 -0
  65. data/lib/interscript/ml/provisioning.rb +189 -0
  66. data/lib/interscript/ml/translator.rb +38 -0
  67. data/lib/interscript/ml/vocab.rb +48 -0
  68. data/lib/interscript/ml.rb +25 -0
  69. data/lib/interscript/node/alias_def.rb +5 -5
  70. data/lib/interscript/node/dependency.rb +7 -7
  71. data/lib/interscript/node/document.rb +10 -10
  72. data/lib/interscript/node/group.rb +11 -9
  73. data/lib/interscript/node/item/alias.rb +14 -10
  74. data/lib/interscript/node/item/any.rb +14 -7
  75. data/lib/interscript/node/item/capture.rb +14 -9
  76. data/lib/interscript/node/item/group.rb +22 -17
  77. data/lib/interscript/node/item/repeat.rb +3 -3
  78. data/lib/interscript/node/item/stage.rb +4 -5
  79. data/lib/interscript/node/item/string.rb +18 -13
  80. data/lib/interscript/node/item.rb +27 -17
  81. data/lib/interscript/node/metadata.rb +6 -5
  82. data/lib/interscript/node/rule/funcall.rb +4 -5
  83. data/lib/interscript/node/rule/run.rb +4 -5
  84. data/lib/interscript/node/rule/sub.rb +55 -55
  85. data/lib/interscript/node/rule.rb +6 -5
  86. data/lib/interscript/node/stage.rb +11 -14
  87. data/lib/interscript/node/tests.rb +6 -6
  88. data/lib/interscript/node.rb +14 -17
  89. data/lib/interscript/stdlib/functions/rababa_adapter.rb +56 -0
  90. data/lib/interscript/stdlib/functions/secryst_adapter.rb +37 -0
  91. data/lib/interscript/stdlib/functions.rb +49 -0
  92. data/lib/interscript/stdlib.rb +25 -95
  93. data/lib/interscript/utils/helpers.rb +8 -5
  94. data/lib/interscript/utils/regexp_converter.rb +143 -144
  95. data/lib/interscript/version.rb +1 -1
  96. data/lib/interscript/visualize/json.rb +12 -13
  97. data/lib/interscript/visualize/nodes.rb +13 -13
  98. data/lib/interscript/visualize.rb +18 -18
  99. data/lib/interscript.rb +59 -45
  100. metadata +88 -31
  101. data/.github/workflows/assets.yml +0 -91
  102. data/.github/workflows/rake.yml +0 -52
  103. data/.github/workflows/release.yml +0 -68
  104. data/.gitignore +0 -79
  105. data/.rspec +0 -3
  106. data/Gemfile +0 -38
  107. data/Rakefile +0 -139
  108. data/bin/console +0 -10
  109. data/bin/interscript +0 -5
  110. data/bin/maps_analyze_staging +0 -168
  111. data/bin/maps_debug_compilers +0 -58
  112. data/bin/maps_debug_ordering +0 -88
  113. data/bin/maps_debug_ruby_compile +0 -24
  114. data/bin/maps_debug_step_by_step +0 -44
  115. data/bin/maps_optimize_order +0 -112
  116. data/bin/maps_v1_analyze_regexps +0 -45
  117. data/bin/maps_v1_to_v2 +0 -426
  118. data/bin/set_version +0 -16
  119. data/bin/setup +0 -8
  120. data/requirements.txt +0 -1
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "interscript/isc/model/test"
5
+ require "interscript/isc/model/alias"
6
+ require "interscript/isc/model/stage"
7
+ require "interscript/isc/model/dependency"
8
+
9
+ module Interscript
10
+ module Isc
11
+ module Model
12
+ class Document < Lutaml::Model::Serializable
13
+ attribute :system_code, :string
14
+ attribute :metadata, :hash
15
+ attribute :tests, Test, collection: true
16
+ attribute :aliases, Alias, collection: true
17
+ attribute :stages, Stage, collection: true
18
+ attribute :dependencies, Dependency, collection: true
19
+
20
+ yaml do
21
+ map "system_code", to: :system_code
22
+ map "metadata", to: :metadata
23
+ map "tests", to: :tests
24
+ map "aliases", to: :aliases
25
+ map "stages", to: :stages
26
+ map "dependencies", to: :dependencies
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "interscript/isc/model"
5
+
6
+ module Interscript
7
+ module Isc
8
+ module Model
9
+ # Polymorphic representation of an ISC item (StringValue, AliasRef, etc.)
10
+ # Serialized as a discriminated union keyed by the `type` field.
11
+ #
12
+ # YAML shape:
13
+ # type: string
14
+ # value: "hello"
15
+ # ---
16
+ # type: alias_ref
17
+ # name: my_alias
18
+ # ---
19
+ # type: concat
20
+ # parts:
21
+ # - type: string
22
+ # value: "a"
23
+ # - type: alias_ref
24
+ # name: consonants
25
+ class Item < Lutaml::Model::Serializable
26
+ attribute :type, :string
27
+ attribute :value, :string
28
+ attribute :name, :string
29
+ attribute :index, :integer
30
+ attribute :lo, :string
31
+ attribute :hi, :string
32
+ attribute :chars, :string, collection: true
33
+ attribute :parts, Item, collection: true
34
+ attribute :inner, Item
35
+
36
+ yaml do
37
+ map "type", to: :type
38
+ map "value", to: :value
39
+ map "name", to: :name
40
+ map "index", to: :index
41
+ map "lo", to: :lo
42
+ map "hi", to: :hi
43
+ map "chars", to: :chars
44
+ map "parts", to: :parts
45
+ map "inner", to: :inner
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "interscript/isc/model/item"
5
+ require "interscript/isc/model/constraint"
6
+
7
+ module Interscript
8
+ module Isc
9
+ module Model
10
+ class Rule < Lutaml::Model::Serializable
11
+ attribute :from, Item
12
+ attribute :to, Item
13
+ attribute :constraints, Constraint, collection: true
14
+
15
+ yaml do
16
+ map "from", to: :from
17
+ map "to", to: :to
18
+ map "constraints", to: :constraints
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "interscript/isc/model/stage_item"
5
+
6
+ module Interscript
7
+ module Isc
8
+ module Model
9
+ class Stage < Lutaml::Model::Serializable
10
+ attribute :name, :string
11
+ attribute :body, StageItem, collection: true
12
+
13
+ yaml do
14
+ map "name", to: :name
15
+ map "body", to: :body
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "interscript/isc/model/rule"
5
+ require "interscript/isc/model/item"
6
+
7
+ module Interscript
8
+ module Isc
9
+ module Model
10
+ # A single item in a stage body. Discriminated by `kind`:
11
+ # parallel, sequence, bare_rule, run, separate, compose, string_case
12
+ class StageItem < Lutaml::Model::Serializable
13
+ attribute :kind, :string
14
+ attribute :rules, Rule, collection: true
15
+ attribute :rule, Rule
16
+ attribute :dependency, :string
17
+ attribute :stage, :string
18
+ attribute :separator, Item
19
+ attribute :op, :string
20
+
21
+ yaml do
22
+ map "kind", to: :kind
23
+ map "rules", to: :rules
24
+ map "rule", to: :rule
25
+ map "dependency", to: :dependency
26
+ map "stage", to: :stage
27
+ map "separator", to: :separator
28
+ map "op", to: :op
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Interscript
6
+ module Isc
7
+ module Model
8
+ class Test < Lutaml::Model::Serializable
9
+ attribute :input, :string
10
+ attribute :expected, :string
11
+ attribute :note, :string
12
+
13
+ yaml do
14
+ map "input", to: :input
15
+ map "expected", to: :expected
16
+ map "note", to: :note
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Interscript
6
+ module Isc
7
+ module Model
8
+ autoload :Item, "interscript/isc/model/item"
9
+ autoload :Constraint, "interscript/isc/model/constraint"
10
+ autoload :Rule, "interscript/isc/model/rule"
11
+ autoload :StageItem, "interscript/isc/model/stage_item"
12
+ autoload :Stage, "interscript/isc/model/stage"
13
+ autoload :Alias, "interscript/isc/model/alias"
14
+ autoload :Test, "interscript/isc/model/test"
15
+ autoload :Dependency, "interscript/isc/model/dependency"
16
+ autoload :Document, "interscript/isc/model/document"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interscript
4
+ module Isc
5
+ # Bridges the ISC document hash (from DocumentBuilder) to the existing
6
+ # Interscript::Node::Document object model, enabling .isc files to be
7
+ # used for actual transliteration via the standard runtime.
8
+ #
9
+ # This is the critical integration point: without it, ISC files can be
10
+ # parsed but cannot transliterate.
11
+ #
12
+ # doc_hash = Interscript::Isc::DocumentBuilder.build(tree)
13
+ # node_doc = Interscript::Isc::NodeAdapter.to_interscript_node(doc_hash)
14
+ # Interscript.transliterate_node(node_doc, "main", "hello")
15
+ #
16
+ class NodeAdapter
17
+ def self.to_interscript_node(isc_doc)
18
+ new(isc_doc).build
19
+ end
20
+
21
+ def initialize(isc_doc)
22
+ @isc_doc = isc_doc
23
+ @imported = {}
24
+ # Local alias name -> raw value fragment, for set/constraint
25
+ # charset resolution (grek_latn_any = any([grek_any, …])).
26
+ @local_raw = Array(isc_doc[:aliases]).to_h { |a| [a[:name].to_sym, a[:value]] }
27
+ @charset_cache = {}
28
+ @resolving = []
29
+ end
30
+
31
+ def build
32
+ Interscript::Node::Document.new.tap do |doc|
33
+ doc.metadata = build_metadata
34
+ doc.tests = build_tests
35
+ # Maps are addressed by their file-derived name (locate() and
36
+ # Maps.transliterate use it); registering under the system code
37
+ # made every map whose code differs from its file name resolve
38
+ # to an auto-created empty entry — stages[:main].call crashed.
39
+ doc.name = if @isc_doc[:filename]
40
+ File.basename(@isc_doc[:filename].to_s, ".isc")
41
+ else
42
+ @isc_doc[:systemCode]
43
+ end
44
+ # The legacy DSL stamps doc_name on every alias and stage; the
45
+ # Ruby/JS compilers emit run directives and alias lookups as
46
+ # Maps.transliterate(stage.doc_name, ...) — without it every
47
+ # aliased-dependency run compiled to transliterate(nil).
48
+ build_dependencies(doc)
49
+ # Imported aliases (charset libraries) resolve NOW, at adapter
50
+ # time: their data is a charset that must compile as a character
51
+ # class in constraints. Left to the runtime, they degrade to
52
+ # vacuous fragments and context-gated rules fire wrongly
53
+ # everywhere (din-grc's word-initial γκ). Dependencies load
54
+ # FIRST so local aliases composed from imported charsets
55
+ # (grek_latn_any = any([grek_any, …])) resolve too.
56
+ @imported = doc.imported_aliases
57
+ doc.aliases = build_aliases(doc.name)
58
+ build_stages(doc.name).each { |name, stage| doc.stages[name] = stage }
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def build_metadata
65
+ meta = Interscript::Node::MetaData.new
66
+ @isc_doc[:metadata].each do |key, value|
67
+ meta[key.to_sym] = value
68
+ end
69
+ meta
70
+ end
71
+
72
+ def build_tests
73
+ return nil if @isc_doc[:tests].empty?
74
+
75
+ tests = Interscript::Node::Tests.new
76
+ @isc_doc[:tests].each do |t|
77
+ tests.data << [t[:input], t[:expected]]
78
+ end
79
+ tests
80
+ end
81
+
82
+ def build_aliases(doc_name)
83
+ @isc_doc[:aliases].each_with_object({}) do |a, h|
84
+ # The runtime resolves aliases through AliasDef#data (see
85
+ # Interpreter::Stage#build_item); a bare item here hands it a
86
+ # raw Array once Any#data unrolls.
87
+ alias_def = Interscript::Node::AliasDef.new(a[:name].to_sym, convert_item(a[:value]))
88
+ alias_def.doc_name = doc_name
89
+ h[a[:name].to_sym] = alias_def
90
+ end
91
+ end
92
+
93
+ # ISC v1 has no import marker; aliased dependencies are reached
94
+ # through dep_aliases, which `import` does not gate. Documents
95
+ # load through the same dispatch the compiler uses, so chains of
96
+ # .isc/.imp dependencies resolve recursively.
97
+ def build_dependencies(doc)
98
+ Array(@isc_doc[:dependencies]).each do |dep_hash|
99
+ dep = Interscript::Node::Dependency.new
100
+ dep.full_name = dep_hash[:target]
101
+ dep.name = dep_hash[:alias]&.to_sym
102
+ # Aliased dependencies are stage-run targets; unaliased ones are
103
+ # library imports (the ISC form of `dependency "posix", import:
104
+ # true` from the .imp corpus) — their aliases merge into scope.
105
+ dep.import = dep.name.nil?
106
+ dep.document = load_dependency_document(dep.full_name)
107
+ doc.dependencies << dep
108
+ doc.dep_aliases[dep.name] = dep if dep.name
109
+ end
110
+ end
111
+
112
+ def load_dependency_document(full_name)
113
+ path = Interscript.locate(full_name)
114
+ if path&.end_with?(".isc")
115
+ Interscript::Compiler.parse_isc(path)
116
+ else
117
+ Interscript::DSL.parse(full_name)
118
+ end
119
+ end
120
+
121
+ def build_stages(doc_name)
122
+ @isc_doc[:stages].each_with_object({}) do |stage, h|
123
+ built = build_stage(stage)
124
+ built.doc_name = doc_name
125
+ h[stage[:name].to_sym] = built
126
+ end
127
+ end
128
+
129
+ def build_stage(stage_def)
130
+ 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
+ )
159
+ end
160
+ end
161
+ stage
162
+ end
163
+
164
+ def build_rule(rule_def)
165
+ from = convert_item(rule_def[:from])
166
+ to = convert_item(rule_def[:to])
167
+ opts = {}
168
+ %i[before after not_before not_after].each do |k|
169
+ next unless rule_def[:constraints]&.any? { |c| c[:kind] == k }
170
+ constraint = rule_def[:constraints].find { |c| c[:kind] == k }
171
+ opts[k] = convert_item(constraint[:item], :class)
172
+ end
173
+ Interscript::Node::Rule::Sub.new(from, to, **opts)
174
+ end
175
+
176
+ def build_run_rule(item)
177
+ stage_ref = Interscript::Node::Item::Stage.new(
178
+ item[:stage].to_sym,
179
+ map: item[:dependency]&.to_sym
180
+ )
181
+ Interscript::Node::Rule::Run.new(stage_ref)
182
+ end
183
+
184
+ def convert_item(item, ctx = :word)
185
+ case item
186
+ when Items::StringValue
187
+ Interscript::Node::Item::String.new(item.value)
188
+ when Items::None
189
+ Interscript::Node::Item::String.new("")
190
+ when Items::Primitive
191
+ convert_primitive(item)
192
+ when Items::AliasRef
193
+ convert_alias_ref(item, ctx)
194
+ when Items::Capture
195
+ Interscript::Node::Item::CaptureRef.new(item.index)
196
+ when Items::Function
197
+ item.name.to_sym
198
+ when Items::Concat
199
+ convert_concat(item)
200
+ when Items::CaptureGroup
201
+ Interscript::Node::Item::CaptureGroup.new(convert_item(item.inner))
202
+ when Items::Maybe
203
+ Interscript::Node::Item::Maybe.new(convert_item(item.inner))
204
+ when Items::Some
205
+ Interscript::Node::Item::Some.new(convert_item(item.inner))
206
+ when Items::Range
207
+ # Keep the range native: both runtimes compile Any(Range) to a
208
+ # codepoint character class [lo-hi]. Expanding it to an Array
209
+ # iterates Ruby String#succ (a, b, … z, aa, ab …), which never
210
+ # reaches non-ASCII codepoints and blows up the node size.
211
+ lo = item.lo.is_a?(Items::StringValue) ? item.lo.value : item.lo
212
+ hi = item.hi.is_a?(Items::StringValue) ? item.hi.value : item.hi
213
+ Interscript::Node::Item::Any.new(lo..hi)
214
+ when Items::Set
215
+ convert_set(item)
216
+ else
217
+ Interscript::Node::Item::String.new(item.to_s)
218
+ end
219
+ end
220
+
221
+ def convert_primitive(item)
222
+ # In the Ruby runtime, zero-width primitives are represented as
223
+ # Alias nodes referencing Stdlib symbols. See Stdlib::ALIASES.
224
+ Interscript::Node::Item::Alias.new(item.name.to_sym)
225
+ end
226
+
227
+ def convert_alias_ref(item, ctx = :word)
228
+ # An alias in class context (constraint position or inside a set)
229
+ # compiles with class semantics when it names a charset — imported
230
+ # library sets, or local aliases composed of them. Word-position
231
+ # aliases stay Alias nodes and resolve at runtime.
232
+ if ctx == :class
233
+ # Unresolvable names stay vacuous here — legacy Any(nil) parity.
234
+ resolve_charset(item.name.to_sym) || Interscript::Node::Item::String.new("")
235
+ else
236
+ Interscript::Node::Item::Alias.new(item.name.to_sym, map: item.map&.to_sym)
237
+ end
238
+ end
239
+
240
+ # The class-semantics node for a charset alias — imported library
241
+ # sets (Any or charset String) or local aliases composed of them —
242
+ # or nil. Memoized; a resolving set guards alias cycles.
243
+ def resolve_charset(name)
244
+ return @charset_cache[name] if @charset_cache.key?(name)
245
+ return nil if @resolving.include?(name)
246
+
247
+ @resolving << name
248
+ node =
249
+ # Stdlib alias values are regex fragments ("\d", "\b") —
250
+ # keep them as Alias nodes so the runtimes interpolate them raw;
251
+ # wrapping in Any escapes the backslashes ([\\d] matches a
252
+ # literal backslash or d).
253
+ if Interscript::Stdlib::ALIASES.key?(name)
254
+ Interscript::Node::Item::Alias.new(name)
255
+ elsif (imp = @imported[name])
256
+ data = imp.data
257
+ case data
258
+ when Interscript::Node::Item::Any
259
+ data
260
+ when Interscript::Node::Item::String
261
+ Interscript::Node::Item::Any.new(data.data)
262
+ end
263
+ elsif (raw = @local_raw[name])
264
+ converted = convert_item(raw)
265
+ converted if converted.is_a?(Interscript::Node::Item::Any) || converted.is_a?(Interscript::Node::Item::String)
266
+ end
267
+ @resolving.delete(name)
268
+ @charset_cache[name] = node
269
+ end
270
+
271
+ def convert_concat(concat)
272
+ parts = concat.parts.map { |p| convert_item(p) }
273
+ return parts.first if parts.size == 1
274
+ parts.reduce { |acc, part| acc + part }
275
+ end
276
+
277
+ def convert_set(set)
278
+ Interscript::Node::Item::Any.new(
279
+ set.chars.map do |c|
280
+ # Entries may be primitives/alias refs (e.g. any([boundary,
281
+ # "\u200c", "\u200d"])) — stringifying them baked an object
282
+ # inspect into the compiled character class.
283
+ case c
284
+ when ::String
285
+ Interscript::Node::Item::String.new(c)
286
+ when Items::StringValue
287
+ Interscript::Node::Item::String.new(c.value)
288
+ when Items::Primitive
289
+ convert_primitive(c)
290
+ when Items::AliasRef
291
+ convert_alias_ref(c, :class) ||
292
+ begin
293
+ # Legacy parity: a bare alias inside any(...) compiles to the
294
+ # Stdlib value (Any(nil) there for imported aliases). Keeping
295
+ # the Alias item resolves to the imported charset *string* at
296
+ # build time, baking a 1000-char literal into the constraint
297
+ # regexp — a lookbehind that never matches.
298
+ name = convert_alias_ref(c).name
299
+ val = Interscript::Stdlib::ALIASES[name]
300
+ val ? Interscript::Node::Item::Any.new(val) : Interscript::Node::Item::String.new("")
301
+ end
302
+ else
303
+ convert_item(c)
304
+ end
305
+ end
306
+ )
307
+ end
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interscript
4
+ module Isc
5
+ # Normalizes the raw parslet tree into the documented Parser.parse
6
+ # shape: scalar leaves (string/identifier nodes) become plain
7
+ # strings, metadata field pairs become keyed hashes, and arrow-form
8
+ # test pairs merge into single {input:, expected:} entries. Deeper
9
+ # item trees (rule from/to, constraints, kwargs) keep their parslet
10
+ # shape — DocumentBuilder materializes those through Transform.
11
+ class Normalizer
12
+ def self.normalize(tree)
13
+ new(tree).normalize
14
+ end
15
+
16
+ def initialize(tree)
17
+ @tree = tree
18
+ end
19
+
20
+ def normalize
21
+ system = @tree[:system]
22
+ return @tree unless system
23
+
24
+ {
25
+ system: {
26
+ system_code: scalar(system[:system_code]),
27
+ body: Array(system[:body]).map { |block| normalize_block(block) }
28
+ }
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ # {string: [{char: "B"@8}, ...]} and {identifier: "main"@31}
35
+ # become plain strings.
36
+ def scalar(node)
37
+ return node if node.is_a?(::String) || node.nil?
38
+ if node.key?(:string)
39
+ # Parts are {char:} hashes and escape fragments ({dquote:},
40
+ # {unicode:}, …) in the general grammar; some constructions
41
+ # (e.g. the rababa directive) capture bare slices instead.
42
+ # Escapes decode via the shared fold — folding only :char here
43
+ # silently dropped every escaped character.
44
+ Interscript::Isc::Transform.decode_string_parts(node[:string])
45
+ elsif node.key?(:identifier)
46
+ node[:identifier].to_s
47
+ elsif node.key?(:raw)
48
+ node[:raw].to_s
49
+ else
50
+ node
51
+ end
52
+ end
53
+
54
+ def normalize_block(block)
55
+ return block unless block.is_a?(Hash)
56
+ if block.key?(:metadata)
57
+ {metadata: Array(block[:metadata]).map { |field| normalize_meta_field(field) }}
58
+ elsif block.key?(:aliases)
59
+ {
60
+ aliases: Array(block[:aliases]).map do |alias_def|
61
+ {name: scalar(alias_def[:name]), value: alias_def[:value]}.compact
62
+ end
63
+ }
64
+ elsif block.key?(:tests)
65
+ # A comment-only or empty tests block captures as its raw
66
+ # text through the artifact; the DSL grammar folds it. Both
67
+ # normalize to no tests.
68
+ items = block[:tests].is_a?(Array) ? block[:tests] : []
69
+ {tests: merge_tests(items)}
70
+ elsif block.key?(:stage)
71
+ {stage_name: scalar(block[:stage_name]), stage: normalize_stage_items(block[:stage])}
72
+ elsif block.key?(:target)
73
+ {target: scalar(block[:target]), alias: scalar(block[:alias])}.compact
74
+ else
75
+ block
76
+ end
77
+ end
78
+
79
+ # A capture-free block (empty or comment-only body — comments are
80
+ # consumed as whitespace) folds to a raw slice under .as; the .parg
81
+ # artifact's empty capture is an array. Match it.
82
+ def normalize_stage_items(items)
83
+ Array(items).map do |item|
84
+ if item.is_a?(Hash)
85
+ %i[parallel sequence].each do |k|
86
+ item = {k => []} if item.key?(k) && !item[k].is_a?(Array)
87
+ end
88
+ end
89
+ item
90
+ end
91
+ end
92
+
93
+ # {field_name: {identifier: "authority"}, field_value: {string: [...]}}
94
+ # becomes {authority: "BGN-PCGN"}.
95
+ def normalize_meta_field(field)
96
+ return field unless field.is_a?(Hash) && field.key?(:field_name)
97
+
98
+ name = scalar(field[:field_name]).to_sym
99
+ value = field.key?(:field_block) ? field[:field_block] : scalar(field[:field_value])
100
+ {name => value}
101
+ end
102
+
103
+ # The arrow form usually parses as one {input:, expected:} hash
104
+ # per test; tolerate adjacent single-key entries as well.
105
+ def merge_tests(entries)
106
+ merged = []
107
+ pending_input = nil
108
+ entries.each do |entry|
109
+ next unless entry.is_a?(Hash)
110
+ if entry.key?(:input) && entry.key?(:expected)
111
+ merged << {input: scalar(entry[:input]), expected: scalar(entry[:expected])}
112
+ pending_input = nil
113
+ elsif entry.key?(:input)
114
+ pending_input = scalar(entry[:input])
115
+ elsif entry.key?(:expected)
116
+ merged << {input: pending_input, expected: scalar(entry[:expected])}
117
+ pending_input = nil
118
+ else
119
+ merged << entry
120
+ end
121
+ end
122
+ merged << {input: pending_input, expected: ""} if pending_input
123
+ merged
124
+ end
125
+ end
126
+ end
127
+ end