Skip to content

Commit 1074db6

Browse files
committed
feat: add Compiler::JsonIR for interscript-ts compatibility
Adds a new compiler that walks the AST and emits JSON IR consumed by interscript-ts. Mirrors the structure of Compiler::Javascript but produces data, not code. ## IR schema (v1) - schemaVersion: 1 - systemCode, dependencies[], metadata, stages[], aliases, functions ## Stage serialisation - Each Stage becomes { kind: 'stage', name, rules: [...] } - Group::Parallel -> { kind: 'parallel', rules: [...] } - Group::Sequential -> { kind: 'sequential', rules: [...] } ## Rule serialisation - Sub: from/to/before/after/notBefore/notAfter/priority (omitted if nil) - Run: stage name + resolved docName (dependency alias -> system code) - Funcall: name + kwargs ## Item serialisation - String, CaptureGroup, CaptureRef, Alias, Any, Group, Repeat, Stage ## Resolution - Run rule's docName resolves via dep_aliases so consumers don't need the Ruby dep_aliases indirection ## Rakefile - New task compile:json_ir (parallel to existing compile:javascript) Refs: interscript/interscript#3
1 parent 3ee44b3 commit 1074db6

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

Rakefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ task :compile, [:compiler, :target] do |t, args|
1717
when "python"
1818
require "interscript/compiler/python"
1919
[Interscript::Compiler::Python, "py"]
20+
when "json_ir"
21+
require "interscript/compiler/json_ir"
22+
[Interscript::Compiler::JsonIR, "json"]
2023
end
2124

