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:

Output

Source

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