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