Login
gpcr/spec/roundtrip_spec.cr
Login

[edit]

spec/roundtrip_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": []
}

compiler/crystal/syntax

require 'compiler/crystal/syntax'
scale = 0.75
fontscale = 1.500
box wid 2.796 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.796 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.656, -0.840) to (2.656, -1.180) color white
text "require 'compiler/crystal/syntax'" color white at (1.398, -1.010)

crystal source

require "compiler/crystal/syntax"

json block

{
  "kind": "require",
  "name": "compiler/crystal/syntax",
  "doc": null,
  "children": []
}

../src/gpcr/render

require '../src/gpcr/render'
scale = 0.75
fontscale = 1.500
box wid 2.343 ht 0.340 with .nw at (0.035, -1.295) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.343 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 (2.203, -1.260) to (2.203, -1.600) color white
text "require '../src/gpcr/render'" color white at (1.172, -1.430)

crystal source

require "../src/gpcr/render"

json block

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

../src/gpcr/draw

require '../src/gpcr/draw'
scale = 0.75
fontscale = 1.500
box wid 2.215 ht 0.340 with .nw at (0.035, -1.715) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.215 ht 0.340 with .nw at (0.000, -1.680) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.703) fill 0xffffff color 0xffffff
line from (0.140, -1.680) to (0.140, -2.020) color white
line from (2.075, -1.680) to (2.075, -2.020) color white
text "require '../src/gpcr/draw'" color white at (1.108, -1.850)

crystal source

require "../src/gpcr/draw"

json block

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

../src/gpcr/roundtrip

require '../src/gpcr/roundtrip'
scale = 0.75
fontscale = 1.500
box wid 2.530 ht 0.340 with .nw at (0.035, -2.135) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.530 ht 0.340 with .nw at (0.000, -2.100) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.123) fill 0xffffff color 0xffffff
line from (0.140, -2.100) to (0.140, -2.440) color white
line from (2.390, -2.100) to (2.390, -2.440) color white
text "require '../src/gpcr/roundtrip'" color white at (1.265, -2.270)

crystal source

require "../src/gpcr/roundtrip"

json block

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

private def compile_and_invoke(reconstructed : String, invoke_stmt : String, require_path : String = "../../src/gpcr") : String
  checkout = File.expand_path(File.join(__DIR__, ".."))
  tmp_path = File.join(checkout, "spec", "fixtures", "_invoke_check_#{Random.rand(1_000_000)}_tmp.cr")
  full_src = %(require "#{require_path}"\n\n) + reconstructed + "\n#{invoke_stmt}\n"
  begin
    File.write(tmp_path, full_src)
    stdout_io = IO::Memory.new
    stderr_io = IO::Memory.new
    result = Process.run("crystal", ["run", tmp_path], chdir: checkout, output: stdout_io, error: stderr_io)
    result.success?.should be_true, "crystal run failed:\n#{stderr_io}"
    stdout_io.to_s
  ensure
    File.delete(tmp_path) if File.exists?(tmp_path)
  end
end

# Simple graph fixture — no doc, no require, only unambiguous arg types.
RT_GRAPH_SRC = <<-'CRYSTAL'
@[GPCR::Graph]
def blink
  on_start
  forever do
    set_user_led true
    wait 0.5
    set_user_led false
    wait 0.5
  end
end
CRYSTAL

# Block fixture — body has a single call with no args.
RT_BLOCK_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :command, label: "hello world", category: "Test")]
def hello_world
  on_start
end
CRYSTAL

# Block with a typed param. Body is deliberately invoke-safe (no on_start —
# that blocks forever waiting on GPCR::Runtime's "start" channel, which
# nothing fires in a standalone invoke check) so the round-trip spec can
# actually CALL the reconstructed def, not just compile it uncalled.
RT_BLOCK_INPUTS_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :reporter, label: "greet #{n} times", category: "Test", returns: :number)]
def greet(n : Int32) : Int32
  n + 1
end
CRYSTAL

# fmt fixture — operator call body, typed Float64 param; used with face_map.
RT_FMT_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :reporter, label: "format #{v}", category: "Draw", returns: :string)]
def fmt(v : Float64) : String
  "%.3f" % v
