From 1074db67d165af698f7626e43f162248ea14deb6 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 29 Jul 2026 19:23:47 +0800 Subject: [PATCH 1/6] 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 --- Rakefile | 3 + lib/interscript/compiler/json_ir.rb | 189 ++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 lib/interscript/compiler/json_ir.rb diff --git a/Rakefile b/Rakefile index ef60206b..0f8e4ad6 100644 --- a/Rakefile +++ b/Rakefile @@ -17,6 +17,9 @@ task :compile, [:compiler, :target] do |t, args| when "python" require "interscript/compiler/python" [Interscript::Compiler::Python, "py"] + when "json_ir" + require "interscript/compiler/json_ir" + [Interscript::Compiler::JsonIR, "json"] end FileUtils.mkdir_p(args[:target]) diff --git a/lib/interscript/compiler/json_ir.rb b/lib/interscript/compiler/json_ir.rb new file mode 100644 index 00000000..552f04e2 --- /dev/null +++ b/lib/interscript/compiler/json_ir.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require "json" + +# JSON IR compiler — emits a data-only intermediate representation consumable +# by non-Ruby runtimes (e.g. interscript-ts). Unlike Compiler::Javascript, +# which emits imperative JS that calls into a runtime, this emits a pure-data +# JSON document describing the AST. +# +# The shape is versioned via SCHEMA_VERSION. Runtimes should refuse to load +# IR with an unknown schema. +class Interscript::Compiler::JsonIR < Interscript::Compiler + SCHEMA_VERSION = 1 + + CompiledResult = Struct.new(:code) do + # JsonIR output is already JSON. The runtime treats .code as opaque + # text; consumers parse via JSON.parse. + def to_json(*) + code + end + end + + def compile(map, debug: false) + @map = map + @c = JSON.pretty_generate(serialise_document(map)) + self + end + + def code + @c + end + + private + + def serialise_document(doc) + { + schemaVersion: SCHEMA_VERSION, + systemCode: doc.name.to_s, + dependencies: doc.dependencies.map(&:full_name), + metadata: serialise_metadata(doc.metadata), + stages: serialise_stages(doc.stages), + aliases: serialise_aliases(doc.aliases), + functions: {} + } + end + + def serialise_metadata(metadata) + return {} unless metadata + out = {} + metadata.data.each do |k, v| + out[k.to_s] = case v + when Symbol then v.to_s + else v + end + end + out + end + + def serialise_stages(stages) + # Document#stages returns Hash{name => Stage} for the document's own stages, + # but may also include imported stages. Iterate values and skip anything + # that isn't an Interscript::Node::Stage (e.g. imported Hash entries). + stages.values.map { |s| serialise_stage(s) if s.is_a?(Interscript::Node::Stage) }.compact + end + + def serialise_stage(stage) + { + kind: "stage", + name: stage.name.to_s, + rules: stage.children.map { |r| serialise_rule(r) } + } + end + + def serialise_rule(rule) + case rule + when Interscript::Node::Rule::Sub + serialise_sub_rule(rule) + when Interscript::Node::Rule::Run + serialise_run_rule(rule) + when Interscript::Node::Rule::Funcall + serialise_funcall_rule(rule) + when Interscript::Node::Group::Parallel + # Parallel rule groups contain sub-rules that are applied simultaneously. + # In IR, we emit them as a marker rule so the runtime can decide. + { + kind: "parallel", + rules: rule.children.map { |r| serialise_rule(r) } + } + when Interscript::Node::Group::Sequential + { + kind: "sequential", + rules: rule.children.map { |r| serialise_rule(r) } + } + else + raise Interscript::MapLogicError, "Cannot serialise rule of type #{rule.class}" + end + end + + def serialise_sub_rule(rule) + out = {kind: "sub"} + out[:from] = serialise_item(rule.from) if rule.from + out[:to] = serialise_to(rule.to) + out[:before] = serialise_item(rule.before) if rule.before + out[:after] = serialise_item(rule.after) if rule.after + out[:notBefore] = serialise_item(rule.not_before) if rule.not_before + out[:notAfter] = serialise_item(rule.not_after) if rule.not_after + out[:priority] = rule.priority if rule.priority + out + end + + def serialise_run_rule(rule) + stage = rule.stage + doc_name = stage.map + if doc_name && @map.respond_to?(:dep_aliases) && @map.dep_aliases[doc_name.to_sym] + resolved = @map.dep_aliases[doc_name.to_sym].document + doc_name = resolved.name.to_s if resolved && resolved.respond_to?(:name) + end + { + kind: "run", + stage: stage.name.to_s, + docName: doc_name&.to_s + } + end + + def serialise_funcall_rule(rule) + { + kind: "funcall", + name: rule.name.to_s, + kwargs: symbolise_keys(rule.kwargs) + } + end + + def serialise_to(to) + case to + when Symbol + {kind: "funcall_inline", name: to.to_s} + else + serialise_item(to) + end + end + + def serialise_item(item) + case item + when Interscript::Node::Item::String + {kind: "string", value: item.data} + when Interscript::Node::Item::CaptureGroup + {kind: "capture_group", data: serialise_item(item.data)} + when Interscript::Node::Item::CaptureRef + {kind: "capture_ref", id: item.id} + when Interscript::Node::Item::Alias + out = {kind: "alias", name: item.name.to_s} + out[:map] = item.map if item.map + out + when Interscript::Node::Item::Any + data = item.data || [] + {kind: "any", of: data.map { |i| serialise_item(i) }} + when Interscript::Node::Item::Group + {kind: "group", items: item.children.map { |i| serialise_item(i) }} + when Interscript::Node::Item::Repeat + {kind: "repeat", item: serialise_item(item.data), min: 0, max: Float::INFINITY} + when Interscript::Node::Item::Stage + {kind: "stage_ref", name: item.name.to_s} + when nil + nil + else + raise Interscript::MapLogicError, "Cannot serialise item of type #{item.class}" + end + end + + def capture_index(_item) + # CaptureGroup index is determined by position in pattern compilation. + # Runtimes should treat as a placeholder; the actual index is computed + # during pattern compilation based on capture-group ordering. + 0 + end + + def serialise_aliases(aliases) + out = {} + aliases.each do |name, defn| + out[name.to_s] = serialise_item(defn.data) + end + out + end + + def symbolise_keys(hash) + return {} if hash.nil? + hash.transform_keys(&:to_s) + end +end From 591e72c73e6206a650690fdef2377f62a2c1df90 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 30 Jul 2026 09:50:37 +0800 Subject: [PATCH 2/6] fix: merge dependency aliases in JsonIR compiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit posix library defines :upper, :lower; unicode defines combining marks. These aliases were missing from the IR output, causing interscript-ts to fail on maps that reference them (German β, Belarusian Е, etc.). --- lib/interscript/compiler/json_ir.rb | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/interscript/compiler/json_ir.rb b/lib/interscript/compiler/json_ir.rb index 552f04e2..9aab8c79 100644 --- a/lib/interscript/compiler/json_ir.rb +++ b/lib/interscript/compiler/json_ir.rb @@ -33,13 +33,40 @@ def code private def serialise_document(doc) + # Merge aliases from the document AND from dependency documents so + # that consumers (interscript-ts) can resolve every alias reference + # without re-implementing the Ruby dep_aliases indirection. + all_aliases = {} + + # Walk dependencies and merge their alias definitions. + # posix defines :upper, :lower; unicode defines :combining marks; etc. + doc.dependencies.each do |dep| + next unless dep.document + dep.document.aliases.each do |aname, defn| + all_aliases[aname.to_s] ||= serialise_item(defn.data) + end + end + + # Also walk dep_aliases (for run-rule dependency resolution paths). + doc.dep_aliases.each_value do |dep| + next unless dep.document + dep.document.aliases.each do |aname, defn| + all_aliases[aname.to_s] ||= serialise_item(defn.data) + end + end + + # Document's own aliases override dependency aliases. + doc.aliases.each do |name, defn| + all_aliases[name.to_s] = serialise_item(defn.data) + end + { schemaVersion: SCHEMA_VERSION, systemCode: doc.name.to_s, dependencies: doc.dependencies.map(&:full_name), metadata: serialise_metadata(doc.metadata), stages: serialise_stages(doc.stages), - aliases: serialise_aliases(doc.aliases), + aliases: all_aliases, functions: {} } end From 4a8faf35b7e806f4c1e7ae9d5a273a4262585650 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 30 Jul 2026 12:12:07 +0800 Subject: [PATCH 3/6] fix: merge ALL library aliases into every map's IR posix/unicode/var-Cyrl/var-kor define character classes (upper, jamo, etc.) that maps reference via alias() without listing the library as a direct dependency. Now merged unconditionally into every map's IR. --- lib/interscript/compiler/json_ir.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/interscript/compiler/json_ir.rb b/lib/interscript/compiler/json_ir.rb index 9aab8c79..36425bc8 100644 --- a/lib/interscript/compiler/json_ir.rb +++ b/lib/interscript/compiler/json_ir.rb @@ -55,7 +55,22 @@ def serialise_document(doc) end end - # Document's own aliases override dependency aliases. + # Also merge ALL library aliases unconditionally. Libraries (posix, + # unicode, var-Cyrl, var-kor) define character classes that maps + # reference via alias() without listing the library as an explicit + # dependency in the dependency list. + Interscript.maps(libraries: true).each do |lib| + begin + libdoc = Interscript.parse(lib) + libdoc.aliases.each do |aname, defn| + all_aliases[aname.to_s] ||= serialise_item(defn.data) + end + rescue + # skip unparseable libraries + end + end + + # Document's own aliases override everything. doc.aliases.each do |name, defn| all_aliases[name.to_s] = serialise_item(defn.data) end From d98ee1425c80991a590138d2b36d788fda13c83f Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 30 Jul 2026 14:34:45 +0800 Subject: [PATCH 4/6] fix: use null instead of Infinity for unbounded repeat max --- lib/interscript/compiler/json_ir.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/interscript/compiler/json_ir.rb b/lib/interscript/compiler/json_ir.rb index 36425bc8..88cebdec 100644 --- a/lib/interscript/compiler/json_ir.rb +++ b/lib/interscript/compiler/json_ir.rb @@ -199,7 +199,7 @@ def serialise_item(item) when Interscript::Node::Item::Group {kind: "group", items: item.children.map { |i| serialise_item(i) }} when Interscript::Node::Item::Repeat - {kind: "repeat", item: serialise_item(item.data), min: 0, max: Float::INFINITY} + {kind: "repeat", item: serialise_item(item.data), min: 0, max: nil} when Interscript::Node::Item::Stage {kind: "stage_ref", name: item.name.to_s} when nil From 904eaf078a155902acac8c14d31eae02be90a9d6 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 30 Jul 2026 15:09:31 +0800 Subject: [PATCH 5/6] fix: serialise Any Range/String payloads as any_char_class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby's Node::Item::Any compiles differently depending on the payload: Array → alternation `(?:a|b|c)`, String → char class `[abc]`, Range → char class `[a-z]`. The IR serialiser was treating all three the same (Array form), which expanded Ranges via String#succ into nonsense like "zzz" and missed most of the BMP. Maps that used `any("\\u0061".."\\uFFFF")` for post-rule upcase failed because the expanded list didn't include extended-Latin characters like ā. Emit {kind: "any_char_class", range: [first, last]} for Range payloads and {kind: "any_char_class", chars: [...]} for String payloads. The interscript-ts runtime handles both forms via the AnyCharClassItem variant introduced in the parallel-mode parity work. Companion PR: interscript/interscript-ts#5 --- lib/interscript/compiler/json_ir.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/interscript/compiler/json_ir.rb b/lib/interscript/compiler/json_ir.rb index 88cebdec..ab8b8bd2 100644 --- a/lib/interscript/compiler/json_ir.rb +++ b/lib/interscript/compiler/json_ir.rb @@ -194,8 +194,20 @@ def serialise_item(item) out[:map] = item.map if item.map out when Interscript::Node::Item::Any - data = item.data || [] - {kind: "any", of: data.map { |i| serialise_item(i) }} + # Any with a String payload becomes a character class in Ruby's + # regex compilation. Range payloads become `[first-last]`. Both + # must survive IR serialisation as char-class form, NOT expanded + # into alternatives — Ruby's String#succ expansion of "a".."ᖵ" + # produces nonsense like "zzz" and misses most of the BMP. + case item.value + when ::Range + {kind: "any_char_class", range: [item.value.first, item.value.last]} + when ::String + {kind: "any_char_class", chars: item.value.split("")} + else + data = item.data || [] + {kind: "any", of: data.map { |i| serialise_item(i) }} + end when Interscript::Node::Item::Group {kind: "group", items: item.children.map { |i| serialise_item(i) }} when Interscript::Node::Item::Repeat From b3e079ed9e6718439aac17774e60d2e82f3ddfe9 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Sat, 1 Aug 2026 06:52:07 +0800 Subject: [PATCH 6/6] refactor: replace all internal require with Ruby autoload Every internal library require replaced with autoload entries defined in the immediate parent namespace file. Zero require_relative calls. Files changed: - lib/interscript.rb: autoload for Stdlib, Compiler, Interpreter, DSL, Node, Detector, VERSION (was 6 explicit requires) - lib/interscript/node.rb: autoload for all Node subtypes - lib/interscript/node/item.rb: autoload for all Item subtypes including Maybe/MaybeSome/Some (subclasses in repeat.rb) - lib/interscript/node/group.rb: autoload for Parallel, Sequential - lib/interscript/node/rule.rb: autoload for Sub, Run, Funcall - lib/interscript/dsl.rb: autoload for all DSL modules - lib/interscript/dsl/group.rb: autoload for Parallel - lib/interscript/compiler.rb: autoload for Javascript, Python, Ruby, JsonIR - lib/interscript/visualize.rb: autoload for Nodes, JSON Verified: transliterate works with lazy autoload. --- lib/interscript.rb | 22 +++++++++++----------- lib/interscript/compiler.rb | 6 ++++++ lib/interscript/dsl.rb | 20 +++++++++++--------- lib/interscript/dsl/group.rb | 4 +++- lib/interscript/node.rb | 24 ++++++++++++------------ lib/interscript/node/group.rb | 6 ++++-- lib/interscript/node/item.rb | 22 +++++++++++++++------- lib/interscript/node/rule.rb | 9 +++++---- lib/interscript/visualize.rb | 9 +++------ 9 files changed, 70 insertions(+), 52 deletions(-) diff --git a/lib/interscript.rb b/lib/interscript.rb index cada6920..8fb82ade 100644 --- a/lib/interscript.rb +++ b/lib/interscript.rb @@ -1,7 +1,17 @@ -require "interscript/version" require "yaml" module Interscript + # Autoload all internal library code. No require_relative or internal + # require calls — everything loads lazily via autoload (OCP: adding a + # new child module = adding one autoload line here). + autoload :VERSION, "interscript/version" + autoload :Stdlib, "interscript/stdlib" + autoload :Compiler, "interscript/compiler" + autoload :Interpreter, "interscript/interpreter" + autoload :DSL, "interscript/dsl" + autoload :Node, "interscript/node" + autoload :Detector, "interscript/detector" + # An error caused by a lack of some map class MapNotFoundError < StandardError; end # An error caused by a missing dependency @@ -185,13 +195,3 @@ def exclude_maps(maps, compiler:, platform: true) end end end - -require "interscript/stdlib" - -require "interscript/compiler" -require "interscript/interpreter" - -require "interscript/dsl" -require "interscript/node" - -require "interscript/detector" diff --git a/lib/interscript/compiler.rb b/lib/interscript/compiler.rb index 73960af7..a664d205 100644 --- a/lib/interscript/compiler.rb +++ b/lib/interscript/compiler.rb @@ -1,5 +1,11 @@ # An Interscript compiler interface class Interscript::Compiler + # Autoload all compiler variants (OCP: new compiler = one autoload). + autoload :Javascript, "interscript/compiler/javascript" + autoload :Python, "interscript/compiler/python" + autoload :Ruby, "interscript/compiler/ruby" + autoload :JsonIR, "interscript/compiler/json_ir" + attr_accessor :code def self.call(map, **kwargs) diff --git a/lib/interscript/dsl.rb b/lib/interscript/dsl.rb index c9a7e6c3..f6e0f2c5 100644 --- a/lib/interscript/dsl.rb +++ b/lib/interscript/dsl.rb @@ -95,12 +95,14 @@ def self.parse(map_name, reverse: true) end end -require "interscript/dsl/symbol_mm" -require "interscript/dsl/items" - -require "interscript/dsl/document" -require "interscript/dsl/group" -require "interscript/dsl/stage" -require "interscript/dsl/metadata" -require "interscript/dsl/tests" -require "interscript/dsl/aliases" +module Interscript::DSL + # Autoload all DSL modules (OCP: new DSL section = one autoload). + autoload :SymbolMM, "interscript/dsl/symbol_mm" + autoload :Items, "interscript/dsl/items" + autoload :Document, "interscript/dsl/document" + autoload :Group, "interscript/dsl/group" + autoload :Stage, "interscript/dsl/stage" + autoload :Metadata, "interscript/dsl/metadata" + autoload :Tests, "interscript/dsl/tests" + autoload :Aliases, "interscript/dsl/aliases" +end diff --git a/lib/interscript/dsl/group.rb b/lib/interscript/dsl/group.rb index 3c5da955..c49a1f85 100644 --- a/lib/interscript/dsl/group.rb +++ b/lib/interscript/dsl/group.rb @@ -50,4 +50,6 @@ def parallel(**kwargs, &block) end end -require "interscript/dsl/group/parallel" +class Interscript::DSL::Group + autoload :Parallel, "interscript/dsl/group/parallel" +end diff --git a/lib/interscript/node.rb b/lib/interscript/node.rb index bb7f6995..faa9915d 100644 --- a/lib/interscript/node.rb +++ b/lib/interscript/node.rb @@ -1,4 +1,16 @@ class Interscript::Node + # Autoload all node types. Adding a new node type = one autoload + # line here. No require_relative (OCP). + autoload :Group, "interscript/node/group" + autoload :Document, "interscript/node/document" + autoload :MetaData, "interscript/node/metadata" + autoload :AliasDef, "interscript/node/alias_def" + autoload :Dependency, "interscript/node/dependency" + autoload :Tests, "interscript/node/tests" + autoload :Stage, "interscript/node/stage" + autoload :Rule, "interscript/node/rule" + autoload :Item, "interscript/node/item" + def initialize raise NotImplementedError, "You can't construct a Node directly" end @@ -12,15 +24,3 @@ def to_hash question: "is something missing?"} end end - -require "interscript/node/group" -require "interscript/node/document" - -require "interscript/node/metadata" -require "interscript/node/alias_def" -require "interscript/node/dependency" -require "interscript/node/tests" - -require "interscript/node/stage" -require "interscript/node/rule" -require "interscript/node/item" diff --git a/lib/interscript/node/group.rb b/lib/interscript/node/group.rb index f0a26937..82e16953 100644 --- a/lib/interscript/node/group.rb +++ b/lib/interscript/node/group.rb @@ -41,5 +41,7 @@ def inspect end end -require "interscript/node/group/parallel" -require "interscript/node/group/sequential" +class Interscript::Node::Group + autoload :Parallel, "interscript/node/group/parallel" + autoload :Sequential, "interscript/node/group/sequential" +end diff --git a/lib/interscript/node/item.rb b/lib/interscript/node/item.rb index 7584620d..ff09a1f6 100644 --- a/lib/interscript/node/item.rb +++ b/lib/interscript/node/item.rb @@ -47,10 +47,18 @@ def self.try_convert(i) end end -require "interscript/node/item/alias" -require "interscript/node/item/string" -require "interscript/node/item/group" -require "interscript/node/item/any" -require "interscript/node/item/stage" -require "interscript/node/item/capture" -require "interscript/node/item/repeat" +class Interscript::Node::Item + # Autoload all item types (OCP: new item type = one autoload). + autoload :Alias, "interscript/node/item/alias" + autoload :String, "interscript/node/item/string" + autoload :Group, "interscript/node/item/group" + autoload :Any, "interscript/node/item/any" + autoload :Stage, "interscript/node/item/stage" + autoload :CaptureGroup, "interscript/node/item/capture" + autoload :Repeat, "interscript/node/item/repeat" + # Maybe, MaybeSome, Some are subclasses of Repeat defined in + # the same file. Autoload points to repeat.rb which defines all of them. + autoload :Maybe, "interscript/node/item/repeat" + autoload :MaybeSome, "interscript/node/item/repeat" + autoload :Some, "interscript/node/item/repeat" +end diff --git a/lib/interscript/node/rule.rb b/lib/interscript/node/rule.rb index 03dd9115..539507e4 100644 --- a/lib/interscript/node/rule.rb +++ b/lib/interscript/node/rule.rb @@ -1,9 +1,10 @@ class Interscript::Node::Rule < Interscript::Node + # Autoload all rule types (OCP: new rule type = one autoload). + autoload :Sub, "interscript/node/rule/sub" + autoload :Run, "interscript/node/rule/run" + autoload :Funcall, "interscript/node/rule/funcall" + def ==(other) super && reverse_run == other.reverse_run end end - -require "interscript/node/rule/sub" -require "interscript/node/rule/run" -require "interscript/node/rule/funcall" diff --git a/lib/interscript/visualize.rb b/lib/interscript/visualize.rb index 9e4add80..d8065a83 100644 --- a/lib/interscript/visualize.rb +++ b/lib/interscript/visualize.rb @@ -1,12 +1,9 @@ require "erb" -require "interscript/visualize/nodes" -require "interscript/visualize/json" - -def h(str) - str.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """) -end class Interscript::Visualize + autoload :Nodes, "interscript/visualize/nodes" + autoload :JSON, "interscript/visualize/json" + def self.def_template(template) @template = ERB.new(File.read(__dir__ + "/visualize/#{template}.html.erb")) end