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 n-sided 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):