src/gpcr.cr @ trunk
json
scale = 0.75 fontscale = 1.500 box wid 1.330 ht 0.340 with .nw at (0.035, -0.035) fill 0xCBD5E1 color 0xCBD5E1 box wid 1.330 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.190, 0.000) to (1.190, -0.340) color white text "require 'json'" color white at (0.665, -0.170)→ /pikchrshow
crystal source
require "json"
json block
{
"kind": "require",
"name": "json",
"doc": null,
"children": []
}
VERSION = "0.1.0"
# A primitive: a `def` whose body is Crystal-native code — the places where
# the event loop, an IO, or hardware enters GPCR. The annotation declares the
# block face (shape reference + palette metadata); the machine form is
# DERIVED from the def by `GPCR.blocks_of` — there is no hand-authored spec.
annotation Primitive
end
# A block defined in GPCR: same face declaration as Primitive, but the body
# promises to use only renderable elements — calls to blocks/primitives and
# Crystal-native forms with node-kind renderings. `GPCR.check_blocks`
# enforces that promise at compile time.
annotation Block
end
# Canvas anchor for a graph (an independently-runnable def). Block layout
# inside the graph is implicit from source order; only the graph is placed.
annotation Graph
end
# Marks a literal array of struct constructors as a data table.
# The renderer derives column names from the struct definition's properties
# and renders the literal as a Pikchr grid: header row + one row per element.
annotation TableLiteral
end
# Marks a struct definition as a table schema.
# The renderer extracts property name+type pairs as typed column headers,
# scans the file for a matching array literal, and renders the complete
# table (header with "name: Type" labels + data rows) in one Pikchr grid.
# The struct is the graphical anchor; the array literal stays plain Crystal.
annotation TableSchema
end
# Marks a scalar constant definition as a GPCR constant block.
# The renderer displays the constant name and value as a reporter-shaped block.
# Eligible values: any Crystal scalar literal (String, Number, Bool, Char, Symbol).
# Complex values (struct constructors, array/hash literals) belong to TableLiteral.
annotation Constant
end
# Marks a Crystal `record` macro call as a GPCR record block.
# The renderer displays the record name as a command-shaped block (data-type color).
# Field names/types are preserved in the node for tooling; the block face shows
# the record name only so the pikchr stays readable regardless of field count.
annotation Record
end
# Derived machine form of one input slot: the def's parameter.
struct Input
include JSON::Serializable
getter name : String
getter type : String
getter default : JSON::Any?
def initialize(@name, @type, @default = nil)
end
end
# Derived machine form of one block face. Generated by `blocks_of`, never
# authored. Blocks are referenced by their def name (`name`).
struct Face
include JSON::Serializable
getter name : String
getter shape : String
getter label : String
getter category : String?
getter returns : String?
getter inputs : Array(Input)
getter kind : String
def initialize(@name, @shape, @label, @category = nil, @returns = nil, @inputs = [] of Input, @kind = "block_definition")
end
end
# Crystal type restriction -> slot type (see README "Crystal type → slot type").
SLOT_TYPES = {
"Bool" => "boolean",
"Float64" => "number",
"Int32" => "number",
"String" => "string",
}
def self.slot_type(restriction : String?) : String
return "any" if restriction.nil? || restriction.empty?
SLOT_TYPES[restriction]? || restriction
end
# Walk `type`'s methods at compile time and derive a Block for every
# @[GPCR::Primitive] or @[GPCR::Block] def: shape/label/category/returns
# from the annotation, inputs from the parameter list.
#
# Labels use Crystal's own interpolation syntax for slots — `"set LED #{state}"`.
# Annotation values are stored unevaluated, so the label arrives as a
# StringInterpolation node already split into text and slot parts; each slot
# is checked against the def's parameters here, at compile time.
macro blocks_of(type = nil)
begin
%blocks = [] of GPCR::Face
{% scope = type ? type.resolve : @top_level %}
{% for m in scope.methods %}
{% prim_ann = m.annotation(GPCR::Primitive) %}
{% ann = prim_ann || m.annotation(GPCR::Block) %}
{% if ann %}
{% lbl = ann[:label] %}
{% if lbl.is_a?(StringInterpolation) %}
{% label_text = "" %}
{% for part in lbl.expressions %}
{% if part.is_a?(StringLiteral) %}
{% label_text = label_text + part %}
{% else %}
{% unless m.args.any? { |a| a.name == part.name } %}
{% raise "GPCR: label slot #{part} on `#{m.name}` has no matching parameter" %}
{% end %}
{% label_text = label_text + "\#{#{part.name}}" %}
{% end %}
{% end %}
{% else %}
{% label_text = lbl %}
{% end %}
%inputs = [] of GPCR::Input
{% for a in m.args %}
%inputs << GPCR::Input.new(
{{ a.name.stringify }},
GPCR.slot_type({{ a.restriction.stringify }}),
{% if a.default_value.is_a?(Nop) %} nil {% else %} JSON.parse({{ a.default_value }}.to_json) {% end %}
)
{% end %}
%blocks << GPCR::Face.new(
{{ m.name.stringify }},
{{ ann[:shape].id.stringify }},
{{ label_text }},
{% if ann[:category] %}{{ ann[:category] }}{% else %}nil{% end %},
{% if ann[:returns] %}{{ ann[:returns].id.stringify }}{% else %}nil{% end %},
%inputs,
{{ prim_ann ? "primitive_definition" : "block_definition" }}
)
{% end %}
{% end %}
%blocks
end
end
# Compile-time primitive: enforce the @[GPCR::Block] body contract. Every
# node in a Block-annotated def must be a renderable element — a call
# (to a block, a primitive, or a Crystal-native method/operator) or a
# Crystal-native form with a node-kind rendering. Anything else (class/
# instance/global state, yields, pointers, ...) is a compile error naming
# the def and the offending node kind: those belong behind a Primitive.
#
# Walks each body with a cursor over an append-only worklist (macro-land
# has no recursion); RENDERABLE_BODY_LIMIT bounds pathological bodies.
# Leaf node kinds that check_blocks treats as renderable without recursion,
# AND the rendering commitment registry — a kind joins this list only once its
# node-kind rendering is confirmed to exist. Case and When are the first
# structural (non-leaf) entries added under that rule; their explicit branches
# in check_blocks recurse into children, so their presence here is the
# registry record of the visual commitment, not a behavior change.
RENDERABLE_KINDS = %w(NumberLiteral StringLiteral BoolLiteral CharLiteral
SymbolLiteral ArrayLiteral HashLiteral TupleLiteral RangeLiteral Var Nop Return Next
Break Path Case When)
macro check_blocks(type = nil)
{% scope = type ? type.resolve : @top_level %}
{% for m in scope.methods %}
{% if m.annotation(GPCR::Block) %}
{% work = [m.body] %}
{% offenders = [] of String %}
{% for i in 0..999 %}
{% if i < work.size %}
{% n = work[i] %}
{% k = n.class_name %}
{% if k == "Expressions" %}
{% for e in n.expressions %}{% work << e %}{% end %}
{% elsif k == "Call" %}
{% if n.receiver %}{% work << n.receiver %}{% end %}
{% for a in n.args %}{% work << a %}{% end %}
{% if n.block %}{% work << n.block.body %}{% end %}
{% elsif k == "If" || k == "Unless" %}
{% work << n.cond %}{% work << n.then %}{% work << n.else %}
{% elsif k == "While" || k == "Until" %}
{% work << n.cond %}{% work << n.body %}
{% elsif k == "Case" %}
{% work << n.cond %}
{% for w in n.whens %}{% work << w %}{% end %}
{% work << n.else %}
{% elsif k == "When" %}
{% for c in n.conds %}{% work << c %}{% end %}
{% work << n.body %}
{% elsif k == "Assign" || k == "OpAssign" %}
{% work << n.target %}{% work << n.value %}
{% elsif k == "StringInterpolation" %}
{% for e in n.expressions %}{% work << e %}{% end %}
{% elsif GPCR::RENDERABLE_KINDS.includes?(k) %}
{% else %}
{% offenders << "#{m.name}: #{k.id}" %}
{% end %}
{% end %}
{% end %}
{% if offenders.size > 0 %}
{% raise "GPCR: @[GPCR::Block] def contains a non-renderable element (#{offenders.first.id}) — renderable elements are calls and Crystal-native forms with node-kind renderings; anything else belongs behind a @[GPCR::Primitive]" %}
{% end %}
{% end %}
{% end %}
end
# Compile-time primitive: embed a source file's text into the binary, so a
# compiled gpcr program carries its own source and can render itself with
# the same parse path the editor uses (comments included — macro reflection
# cannot see comments, source text can). Call as
# `GPCR.embed_source(__FILE__)`; the text lands in GPCR_SOURCE.
macro embed_source(path)
GPCR_SOURCE = {{ read_file(path) }}
end