Previously, I wrote a post about making a DSL in Ruby that could render magic circles/runes. It worked pretty well. I could turn things like this:
rune do
scale 0.9 do
circle
polygon 7
star 14, 3
star 7, 2
children 7, scale: 1/8r, offset: 1 do i
circle
invert do
text (0x2641 + i).chr Encoding::UTF_8
end
end
end
scale 0.15 do
translate x: 2 do circle; moon 0.45 end
circle
translate x: 2 do circle; moon 0.55 end
end
end
Into this:
But… I decided to completely rewrite it. Now it’s an entirely separate language:
Output
Source
Log (most recent messages first):
My main reasons for doing this?

It’s written in JavaScript, so I can run it in the browser!
Why yes. The example above is live. Try it out. See what you can make.
Pretty cool, no?

It lets me play with a full compiler stack: lexer, parser, evaluator/code generator
Language specification
So, for this first post, what does the final language specification look like?

Tokens
 Numeric literals integers:
1
, decimal:2.3
, rational/fractions:4/5
, negatives (any of the previous):7
, hexadecimal:0xDEADBEEF
, angles:30deg
/1rad
 String literals:
"hello world"
, includes escape sequences  Booleans:
true
/false
 Identifiers: start with a letter, include letters, numbers, dashes, or underscores
 Operators:
+

*
/
, also..
for making ranges
 Numeric literals integers:

Types of groups

Parameter groups, bound by
()
, including both positional (args
) and named (kwargs
) parameters When defining,
kwargs
represent the default values  When calling,
kwargs
represent the value passed to that parameter  In
params
, you can useexpressions
which are a large subset of mathematical expressions with proper operator precedence
 When defining,

Function groups, bound by
{}
contain zero or more nodes that will be evaluated as a single group and that can be passed tomodifiers
(see below) 
Lists, bound by
[]
represent zero or more nodes, can be evaluated similarly to Pythongenerators
, and can be passed tostackers
(see below)Lists can have three different forms:
[ NODE* ]
a static list of items[ NODE* for VARIABLE in ITERABLE ]
acts like a python generator, assigning each value inITERABLE
in turn toVARIABLE
than evaluating eachNODE
[ NODE* times NUMBER ]
a shortcut/alternative for writing[ ... for i in 0..NUMBER ]
, although it does not bindi
(or anything) to the counter
Both the 2nd and 3rd case above can use the full
expression
language for theITERABLE
/NUMBER

Each node (see below) can have params, a list, and a group, each optional and in that order. If no group is specified for a node that expects one, the next node will be used instead.
This means that
scale(0.9) circle
is equivalent toscale(0.9) { circle }


Types of builtins

Terminals: represents a single shape, can take
params
but not alist
orgroup
line(min: 0, max: 1)
 a line from the center (0) upwards (1)circle
 a unit circlepolygon(n: 5)
 an nsided polygonstar(n: 5, m: 2)
 a star with n points, skipping to the mth point each time, the ‘normal’ five pointed star hasm=2
, a polygon is a star withm=1
character(c, scale: 1)
 a single character from it’s Unicode codepoint, so for examplecharacter(0x03B1)
= α, scale modifies font sizetext(s, scale: 1)
 a string, scale modified font sizetextCircle(s, scale: 1, outward: true)
 a string rendered onto a circle, outward modifies if the text is upright at the top of the circle or invertedtextStar(n: 5, m: 2, s: "", scale: 1)
 a string rendered onto a star patharc(min: 0, max: 1)
 an arc around a circle ranging from 0 at the top, clockwise around to 1 as a full circle (can use angle literals as well)moon(phase)
 a moon ranging from 0 as new moon to 0.5 as full, back to 1 as a new moon again

Modifiers: modifier a single child or a group, can take
params
and agroup
but not alist
; if no group is specified will apply to the next node insteadrune
 the base of all shapesgroup
 a ‘null’ operator, used for grouping inlists
scale(x, y)
 scale children on the x/y axis; if only one argument is supplied use it for both x and y, a scale of 1 is the identityfill(color)
 change the fill color for all children, can be any SVG compatible color (names or hex strings)stroke(weight, color)
 change the stroke weight/color, if either param is not supplied, it will be skipped rather than using a defaultinvert
 rotate by 180 degreesdouble(scale)
 draw the child node twice with the second scaled by scale, most useful for double circles like:double(0.9) circle
translate(x: 0, y: 0)
 offset by x/y coordinates where 1 is the unit circlerotate(a)
 rotate by an angle where 0 is no rotation, 1/4 is a quarter circle (clockwise), 1/2 is 180 degrees, 1 is a full circleskew(x, y)
 skew by angles on the x and y axis, angles defined as above

Stackers:
stack
 the base stacker if you just want to stack children without doing anything to them, mostly equivalent to agroup
radial(scale: 1, offset: 1, rotate: false)
 place children around a circle with the first at the top and going clockwise, evenly spaced;scale
will apply to each child,offset
is 0 at the center of the circle to 1 at the edge, ifrotate
istrue
, each shape will be rotated to point ‘outwards’, otherwise they’ll all stay upwards relative to theradial
nodelinear(scale: 1, min: 0, max: 1)
 place children in a line frommin
(default 0 is the middle of the circle) tomax
(default 1 is the top of the circle),scale
acts like above


define
 a special form that allows you to define new elements, agroup
is always required, but what it does depends on the formdefine NAME ( PARAMS ) { BODY }
defines a newterminal
define NAME ( PARAMS ) ( CHILD_PARAM ) { BODY }
defines a newmodifier
and will bind the children (either one node or a group) toCHILD_PARAM
in the call, that param list must be exactly onearg
, nokwarg
define NAME ( PARAMS ) [ LIST_PARAM ] { BODY }
like above, defines a newstacker
(warning: currently not implemented)
And… that’s it!
Yeah… I know that’s a ridiculous block of text. And that’s only the first of it! Next up:
 Lexing
 Parsing
 Evaluating: generating basic
SVGs
 Evaluating: allowing basic infix expression with operator precedence
 Adding functions to the
expression
language  Adding
define
 Adding
import
If you’d like to see the source or even help contribute, take a look here: jpverkamp/runelang
That’s well ahead of where this post is (it’s already doing everything above, except import
doesn’t work in the browser). One thing I really need to do is write tests. That link may always be ahead. So … spoilers I guess?
All posts (as I write them):