Login
gpcr/spec/regex_subset_spec.cr
Login

[edit]

spec/regex_subset_spec.cr @ trunk

spec

requireย 'spec'
scale = 0.75
fontscale = 1.500
box wid 1.380 ht 0.340 with .nw at (0.035, -0.035) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.380 ht 0.340 with .nw at (0.000, 0.000) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.022) fill 0xffffff color 0xffffff
line from (0.140, 0.000) to (0.140, -0.340) color white
line from (1.240, 0.000) to (1.240, -0.340) color white
text "require 'spec'" color white at (0.690, -0.170)

crystal source

require "spec"

json block

{
  "kind": "require",
  "name": "spec",
  "doc": null,
  "children": []
}

json

requireย 'json'
scale = 0.75
fontscale = 1.500
box wid 1.330 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.330 ht 0.340 with .nw at (0.000, -0.420) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.443) fill 0xffffff color 0xffffff
line from (0.140, -0.420) to (0.140, -0.760) color white
line from (1.190, -0.420) to (1.190, -0.760) color white
text "require 'json'" color white at (0.665, -0.590)

crystal source

require "json"

json block

{
  "kind": "require",
  "name": "json",
  "doc": null,
  "children": []
}

../src/gpcr/regex_subset

requireย '../src/gpcr/regex_subset'
scale = 0.75
fontscale = 1.500
box wid 2.894 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.894 ht 0.340 with .nw at (0.000, -0.840) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.863) fill 0xffffff color 0xffffff
line from (0.140, -0.840) to (0.140, -1.180) color white
line from (2.754, -0.840) to (2.754, -1.180) color white
text "require '../src/gpcr/regex_subset'" color white at (1.447, -1.010)

crystal source

require "../src/gpcr/regex_subset"

json block

{
  "kind": "require",
  "name": "../src/gpcr/regex_subset",
  "doc": null,
  "children": []
}

../examples/robots_grammar

requireย '../examples/robots_grammar'
scale = 0.75
fontscale = 1.500
box wid 3.258 ht 0.340 with .nw at (0.035, -1.295) fill 0xCBD5E1 color 0xCBD5E1
box wid 3.258 ht 0.340 with .nw at (0.000, -1.260) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.283) fill 0xffffff color 0xffffff
line from (0.140, -1.260) to (0.140, -1.600) color white
line from (3.118, -1.260) to (3.118, -1.600) color white
text "require '../examples/robots_grammar'" color white at (1.629, -1.430)

crystal source

require "../examples/robots_grammar"

json block

{
  "kind": "require",
  "name": "../examples/robots_grammar",
  "doc": null,
  "children": []
}

