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.
- checksums.yaml +4 -4
- data/README.md +15 -0
- data/docs/demo/20191118-interscript-demo-cast.gif +0 -0
- data/exe/codemod-imp-to-isc +5 -0
- data/exe/diagnose_parse_failures +47 -0
- data/exe/interscript +2 -1
- data/exe/verify_isc_deep +211 -0
- data/exe/verify_isc_equivalence +128 -0
- data/interscript.gemspec +27 -20
- data/lib/interscript/command.rb +21 -12
- data/lib/interscript/compiler/javascript.rb +34 -35
- data/lib/interscript/compiler/json_ir.rb +214 -0
- data/lib/interscript/compiler/python.rb +354 -0
- data/lib/interscript/compiler/ruby.rb +23 -22
- data/lib/interscript/compiler.rb +20 -3
- data/lib/interscript/detector.rb +14 -7
- data/lib/interscript/dsl/aliases.rb +1 -1
- data/lib/interscript/dsl/document.rb +6 -3
- data/lib/interscript/dsl/group/parallel.rb +1 -1
- data/lib/interscript/dsl/group.rb +18 -9
- data/lib/interscript/dsl/items.rb +28 -16
- data/lib/interscript/dsl/metadata.rb +19 -18
- data/lib/interscript/dsl/stage.rb +1 -1
- data/lib/interscript/dsl/symbol_mm.rb +4 -2
- data/lib/interscript/dsl/tests.rb +1 -1
- data/lib/interscript/dsl.rb +21 -13
- data/lib/interscript/interpreter.rb +32 -21
- data/lib/interscript/isc/codemod.rb +791 -0
- data/lib/interscript/isc/document_builder.rb +354 -0
- data/lib/interscript/isc/generator.rb +191 -0
- data/lib/interscript/isc/grammar/concerns/aliases.rb +29 -0
- data/lib/interscript/isc/grammar/concerns/dependencies.rb +23 -0
- data/lib/interscript/isc/grammar/concerns/items.rb +176 -0
- data/lib/interscript/isc/grammar/concerns/metadata.rb +142 -0
- data/lib/interscript/isc/grammar/concerns/primitives.rb +112 -0
- data/lib/interscript/isc/grammar/concerns/stages.rb +129 -0
- data/lib/interscript/isc/grammar/concerns/system.rb +37 -0
- data/lib/interscript/isc/grammar/concerns/tests.rb +31 -0
- data/lib/interscript/isc/grammar/concerns.rb +18 -0
- data/lib/interscript/isc/grammar/core.rb +23 -0
- data/lib/interscript/isc/grammar/isc.artifact.json +1 -0
- data/lib/interscript/isc/grammar/isc.parg +196 -0
- data/lib/interscript/isc/grammar.rb +12 -0
- data/lib/interscript/isc/items.rb +185 -0
- data/lib/interscript/isc/model/alias.rb +20 -0
- data/lib/interscript/isc/model/constraint.rb +20 -0
- data/lib/interscript/isc/model/dependency.rb +19 -0
- data/lib/interscript/isc/model/document.rb +31 -0
- data/lib/interscript/isc/model/item.rb +50 -0
- data/lib/interscript/isc/model/rule.rb +23 -0
- data/lib/interscript/isc/model/stage.rb +20 -0
- data/lib/interscript/isc/model/stage_item.rb +33 -0
- data/lib/interscript/isc/model/test.rb +21 -0
- data/lib/interscript/isc/model.rb +19 -0
- data/lib/interscript/isc/node_adapter.rb +310 -0
- data/lib/interscript/isc/normalizer.rb +127 -0
- data/lib/interscript/isc/parser.rb +78 -0
- data/lib/interscript/isc/serializer.rb +248 -0
- data/lib/interscript/isc/transform.rb +138 -0
- data/lib/interscript/isc/yaml_bridge.rb +236 -0
- data/lib/interscript/isc.rb +30 -0
- data/lib/interscript/ml/byt5_onnx.rb +102 -0
- data/lib/interscript/ml/imf.rb +182 -0
- data/lib/interscript/ml/model.rb +52 -0
- data/lib/interscript/ml/provisioning.rb +189 -0
- data/lib/interscript/ml/translator.rb +38 -0
- data/lib/interscript/ml/vocab.rb +48 -0
- data/lib/interscript/ml.rb +25 -0
- data/lib/interscript/node/alias_def.rb +5 -5
- data/lib/interscript/node/dependency.rb +7 -7
- data/lib/interscript/node/document.rb +10 -10
- data/lib/interscript/node/group.rb +11 -9
- data/lib/interscript/node/item/alias.rb +14 -10
- data/lib/interscript/node/item/any.rb +14 -7
- data/lib/interscript/node/item/capture.rb +14 -9
- data/lib/interscript/node/item/group.rb +22 -17
- data/lib/interscript/node/item/repeat.rb +3 -3
- data/lib/interscript/node/item/stage.rb +4 -5
- data/lib/interscript/node/item/string.rb +18 -13
- data/lib/interscript/node/item.rb +27 -17
- data/lib/interscript/node/metadata.rb +6 -5
- data/lib/interscript/node/rule/funcall.rb +4 -5
- data/lib/interscript/node/rule/run.rb +4 -5
- data/lib/interscript/node/rule/sub.rb +55 -55
- data/lib/interscript/node/rule.rb +6 -5
- data/lib/interscript/node/stage.rb +11 -14
- data/lib/interscript/node/tests.rb +6 -6
- data/lib/interscript/node.rb +14 -17
- data/lib/interscript/stdlib/functions/rababa_adapter.rb +56 -0
- data/lib/interscript/stdlib/functions/secryst_adapter.rb +37 -0
- data/lib/interscript/stdlib/functions.rb +49 -0
- data/lib/interscript/stdlib.rb +25 -95
- data/lib/interscript/utils/helpers.rb +8 -5
- data/lib/interscript/utils/regexp_converter.rb +143 -144
- data/lib/interscript/version.rb +1 -1
- data/lib/interscript/visualize/json.rb +12 -13
- data/lib/interscript/visualize/nodes.rb +13 -13
- data/lib/interscript/visualize.rb +18 -18
- data/lib/interscript.rb +59 -45
- metadata +88 -31
- data/.github/workflows/assets.yml +0 -91
- data/.github/workflows/rake.yml +0 -52
- data/.github/workflows/release.yml +0 -68
- data/.gitignore +0 -79
- data/.rspec +0 -3
- data/Gemfile +0 -38
- data/Rakefile +0 -139
- data/bin/console +0 -10
- data/bin/interscript +0 -5
- data/bin/maps_analyze_staging +0 -168
- data/bin/maps_debug_compilers +0 -58
- data/bin/maps_debug_ordering +0 -88
- data/bin/maps_debug_ruby_compile +0 -24
- data/bin/maps_debug_step_by_step +0 -44
- data/bin/maps_optimize_order +0 -112
- data/bin/maps_v1_analyze_regexps +0 -45
- data/bin/maps_v1_to_v2 +0 -426
- data/bin/set_version +0 -16
- data/bin/setup +0 -8
- 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
|