# Runelang: Language Specification

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:

### Log (most recent messages first):

My main reasons for doing this?

1. 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?

2. 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
• 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 use expressions which are a large subset of mathematical expressions with proper operator precedence
• Function groups, bound by {} contain zero or more nodes that will be evaluated as a single group and that can be passed to modifiers (see below)

• Lists, bound by [] represent zero or more nodes, can be evaluated similarly to Python generators, and can be passed to stackers (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 in ITERABLE in turn to VARIABLE than evaluating each NODE
• [ NODE* times NUMBER ] a shortcut/alternative for writing [ ... for i in 0..NUMBER ], although it does not bind i (or anything) to the counter

Both the 2nd and 3rd case above can use the full expression language for the ITERABLE / 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 to scale(0.9) { circle }

• Types of builtins

• Terminals: represents a single shape, can take params but not a list or group

• line(min: 0, max: 1) - a line from the center (0) upwards (1)
• circle - a unit circle
• polygon(n: 5) - an n-sided polygon
• star(n: 5, m: 2) - a star with n points, skipping to the mth point each time, the ’normal’ five pointed star has m=2, a polygon is a star with m=1
• character(c, scale: 1) - a single character from it’s Unicode codepoint, so for example character(0x03B1) = α, scale modifies font size
• text(s, scale: 1) - a string, scale modified font size
• textCircle(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 inverted
• textStar(n: 5, m: 2, s: "", scale: 1) - a string rendered onto a star path
• arc(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 a group but not a list; if no group is specified will apply to the next node instead

• rune - the base of all shapes
• group - a ’null’ operator, used for grouping in lists
• 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 identity
• fill(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 default
• invert - rotate by 180 degrees
• double(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 circle
• rotate(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 circle
• skew(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 a group
• 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, if rotate is true, each shape will be rotated to point ‘outwards’, otherwise they’ll all stay upwards relative to the radial node
• linear(scale: 1, min: 0, max: 1) - place children in a line from min (default 0 is the middle of the circle) to max (default 1 is the top of the circle), scale acts like above
• define - a special form that allows you to define new elements, a group is always required, but what it does depends on the form

• define NAME ( PARAMS ) { BODY } defines a new terminal
• define NAME ( PARAMS ) ( CHILD_PARAM ) { BODY } defines a new modifier and will bind the children (either one node or a group) to CHILD_PARAM in the call, that param list must be exactly one arg, no kwarg
• define NAME ( PARAMS ) [ LIST_PARAM ] { BODY } like above, defines a new stacker (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):