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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Interscript
6
+ module Isc
7
+ # Entry point for parsing ISC source files.
8
+ #
9
+ # tree = Interscript::Isc::Parser.parse(source)
10
+ # # => { system: { system_code: "...", body: [...] } }
11
+ #
12
+ # The returned tree is a parslet-shaped hash — lists of hashes with
13
+ # symbol keys. Convert to a domain object via Interscript::Isc::DocumentBuilder.
14
+ class Parser < Parsanol::Parser
15
+ include Grammar::Core
16
+
17
+ root :isc_source
18
+
19
+ # The compiled grammar, shipped alongside the .parg source; the
20
+ # runtime parses through it when parsanol carries PARG (>= 1.3.61)
21
+ # and falls back to the Ruby-DSL grammar otherwise. Trees are
22
+ # byte-identical by the corpus gate in the parg_diff spec.
23
+ ARTIFACT_PATH = File.expand_path("grammar/isc.artifact.json", __dir__)
24
+
25
+ class ArtifactUnavailable < StandardError; end
26
+
27
+ class << self
28
+ def parse(source, filename: nil)
29
+ parse_with_callbacks(source, filename: filename)
30
+ end
31
+
32
+ def parse_with_callbacks(source, filename: nil)
33
+ tree = begin
34
+ parse_with_artifact(source)
35
+ rescue ArtifactUnavailable
36
+ parse_with_dsl(source)
37
+ end
38
+ Normalizer.normalize(tree)
39
+ rescue Parsanol::ParseFailed => e
40
+ raise ParseError.new(e.message, filename: filename, source: source, cause: e)
41
+ end
42
+
43
+ def parse_with_artifact(source)
44
+ artifact.parse("isc", source)
45
+ end
46
+
47
+ def parse_with_dsl(source)
48
+ new.parse(source)
49
+ end
50
+
51
+ def artifact
52
+ @artifact ||= begin
53
+ require "parsanol/parg"
54
+ Parsanol::PARG::Artifact.load(ARTIFACT_PATH)
55
+ rescue LoadError, StandardError => e
56
+ raise ArtifactUnavailable, "parg artifact unavailable: #{e.class}: #{e.message}"
57
+ end
58
+ end
59
+
60
+ def reset_artifact
61
+ @artifact = nil
62
+ end
63
+ end
64
+ end
65
+
66
+ class ParseError < StandardError
67
+ attr_reader :filename, :source
68
+
69
+ def initialize(message, filename:, source:, cause: nil)
70
+ @filename = filename
71
+ @source = source
72
+ @cause = cause
73
+ loc = filename ? "#{filename}: " : ""
74
+ super("#{loc}#{message}")
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interscript/isc/items"
4
+
5
+ module Interscript
6
+ module Isc
7
+ # Serializes a document hash back to ISC source text.
8
+ # This is the reverse of Parser → DocumentBuilder.
9
+ #
10
+ # isc_source = Serializer.serialize(doc_hash)
11
+ #
12
+ class Serializer
13
+ def self.serialize(doc_hash)
14
+ new(doc_hash).serialize
15
+ end
16
+
17
+ def initialize(doc_hash)
18
+ @doc = doc_hash
19
+ @out = +""
20
+ end
21
+
22
+ def serialize
23
+ emit_system_open
24
+ emit_metadata if @doc[:metadata]&.any?
25
+ emit_tests if @doc[:tests]&.any?
26
+ emit_aliases if @doc[:aliases]&.any?
27
+ emit_dependencies if @doc[:dependencies]&.any?
28
+ emit_stages
29
+ emit_system_close
30
+ @out
31
+ end
32
+
33
+ private
34
+
35
+ def emit_system_open
36
+ @out << %(system "#{@doc[:systemCode]}" {\n)
37
+ end
38
+
39
+ def emit_system_close
40
+ @out << "}\n"
41
+ end
42
+
43
+ def emit_blank
44
+ @out << "\n"
45
+ end
46
+
47
+ def emit_metadata
48
+ @out << "\nmetadata {\n"
49
+ @doc[:metadata].each do |key, val|
50
+ emit_metadata_field(key, val)
51
+ end
52
+ @out << "}\n"
53
+ end
54
+
55
+ def emit_metadata_field(key, val)
56
+ k = key.to_s
57
+ case k
58
+ when "description"
59
+ emit_description(val)
60
+ when "notes", "implementation_notes", "original_notes"
61
+ emit_notes(k, val)
62
+ else
63
+ case val
64
+ when Array
65
+ val.each { |v| @out << " #{k} #{emit_meta_value(v)}\n" }
66
+ when String
67
+ @out << " #{k} #{emit_meta_value(val)}\n"
68
+ when NilClass
69
+ @out << " #{k}\n"
70
+ else
71
+ @out << " #{k} #{val}\n"
72
+ end
73
+ end
74
+ end
75
+
76
+ def emit_description(val)
77
+ s = val.is_a?(Array) ? val.join(" ") : val.to_s
78
+ s = s.strip
79
+ if s.empty?
80
+ @out << " description { }\n"
81
+ elsif s.include?("\n") || s.length > 60
82
+ escaped = s.gsub("\\", "\\\\\\\\").gsub("{", "\\{").gsub("}", "\\}")
83
+ @out << " description {\n #{escaped.split("\n").join("\n ")}\n }\n"
84
+ else
85
+ @out << " description { #{s.gsub("\\", "\\\\\\\\").gsub("{", "\\{").gsub("}", "\\}")} }\n"
86
+ end
87
+ end
88
+
89
+ def emit_notes(key, val)
90
+ arr = Array(val).compact.reject(&:empty?)
91
+ if arr.empty?
92
+ @out << " #{key} { }\n"
93
+ else
94
+ @out << " #{key} {\n"
95
+ arr.each do |note|
96
+ escaped = note.to_s.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
97
+ @out << " note \"#{escaped}\"\n"
98
+ end
99
+ @out << " }\n"
100
+ end
101
+ end
102
+
103
+ def emit_meta_value(val)
104
+ s = val.to_s
105
+ # Quote values that contain spaces, special chars, or look like numbers
106
+ if s.match?(/[\s"{}#]/) || s.empty?
107
+ s.inspect
108
+ else
109
+ s
110
+ end
111
+ end
112
+
113
+ def emit_tests
114
+ @out << "\ntests {\n"
115
+ @doc[:tests].each do |t|
116
+ @out << %( "#{escape(t[:input])}" -> "#{escape(t[:expected])}")
117
+ @out << %( note "#{escape(t[:note])}") if t[:note]
118
+ @out << "\n"
119
+ end
120
+ @out << "}\n"
121
+ end
122
+
123
+ def emit_aliases
124
+ @out << "\naliases {\n"
125
+ @doc[:aliases].each do |a|
126
+ @out << " #{a[:name]} = #{emit_item(a[:value])}\n"
127
+ end
128
+ @out << "}\n"
129
+ end
130
+
131
+ def emit_dependencies
132
+ @doc[:dependencies].each do |d|
133
+ dep = %(dependency "#{d[:target]}")
134
+ dep << %( as #{d[:alias]}) if d[:alias]
135
+ @out << dep << "\n"
136
+ end
137
+ @out << "\n" if @doc[:dependencies].any?
138
+ end
139
+
140
+ def emit_stages
141
+ @doc[:stages].each do |stage|
142
+ @out << "\nstage #{stage[:name]} {\n"
143
+ stage[:body].each { |item| emit_stage_item(item) }
144
+ @out << "}\n"
145
+ end
146
+ end
147
+
148
+ def emit_stage_item(item)
149
+ case item[:kind]
150
+ when :parallel
151
+ @out << " parallel {\n"
152
+ item[:rules].each { |r| emit_rule(r, 4) }
153
+ @out << " }\n"
154
+ when :sequence
155
+ @out << " sequence {\n"
156
+ item[:rules].each { |r| emit_rule(r, 4) }
157
+ @out << " }\n"
158
+ when :bare_rule
159
+ emit_rule(item[:rule], 2)
160
+ when :run
161
+ dep = item[:dependency]
162
+ stage = item[:stage]
163
+ @out << if dep
164
+ " run map.#{dep}.stage.#{stage}\n"
165
+ else
166
+ " run stage.#{stage}\n"
167
+ end
168
+ when :separate
169
+ sep = item[:separator]
170
+ @out << " separate separator #{emit_item(sep)}\n"
171
+ when :compose
172
+ @out << " compose\n"
173
+ when :string_case
174
+ @out << " #{item[:op]}\n"
175
+ end
176
+ end
177
+
178
+ def emit_rule(rule, indent)
179
+ pad = " " * indent
180
+ from_str = emit_item(rule[:from])
181
+ to_str = emit_item(rule[:to])
182
+
183
+ needs_block = rule[:from].is_a?(Items::Concat) || rule[:to].is_a?(Items::Concat)
184
+
185
+ if needs_block
186
+ inner = " " * (indent + 2)
187
+ @out << "#{pad}sub {\n"
188
+ @out << "#{inner}from #{from_str}\n"
189
+ @out << "#{inner}to #{to_str}\n"
190
+ rule[:constraints]&.each { |c| @out << "#{inner}#{emit_constraint(c)}\n" }
191
+ @out << "#{pad}}\n"
192
+ else
193
+ constraints_str = rule[:constraints]&.map { |c| emit_constraint(c) }&.join(" ") || ""
194
+ @out << "#{pad}sub #{from_str} #{to_str}"
195
+ @out << " #{constraints_str}" unless constraints_str.empty?
196
+ @out << "\n"
197
+ end
198
+ end
199
+
200
+ def emit_constraint(constraint)
201
+ "#{constraint[:kind]} #{emit_item(constraint[:item])}"
202
+ end
203
+
204
+ def emit_item(item)
205
+ return "none" unless item
206
+
207
+ case item
208
+ when Items::StringValue
209
+ escape_string(item.value)
210
+ when Items::None
211
+ "none"
212
+ when Items::Primitive
213
+ item.name
214
+ when Items::Function
215
+ item.name
216
+ when Items::AliasRef
217
+ item.name
218
+ when Items::Capture
219
+ "ref(#{item.index})"
220
+ when Items::CaptureGroup
221
+ "capture(#{emit_item(item.inner)})"
222
+ when Items::Concat
223
+ item.parts.map { |p| emit_item(p) }.join(" + ")
224
+ when Items::Set
225
+ %(any("#{item.chars.map { |c| escape(c) }.join}"))
226
+ when Items::Range
227
+ %(any("#{escape(item.lo)}".."#{escape(item.hi)}"))
228
+ when Items::Maybe
229
+ "maybe(#{emit_item(item.inner)})"
230
+ when Items::Some
231
+ "some(#{emit_item(item.inner)})"
232
+ else
233
+ item.to_s
234
+ end
235
+ end
236
+
237
+ def escape_string(str)
238
+ return '""' if str.nil?
239
+ escaped = str.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
240
+ "\"#{escaped}\""
241
+ end
242
+
243
+ def escape(str)
244
+ str.to_s.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Interscript
6
+ module Isc
7
+ # Transforms the raw parslet tree into a clean intermediate hash with
8
+ # stable shape for the DocumentBuilder to consume.
9
+ #
10
+ # String escapes are unescaped here. Items are flattened. Constraints
11
+ # are tagged by kind.
12
+ class Transform < Parsanol::Transform
13
+ # String atom: parslet gives us a parslet slice for simple strings,
14
+ # or an array of pieces (escape-sequence fragments interleaved with
15
+ # raw chars) for strings containing escapes. Flatten both to a single
16
+ # StringValue.
17
+ rule(string: simple(:s)) { Items::StringValue.new(s.to_s) }
18
+ rule(string: sequence(:parts)) do
19
+ Items::StringValue.new(Transform.decode_string_parts(parts))
20
+ end
21
+ rule(run: simple(:s)) { s.to_s }
22
+
23
+ rule(identifier: simple(:i)) { i.to_s }
24
+
25
+ rule(none: simple(:_)) { Items::None.new }
26
+ # any_character is the Stdlib any-char alias (".") — without this
27
+ # rule the fragment stringified to "any_charany_character".
28
+ rule(any_char: simple(:_)) { Items::AliasRef.new("any_character") }
29
+ rule(primitive: simple(:p)) { Items::Primitive.new(p.to_s) }
30
+ rule(function: simple(:f)) { Items::Function.new(f.to_s) }
31
+ rule(alias: {identifier: simple(:n), qualified_name: simple(:q)}) {
32
+ Items::AliasRef.new(q.to_s, map: n.to_s)
33
+ }
34
+ rule(alias: simple(:n)) { Items::AliasRef.new(n.to_s) }
35
+ # Bare identifiers inside any(...) lists parse as :alias_ref (the
36
+ # :alias key only covers direct item position). Unmatched, the hash
37
+ # degraded downstream into the debug string "[:alias_ref, \"name\"]".
38
+ rule(alias_ref: {identifier: simple(:n)}) { Items::AliasRef.new(n.to_s) }
39
+ rule(alias_ref: simple(:n)) { Items::AliasRef.new(n.to_s) }
40
+ rule(ref: subtree(:h)) { Items::Capture.new(h[:digit].to_s.to_i) }
41
+ rule(capture_inner: subtree(:inner)) { Items::CaptureGroup.new(Interscript::Isc::Transform.materialize_item(inner)) }
42
+ rule(maybe_inner: subtree(:inner)) { Items::Maybe.new(Interscript::Isc::Transform.materialize_item(inner)) }
43
+ rule(some_inner: subtree(:inner)) { Items::Some.new(Interscript::Isc::Transform.materialize_item(inner)) }
44
+
45
+ rule(dquote: simple(:_)) { '"' }
46
+ rule(backslash: simple(:_)) { "\\" }
47
+ rule(newline: simple(:_)) { "\n" }
48
+ rule(carriage_return: simple(:_)) { "\r" }
49
+ rule(tab: simple(:_)) { "\t" }
50
+ rule(u_lone: simple(:_)) { "U" }
51
+ rule(unicode: simple(:hex)) do
52
+ [hex.to_s.to_i(16)].pack("U")
53
+ rescue
54
+ hex.to_s
55
+ end
56
+
57
+ # Shared decoder for the pieces of a :string capture — used by the
58
+ # transform rules above and by the Normalizer's scalar folding. A
59
+ # fragment without an escape key is a raw run slice.
60
+ def self.decode_string_parts(parts)
61
+ Array(parts).map do |p|
62
+ case p
63
+ when Hash
64
+ if p.key?(:run)
65
+ p[:run].to_s
66
+ elsif p.key?(:newline)
67
+ "\n"
68
+ elsif p.key?(:carriage_return)
69
+ "\r"
70
+ elsif p.key?(:tab)
71
+ "\t"
72
+ elsif p.key?(:dquote)
73
+ '"'
74
+ elsif p.key?(:backslash)
75
+ "\\"
76
+ elsif p.key?(:unicode)
77
+ [p[:unicode].to_s.to_i(16)].pack("U")
78
+ elsif p.key?(:u_lone)
79
+ "U"
80
+ else
81
+ p.to_s
82
+ end
83
+ else
84
+ p.to_s
85
+ end
86
+ end.join
87
+ end
88
+
89
+ rule(lo: simple(:lo), hi: simple(:hi)) do
90
+ Items::Range.new(lo.to_s, hi.to_s)
91
+ end
92
+ rule(single: simple(:s)) { Items::Set.from_string(s.to_s) }
93
+ # List entries are already Items (strings, primitives, alias refs).
94
+ # Stringifying here baked an object inspect into the char set.
95
+ rule(list: sequence(:arr)) do
96
+ Items::Set.new(arr)
97
+ end
98
+ rule(any: subtree(:h)) do
99
+ # A single alias argument keeps set semantics: the legacy runtime
100
+ # builds Any(Alias) which resolves through Stdlib (Any(nil) for
101
+ # imported aliases). Unwrapped, the bare Alias resolves to the
102
+ # imported charset string and compiles as a literal.
103
+ h.is_a?(Items::AliasRef) ? Items::Set.new([h]) : h
104
+ end
105
+
106
+ rule(concatenation: subtree(:parts)) do
107
+ Items::Concat.from_parts(Array(parts))
108
+ end
109
+
110
+ def self.materialize_item(fragment)
111
+ case fragment
112
+ when Hash
113
+ if fragment.key?(:concatenation)
114
+ new.apply(fragment)
115
+ else
116
+ new.apply(concatenation: [fragment])
117
+ end
118
+ when Array
119
+ new.apply(concatenation: fragment)
120
+ when NilClass
121
+ Items::None.new
122
+ else
123
+ fragment
124
+ end
125
+ end
126
+
127
+ def materialize_item(fragment)
128
+ self.class.materialize_item(fragment)
129
+ end
130
+
131
+ rule(before: subtree(:x)) { {kind: :before, item: x} }
132
+ rule(after: subtree(:x)) { {kind: :after, item: x} }
133
+ rule(not_before: subtree(:x)) { {kind: :not_before, item: x} }
134
+ rule(not_after: subtree(:x)) { {kind: :not_after, item: x} }
135
+ rule(constraints: sequence(:c)) { c }
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "interscript/isc/model"
5
+ require "interscript/isc/items"
6
+
7
+ module Interscript
8
+ module Isc
9
+ # Bridges the ISC document hash (from DocumentBuilder) and lutaml-model
10
+ # objects for YAML serialization.
11
+ #
12
+ # yaml_str = YamlBridge.to_yaml(doc_hash)
13
+ # doc_hash = YamlBridge.from_yaml(yaml_str)
14
+ #
15
+ module YamlBridge
16
+ class << self
17
+ def to_yaml(doc_hash)
18
+ hash_to_model(doc_hash).to_yaml
19
+ end
20
+
21
+ def from_yaml(yaml_str)
22
+ model_to_hash(Model::Document.from_yaml(yaml_str))
23
+ end
24
+
25
+ private
26
+
27
+ def hash_to_model(doc_hash)
28
+ Model::Document.new(
29
+ system_code: doc_hash[:systemCode],
30
+ metadata: normalize_metadata(doc_hash[:metadata]),
31
+ tests: (doc_hash[:tests] || []).map { |t| Model::Test.new(t.transform_keys(&:to_s)) },
32
+ aliases: (doc_hash[:aliases] || []).map { |a| build_alias_model(a) },
33
+ stages: (doc_hash[:stages] || []).map { |s| build_stage_model(s) },
34
+ dependencies: (doc_hash[:dependencies] || []).map { |d| build_dependency_model(d) }
35
+ )
36
+ end
37
+
38
+ def normalize_metadata(meta)
39
+ return {} unless meta.is_a?(Hash)
40
+ meta.transform_values do |v|
41
+ case v
42
+ when Array then v.map { |i| normalize_meta_value(i) }
43
+ else normalize_meta_value(v)
44
+ end
45
+ end.transform_keys(&:to_s)
46
+ end
47
+
48
+ def normalize_meta_value(v)
49
+ v.is_a?(Symbol) ? v.to_s : v
50
+ end
51
+
52
+ def build_alias_model(alias_hash)
53
+ Model::Alias.new(
54
+ name: alias_hash[:name],
55
+ value: item_to_model(alias_hash[:value])
56
+ )
57
+ end
58
+
59
+ def build_stage_model(stage_hash)
60
+ Model::Stage.new(
61
+ name: stage_hash[:name],
62
+ body: (stage_hash[:body] || []).map { |item| stage_item_to_model(item) }
63
+ )
64
+ end
65
+
66
+ def build_dependency_model(dep_hash)
67
+ Model::Dependency.new(
68
+ target: dep_hash[:target],
69
+ alias_name: dep_hash[:alias]
70
+ )
71
+ end
72
+
73
+ def stage_item_to_model(item)
74
+ attrs = {kind: item[:kind].to_s}
75
+ case item[:kind]
76
+ when :parallel, :sequence
77
+ attrs[:rules] = item[:rules].map { |r| rule_to_model(r) }
78
+ when :bare_rule
79
+ attrs[:rule] = rule_to_model(item[:rule])
80
+ when :run
81
+ attrs[:dependency] = item[:dependency]
82
+ attrs[:stage] = item[:stage]
83
+ when :separate
84
+ attrs[:separator] = item[:separator] ? item_to_model(item[:separator]) : nil
85
+ when :string_case
86
+ attrs[:op] = item[:op]
87
+ end
88
+ Model::StageItem.new(attrs.compact)
89
+ end
90
+
91
+ def rule_to_model(rule_hash)
92
+ Model::Rule.new(
93
+ from: item_to_model(rule_hash[:from]),
94
+ to: item_to_model(rule_hash[:to]),
95
+ constraints: (rule_hash[:constraints] || []).map { |c| constraint_to_model(c) }
96
+ )
97
+ end
98
+
99
+ def constraint_to_model(constraint_hash)
100
+ Model::Constraint.new(
101
+ kind: constraint_hash[:kind]&.to_s,
102
+ item: constraint_hash[:item] ? item_to_model(constraint_hash[:item]) : nil
103
+ )
104
+ end
105
+
106
+ # Convert an Items::* object to a Model::Item
107
+ def item_to_model(item)
108
+ return nil unless item
109
+
110
+ case item
111
+ when Items::StringValue
112
+ Model::Item.new(type: "string", value: item.value)
113
+ when Items::None
114
+ Model::Item.new(type: "none")
115
+ when Items::Primitive
116
+ Model::Item.new(type: "primitive", name: item.name)
117
+ when Items::Function
118
+ Model::Item.new(type: "function", name: item.name)
119
+ when Items::AliasRef
120
+ Model::Item.new(type: "alias_ref", name: item.name)
121
+ when Items::Capture
122
+ Model::Item.new(type: "capture", index: item.index)
123
+ when Items::CaptureGroup
124
+ Model::Item.new(type: "capture_group", inner: item_to_model(item.inner))
125
+ when Items::Maybe
126
+ Model::Item.new(type: "maybe", inner: item_to_model(item.inner))
127
+ when Items::Some
128
+ Model::Item.new(type: "some", inner: item_to_model(item.inner))
129
+ when Items::Range
130
+ Model::Item.new(type: "range", lo: item.lo, hi: item.hi)
131
+ when Items::Set
132
+ Model::Item.new(type: "set", chars: item.chars)
133
+ when Items::Concat
134
+ Model::Item.new(type: "concat", parts: item.parts.map { |p| item_to_model(p) })
135
+ else
136
+ Model::Item.new(type: "unknown", value: item.to_s)
137
+ end
138
+ end
139
+
140
+ # --- Reverse direction: model → document hash ---
141
+
142
+ def model_to_hash(model)
143
+ {
144
+ schemaVersion: 1,
145
+ systemCode: model.system_code,
146
+ metadata: model_metadata_to_hash(model.metadata),
147
+ tests: (model.tests || []).map { |t| {input: t.input || "", expected: t.expected || "", note: t.note}.compact },
148
+ aliases: (model.aliases || []).map { |a| {name: a.name, value: model_to_item(a.value)} },
149
+ stages: (model.stages || []).map { |s| stage_model_to_hash(s) },
150
+ dependencies: (model.dependencies || []).map { |d| {target: d.target, alias: d.alias_name}.compact }
151
+ }
152
+ end
153
+
154
+ def model_metadata_to_hash(meta)
155
+ return {} unless meta.is_a?(Hash)
156
+ meta.transform_keys(&:to_sym)
157
+ end
158
+
159
+ def stage_model_to_hash(stage_model)
160
+ {
161
+ name: stage_model.name,
162
+ body: stage_model.body.map { |item| stage_item_model_to_hash(item) }
163
+ }
164
+ end
165
+
166
+ def stage_item_model_to_hash(item)
167
+ case item.kind
168
+ when "parallel", "sequence"
169
+ {kind: item.kind.to_sym, rules: item.rules.map { |r| rule_model_to_hash(r) }}
170
+ when "bare_rule"
171
+ {kind: :bare_rule, rule: rule_model_to_hash(item.rule)}
172
+ when "run"
173
+ {kind: :run, dependency: item.dependency, stage: item.stage}
174
+ when "separate"
175
+ {kind: :separate, separator: item.separator ? model_to_item(item.separator) : nil}
176
+ when "compose"
177
+ {kind: :compose}
178
+ when "string_case"
179
+ {kind: :string_case, op: item.op}
180
+ else
181
+ {kind: item.kind&.to_sym}
182
+ end
183
+ end
184
+
185
+ def rule_model_to_hash(rule_model)
186
+ {
187
+ from: model_to_item(rule_model.from),
188
+ to: model_to_item(rule_model.to),
189
+ constraints: (rule_model.constraints || []).map { |c| constraint_model_to_hash(c) }
190
+ }
191
+ end
192
+
193
+ def constraint_model_to_hash(constraint_model)
194
+ {
195
+ kind: constraint_model.kind&.to_sym,
196
+ item: constraint_model.item ? model_to_item(constraint_model.item) : nil
197
+ }
198
+ end
199
+
200
+ # Convert a Model::Item back to an Items::* object
201
+ def model_to_item(item_model)
202
+ return nil unless item_model
203
+
204
+ case item_model.type
205
+ when "string"
206
+ Items::StringValue.new(item_model.value || "")
207
+ when "none"
208
+ Items::None.new
209
+ when "primitive"
210
+ Items::Primitive.new(item_model.name)
211
+ when "function"
212
+ Items::Function.new(item_model.name)
213
+ when "alias_ref"
214
+ Items::AliasRef.new(item_model.name)
215
+ when "capture"
216
+ Items::Capture.new(item_model.index)
217
+ when "capture_group"
218
+ Items::CaptureGroup.new(model_to_item(item_model.inner))
219
+ when "maybe"
220
+ Items::Maybe.new(model_to_item(item_model.inner))
221
+ when "some"
222
+ Items::Some.new(model_to_item(item_model.inner))
223
+ when "range"
224
+ Items::Range.new(item_model.lo, item_model.hi)
225
+ when "set"
226
+ Items::Set.new(item_model.chars || [])
227
+ when "concat"
228
+ Items::Concat.new((item_model.parts || []).map { |p| model_to_item(p) })
229
+ else
230
+ Items::StringValue.new(item_model.value.to_s)
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end