spec/self_conformance_spec.cr @ trunk
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)→ /pikchrshow
crystal source
require "spec"
json block
{
"kind": "require",
"name": "spec",
"doc": null,
"children": []
}
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)→ /pikchrshow
crystal source
require "json"
json block
{
"kind": "require",
"name": "json",
"doc": null,
"children": []
}
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)→ /pikchrshow
crystal source
require "compiler/crystal/syntax"
json block
{
"kind": "require",
"name": "compiler/crystal/syntax",
"doc": null,
"children": []
}
../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)→ /pikchrshow
crystal source
require "../src/gpcr/render"
json block
{
"kind": "require",
"name": "../src/gpcr/render",
"doc": null,
"children": []
}
../src/gpcr/apply
scale = 0.75 fontscale = 1.500 box wid 2.255 ht 0.340 with .nw at (0.035, -1.715) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.255 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.115, -1.680) to (2.115, -2.020) color white text "require '../src/gpcr/apply'" color white at (1.127, -1.850)→ /pikchrshow
crystal source
require "../src/gpcr/apply"
json block
{
"kind": "require",
"name": "../src/gpcr/apply",
"doc": null,
"children": []
}
../src/gpcr/census
scale = 0.75 fontscale = 1.500 box wid 2.393 ht 0.340 with .nw at (0.035, -2.135) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.393 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.253, -2.100) to (2.253, -2.440) color white text "require '../src/gpcr/census'" color white at (1.196, -2.270)→ /pikchrshow
crystal source
require "../src/gpcr/census"
json block
{
"kind": "require",
"name": "../src/gpcr/census",
"doc": null,
"children": []
}
PRODUCIBILITY_GAP_KINDS = [:c_call, :if, :while, :case, :when, :other]
# GPCR self-conformance spec.
#
# (a) Contract sweep: compiles a temp fixture requiring every src/gpcr/*.cr
# and calls GPCR.check_blocks, so any future file that omits its trailing
# check is still swept here.
#
# (b) Self-render: runs GPCR::Renderer over every src/gpcr/*.cr and asserts:
# - all backtick fences in the output are balanced
# - each file that declares @[GPCR::Block] defs has at least one section
# where the pikchr fence renders a body stack (face + children)
#
# (c) Pikchr syntax: each pikchr fence is validated via `fossil pikchr`;
# the whole group is pending-with-reason when the fossil binary is absent.
#
# (d) OP-PRODUCIBILITY: every @[GPCR::Graph] and @[GPCR::Block] def is checked
# for op-producibility. Flat :call bodies are round-tripped through Buffer.
# Non-producible node kinds are reported as an OP-COVERAGE GAP table.
#
# (e) CENSUS RATCHET: project-wide GPCR line percentage and per-file GPCR
# line counts must never decrease vs the committed baseline in
# spec/fixtures/census_baseline.json.
# Track fence state to determine if all backtick fences are balanced.
private def fences_balanced?(md : String) : Bool
open = false
marker = ""
md.each_line do |line|
s = line.rstrip
if !open
if m = s.match(/^(`{3,})/)
marker = m[1]
open = true
end
elsif s == marker
open = false
marker = ""
end
end
!open
end
# Extract pikchr fence bodies using stateful fence tracking.
# Uses chomp: false so each appended line retains its trailing newline,
# producing valid multiline pikchr content. Handles variable-length fence
# markers (3, 4, ... backticks), so backtick strings inside 4-backtick crystal
# fences are not confused with pikchr fence openers.
private def extract_pikchr_fences(md : String) : Array(String)
result = [] of String
in_fence = false
fence_marker = ""
is_pikchr = false
body = [] of String
md.each_line(chomp: false) do |line|
s = line.rstrip
if !in_fence
if m = s.match(/^(`{3,})/)
fence_marker = m[1]
is_pikchr = s.starts_with?(fence_marker + "pikchr")
in_fence = true
end
elsif s == fence_marker
result << body.join if is_pikchr
body.clear
in_fence = false
is_pikchr = false
elsif is_pikchr
body << line
end
end
result
end
# Check whether a section of rendered markdown for node_name has a body stack.
# A body stack appears below the face: face-only sections are ≤5 pikchr lines
# (scale + 2-4 face elements). Any body element adds ≥4 lines (command block:
# box + 2 tabs + text), so > 6 lines reliably signals a body stack.
private def section_has_body_stack?(md : String, node_name : String) : Bool
parts = md.split("## #{node_name}")
return false if parts.size < 2
pikchr = extract_pikchr_fences(parts[1]).first?
return false unless pikchr
pikchr.lines.size > 6
end
# ── Producibility helpers ─────────────────────────────────────────────────────
# Recursively collect all GPCR::Node kinds found in a body node list.
private def collect_body_kinds(nodes : Array(GPCR::Node)) : Array(Symbol)
kinds = [] of Symbol
nodes.each do |n|
kinds << n.kind
kinds.concat(collect_body_kinds(n.children))
kinds.concat(collect_body_kinds(n.else_nodes))
end
kinds.uniq
end
# Structural equality of two GPCR::Node arrays (kind/name/args/children, not line numbers).
private def nodes_struct_equal?(a : Array(GPCR::Node), b : Array(GPCR::Node)) : Bool
return false unless a.size == b.size
a.zip(b).all? do |(na, nb)|
na.kind == nb.kind &&
na.name == nb.name &&
na.args == nb.args &&
nodes_struct_equal?(na.children, nb.children) &&
nodes_struct_equal?(na.else_nodes, nb.else_nodes)
end
end
# Attempt a round-trip for a def whose body contains only flat :call nodes.
# Builds a skeleton (original source with body = first call only), then applies
# AddOp for each subsequent call, and compares the resulting Node tree.
# Returns {passed, reason}.
private def attempt_round_trip(source : String, def_name : String, orig_children : Array(GPCR::Node)) : {Bool, String}
return {true, "empty body"} if orig_children.empty?
index_nodes, params = GPCR::Ops::NodeIndexer.new(source).index
def_ln = index_nodes.find { |n| n.kind == :def && n.name == def_name }
return {false, "def not in NodeIndex"} unless def_ln
def_idx = def_ln.not_nil!.index
next_def = index_nodes.find { |n| n.kind == :def && n.index > def_idx }
upper = next_def.try(&.index) || (index_nodes.size + 1)
body_calls = index_nodes.select { |n| n.index > def_idx && n.index < upper && n.kind == :call }
if orig_children.size <= 1
# Trivial: single call or no calls — skeleton IS the original source.
# Just verify parse_source produces the same tree.
prims = {} of String => GPCR::Prim
result_nodes = GPCR.parse_source(source, prims)
result_def = result_nodes.find { |n| n.name == def_name }
return {false, "def not in parse result"} unless result_def
equal = nodes_struct_equal?(result_def.not_nil!.children, orig_children)
return {equal, "trivial-single (#{orig_children.size} call)"}
end
# Multi-call: build skeleton = source with body reduced to first call only.
lines = source.lines(chomp: false)
# Guard: NodeIndexer only recurses into @[GPCR::Graph] bodies, so @[GPCR::Block]
# defs with multiple children have no indexed body calls. Fall back to parse-tree
# verification only — AddOp targeting is unsupported for non-graph def bodies.
if body_calls.empty?
prims = {} of String => GPCR::Prim
result_nodes = GPCR.parse_source(source, prims)
result_def = result_nodes.find { |n| n.name == def_name }
return {false, "def not in parse result"} unless result_def
equal = nodes_struct_equal?(result_def.not_nil!.children, orig_children)
return {equal, "block def body not indexed by NodeIndexer; parse-tree verification only (#{orig_children.size} children)"}
end
to_remove = body_calls[1..].sort_by { |n| -n.line_start }
to_remove.each { |n| lines.delete_at(n.line_start - 1..n.line_end - 1) }
skeleton = lines.join
buf = GPCR::Ops::Buffer.new(skeleton)
# Apply AddOp for each subsequent call.
orig_children[1..].each_with_index do |gpcr_node, step|
# Find the last call inside this def in the current buffer.
cur_def = buf.nodes.find { |n| n.kind == :def && n.name == def_name }
return {false, "def lost from buffer at step #{step}"} unless cur_def
cur_def_idx = cur_def.not_nil!.index
next_cur = buf.nodes.find { |n| n.kind == :def && n.index > cur_def_idx }
cur_upper = next_cur.try(&.index) || (buf.nodes.size + 1)
cur_body = buf.nodes.select { |n|
n.index > cur_def_idx && n.index < cur_upper && n.kind == :call
}
last_ln = cur_body.last?
return {false, "no target call at step #{step}"} unless last_ln
# Derive slots from GPCR::Node args + the param map.
call_params = params[gpcr_node.name]? || [] of String
slots = {} of String => String
gpcr_node.args.each_with_index do |arg_val, ai|
pname = call_params[ai]?
slots[pname] = arg_val if pname
end
target = GPCR::Ops::IndexTarget.new(last_ln.not_nil!.index, buf.gen)
op = GPCR::Ops::AddOp.new(target, gpcr_node.name, slots, "after")
begin
buf.apply_one(op)
rescue e : GPCR::Ops::OpError
return {false, "AddOp step #{step} failed: #{e.message}"}
end
end
result_prims = {} of String => GPCR::Prim
result_nodes = GPCR.parse_source(buf.source, result_prims)
result_def = result_nodes.find { |n| n.name == def_name }
return {false, "def missing from result"} unless result_def
equal = nodes_struct_equal?(result_def.not_nil!.children, orig_children)
{equal, "multi-call (#{orig_children.size} calls) #{equal ? "PASS" : "FAIL — tree mismatch"}"}
end
describe "GPCR self-conformance" do
root = Path[__DIR__].join("..").normalize.to_s
gpcr_files = Dir.glob(File.join(root, "src", "gpcr", "*.cr")).sort
# Parse each file once at spec-registration time to decide which tests to
# add. This uses Crystal::Parser at runtime (not the Crystal compiler), so
# it's fast enough for spec setup.
file_data = gpcr_files.map do |path|
source = File.read(path)
prims = {} of String => GPCR::Prim
nodes = GPCR.parse_source(source, prims)
{path, source, nodes}
end
describe "(a) GPCR.check_blocks sweeps every src/gpcr/*.cr" do
it "compiles a fixture requiring all src/gpcr/*.cr and GPCR.check_blocks" do
tmp = File.tempfile("gpcr_conformance", ".cr", dir: __DIR__)
begin
# Require each src/gpcr file via path relative to spec/.
gpcr_files.each do |f|
rel = Path[f].relative_to(Path[__DIR__]).to_s
tmp.print %(require #{rel.inspect}\n)
end
# main.cr's standard_main macro requires ≥1 @[GPCR::Graph] def at
# compile time; provide a sentinel so the fixture compiles cleanly.
tmp.print <<-CR
@[GPCR::Graph(x: 0, y: 0)]
def _gpcr_conformance_sentinel
on_start
end
GPCR.check_blocks
CR
tmp.flush
err = IO::Memory.new
status = Process.run("crystal", ["build", "--no-codegen", tmp.path], error: err)
status.success?.should be_true, "crystal --no-codegen failed:\n#{err}"
ensure
tmp.delete
end
end
end
describe "(b) self-render: balanced fences and block stacks" do
file_data.each do |(path, source, nodes)|
filename = File.basename(path)
block_nodes = nodes.select { |n| n.kind == :block && !n.children.empty? }
it "#{filename}: all backtick fences are balanced" do
md = GPCR::Renderer.new(source).render
fences_balanced?(md).should be_true, "unbalanced fences in rendered #{filename}"
end
unless block_nodes.empty?
it "#{filename}: at least one @[GPCR::Block] section renders a body stack" do
md = GPCR::Renderer.new(source).render
found = block_nodes.any? { |n| section_has_body_stack?(md, n.name) }
found.should be_true, "no block section had a rendered body stack in #{filename}"
end
end
end
end
describe "(c) pikchr fence syntax" do
if Process.find_executable("fossil")
file_data.each do |(path, source, _nodes)|
filename = File.basename(path)
it "#{filename}: all pikchr fences produce valid SVG" do
md = GPCR::Renderer.new(source).render
fences = extract_pikchr_fences(md)
fences.each do |fence|
svg_out = IO::Memory.new
File.tempfile("gpcr_pik", ".pikchr") do |tmp|
tmp.print(fence)
tmp.flush
Process.run("fossil", ["pikchr", tmp.path], output: svg_out)
end
svg_out.to_s.should contain("<svg"), "invalid pikchr in #{filename}:\n#{fence}"
end
end
end
else
it "pikchr validation (fossil binary absent — pending)" do
pending!("fossil binary not found; install fossil to enable pikchr syntax checks")
end
end
end
# ── (d) OP-PRODUCIBILITY ────────────────────────────────────────────────────
# For every @[GPCR::Graph] and @[GPCR::Block] def in src/gpcr and examples:
# - scan body for node kinds
# - gap kinds (no producing op) → OP-COVERAGE GAP table
# - flat :call bodies → round-trip through Buffer
example_files = Dir.glob(File.join(root, "examples", "*.cr")).sort
corpus_file_data = file_data + example_files.map { |path|
source = File.read(path)
prims = {} of String => GPCR::Prim
nodes = begin
GPCR.parse_source(source, prims)
rescue
[] of GPCR::Node
end
{path, source, nodes}
}
# Collect gap kinds and producible defs at registration time (parse only, no Buffer).
all_gap_kinds = [] of Symbol
producible_specs = [] of {String, String, String, Array(GPCR::Node)}
corpus_file_data.each do |(path, source, nodes)|
filename = File.basename(path)
annotated = nodes.select { |n| n.kind == :graph || n.kind == :block }
annotated.each do |def_node|
body_kinds = collect_body_kinds(def_node.children + def_node.else_nodes)
gap_in_def = body_kinds.select { |k| PRODUCIBILITY_GAP_KINDS.includes?(k) }
if gap_in_def.any?
all_gap_kinds.concat(gap_in_def)
else
producible_specs << {filename, def_node.name, source, def_node.children}
end
end
end
all_gap_kinds = all_gap_kinds.uniq
describe "(d) op-producibility: round-trip + op-coverage gap table" do
it "op-coverage gap table (findings reported, not failures)" do
gap_sorted = all_gap_kinds.sort_by(&.to_s)
if gap_sorted.any?
puts "\n=== OP-COVERAGE GAP TABLE ==="
puts "Node kinds in renderable def bodies with no direct producing JSON op:"
gap_sorted.each do |k|
hint = case k
when :c_call then ":c_call — block-passing call; WrapOp composition covers simple cases; direct 'add-c-call' op pending (follow-up ticket: OP-CCALL-ADD)"
when :if then ":if — no 'add-if' op; conditional creation not yet in op set (follow-up ticket: OP-IF-ADD)"
when :while then ":while — no 'add-while' op; loop creation not yet in op set (follow-up ticket: OP-WHILE-ADD)"
when :case then ":case — no 'add-case' op; match expression not yet in op set (follow-up ticket: OP-CASE-ADD)"
when :when then ":when — nested inside :case; covered by OP-CASE-ADD"
when :other then ":other — arbitrary Crystal expression (assignment, operator chain, etc.); no op planned; belongs behind @[GPCR::Primitive]"
else "#{k} — follow-up ticket needed"
end
puts " #{hint}"
end
puts "==="
end
# Gaps are a finding (printed), not a spec failure.
true.should be_true
end
# Round-trip test for each def whose body contains only :call nodes.
producible_specs.each do |(filename, def_name, source, children)|
it "#{filename}##{def_name}: round-trips body through JSON ops" do
passed, reason = attempt_round_trip(source, def_name, children)
passed.should be_true, "#{def_name} round-trip failed: #{reason}"
end
end
end
# ── (e) CENSUS RATCHET ──────────────────────────────────────────────────────
# GPCR line percentage and per-file GPCR line counts must never decrease vs
# the baseline. Def counts are reported as secondary context.
describe "(e) census ratchet: GPCR line percentage monotone vs baseline" do
census_files = gpcr_files
file_stats, totals = GPCR::Census.census(census_files)
it "current census printed for reference" do
puts "\n=== GPCR CENSUS (src/gpcr/*.cr) ==="
file_stats.each do |filename, stats|
puts " #{filename}: gpcr_lines=#{stats.gpcr_lines} primitive_lines=#{stats.primitive_lines} native_lines=#{stats.native_lines} total_lines=#{stats.total_lines} gpcr_pct=#{stats.gpcr_pct}% defs(block=#{stats.blocks}, graph=#{stats.graphs}, primitive=#{stats.primitives}, native=#{stats.unannotated})"
end
puts " PROJECT: gpcr_lines=#{totals.gpcr_lines} primitive_lines=#{totals.primitive_lines} native_lines=#{totals.native_lines} total_lines=#{totals.total_lines} gpcr_pct=#{totals.gpcr_pct}% defs(block=#{totals.blocks}, graph=#{totals.graphs}, primitive=#{totals.primitives}, native=#{totals.unannotated})"
puts "==="
true.should be_true
end
it "project GPCR line percentage and per-file GPCR lines have not decreased vs baseline" do
baseline_path = File.join(root, "spec", "fixtures", "census_baseline.json")
unless File.exists?(baseline_path)
fail "census baseline not found at #{baseline_path} — run `gpcr census > spec/fixtures/census_baseline.json` from repo root to create it"
end
baseline = JSON.parse(File.read(baseline_path))
gpcr_pct = totals.gpcr_pct
base_pct = baseline["project"]["gpcr_pct"].as_f
if gpcr_pct < base_pct
fail "GPCR line percentage DECREASED: baseline=#{base_pct}% current=#{gpcr_pct}% — ratchet violation; do not reduce the fraction of source lines covered by @[GPCR::Graph]/@[GPCR::Block] spans"
end
violations = [] of String
file_stats.each do |filename, stats|
base_file = baseline["files"][filename]?
if base_file
base_gpcr_lines = base_file["gpcr_lines"].as_i
if stats.gpcr_lines < base_gpcr_lines
violations << "#{filename}: baseline=#{base_gpcr_lines} current=#{stats.gpcr_lines}"
end
end
end
unless violations.empty?
fail "Per-file GPCR line count DECREASED (ratchet violation):\n #{violations.join("\n ")}"
end
if gpcr_pct > base_pct
puts "\n=== CENSUS RATCHET: GPCR line percentage increased #{base_pct}% → #{gpcr_pct}% ==="
puts "Update spec/fixtures/census_baseline.json in the same commit that improves it."
puts "Run: gpcr census > spec/fixtures/census_baseline.json"
puts "==="
end
true.should be_true
end
end
end
CENSUS_BLOCK_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :command, label: "do #{x}", category: "Test")]
def do_thing(x : Int32)
foo
end
CRYSTAL
CENSUS_PRIM_SRC = <<-'CRYSTAL'
@[GPCR::Primitive(shape: :reporter, label: "read #{path}", category: "IO")]
def read_file(path : String) : String
File.read(path)
end
CRYSTAL
describe "GPCR::Census.census_sources" do
it "counts gpcr_lines for @[GPCR::Block] sources" do
_, totals = GPCR::Census.census_sources([{"block.cr", CENSUS_BLOCK_SRC}])
totals.gpcr_lines.should be > 0
totals.blocks.should eq 1
end
it "counts primitive_lines for @[GPCR::Primitive] sources" do
_, totals = GPCR::Census.census_sources([{"prim.cr", CENSUS_PRIM_SRC}])
totals.primitive_lines.should be > 0
totals.primitives.should eq 1
end
it "aggregates multiple sources correctly" do
pairs = [{"block.cr", CENSUS_BLOCK_SRC}, {"prim.cr", CENSUS_PRIM_SRC}]
file_stats, totals = GPCR::Census.census_sources(pairs)
file_stats.size.should eq 2
totals.blocks.should eq 1
totals.primitives.should eq 1
totals.total_lines.should eq(file_stats.values.sum(&.total_lines))
end
it "returns an empty project stat for an empty pairs list" do
_, totals = GPCR::Census.census_sources([] of {String, String})
totals.total_lines.should eq 0
totals.gpcr_pct.should eq 0.0
end
end