Login
gpcr/src/gpcr.cr
Login

[edit]

src/gpcr.cr @ trunk

json

require '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)

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