@@ -208,3 +208,135 @@ def with_map(name, body)
208208 end
209209 end
210210end
211+
212+ RSpec . describe "ISC semantics — funcall and stdlib" do
213+ def with_map ( name , body )
214+ dir = Dir . mktmpdir
215+ File . write ( File . join ( dir , name ) , body )
216+ Interscript . load_path . unshift ( dir )
217+ yield
218+ ensure
219+ Interscript . load_path . delete ( dir ) if dir
220+ end
221+
222+ def system_map ( id , body )
223+ <<~ISC
224+ system "TST:xx-Latn:Latn:#{ id } " {
225+ metadata { authority_id tst; id #{ id } ; language iso-639-2:xx; source_script Latn; destination_script Latn }
226+ #{ body }
227+ }
228+ ISC
229+ end
230+
231+ it "rules: run <stdlib-name> dispatches the stdlib function" do
232+ with_map ( "tst-fn-1.isc" , system_map ( 51 , "stage main { run downcase }" ) ) do
233+ expect ( Interscript . transliterate ( "tst-fn-1" , "AbC" ) ) . to eq ( "abc" )
234+ end
235+ end
236+
237+ it "rules: stage-level case directives run at their position, on the whole string" do
238+ with_map ( "tst-fn-2.isc" , system_map ( 52 ,
239+ "stage main { downcase parallel { sub { from \" b\" to \" B\" } } }" ) ) do
240+ # downcase applies before the sub (rule order), so b -> B still fires
241+ expect ( Interscript . transliterate ( "tst-fn-2" , "AB" ) ) . to eq ( "aB" )
242+ end
243+ end
244+
245+ it "rules: run accepts no kwargs — kwargs belong to declared funcall directives (rababa)" do
246+ with_map ( "tst-fn-3.isc" , system_map ( 53 , "stage main { run separate separator: \" -\" }" ) ) do
247+ expect { Interscript . transliterate ( "tst-fn-3" , "abc" ) }
248+ . to raise_error ( Interscript ::Isc ::ParseError )
249+ end
250+ end
251+
252+ it "rules: stray identifiers are consumed as no-ops — pinned as a hazard" do
253+ # HAZARD (A4 follow-up): `run nosuchfunction` drops BOTH identifiers
254+ # at parse time and the stage becomes empty — a typo'd map is
255+ # silently wrong. Ruled normative-for-now (legacy .imp corpus
256+ # tolerance, grammar/concerns/stages.rb comment_item); the stricter
257+ # parse is an open item tracked in the IS-1 v2 text, gated on a
258+ # clean parse of the full 293-map corpus across all three compilers.
259+ with_map ( "tst-fn-4.isc" , system_map ( 54 , "stage main { run nosuchfunction parallel { sub { from \" a\" to \" b\" } } }" ) ) do
260+ expect ( Interscript . transliterate ( "tst-fn-4" , "a" ) ) . to eq ( "b" )
261+ end
262+ end
263+ end
264+
265+ RSpec . describe "ISC semantics — item algebra and boundary" do
266+ def with_map ( name , body )
267+ dir = Dir . mktmpdir
268+ File . write ( File . join ( dir , name ) , body )
269+ Interscript . load_path . unshift ( dir )
270+ yield
271+ ensure
272+ Interscript . load_path . delete ( dir ) if dir
273+ end
274+
275+ def system_map ( id , body )
276+ <<~ISC
277+ system "TST:xx-Latn:Latn:#{ id } " {
278+ metadata { authority_id tst; id #{ id } ; language iso-639-2:xx; source_script Latn; destination_script Latn }
279+ #{ body }
280+ }
281+ ISC
282+ end
283+
284+ it "rules: any(<chars>) is a character set of length 1" do
285+ with_map ( "tst-item-1.isc" , system_map ( 61 ,
286+ "stage main { parallel { sub { from any(\" bc\" ) to \" X\" } } }" ) ) do
287+ expect ( Interscript . transliterate ( "tst-item-1" , "abcd" ) ) . to eq ( "aXXd" )
288+ end
289+ end
290+
291+ it "rules: any(<lo>..<hi>) is an inclusive codepoint range" do
292+ with_map ( "tst-item-2.isc" , system_map ( 62 ,
293+ "stage main { parallel { sub { from any(\" 0\" ..\" 9\" ) to \" N\" } } }" ) ) do
294+ expect ( Interscript . transliterate ( "tst-item-2" , "a7b" ) ) . to eq ( "aNb" )
295+ end
296+ end
297+
298+ it "rules: capture + ref substitutes the captured source text" do
299+ with_map ( "tst-item-3.isc" , system_map ( 63 ,
300+ "stage main { sequence { sub { from capture(\" a\" ) + \" b\" to ref(1) + \" Y\" } } }" ) ) do
301+ expect ( Interscript . transliterate ( "tst-item-3" , "ab" ) ) . to eq ( "aY" )
302+ end
303+ end
304+
305+ it "rules: boundary is a zero-width word-edge, not string-edge-only" do
306+ with_map ( "tst-item-4.isc" , system_map ( 64 ,
307+ "stage main { parallel { sub { from \" a\" to \" X\" after boundary } } }" ) ) do
308+ # string end IS a word edge
309+ expect ( Interscript . transliterate ( "tst-item-4" , "xa" ) ) . to eq ( "xX" )
310+ # mid-word: no edge after a
311+ expect ( Interscript . transliterate ( "tst-item-4" , "ab" ) ) . to eq ( "ab" )
312+ end
313+ end
314+ end
315+
316+ RSpec . describe "ISC semantics — metadata contract and embedded tests" do
317+ def with_map ( name , body )
318+ dir = Dir . mktmpdir
319+ File . write ( File . join ( dir , name ) , body )
320+ Interscript . load_path . unshift ( dir )
321+ yield
322+ ensure
323+ Interscript . load_path . delete ( dir ) if dir
324+ end
325+
326+ it "rules: embedded tests are normative data — never executed by transliterate" do
327+ dir = Dir . mktmpdir
328+ File . write ( File . join ( dir , "tst-test-1.isc" ) , <<~ISC )
329+ system "TST:xx-Latn:Latn:71" {
330+ metadata { authority_id tst; id 71; language iso-639-2:xx; source_script Latn; destination_script Latn }
331+ stage main { parallel { sub { from "a" to "b" } } }
332+ tests { "zzz" -> "wrong" }
333+ }
334+ ISC
335+ Interscript . load_path . unshift ( dir )
336+ # a deliberately wrong expectation does not fire; the verification
337+ # layer (conformance runner / artifact build) executes tests
338+ expect ( Interscript . transliterate ( "tst-test-1" , "a" ) ) . to eq ( "b" )
339+ ensure
340+ Interscript . load_path . delete ( dir ) if dir
341+ end
342+ end
0 commit comments