Test-Driven Language Development with Spoofax

Spoofax
This short primer shows how to use tests as a basis for language development with Spoofax. As an example project we create a small 'calculator' language that shows many of the basics of language definitions.

The source for our example project is collected in before.zip?. The complete project, with all functionality implemented to make the test cases succeed is collected in after.zip?.

A Calculator Language

Language definition tests

spt file

basic form

quote with [[ or [[[ or [[[[

Syntax

test Add [[
  1 + 2
]] parse succeeds

test Multiply [[
  1 * 2
]]

test Abstract syntax (1) [[
  1
]]
  parse to Int("1")

test Abstract syntax (2) [[
  1 * 2
]]
  parse to Mul(Int("1"), Int("2"))

test Parentheses [[
  (1 + 2)
]]

"(" Expr ")" -> Expr {bracket}

test Parentheses in abstract syntax [[
  (1 + 2)
]] parse to Plus(_, _)

test Multiply and add (1) [[
  1 + 2 * 3
]] parse to Add(_, Mul(_, _))

test Multiply and add (2) [[
  1 + 2 * 3
]] parse to [[
  1 + (2 * 3)
]]

test Add and multiply [[
  1 * 2 + 3
]] parse to [[
  (1 * 2) + 3
]]

test Add and add [[
  1 + 2 + 3
]] parse to [[
  (1 + 2) + 3
]]

Evaluation of expressions

Base case:

test Constant [[
  1
]] run calc to "1"

evaluate.str:

  calc:
    Int(i) -> i

Operator:

test Add [[
  1 + 1
]] run calc to "2"

evaluate.str:

  calc:
    Plus(x, y) -> <addS> (<calc> x, <calc> y)

test Multiply [[
  2 * 2
]] run calc to "4"

test Multiply and add [[
  2 * 2 + 1
]] run calc to "5"

test Multiply and add (2) [[
  2 * (2 + 1)
]] run calc to "6"

Variables

test Variable [[
  x
]] parse

test Variable [[
  longname
]] parse

Calculang.sdf:

    ID         -> Exp {cons("Var")}

assignment statements

test Assignment [[
  x = 4
]] parse

    Stm        -> Start {cons("Statements")}
    ID "=" Exp -> Stm {cons("Assign")}
    Exp        -> Stm

test Multiple statements [[
   x = 1
   y = 2
]] parse to Statements([Assign(_, _), Assign(_, _)])

    Stm*       -> Start {cons("Statements")}
    ID "=" Exp -> Stm {cons("Assign")}
    Exp        -> Stm
    ID         -> Exp {cons("Var")}

regression in syntax.spt!?

add {prefer} attribute to CalcuLang.sdf

    Exp -> Start {prefer}

what I want eventually:

test Eval variable [[
  x = 4
  x
]] run calc to "4"

but first:

test Evaluate multiple statements [[
  1
  2
]] run calc to "2"

  calc:
     Statements(s*) -> last
     where
        s'*  := <map(calc)> s*;
        last := <last> s'*

test Eval constant [[
  PI
]] run calc to "3.14"

  calc:
    Var("PI") -> "3.14"

revisit:

test Eval variable [[
  x = 4
  x
]] run calc to "4"

  calc:
    Assign(x, v) -> v'
    where
      v' := <calc> v;
      rules(
        GetValue: x -> v'
      )

  calc:
    Var(v) -> <GetValue> v

test Eval multiple variables [[
  x = 2
  y = x * 2 + x
  y
]] run calc to "6"

t4-editor.spt

test Variable unassigned [[
  y
]] 1 error /unassigned/

  analyze =
    topdown(try(record-var))

  record-var:
    Assign(x, e) -> Assign(x, e)
    with
      rules(
        GetVar :+ x -> x
      )
  
  constraint-error:
    Var(v) -> (v, $[Unassigned variable])
    where
      not(<GetVar> v)

test Multiple assignments to same variable [[
  y = 1
  y = 2
  y
]] /multiple/

  constraint-error:
    Var(v) -> (v, $[Multiple assignments to same variable])
    where
      all-vs := <bagof-GetVar> v;
      <gt> (<length> all-vs, 1)

test Reference resolving (1) [[
  x = 4
  [[x]]
]] resolve

test Reference resolving (2) [[
  [[x]] = 4
  [[x]]
]] resolve #2 to #1

test Content completion [[
  avariable = 1
  [[a]]
]] complete to "avariable"

t5-compilation.spt

test Constant [[
  1
]] build generate-java "
      public class Output {
        public static void main(String[] args) {
          System.out.println(1);
          System.exit(0);
        }
      }"

test Constant [[
  1 + 2
]] build generate-java 

  to-java:
    _ -> $<
      public class Output {
        public static void main(String[] args) {
          System.out.println(42);
          System.exit(0);
        }
      }
    >

t6-execution.spt

test 42 [[
  42
]] build generate-result to 42

test 42, part deux [[
  41 + 1
]] build generate-result to 42
...variables, etc....
...add to evaluation tests??...

t7-refactoring

test Basic rename [[
  a = 1
  [[a]]
]] refactor rename-var("b") to [[
  b = 1
  b
]]

test Another rename [[
  a = 1
  b = 2
  [[a]]
]] refactor rename-var("c") to [[
  c = 1
  b = 2
  c
]]

test Rename collission [[
  a = 1
  b = 1
  [[a]]
]] refactor rename-var("b") to _
   1 error

  rename-var:
    (newname, selected-name, position, ast, path, project-path) -> ([(ast, new-ast)], fatal-errors, errors, warnings)
    with
      new-ast  := <topdown(try(rename-type(|selected-name, newname)))> ast; 
      (errors, warnings) := <semantic-constraint-issues> (ast, new-ast);
      fatal-errors := []

  rename-type(|old-name, new-name):
    Assign(old-name, y) -> Assign(new-name, y)

  rename-type(|old-name, new-name):
    Var(old-name) -> Var(new-name)
    
  semantic-constraint-issues:
    (ast, new-ast) -> (<diff>(new-errors, errors), <diff>(new-warnings, warnings))
    where
      (_, errors, warnings, _) := <editor-analyze> (ast, "", "");
      (_, new-errors, new-warnings, _) := <editor-analyze> (new-ast, "", "")

t8-shell

test Arithmetic API
  <mul> (2, 3) => 6

test String-based arithmetic API
  <mulS> ("2", "3") => "6"

test Foo 
  let
    foo = !4
  in
    foo;
    debug
  end