EXPECTED_SOURCE = {
  "main_head"  => "๐ŸโŸฎ๐ŸโŸฏ๐Ÿ”–โŽ",
  "def_head"   => "๐Ÿ”•๐‘ฅ(โŸฎ(๐‘ฅ(๏ผŒ๐‘ฅ)*)?โŸฏ)?โŽ",
  "global"     => "๐ŸŒโŸฎ๐‘ฅ๏ผŒ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŸฏโŽ",
  "literal"    => "[๐Ÿ”ข๐Ÿ”š]",
  "call0"      => "โˆ‰",
  "call"       => "๐‘ฅโŸฎ([โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](๏ผŒ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ])*)?โŸฏ",
  "call1"      => "โˆŠโŸฎ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŸฏ",
  "call2"      => "โˆ‹โŸฎ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]๏ผŒ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŸฏ",
  "paren"      => "โŸฎ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŸฏ",
  "neg"        => "(?<![โ„–๐Ÿ๐Ÿ˜‘๐‘ฅโŸฏ])โŠ–[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]",
  "mul"        => "(?<![โŠ—๏ผโŠ˜๏ผ…])[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ][โŠ—๏ผโŠ˜๏ผ…][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]",
  "add"        => "(?<![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–])[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ][โŠ•โŠ–][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?![โŠ—๏ผโŠ˜๏ผ…])",
  "cmp"        => "(?<![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝ])[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ][โ‰บโ‰ปโ‰ผโ‰ฝ][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–])",
  "eq"         => "(?<![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝโ‰Ÿโ‰ ])[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ][โ‰Ÿโ‰ ][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝ])",
  "and"        => "(?<![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝโ‰Ÿโ‰ โˆง])[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ][โˆง][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝโ‰Ÿโ‰ ])",
  "or"         => "(?<![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝโ‰Ÿโ‰ โˆงโˆจโŠป])[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ][โˆจโŠป][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?![โŠ—๏ผโŠ˜๏ผ…โŠ•โŠ–โ‰บโ‰ปโ‰ผโ‰ฝโ‰Ÿโ‰ โˆง])",
  "command1"   => "โˆŠ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?=[โŽโŸฏ๏ผŒ])",
  "command2"   => "โˆ‹[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]๏ผŒ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?=[โŽโŸฏ๏ผŒ])",
  "assign"     => "๐‘ฅ๏ผ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?=[โŽโŸฏ๏ผŒ])",
  "opassign"   => "๐‘ฅ[โž•โž–โœ–โ’][โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ](?=[โŽโŸฏ๏ผŒ])",
  "if_head"    => "๐Ÿ”œ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "elsif_head" => "๐Ÿ”˜[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "while_head" => "๐Ÿ”ฃ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "until_head" => "๐Ÿ”‚[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "case_head"  => "๐Ÿ””[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "when_head"  => "๐Ÿ”ถ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "return"     => "โ†ฉ[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]?โŽ",
  "break"      => "๐Ÿ”“โŽ",
  "exprstmt"   => "[โ„–๐Ÿ๐Ÿ˜‘๐‘ฅ]โŽ",
  "if"         => "๐Ÿ…ธโข*(๐Ÿ…ดโข*)*(๐Ÿ”—โŽโข*)?๐Ÿ”™โŽ",
  "while"      => "๐Ÿ††โข*๐Ÿ”™โŽ",
  "until"      => "๐Ÿ†„โข*๐Ÿ”™โŽ",
  "case"       => "๐Ÿ…ฒ(๐Ÿ†‚โข*)+(๐Ÿ”—โŽโข*)?๐Ÿ”™โŽ",
  "def"        => "๐Ÿ…ณโข*๐Ÿ”™โŽ",
  "main"       => "๐Ÿ…ผโข*๐Ÿ”™โŽ",
  "program"    => "\\Aโข+\\z",
}

describe "GPCR::RegexSubset against crystal-robots' real GRAMMAR table" do
  it "ROBOTS_GRAMMAR covers every rule (36, verified against parser.cr 2026-09-12)" do
    ROBOTS_GRAMMAR.size.should eq(36)
    ROBOTS_GRAMMAR.map(&.name).to_set.should eq(EXPECTED_SOURCE.keys.to_set)
  end

  ROBOTS_GRAMMAR.each do |rule|
    expected = EXPECTED_SOURCE[rule.name]

    it "#{rule.name}: to_source matches the hand-written regex literal exactly" do
      GPCR::RegexSubset.to_source(rule.pattern).should eq(expected)
    end

    it "#{rule.name}: emitted source builds a working Regex" do
      GPCR::RegexSubset.to_regex(rule.pattern).should be_a(Regex)
    end

    it "#{rule.name}: round-trips through JSON with identical source" do
      json = GPCR::RegexSubset.pattern_to_json(rule.pattern)
      restored = GPCR::RegexSubset.pattern_from_json(JSON.parse(json))
      GPCR::RegexSubset.to_source(restored).should eq(expected)
    end
  end

  describe "behavioral spot checks on the trickiest rules" do
    it "mul (infix, no higher guard) matches a bare multiplication, not addition" do
      re = GPCR::RegexSubset.to_regex(ROBOTS_GRAMMAR.find! { |r| r.name == "mul" }.pattern)
      "โ„–โŠ—โ„–".should match(re)
      "โ„–โŠ•โ„–".should_not match(re)
    end

    it "add (infix, guarded by mul) matches a plain add but not one still claimed by a mul" do
      re = GPCR::RegexSubset.to_regex(ROBOTS_GRAMMAR.find! { |r| r.name == "add" }.pattern)
      "๐Ÿ˜‘โŠ•โ„–".should match(re)
      # the left operand of "โŠ•" here is still adjacent to an unreduced "โŠ—" โ€”
      # the behind-guard must block the whole match, forcing mul to reduce
      # โ„– โŠ— ๐Ÿ˜‘ into one value first. No match anywhere in the string proves
      # the guard is doing its job, not matching a different substring.
      "โ„–โŠ—๐Ÿ˜‘โŠ•โ„–".match(re).should be_nil
    end

    it "if (block rule) matches a minimal if/end with no elsif or else" do
      re = GPCR::RegexSubset.to_regex(ROBOTS_GRAMMAR.find! { |r| r.name == "if" }.pattern)
      "๐Ÿ…ธโข๐Ÿ”™โŽ".should match(re)
    end

    it "if (block rule) matches with elsif and else branches present" do
      re = GPCR::RegexSubset.to_regex(ROBOTS_GRAMMAR.find! { |r| r.name == "if" }.pattern)
      "๐Ÿ…ธโข๐Ÿ…ดโข๐Ÿ”—โŽโข๐Ÿ”™โŽ".should match(re)
    end

    it "case (block rule) requires at least one when branch" do
      re = GPCR::RegexSubset.to_regex(ROBOTS_GRAMMAR.find! { |r| r.name == "case" }.pattern)
      "๐Ÿ…ฒ๐Ÿ”™โŽ".should_not match(re)
      "๐Ÿ…ฒ๐Ÿ†‚โข๐Ÿ”™โŽ".should match(re)
    end
  end
end