2225
FileUtils.mkdir_p(args[:target])
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
# JSON IR compiler — emits a data-only intermediate representation consumable
6+
# by non-Ruby runtimes (e.g. interscript-ts). Unlike Compiler::Javascript,
7+
# which emits imperative JS that calls into a runtime, this emits a pure-data
8+
# JSON document describing the AST.
9+
#
10+
# The shape is versioned via SCHEMA_VERSION. Runtimes should refuse to load
11+
# IR with an unknown schema.
12+
class Interscript::Compiler::JsonIR < Interscript::Compiler
13+
SCHEMA_VERSION = 1
14+
15+
CompiledResult = Struct.new(:code) do
16+
# JsonIR output is already JSON. The runtime treats .code as opaque
17+
# text; consumers parse via JSON.parse.
18+
def to_json(*)
19+
code
20+
end
21+
end
22+
23+
def compile(map, debug: false)
24+
@map = map
25+
@c = JSON.pretty_generate(serialise_document(map))
26+
self
27+
end
28+
29+
def code
30+
@c
31+
end
32+
33+
private
34+
35+
def serialise_document(doc)
36+
{
37+
schemaVersion: SCHEMA_VERSION,
38+
systemCode: doc.name.to_s,
39+
dependencies: doc.dependencies.map(&:full_name),
40+
metadata: serialise_metadata(doc.metadata),
41+
stages: serialise_stages(doc.stages),
42+
aliases: serialise_aliases(doc.aliases),
43+
functions: {}
44+
}
45+
end
46+
47+
def serialise_metadata(metadata)
48+
return {} unless metadata
49+
out = {}
50+
metadata.data.each do |k, v|
51+
out[k.to_s] = case v
52+
when Symbol then v.to_s
53+
else v
54+
end
55+
end
56+
out
57+
end
58+
59+
def serialise_stages(stages)
60+
# Document#stages returns Hash{name => Stage} for the document's own stages,
61+
# but may also include imported stages. Iterate values and skip anything
62+
# that isn't an Interscript::Node::Stage (e.g. imported Hash entries).
63+
stages.values.map { |s| serialise_stage(s) if s.is_a?(Interscript::Node::Stage) }.compact
64+
end
65+
66+
def serialise_stage(stage)
67+
{
68+
kind: "stage",
69+
name: stage.name.to_s,
70+
rules: stage.children.map { |r| serialise_rule(r) }
71+
}
72+
end
73+
74+
def serialise_rule(rule)
75+
case rule
76+
when Interscript::Node::Rule::Sub
77+
serialise_sub_rule(rule)
78+
when Interscript::Node::Rule::Run
79+
serialise_run_rule(rule)
80+
when Interscript::Node::Rule::Funcall
81+
serialise_funcall_rule(rule)
82+
when Interscript::Node::Group::Parallel
83+
# Parallel rule groups contain sub-rules that are applied simultaneously.
84+
# In IR, we emit them as a marker rule so the runtime can decide.
85+
{
86+
kind: "parallel",
87+
rules: rule.children.map { |r| serialise_rule(r) }
88+
}
89+
when Interscript::Node::Group::Sequential
90+
{
91+
kind: "sequential",
92+
rules: rule.children.map { |r| serialise_rule(r) }
93+
}
94+
else
95+
raise Interscript::MapLogicError, "Cannot serialise rule of type #{rule.class}"
96+
end
97+
end
98+
99+
def serialise_sub_rule(rule)
100+
out = {kind: "sub"}
101+
out[:from] = serialise_item(rule.from) if rule.from
102+
out[:to] = serialise_to(rule.to)
103+
out[:before] = serialise_item(rule.before) if rule.before
104+
out[:after] = serialise_item(rule.after) if rule.after
105+
out[:notBefore] = serialise_item(rule.not_before) if rule.not_before
106+
out[:notAfter] = serialise_item(rule.not_after) if rule.not_after
107+
out[:priority] = rule.priority if rule.priority
108+
out
109+
end
110+
111+
def serialise_run_rule(rule)
112+
stage = rule.stage
113+
doc_name = stage.map
114+
if doc_name && @map.respond_to?(:dep_aliases) && @map.dep_aliases[doc_name.to_sym]
115+
resolved = @map.dep_aliases[doc_name.to_sym].document
116+
doc_name = resolved.name.to_s if resolved && resolved.respond_to?(:name)
117+
end
118+
{
119+
kind: "run",
120+
stage: stage.name.to_s,
121+
docName: doc_name&.to_s
122+
}
123+
end
124+
125+
def serialise_funcall_rule(rule)
126+
{
127+
kind: "funcall",
128+
name: rule.name.to_s,
129+
kwargs: symbolise_keys(rule.kwargs)
130+
}
131+
end
132+
133+
def serialise_to(to)
134+
case to
135+
when Symbol
136+
{kind: "funcall_inline", name: to.to_s}
137+
else
138+
serialise_item(to)
139+
end
140+
end
141+
142+
def serialise_item(item)
143+
case item
144+
when Interscript::Node::Item::String
145+
{kind: "string", value: item.data}
146+
when Interscript::Node::Item::CaptureGroup
147+
{kind: "capture_group", data: serialise_item(item.data)}
148+
when Interscript::Node::Item::CaptureRef
149+
{kind: "capture_ref", id: item.id}
150+
when Interscript::Node::Item::Alias
151+
out = {kind: "alias", name: item.name.to_s}
152+
out[:map] = item.map if item.map
153+
out
154+
when Interscript::Node::Item::Any
155+
data = item.data || []
156+
{kind: "any", of: data.map { |i| serialise_item(i) }}
157+
when Interscript::Node::Item::Group
158+
{kind: "group", items: item.children.map { |i| serialise_item(i) }}
159+
when Interscript::Node::Item::Repeat
160+
{kind: "repeat", item: serialise_item(item.data), min: 0, max: Float::INFINITY}
161+
when Interscript::Node::Item::Stage
162+
{kind: "stage_ref", name: item.name.to_s}
163+
when nil
164+
nil
165+
else
166+
raise Interscript::MapLogicError, "Cannot serialise item of type #{item.class}"
167+
end
168+
end
169+
170+
def capture_index(_item)
171+
# CaptureGroup index is determined by position in pattern compilation.
172+
# Runtimes should treat as a placeholder; the actual index is computed
173+
# during pattern compilation based on capture-group ordering.
174+
0
175+
end
176+
177+
def serialise_aliases(aliases)
178+
out = {}
179+
aliases.each do |name, defn|
180+
out[name.to_s] = serialise_item(defn.data)
181+
end
182+
out
183+
end
184+
185+
def symbolise_keys(hash)
186+
return {} if hash.nil?
187+
hash.transform_keys(&:to_s)
188+
end
189+
end

0 commit comments

Comments
 (0)