end
CRYSTAL

# Record fixture.
RT_RECORD_SRC = <<-'CRYSTAL'
@[GPCR::Record]
record Point, x : Int32, y : Int32
CRYSTAL

# Constant fixture — integer value, no ambiguity.
RT_CONSTANT_SRC = <<-'CRYSTAL'
@[GPCR::Constant]
MAX_SIZE = 100
CRYSTAL

# Table schema fixture — no rows, clean type names.
RT_TABLE_SRC = <<-'CRYSTAL'
@[GPCR::TableSchema]
class Coord
  property x : Int32
  property y : Int32
end
CRYSTAL

# Block with if/while/case body nodes.
RT_BLOCK_CONTROL_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :command, label: "control demo", category: "Test")]
def control_demo
  if true
    on_start
  end
  while false
    on_start
  end
end
CRYSTAL

describe "gpcr_block_json_to_crystal roundtrip" do
  describe ":graph kind — blink fixture" do
    it "reconstructed source round-trips to the same JSON" do
      original_json = gpcr_serialize_node(RT_GRAPH_SRC, "blink")
      original_json.should_not be_empty
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("@[GPCR::Graph]")
      reconstructed.should contain("def blink")
      roundtrip_json = gpcr_serialize_node(reconstructed, "blink")
      original_json.should eq(roundtrip_json)
    end

    it "reconstructed source contains the c_call body structure" do
      json = gpcr_serialize_node(RT_GRAPH_SRC, "blink")
      reconstructed = gpcr_block_json_to_crystal(json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("forever do")
      reconstructed.should contain("set_user_led true")
      reconstructed.should contain("wait 0.5")
      reconstructed.should contain("end")
    end
  end

  describe ":block kind" do
    it "reconstructed source round-trips to the same JSON" do
      original_json = gpcr_serialize_node(RT_BLOCK_SRC, "hello_world")
      original_json.should_not be_empty
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("@[GPCR::Block(")
      reconstructed.should contain("def hello_world")
      roundtrip_json = gpcr_serialize_node(reconstructed, "hello_world")
      original_json.should eq(roundtrip_json)
    end

    it "block with inputs round-trips WITH its parameter (ticket a4942e5c88) and is actually callable" do
      original_json = gpcr_serialize_node(RT_BLOCK_INPUTS_SRC, "greet")
      original_json.should_not be_empty
      parsed_in = JSON.parse(original_json)
      parsed_in["inputs"]?.should_not be_nil
      parsed_in["inputs"].as_a.first["name"].as_s.should eq("n")
      parsed_in["inputs"].as_a.first["type"].as_s.should eq("Int32")
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("def greet(n : Int32)")
      # Invoke check: a dropped parameter would compile cleanly as def greet()
      # but fail here with "wrong number of arguments" — proving the
      # signature, not just the declaration, survived reconstruction.
      compile_and_invoke(reconstructed, "puts greet(3)").strip.should eq("4")
      roundtrip_json = gpcr_serialize_node(reconstructed, "greet")
      original_json.should eq(roundtrip_json)
    end

    it "block with if/while body round-trips to the same JSON" do
      original_json = gpcr_serialize_node(RT_BLOCK_CONTROL_SRC, "control_demo")
      original_json.should_not be_empty
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      roundtrip_json = gpcr_serialize_node(reconstructed, "control_demo")
      original_json.should eq(roundtrip_json)
    end

    it "fmt — operator-call body and typed param reconstruct as parseable Crystal" do
      faces = GPCR.blocks_of
      face_map = faces.reduce({} of String => GPCR::Face) { |m, f| m[f.name] = f; m }
      original_json = gpcr_serialize_node(RT_FMT_SRC, "fmt", face_map)
      original_json.should_not be_empty
      parsed_in = JSON.parse(original_json)
      parsed_in["inputs"]?.should_not be_nil
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("def fmt")
      reconstructed.should contain("Float64")
      reconstructed.should contain(%("%.3f" % v))
      # Invoke check: proves the Float64 param is real, not just declared.
      compile_and_invoke(reconstructed, "puts fmt(3.14159)").strip.should eq("3.142")
      roundtrip_json = gpcr_serialize_node(reconstructed, "fmt", face_map)
      original_json.should eq(roundtrip_json)
    end
  end

  describe ":other body node — no-op (Q3)" do
    it "returns empty string for :primitive root" do
      json = %({"kind":"primitive","name":"foo","doc":null,"children":[]})
      result = gpcr_block_json_to_crystal(json)
      Crystal::Parser.new(result).parse
      result.should eq("")
    end

    it "returns empty string for unknown root kind" do
      json = %({"kind":"require","name":"foo/bar","doc":null,"children":[]})
      result = gpcr_block_json_to_crystal(json)
      Crystal::Parser.new(result).parse
      result.should eq("")
    end

    it "other body nodes emit text verbatim" do
      other_src = <<-'CRYSTAL'
        @[GPCR::Graph]
        def other_demo
          x = 1 + 2
        end
        CRYSTAL
      other_json = gpcr_serialize_node(other_src, "other_demo")
      reconstructed = gpcr_block_json_to_crystal(other_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("def other_demo")
    end
  end

  describe ":record kind (Q4)" do
    it "reconstructed source round-trips to the same JSON" do
      original_json = gpcr_serialize_node(RT_RECORD_SRC, "Point")
      original_json.should_not be_empty
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("@[GPCR::Record]")
      reconstructed.should contain("record Point")
      reconstructed.should contain("x : Int32")
      roundtrip_json = gpcr_serialize_node(reconstructed, "Point")
      original_json.should eq(roundtrip_json)
    end
  end

  describe ":constant kind (Q4)" do
    it "reconstructed source round-trips to the same JSON" do
      original_json = gpcr_serialize_node(RT_CONSTANT_SRC, "MAX_SIZE")
      original_json.should_not be_empty
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("@[GPCR::Constant]")
      reconstructed.should contain("MAX_SIZE = 100")
      roundtrip_json = gpcr_serialize_node(reconstructed, "MAX_SIZE")
      original_json.should eq(roundtrip_json)
    end
  end

  describe ":table kind (Q4)" do
    it "empty-rows table round-trips to the same JSON" do
      original_json = gpcr_serialize_node(RT_TABLE_SRC, "Coord")
      original_json.should_not be_empty
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("@[GPCR::TableSchema]")
      reconstructed.should contain("struct Coord")
      roundtrip_json = gpcr_serialize_node(reconstructed, "Coord")
      original_json.should eq(roundtrip_json)
    end

    it "PACKAGES table with rows — string cells quoted, bool unquoted, JSON round-trips, compiles" do
      pkg_src = File.read(File.join(__DIR__, "..", "examples", "toolchain_packages.cr"))
      original_json = gpcr_serialize_node(pkg_src, "PACKAGES")
      original_json.should_not be_empty
      original_parsed = JSON.parse(original_json)
      original_parsed["type_name"]?.try(&.as_s).should eq("Package")
      reconstructed = gpcr_block_json_to_crystal(original_json)
      Crystal::Parser.new(reconstructed).parse
      reconstructed.should contain("@[GPCR::TableSchema]")
      reconstructed.should contain("struct Package")
      reconstructed.should contain("PACKAGES = [")
      reconstructed.should contain("Package.new(")
      reconstructed.should contain(%("--disable-nls --with-sysroot"))
      # Compile check: constant names are distinct so no "already initialized constant" error.
      checkout = File.expand_path(File.join(__DIR__, ".."))
      tmp_path = File.join(checkout, "spec", "fixtures", "_pkg_roundtrip_tmp.cr")
      begin
        File.write(tmp_path, %(require "../../src/gpcr"\n\n) + reconstructed)
        stderr_io = IO::Memory.new
        compile_result = Process.run(
          "crystal", ["build", "--no-codegen", tmp_path],
          chdir: checkout,
          output: Process::Redirect::Close,
          error: stderr_io
        )
        compile_result.success?.should be_true, "crystal build --no-codegen failed:\n#{stderr_io}"
      ensure
        File.delete(tmp_path) if File.exists?(tmp_path)
      end
      roundtrip_json = gpcr_serialize_node(reconstructed, "PACKAGES")
      original_json.should eq(roundtrip_json)
    end
  end
end