@@ -263,3 +263,113 @@ def parse_and_adapt(src)
263263 expect ( node . name ) . to eq ( "X:a-b:C-D:1" )
264264 end
265265end
266+
267+ RSpec . describe "NodeAdapter range handling" do
268+ it "keeps ISC ranges as native Any ranges — codepoint semantics, not string-succ expansion" do
269+ src = <<~ISC
270+ system "TEST:aze-Arab:Latn:2026" {
271+ metadata { name "T" }
272+ stage main {
273+ parallel { sub "q" "k" }
274+ sub { from any("a".."") to upcase before boundary }
275+ }
276+ }
277+ ISC
278+ node = Interscript ::Isc ::NodeAdapter . to_interscript_node (
279+ Interscript ::Isc ::DocumentBuilder . build ( Interscript ::Isc ::Parser . parse ( src ) )
280+ )
281+ upcase_rule = node . stages [ :main ] . children
282+ . select { |c | c . is_a? ( Interscript ::Node ::Rule ::Sub ) }
283+ . find { |r | r . to == :upcase }
284+ # A Ruby String range expands via String#succ (a, b, ..., z, aa, ab …)
285+ # which never reaches non-ASCII codepoints — the range must stay a range.
286+ expect ( upcase_rule . from . value ) . to be_a ( Range )
287+ interp = Interscript ::Interpreter . new
288+ interp . compile ( node )
289+ expect ( interp . call ( "īş" ) ) . to eq ( "Īş" )
290+ end
291+ end
292+
293+ RSpec . describe "NodeAdapter any-list constraints" do
294+ it "keeps primitives as aliases, never their inspect" do
295+ tree = Interscript ::Isc ::Parser . parse (
296+ 'system "x" { stage main { sub { from "ം" to "m" after any([boundary, "", ""]) } } }'
297+ )
298+ doc = Interscript ::Isc ::DocumentBuilder . build ( tree )
299+ node = Interscript ::Isc ::NodeAdapter . to_interscript_node ( doc )
300+ stage = Interscript ::Interpreter ::Stage . new ( node , "" )
301+ re = stage . send ( :build_regexp , node . stages [ :main ] . children . first )
302+ # The constraint must match boundary positions — an inspect leak
303+ # turns the lookahead into a garbage char class.
304+ expect ( re ) . not_to include ( "Primitive" )
305+ expect ( "പ്പം ഹ" ) . to match ( Regexp . new ( re ) )
306+ end
307+ end
308+
309+ RSpec . describe "NodeAdapter string escapes" do
310+ it "decodes escape sequences in test strings" do
311+ src = <<~'ISC'
312+ system "T:a-b:C-D:1" {
313+ metadata { name "T" }
314+ tests {
315+ "pod\"ezd" -> "p\"ezd"
316+ "a\tb\nc" -> "déjà"
317+ }
318+ stage main { sub "a" "b" }
319+ }
320+ ISC
321+ node = Interscript ::Isc ::NodeAdapter . to_interscript_node (
322+ Interscript ::Isc ::DocumentBuilder . build ( Interscript ::Isc ::Parser . parse ( src ) )
323+ )
324+ tests = node . tests . data
325+ expect ( tests [ 0 ] ) . to eq ( [ 'pod"ezd' , 'p"ezd' ] )
326+ expect ( tests [ 1 ] ) . to eq ( [ "a\t b\n c" , "déjà" ] )
327+ end
328+ end
329+
330+ RSpec . describe "NodeAdapter alias refs in constraints" do
331+ it "compiles bare alias refs in sets like the legacy DSL — no debug dump, no literal-charset lookbehind" do
332+ src = <<~ISC
333+ system "t:g-c:C-D:1" {
334+ metadata { name "T" }
335+ stage main {
336+ sub {
337+ from "γ"
338+ to "n"
339+ before any(greek)
340+ after any("κΚ") + any(greek)
341+ not_before boundary
342+ }
343+ }
344+ }
345+ ISC
346+ node = Interscript ::Isc ::NodeAdapter . to_interscript_node (
347+ Interscript ::Isc ::DocumentBuilder . build ( Interscript ::Isc ::Parser . parse ( src ) )
348+ )
349+ rule = node . stages [ :main ] . children . first
350+ expect ( rule . before ) . to be_a ( Interscript ::Node ::Item ::Any )
351+ expect ( rule . before . inspect ) . not_to include ( "alias_ref" )
352+ expect ( rule . after . inspect ) . not_to include ( "alias_ref" )
353+ # The legacy runtime compiles an imported alias in a set to a vacuous
354+ # fragment (Any(nil)); resolving it to the charset *string* would bake
355+ # a 1000-char literal into the lookbehind and never match.
356+ interp = Interscript ::Interpreter . new
357+ interp . compile ( node )
358+ expect ( interp . call ( "αγκα" ) ) . to eq ( "αnκα" )
359+ end
360+ end
361+
362+ RSpec . describe "NodeAdapter document name derivation" do
363+ it "names the document after the file, not the system code — transliteration addresses maps by file name" do
364+ tree = Interscript ::Isc ::Parser . parse (
365+ 'system "TOTALLY:diff-Code:frm:File" { stage main { sub { from "a" to "b" } } }'
366+ )
367+ doc = Interscript ::Isc ::DocumentBuilder . build ( tree , filename : "test-map.isc" )
368+ node = Interscript ::Isc ::NodeAdapter . to_interscript_node ( doc )
369+ # The Ruby compiler registers compiled maps under Document#name and
370+ # Maps.transliterate looks them up by the map name (file-derived).
371+ # Registering under the system code auto-creates an empty entry via
372+ # the Hash default block and crashes on stages[:main].call.
373+ expect ( node . name ) . to eq ( "test-map" )
374+ end
375+ end
0 commit comments