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,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interscript
4
+ module Isc
5
+ # Builds an intermediate "document hash" from a raw parslet tree.
6
+ # The hash shape is intentionally simple so it can be:
7
+ # - inspected in tests
8
+ # - serialized to JSON IR for cross-runtime conformance
9
+ # - consumed by interscript-ruby's existing Node::Document builder
10
+ #
11
+ # This is the bridge between the parser and the runtime. It does no
12
+ # semantic validation beyond shape; that lives in the runtime layer.
13
+ class DocumentBuilder
14
+ SCHEMA_VERSION = 1
15
+
16
+ # Metadata fields that the Ruby DSL stores as Arrays (STANDARD_ARRAY_KEYS).
17
+ # ISC stores them as generic fields, so we wrap in an Array to match.
18
+ ARRAY_METADATA_FIELDS = %i[notes implementation_notes original_notes url].freeze
19
+
20
+ def self.build(tree, filename: nil)
21
+ new(tree, filename: filename).build
22
+ end
23
+
24
+ def initialize(tree, filename: nil)
25
+ @tree = tree
26
+ @filename = filename
27
+ @transform = Transform.new
28
+ end
29
+
30
+ def build
31
+ node = @tree[:system] || @tree
32
+ code = unquote(node[:system_code])
33
+
34
+ body = Array(node[:body])
35
+ metadata_hash = {}
36
+ aliases_arr = []
37
+ tests_arr = []
38
+ stages_arr = []
39
+ dependencies_arr = []
40
+
41
+ body.each do |item|
42
+ if item[:metadata]
43
+ metadata_hash.merge!(extract_metadata(item[:metadata]))
44
+ elsif item[:aliases]
45
+ aliases_arr.concat(extract_aliases(item[:aliases]))
46
+ elsif item[:tests]
47
+ tests_arr.concat(extract_tests(item[:tests]))
48
+ elsif item[:stage]
49
+ stages_arr << extract_stage(item)
50
+ elsif item[:target]
51
+ dependencies_arr << extract_dependency(item)
52
+ end
53
+ end
54
+
55
+ {
56
+ schemaVersion: SCHEMA_VERSION,
57
+ filename: @filename,
58
+ systemCode: code,
59
+ urn: derive_urn(code),
60
+ metadata: metadata_hash,
61
+ aliases: aliases_arr,
62
+ tests: tests_arr,
63
+ stages: stages_arr,
64
+ dependencies: dependencies_arr
65
+ }
66
+ end
67
+
68
+ private
69
+
70
+ # Apply Transform to a quoted_string fragment and read back the string value.
71
+ def unquote(fragment)
72
+ return "" if fragment.nil?
73
+ return fragment.to_s unless fragment.is_a?(Hash)
74
+ # Direct decode for the overwhelmingly common shape — routing
75
+ # every quoted string through Transform pattern matching
76
+ # dominated large-map builds.
77
+ return Transform.decode_string_parts(fragment[:string]) if fragment.key?(:string)
78
+
79
+ fragment.to_s
80
+ end
81
+
82
+ def unescape_braces(text)
83
+ text.gsub(/\\([{}\\])/, '\1')
84
+ end
85
+
86
+ def parse_array_field(val)
87
+ return [] if val.nil? || val.to_s.strip.empty?
88
+ text = val.to_s
89
+ return [text.strip] unless text.include?("\n-")
90
+ # Multi-line YAML list: merge continuation lines preserving paragraph breaks
91
+ items = []
92
+ text.lines.each do |l|
93
+ stripped = l.strip
94
+ if stripped.start_with?("- ")
95
+ items << stripped[2..]
96
+ elsif stripped.empty? && items.any?
97
+ items[-1] = (items[-1] || "") + "\n\n"
98
+ elsif !stripped.empty? && items.any?
99
+ items[-1] = (items[-1] || "") + "\n" + stripped
100
+ end
101
+ end
102
+ items = items.empty? ? [text.strip] : items
103
+ # Treat single-item [""] arrays (YAML empty list markers) as empty arrays
104
+ (items == [""]) ? [] : items
105
+ end
106
+
107
+ def normalize_heredoc(text)
108
+ lines = text.lines.map(&:chomp)
109
+ content_lines = lines.reject { |l| l.strip.empty? }
110
+ return "" if content_lines.empty?
111
+ return content_lines.first.strip if content_lines.size == 1
112
+
113
+ # YAML-style dedent: strip the minimum indent across all non-blank
114
+ # lines. The first line's indent may have been partially consumed
115
+ # by the grammar, so we compute min_indent from lines 2+ and treat
116
+ # the first line as having at least that much indent.
117
+ min_indent = content_lines
118
+ .drop(1)
119
+ .map { |l| l[/\A[ \t]*/].length }
120
+ .min || 0
121
+
122
+ lines.map do |l|
123
+ if l.strip.empty?
124
+ ""
125
+ elsif l[/\A[ \t]*/].length >= min_indent
126
+ l[min_indent..]
127
+ else
128
+ l.strip
129
+ end
130
+ end.join("\n").rstrip.then { |s| (text.end_with?("\n") && !text.end_with?("\n\n")) ? s + "\n" : s }
131
+ end
132
+
133
+ # Apply Transform to an identifier fragment.
134
+ def ident(fragment)
135
+ return "" if fragment.nil?
136
+ return fragment.to_s unless fragment.is_a?(Hash)
137
+ return fragment.to_s unless fragment.key?(:identifier)
138
+
139
+ @transform.apply(fragment).to_s
140
+ end
141
+
142
+ # Extract `name: "value"` pairs from a parslet kwarg tree into a
143
+ # string-keyed Hash. The tree is `{kwarg: [{kwarg_name:, kwarg_value:}, ...]}`
144
+ # or a single `{kwarg_name:, kwarg_value:}` for one kwarg.
145
+ def extract_kwargs(fragment)
146
+ return {} if fragment.nil?
147
+ raw = fragment.is_a?(Hash) ? fragment[:kwarg] : fragment
148
+ kwargs = case raw
149
+ when Hash then [raw]
150
+ when Array then raw.select { |k| k.is_a?(Hash) }
151
+ else []
152
+ end
153
+ kwargs.each_with_object({}) do |kw, h|
154
+ next unless kw.is_a?(Hash)
155
+ h[ident(kw[:kwarg_name])] = unquote(kw[:kwarg_value])
156
+ end
157
+ end
158
+
159
+ def derive_urn(code)
160
+ "urn:iso:24229:system:#{code.downcase}"
161
+ end
162
+
163
+ def extract_metadata(arr)
164
+ h = {}
165
+ Array(arr).each do |field|
166
+ if field.key?(:specification)
167
+ h[:specification] ||= []
168
+ h[:specification] << unquote(field[:specification])
169
+ elsif field.key?(:notes)
170
+ h[:notes] ||= []
171
+ Array(field[:notes]).each do |n|
172
+ note_val = n.is_a?(Hash) ? n[:note] : n
173
+ h[:notes] << normalize_heredoc(unquote(note_val).to_s)
174
+ end
175
+ elsif field.key?(:note)
176
+ h[:notes] ||= []
177
+ h[:notes] << normalize_heredoc(unquote(field[:note]).to_s)
178
+ elsif field.key?(:provenance)
179
+ h[:provenance] ||= []
180
+ h[:provenance] << unquote(field[:provenance])
181
+ elsif field.key?(:relations)
182
+ h[:relations] = extract_relations(field[:relations])
183
+ elsif field.key?(:description)
184
+ desc = field[:description]
185
+ desc_str = desc.is_a?(Array) ? desc.join : desc.to_s
186
+ h[:description] = normalize_heredoc(unescape_braces(desc_str)) + "\n"
187
+ elsif field.key?(:field_name)
188
+ # Generic field: identifier + raw value
189
+ name = ident(field[:field_name]).to_sym
190
+ if field.key?(:field_block)
191
+ val = normalize_heredoc(unescape_braces(field[:field_block].to_s))
192
+ else
193
+ raw = field[:field_value]
194
+ val = case raw
195
+ when Hash
196
+ raw.key?(:string) ? unquote(raw) : (raw[:raw]&.to_s || "").strip
197
+ when nil then ""
198
+ else raw.to_s.strip
199
+ end
200
+ end
201
+ # DSL stores these as Arrays — match that convention.
202
+ h[name] = if ARRAY_METADATA_FIELDS.include?(name)
203
+ parse_array_field(val)
204
+ else
205
+ val
206
+ end
207
+ else
208
+ # Specific named field (authority, name, system_status, etc.)
209
+ field.each do |key, val|
210
+ next if val.nil?
211
+ h[key] = (val.is_a?(Hash) && val.key?(:string)) ? unquote(val) : val.to_s
212
+ end
213
+ end
214
+ end
215
+ h
216
+ end
217
+
218
+ def extract_relations(arr)
219
+ Array(arr).map do |r|
220
+ {
221
+ type: r[:type].to_s,
222
+ system: unquote(r[:system]),
223
+ note: r[:note] && unquote(r[:note])
224
+ }.compact
225
+ end
226
+ end
227
+
228
+ def extract_aliases(arr)
229
+ Array(arr).map do |a|
230
+ {name: ident(a[:name]), value: materialize(a[:value])}
231
+ end
232
+ end
233
+
234
+ def extract_tests(arr)
235
+ Array(arr).map do |t|
236
+ next {input: "", expected: ""} unless t.is_a?(Hash)
237
+
238
+ input_val = t[:input]
239
+ expected_val = t[:expected]
240
+ note_val = t[:note]
241
+
242
+ {
243
+ input: input_val.is_a?(Hash) ? unquote(input_val) : input_val.to_s,
244
+ expected: expected_val.is_a?(Hash) ? unquote(expected_val) : expected_val.to_s,
245
+ note: note_val.is_a?(Hash) ? unquote(note_val) : note_val&.to_s
246
+ }.compact
247
+ end
248
+ end
249
+
250
+ def extract_stage(item)
251
+ node = item[:stage]
252
+ name = ident(item[:stage_name])
253
+ body = Array(node).flat_map { |n| extract_stage_items(n) }
254
+ {name: name, body: body}
255
+ end
256
+
257
+ def extract_stage_items(n)
258
+ return [] unless n.is_a?(Hash)
259
+ if n[:sequence]
260
+ [{kind: :sequence, rules: filter_noop(Array(n[:sequence]).map { |r| extract_rule(r) })}]
261
+ elsif n[:parallel]
262
+ [{kind: :parallel, rules: filter_noop(Array(n[:parallel]).map { |r| extract_rule(r) })}]
263
+ elsif n[:separate]
264
+ [{kind: :separate, separator: n[:separator] ? materialize(n[:separator]) : nil}]
265
+ elsif n[:compose]
266
+ # The grammar captures both `compose` and `decompose` under this
267
+ # key — the value decides the directive. Dropping it compiled
268
+ # every decompose as NFC compose.
269
+ [{kind: n[:compose].to_s.to_sym}]
270
+ elsif n[:case]
271
+ item = {kind: :string_case, op: n[:case].to_s}
272
+ item[:kwargs] = extract_kwargs(n[:case_kwargs]) if n[:case_kwargs]
273
+ [item]
274
+ elsif n[:funcall_name]
275
+ [{kind: :funcall, name: n[:funcall_name].to_s, kwargs: extract_kwargs(n[:funcall_kwargs])}]
276
+ elsif n[:dep]
277
+ [{kind: :run, dependency: ident(n[:dep]), stage: ident(n[:stage])}]
278
+ elsif n[:run_stage_only]
279
+ [{kind: :run, dependency: nil, stage: ident(n[:run_stage_only][:stage])}]
280
+ elsif n[:bare_rule]
281
+ [{kind: :bare_rule, rule: extract_rule(n[:bare_rule])}]
282
+ elsif n[:comment]
283
+ []
284
+ elsif n[:noop]
285
+ []
286
+ else
287
+ []
288
+ end
289
+ end
290
+
291
+ # Filter out noop rules (from: None, to: None) created by empty
292
+ # parallel/sequence blocks that contain only comments or whitespace.
293
+ def filter_noop(rules)
294
+ rules.reject do |r|
295
+ r.is_a?(Hash) &&
296
+ r[:from].is_a?(Items::None) &&
297
+ r[:to].is_a?(Items::None)
298
+ end
299
+ end
300
+
301
+ def extract_rule(r)
302
+ from_val = r.is_a?(Hash) ? r[:from] : nil
303
+ to_val = r.is_a?(Hash) ? r[:to] : nil
304
+ constraints_val = r.is_a?(Hash) ? r[:constraints] : nil
305
+ {
306
+ from: from_val ? materialize(from_val) : Items::None.new,
307
+ to: to_val ? materialize(to_val) : Items::None.new,
308
+ constraints: extract_constraints(constraints_val)
309
+ }
310
+ end
311
+
312
+ def extract_constraints(arr)
313
+ Array(arr).map do |c|
314
+ # c is a one-key hash like { before: <item> }. Apply Transform to
315
+ # get { kind: :before, item: <materialized> }.
316
+ transformed = @transform.apply(c)
317
+ if transformed.is_a?(Hash) && transformed.key?(:kind)
318
+ transformed
319
+ else
320
+ # Transform rule didn't match — likely an empty/None constraint.
321
+ {kind: nil, item: nil}
322
+ end
323
+ end
324
+ end
325
+
326
+ def extract_dependency(item)
327
+ {
328
+ target: unquote(item[:target]),
329
+ alias: item[:alias] && ident(item[:alias])
330
+ }.compact
331
+ end
332
+
333
+ # Convert a grammar tree fragment into a concrete Item object via
334
+ # Transform. Reuses the builder's Transform instance — allocating
335
+ # one per fragment dominated large-map builds.
336
+ def materialize(fragment)
337
+ case fragment
338
+ when Hash
339
+ if fragment.key?(:concatenation)
340
+ @transform.apply(fragment)
341
+ else
342
+ @transform.apply(concatenation: [fragment])
343
+ end
344
+ when Array
345
+ @transform.apply(concatenation: fragment)
346
+ when NilClass
347
+ Items::None.new
348
+ else
349
+ fragment
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interscript
4
+ module Isc
5
+ # Serializes a Node::Document back to ISC source — the inverse of
6
+ # Parser + DocumentBuilder + NodeAdapter. The Python compiler
7
+ # bridge generates ISC from in-memory documents because the
8
+ # rewritten python runtime executes ISC sources, not compiled
9
+ # modules. Every construct the generator emits must round-trip:
10
+ # spec/interscript/isc/generator_spec.rb pins behavior equality.
11
+ class Generator
12
+ def self.generate(doc)
13
+ new(doc).generate
14
+ end
15
+
16
+ def initialize(doc)
17
+ @doc = doc
18
+ end
19
+
20
+ def generate
21
+ out = "system #{quote(@doc.name.to_s)} {\n"
22
+ out << generate_aliases
23
+ out << generate_dependencies
24
+ @doc.stages.each do |name, group|
25
+ out << " stage #{name} {\n" << generate_group(group, 2) << " }\n"
26
+ end
27
+ out << "}\n"
28
+ end
29
+
30
+ private
31
+
32
+ def generate_aliases
33
+ return "" if @doc.aliases.empty?
34
+
35
+ out = +" aliases {\n"
36
+ @doc.aliases.each_value do |alias_def|
37
+ out << " #{alias_def.name} = #{item(alias_def.data)}\n"
38
+ end
39
+ out << " }\n"
40
+ end
41
+
42
+ def generate_dependencies
43
+ return "" if @doc.dependencies.empty?
44
+
45
+ out = +""
46
+ @doc.dependencies.each do |dep|
47
+ out << " dependency #{quote(dep.full_name.to_s)}"
48
+ out << " as #{dep.name}" if dep.name
49
+ out << "\n"
50
+ end
51
+ out
52
+ end
53
+
54
+ def generate_group(group, indent)
55
+ Array(group.children).map { |child| emit(child, indent) }.join
56
+ end
57
+
58
+ def emit(child, indent)
59
+ pad = " " * indent
60
+ case child
61
+ when Interscript::Node::Group::Parallel
62
+ "#{pad}parallel {\n#{generate_group(child, indent + 2)}#{pad}}\n"
63
+ when Interscript::Node::Group
64
+ generate_group(child, indent)
65
+ when Interscript::Node::Rule::Sub
66
+ rule(child, pad)
67
+ when Interscript::Node::Rule::Run
68
+ "#{pad}run #{run_target(child)}\n"
69
+ when Interscript::Node::Rule::Funcall
70
+ funcall(child, pad)
71
+ else
72
+ raise UnsupportedConstruct, "cannot generate #{child.class} as a stage item"
73
+ end
74
+ end
75
+
76
+ def rule(sub, pad)
77
+ out = "#{pad}sub {\n"
78
+ out << "#{pad} from #{item(sub.from)}\n"
79
+ out << "#{pad} to #{target(sub.to)}\n"
80
+ {before: sub.before, after: sub.after,
81
+ not_before: sub.not_before, not_after: sub.not_after}.each do |kw, value|
82
+ out << "#{pad} #{kw} #{item(value)}\n" if value
83
+ end
84
+ out << "#{pad}}\n"
85
+ end
86
+
87
+ def target(to)
88
+ return to.to_s if %i[upcase downcase].include?(to)
89
+
90
+ item(to)
91
+ end
92
+
93
+ def run_target(run)
94
+ stage = run.stage
95
+ stage.map ? "map.#{stage.map}.stage.#{stage.name}" : "stage.#{stage.name}"
96
+ end
97
+
98
+ def funcall(rule, pad)
99
+ case rule.name
100
+ when :separate
101
+ separator = rule.kwargs[:separator]
102
+ return "#{pad}separate\n" if separator.nil?
103
+
104
+ "#{pad}separate separator #{item(separator)}\n"
105
+ when :title_case
106
+ word_separator = rule.kwargs[:word_separator]
107
+ return "#{pad}title_case\n" if word_separator.nil?
108
+
109
+ "#{pad}title_case word_separator: #{quote(word_separator)}\n"
110
+ else
111
+ unless rule.kwargs.empty?
112
+ raise UnsupportedConstruct,
113
+ "cannot generate #{rule.name}(...) with kwargs as ISC"
114
+ end
115
+
116
+ "#{pad}#{rule.name}\n"
117
+ end
118
+ end
119
+
120
+ # -- items --
121
+
122
+ def item(i)
123
+ case i
124
+ when Interscript::Node::Item::String
125
+ quote(i.data)
126
+ when Interscript::Node::Item::Any
127
+ any(i)
128
+ when Interscript::Node::Item::Alias
129
+ i.map ? "#{i.map}.#{identifier(i.name)}" : identifier(i.name)
130
+ when Interscript::Node::Item::CaptureGroup
131
+ "capture(#{item(i.data)})"
132
+ when Interscript::Node::Item::CaptureRef
133
+ "ref(#{i.id})"
134
+ when Interscript::Node::Item::Group
135
+ i.children.map { |c| item(c) }.join(" + ")
136
+ when Interscript::Node::Item::Maybe
137
+ "maybe(#{item(i.data)})"
138
+ when Interscript::Node::Item::Some
139
+ "some(#{item(i.data)})"
140
+ when ::String
141
+ quote(i)
142
+ when ::Symbol
143
+ identifier(i)
144
+ else
145
+ raise UnsupportedConstruct, "cannot generate #{i.class} as an item"
146
+ end
147
+ end
148
+
149
+ def any(node)
150
+ case node.value
151
+ when ::String
152
+ "any(#{quote(node.value)})"
153
+ when ::Range
154
+ "any(#{quote(node.value.first)}..#{quote(node.value.last)})"
155
+ when ::Array
156
+ inner = node.value.map { |v| item(v) }
157
+ return "any(#{inner.first})" if inner.size == 1
158
+
159
+ "any([#{inner.join(", ")}])"
160
+ else
161
+ raise UnsupportedConstruct, "cannot generate Any of #{node.value.class}"
162
+ end
163
+ end
164
+
165
+ def identifier(sym)
166
+ name = sym.to_s
167
+ raise UnsupportedConstruct, "#{name.inspect} is not a bare identifier" unless /\A[A-Za-z_][A-Za-z0-9_]*\z/.match?(name)
168
+
169
+ name
170
+ end
171
+
172
+ ESCAPE_MAP = {
173
+ "\\" => "\\\\",
174
+ "\"" => "\\\""
175
+ }.freeze
176
+
177
+ def quote(str)
178
+ escaped = str.to_s.gsub(/[\\"]/) { |c| ESCAPE_MAP.fetch(c) }
179
+ # Only control and DEL characters need escaping; both parsers
180
+ # read raw UTF-8, including astral code points (surrogate-pair
181
+ # \u escapes are NOT re-joined).
182
+ escaped = escaped.each_char.map { |c|
183
+ (c.ord < 0x20 || c.ord == 0x7f) ? format("\\u%04x", c.ord) : c
184
+ }.join
185
+ "\"" + escaped + "\""
186
+ end
187
+
188
+ class UnsupportedConstruct < StandardError; end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Interscript
6
+ module Isc
7
+ module Grammar
8
+ module Concerns
9
+ # Aliases block: named expressions reused throughout the system.
10
+ module Aliases
11
+ include Parsanol
12
+
13
+ rule(:aliases_block) do
14
+ str("aliases") >> whitespace? >>
15
+ braced(alias_decl.repeat(0)).as(:aliases)
16
+ end
17
+
18
+ rule(:alias_decl) do
19
+ whitespace? >>
20
+ identifier.as(:name) >> whitespace? >>
21
+ str("=") >> whitespace? >>
22
+ item.as(:value) >>
23
+ whitespace?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Interscript
6
+ module Isc
7
+ module Grammar
8
+ module Concerns
9
+ # Dependencies: references to other systems whose stages may be invoked.
10
+ module Dependencies
11
+ include Parsanol
12
+
13
+ rule(:dependency_decl) do
14
+ str("dependency") >> whitespace >>
15
+ quoted_string.as(:target) >>
16
+ (whitespace >> str("as") >> whitespace >>
17
+ identifier.as(:alias)).maybe
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end