Module: Ea::Shapescript::Parser
- Defined in:
- lib/ea/shapescript/parser.rb
Overview
Tokenizer + parser for ShapeScript. Returns a list of Shape primitives extracted from a single shape body.
Supported grammar (extended):
script := statement*
statement := shape | var_decl | primitive | if_stmt | label
shape := "shape" identifier "{" statement* "}"
var_decl := "var" identifier "=" expr ";"
primitive := call "(" args ")" ";"
call := "rectangle" | "ellipse" | "polygon" |
"line" | "path" | "label"
args := expr ("," expr)*
if_stmt := "if" "(" expr ")" "{" statement* "}"
("else" "{" statement* "}")?
expr := identifier | number | string | binop
Variables are resolved at parse time when possible; otherwise the identifier is left as a string for late binding.
Constant Summary collapse
- PRIMITIVES =
{ "rectangle" => :rectangle, "ellipse" => :ellipse, "polygon" => :polygon, "line" => :line, "path" => :path }.freeze
Class Method Summary collapse
- .apply_binop(op, left, right) ⇒ Object
- .consume_if(tokens, env, then_shapes) ⇒ Object
- .consume_label(tokens, env) ⇒ Object
- .consume_primitive(name, tokens, env) ⇒ Object
- .consume_semicolon(tokens) ⇒ Object
-
.consume_statements(tokens, env, shapes, top_level: true) ⇒ Object
Consume statements until matching close brace or end.
- .consume_var_decl(tokens, env) ⇒ Object
-
.parse(source) ⇒ Array<Shape>
Flat list of all primitives across all shapes in the source.
-
.resolve_expr(tok, tokens, env) ⇒ Object
Resolve an expression to a value.
- .skip_block(tokens) ⇒ Object
-
.tokenize(source) ⇒ Object
-- Tokenizer ---------------------------------------------------.
Class Method Details
.apply_binop(op, left, right) ⇒ Object
196 197 198 199 200 201 202 203 204 205 |
# File 'lib/ea/shapescript/parser.rb', line 196 def apply_binop(op, left, right) return nil unless left.is_a?(Numeric) && right.is_a?(Numeric) case op when "+" then left + right when "-" then left - right when "*" then left * right when "/" then left / right end end |
.consume_if(tokens, env, then_shapes) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/ea/shapescript/parser.rb', line 120 def consume_if(tokens, env, then_shapes) tokens.shift # "(" condition = resolve_expr(tokens.shift, tokens, env) tokens.shift until tokens.empty? || tokens.first == ")" tokens.shift # ")" tokens.shift # "{" # Evaluate condition; if true, consume then-block into shapes. # If false, skip then-block and consume optional else. truthy = condition == true || (condition.is_a?(Numeric) && condition.nonzero?) if truthy consume_statements(tokens, env, then_shapes, top_level: false) else skip_block(tokens) if tokens.first == "else" tokens.shift tokens.shift # "{" consume_statements(tokens, env, then_shapes, top_level: false) end end end |
.consume_label(tokens, env) ⇒ Object
102 103 104 105 106 107 108 109 110 |
# File 'lib/ea/shapescript/parser.rb', line 102 def consume_label(tokens, env) tokens.shift # "(" text = tokens.shift text = text.to_s.gsub(/\A["']|["']\z/, "") tokens.shift until tokens.empty? || tokens.first == ")" tokens.shift # ")" consume_semicolon(tokens) Shape.new(kind: :label, params: [text]) end |
.consume_primitive(name, tokens, env) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/ea/shapescript/parser.rb', line 85 def consume_primitive(name, tokens, env) kind = PRIMITIVES[name] open_paren = tokens.shift # "(" params = [] if open_paren == "(" until tokens.empty? tok = tokens.shift break if tok == ")" value = resolve_expr(tok, tokens, env) params << value if value.is_a?(Numeric) end end consume_semicolon(tokens) Shape.new(kind: kind, params: params) end |
.consume_semicolon(tokens) ⇒ Object
152 153 154 |
# File 'lib/ea/shapescript/parser.rb', line 152 def consume_semicolon(tokens) tokens.shift if tokens.first == ";" end |
.consume_statements(tokens, env, shapes, top_level: true) ⇒ Object
Consume statements until matching close brace or end.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/ea/shapescript/parser.rb', line 51 def consume_statements(tokens, env, shapes, top_level: true) until tokens.empty? tok = tokens.first if tok == "}" tokens.shift if top_level == false break end tokens.shift # consume tok case tok when "shape" _name = tokens.shift _open = tokens.shift # "{" consume_statements(tokens, env.dup, shapes, top_level: false) when "var" consume_var_decl(tokens, env) when "if" consume_if(tokens, env, shapes) when "label" shapes << consume_label(tokens, env) else shapes << consume_primitive(tok, tokens, env) if PRIMITIVES.key?(tok) end end end |
.consume_var_decl(tokens, env) ⇒ Object
112 113 114 115 116 117 118 |
# File 'lib/ea/shapescript/parser.rb', line 112 def consume_var_decl(tokens, env) name = tokens.shift eq = tokens.shift # "=" value = resolve_expr(tokens.shift, tokens, env) consume_semicolon(tokens) env[name] = value if value.is_a?(Numeric) || value.is_a?(String) end |
.parse(source) ⇒ Array<Shape>
Returns flat list of all primitives across all shapes in the source. Shape nesting is flattened. Variables, conditionals, and labels are resolved/inlined where possible; unresolvable expressions are skipped.
32 33 34 35 36 37 38 39 40 |
# File 'lib/ea/shapescript/parser.rb', line 32 def parse(source) return [] if source.nil? || source.empty? tokens = tokenize(source) env = {} shapes = [] consume_statements(tokens, env, shapes) shapes end |
.resolve_expr(tok, tokens, env) ⇒ Object
Resolve an expression to a value. Returns Numeric/String/Boolean or nil when unresolvable. Handles simple binary ops (+, -, *, /).
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/ea/shapescript/parser.rb', line 158 def resolve_expr(tok, tokens, env) return nil unless tok # Parenthesized sub-expression if tok == "(" value = resolve_expr(tokens.shift, tokens, env) tokens.shift if tokens.first == ")" return value end # Numeric literal return tok.to_i if tok.match?(/\A-?\d+\z/) return tok.to_f if tok.match?(/\A-?\d+\.\d+\z/) # String literal return tok[1..-2] if tok.match?(/\A["'].*["']\z/) # Boolean literals return true if tok == "true" return false if tok == "false" # Variable reference if env.key?(tok) value = env[tok] # Look ahead for binary op if %w[+ - * /].include?(tokens.first) op = tokens.shift rhs = resolve_expr(tokens.shift, tokens, env) return apply_binop(op, value, rhs) end return value end # Bare identifier (function call? hasproperty? — leave as nil # for now; treat as false in conditionals). nil end |
.skip_block(tokens) ⇒ Object
142 143 144 145 146 147 148 149 150 |
# File 'lib/ea/shapescript/parser.rb', line 142 def skip_block(tokens) tokens.shift # "{" depth = 1 until tokens.empty? || depth.zero? t = tokens.shift depth += 1 if t == "{" depth -= 1 if t == "}" end end |
.tokenize(source) ⇒ Object
-- Tokenizer ---------------------------------------------------
44 45 46 47 48 |
# File 'lib/ea/shapescript/parser.rb', line 44 def tokenize(source) clean = source.gsub(%r{//[^\n]*}, "") .gsub(/\/\*.*?\*\//m, "") clean.scan(/[A-Za-z_][A-Za-z0-9_]*|-?\d+\.?\d*|"[^"]*"|'[^']*'|[{}(),;=+\-*\/]/) end |