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,
kwargsrepresent the default values - When calling,
kwargsrepresent the value passed to that parameter - In
params, you can useexpressionswhich 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 inITERABLEin turn toVARIABLEthan 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
expressionlanguage for theITERABLE/NUMBEREach 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) circleis equivalent toscale(0.9) { circle }
Types of builtins
Terminals: represents a single shape, can take
paramsbut not alistorgroupline(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=1character(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
paramsand agroupbut 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 inlistsscale(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) circletranslate(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 agroupradial(scale: 1, offset: 1, rotate: false)- place children around a circle with the first at the top and going clockwise, evenly spaced;scalewill apply to each child,offsetis 0 at the center of the circle to 1 at the edge, ifrotateistrue, each shape will be rotated to point ‘outwards’, otherwise they’ll all stay upwards relative to theradialnodelinear(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),scaleacts like above
define- a special form that allows you to define new elements, agroupis always required, but what it does depends on the formdefine NAME ( PARAMS ) { BODY }defines a newterminaldefine NAME ( PARAMS ) ( CHILD_PARAM ) { BODY }defines a newmodifierand will bind the children (either one node or a group) toCHILD_PARAMin the call, that param list must be exactly onearg, nokwargdefine 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
expressionlanguage - 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):