LemuelKL

HolloRay

HolloRay is an external domain-specific language for 3D computer-aided design (CAD), written in JavaFX and Ambiguity Resolved Translators (ART).

ART is a work by Dr. Adrian Johnstone and Dr. Elizabeth Scott at Royal Holloway, University of London.

This is a coursework project for the module Software Language Engineering at Royal Holloway, University of London. At the writing of this (26 Feb, 2023), the project is still in progress.

Full source code is available at GitHub

External DSL

This DSL is external, meaning it has its own syntax and semantics. It is not a subset of any other language, and is not embedded in any other language. ART provides a framework for writing a parser and the language specifications.

HolloRay supports while loops, if statements, variable assignments and deferences, basic arithmetic. It is a full-fledged DSL, and so it supports infinitely nested expressions.

3D CAD

HolloRay does 3D CAD. It can create and manipulate 3D objects, and render them to a 3D space. The 3D capabilities are provided by JavaFX. There is a Java Plugin behind the scene, also developed by me, and is connected to the front-end via ART.

Simple solids such as cubes, spheres, cylinders, cones, and tori can be created. These solids can be transformed by translation, rotation, and scaling. They can also be combined into a single object using union, intersection, and difference operations. The resulting object can be rendered to a 3D space.

The 3D space can also be manipulated by the user. The user can rotate the camera, and zoom in and out. The user can also move the camera around the 3D space.

Structure of the Project

There are mainly two ways of executing a final DSL program. One is to parse the program into a context-free grammar, and then translate it to an internal syntax (discussed below), and then the internal syntax is executed, along with the JavaFX plugin.

The other way is to write a interpreter based on the attributes and actions syntax. The program is executed directly, and the JavaFX plugin is called directly.

eSOS rules for internal syntax

The internal language specifications are written in ART’s format, in artSpecification.art. It is a list of rules specifing how the internal functionalities of the language, represented by a bunch of functions, should be rewritten. In short, it specifies how to carry out term rewriting.

Term rewriting is a technique for transforming a term into another term. It is a form of pattern matching. The term is a string of symbols, and the pattern is a string of symbols with some wildcards. The wildcards are replaced by the symbols in the term, and the result is the rewritten term. The rewriting is done recursively, until no more rewriting can be done. An important concept is that rewriting would not change the behavior of the program. In other words, the rewritten program should be equivalent to the original program, in fact, after every single rewriting step.

The specifications are actually very much like writting Haskell code. It is highly declarative. Here is a segment as an example:

-sequenceDone
---
seq(__done, _C), _sig -> _C, _sig

-sequence 
_C1, _sig -> _C1P, _sigP
---
seq(_C1, _C2), _sig -> seq(_C1P, _C2), _sigP

-ifTrue 
---
if(True, _C1, _C2),_sig -> _C1, _sig

-ifFalse 
---
if(False, _C1, _C2),_sig -> _C2,_sig

-ifResolve
_E, _sig ->_EP, _sigP
---
if(_E,_C1,_C2),_sig -> if(_EP, _C1, _C2), _sigP

-while
---
while(_E, _C),_sig -> if(_E, seq(_C, while(_E,_C)), __done), _sig

There are 6 rules in total. Each rule starts by its name, followed by (if any) a list of rules for matching parameters, then how the rewrite should be carried out. Left side of the arrow is the pattern, and right side is the rewrite. For sequence, you can see there is a recursive nature.

External Language

I deisnged the language to resemble that of C.

for (int i = 0; i < 10; i = i + 1;) {
    println(i);
}
// 3x + 1 conjecture
int x = 1;
while (x != 1) {
    if (x % 2 == 0) {
        x = x / 2;
    } else {
        x = 3 * x + 1;
    }
}
// 3D CAD
int x = 1;
int y = 2;
int z = 3;
int r = 4;

// initialise JavaFX
init(640, 480);

// create a cube
cube = CUBE(x, y, z);
cube.translate(0, 10, 30);
cube.rotate(0, 15, 0);
cube.scale(2, 2, 2);

// create a sphere
sphere = SPHERE(r);
sphere.translate(10, 0, 0);

//final render
paint();

Some notable points of this language:

  • Imperative, and so it supports while loops, for loops, if statements, and variable assignments.
  • Optional arguments for for loops.
  • Supports basic arithmetic and logical operations.
  • Supports infinitely nested expressions.
  • Handles order of operations.
  • Default value 0 when dereferencing an undeclared variable.
  • Infinite number of arguments of mixed types for println(...).
  • Optional else for if statements.

Parser for External to Internal

It is written in a BNF-like format, with some specialised features of ART. One could probably understand it to some extent by just looking at the code.

For the hat operators ^ and ^^, they basically mean “pushing up” the child node upward in a parse tree. This is useful becuase it allows easy access to the child content at the parent level. For example, this is particularly useful when engineering a println() function that takes one to infinite number of arguments. It is useful because by adjusting the hat operators, we can have either a nested parse tree where each print element goes one leve deeper, or a flat parse tree where all print elements are on the same, one level immediately below the parent (the println() function). For my parsing purpose, I chose the former because it is easier to implement with eSOS rules where the next stage of execution occurs.

statement ::= seq^^ | assign^^ | compassign^^ | if^^ | while^^ | for^^ | println^^ | init^^ | paint^^ | translate^^ | rotate^^ | scale^^

seq ::= statement statement
assign ::= ID '='^ relExpr ';'^
compassign ::= ID '+='^ relExpr ';'^
if ::= 
    'if'^ '('^ relExpr ')'^ '{'^ statement '}'^ 'else'^ '{'^ statement '}'^
  | 'if'^ '('^ relExpr ')'^ '{'^ statement '}'^
while ::= 'while'^ '('^ relExpr ')'^ '{'^ statement '}'^
for ::= 'for'^ '('^ statement relExpr ';'^ statement ')'^ '{'^ statement '}'^

println ::= 'println'^ '('^ printlnElem^ ')'^ ';'^
printlnElem ::= relExpr | relExpr ','^ printlnElem
init ::= 'init'^ '('^ INTEGER ','^ INTEGER  ')'^ ';'^
paint ::= 'paint'^ '('^ ')'^ ';'^
translate ::= deref '.translate'^ '('^ subExpr ','^ subExpr ','^ REAL ')'^ ';'^
rotate ::= deref '.rotate'^ '('^ subExpr ','^ subExpr ','^ subExpr ')'^ ';'^
scale ::= deref '.scale'^ '('^ subExpr ','^ subExpr ','^ subExpr ')'^ ';'^

relExpr ::= subExpr^^ | eq^^ | ne^^ | gt^^ | ge^^ | lt^^ | le^^ | and^^ | or^^ | not^^ | xor^^ | '('^ relExpr^^ ')'^
eq ::= relExpr '=='^ subExpr
ne ::= relExpr '!='^ subExpr
gt ::= relExpr '>'^ subExpr
ge ::= relExpr '>='^ subExpr
lt ::= relExpr '<'^ subExpr
le ::= relExpr '<='^ subExpr
and ::= relExpr '&&'^ relExpr
or ::= relExpr '||'^ relExpr
not ::= '!'^ relExpr
xor ::= relExpr '^'^ relExpr

subExpr ::= operand^^ | sub^^ | add^^ | mul^^ | div^^ | mod^^ | neg^^ | exp^^ | '('^ subExpr^^ ')'^
sub ::= subExpr '-'^ operand
add ::= subExpr '+'^ operand
mul ::= subExpr '*'^ operand
div ::= subExpr '/'^ operand
mod ::= subExpr '%'^ operand
neg ::= '-'^ operand
exp ::= subExpr '**'^ operand

operand ::= deref^^ | INTEGER^^ | REAL^^ | STRING_DQ^^ | '('^ subExpr^^ ')'^ | box^^ | cube^^ | sphere^^ | cylinder^^ | cone^^ | torus^^ | tetrahedron^^ | pyramid^^
deref ::= ID

box ::= 'BOX'^ '('^ REAL ','^ REAL ','^ REAL ')'^
cube ::= 'CUBE'^ '('^ REAL ')'^
sphere ::= 'SPHERE'^ '('^ REAL ')'^
cylinder ::= 'CYLINDER'^ '('^ REAL ','^ REAL ')'^
cone ::= 'CONE'^ '('^ REAL ','^ REAL ')'^
torus ::= 'TORUS'^ '('^ REAL ','^ REAL ')'^
tetrahedron ::= 'TETRAHEDRON'^ '('^ REAL ')'^
pyramid ::= 'PYRAMID'^ '('^ REAL ','^ REAL ')'^

Attributes and Actions Interpreter

prelude { 
    import java.util.HashMap;
}
support { 
    HashMap<String, Integer> variables = new HashMap<String, Integer>();
    HashMap<String, ARTGLLRDTHandle> procedures = new HashMap<String, ARTGLLRDTHandle>();
    ValueUserPlugin valueUserPlugin = new ValueUserPlugin();
    class Helper {
        public static final String ANSI_RED = "";
        public static final String ANSI_GREEN = "";
        public static final String ANSI_RESET = "";
        public static void Shout(String s) {
            System.out.println(ANSI_GREEN + s + ANSI_RESET);
        }
    }
}

stms ::= stm stms | stm

stm ::= ID '=' relExpr ';' {
    variables.put(ID1.v, relExpr1.v);
    Helper.Shout("[ASSIGN] " + ID1.v + " = " + relExpr1.v);
}
| ID '+=' relExpr ';' {
    if (!variables.containsKey(ID1.v)) {
        variables.put(ID1.v, 0);
    }
    variables.put(ID1.v, variables.get(ID1.v) + relExpr1.v);
    Helper.Shout("[ASSIGN] " + ID1.v + " += " + relExpr1.v);
}
| 'call' ID ';' {
    if (procedures.containsKey(ID1.v)) {
        Helper.Shout("[CALL PROCEDURE] " + ID1.v);
        artEvaluate(procedures.get(ID1.v), null);
    } else {
        Helper.Shout("[ERROR] " + ID1.v + " is not a procedure");
    }
}
| 'procedure' ID '{' stms< '}' {
    procedures.put(ID1.v, stm.stms1);
    Helper.Shout("[NEW PROCEDURE] " + ID1.v);
}
| 'println' '(' printlnElements ')' ';'
| 'if' '(' relExpr ')' '{' stm< '}' {
    if (relExpr1.v != 0) {
        artEvaluate(stm.stm1, stm1);
    }
}
| 'if' '(' relExpr ')' '{' stm< '}' 'else' '{' stm< '}' {
    if (relExpr1.v != 0) {
        artEvaluate(stm.stm1, stm1);
    } else {
        artEvaluate(stm.stm2, stm2);
    }
}
| 'while' '(' relExpr< ')' '{' stms< '}' {
    artEvaluate(stm.relExpr1, relExpr1);
    while (relExpr1.v != 0) {
        artEvaluate(stm.stms1, stms1);
        artEvaluate(stm.relExpr1, relExpr1);
    }
}
| 'while' '(' relExpr< ')' {
    artEvaluate(stm.relExpr1, relExpr1);
    while (relExpr1.v != 0) {
        artEvaluate(stm.relExpr1, relExpr1);
    }
}
| 'for' '(' stms< relExpr< ';' stms< ')' '{' stms< '}' {
    artEvaluate(stm.stms1, stms1);
    artEvaluate(stm.relExpr1, relExpr1);
    while (relExpr1.v != 0) {
        artEvaluate(stm.stms3, stms3);
        artEvaluate(stm.stms2, stms2);
        artEvaluate(stm.relExpr1, relExpr1);
    }
}
| 'for' '(' stms< relExpr< ';' stms< ')' {
    artEvaluate(stm.stms1, stms1);
    artEvaluate(stm.relExpr1, relExpr1);
    while (relExpr1.v != 0) {
        artEvaluate(stm.stms2, stms2);
        artEvaluate(stm.relExpr1, relExpr1);
    }
}
| 'for' '(' ';' relExpr< ';' stms< ')' {
    artEvaluate(stm.relExpr1, relExpr1);
    while (relExpr1.v != 0) {
        artEvaluate(stm.stms1, stms1);
        artEvaluate(stm.relExpr1, relExpr1);
    }
}
| 'for' '(' ';' relExpr< ';' ')' '{' stms< '}' {
    artEvaluate(stm.relExpr1, relExpr1);
    while (relExpr1.v != 0) {
        artEvaluate(stm.stms1, stms1);
        artEvaluate(stm.relExpr1, relExpr1);
    }
}
| 'init' '(' INTEGER ',' INTEGER ')' ';' {
    // UNCOMMENT THIS TO SEE BUG
    // valueUserPlugin.initialise(INTEGER1.v, INTEGER2.v);
}

printlnElements ::= STRING_DQ { System.out.printf("%s\n", STRING_DQ1.v); }
    | STRING_DQ { System.out.printf("%s", STRING_DQ1.v); } ',' printlnElements
    | relExpr { System.out.printf("%d\n", relExpr1.v); }
    | relExpr { System.out.printf("%d", relExpr1.v); } ',' printlnElements  

relExpr<v:int> ::=
    subExpr { relExpr.v = subExpr1.v; }
    | relExpr '<' subExpr { relExpr.v = relExpr1.v < subExpr1.v ? 1 : 0; }
    | relExpr '>' subExpr { relExpr.v = relExpr1.v > subExpr1.v ? 1 : 0; }
    | relExpr '<=' subExpr { relExpr.v = relExpr1.v <= subExpr1.v ? 1 : 0; }
    | relExpr '>=' subExpr { relExpr.v = relExpr1.v >= subExpr1.v ? 1 : 0; }
    | relExpr '==' subExpr { relExpr.v = relExpr1.v == subExpr1.v ? 1 : 0; }
    | relExpr '!=' subExpr { relExpr.v = relExpr1.v != subExpr1.v ? 1 : 0; }
    | relExpr '&&' subExpr { relExpr.v = relExpr1.v != 0 && subExpr1.v != 0 ? 1 : 0; }
    | relExpr '||' subExpr { relExpr.v = relExpr1.v != 0 || subExpr1.v != 0 ? 1 : 0; }
    | relExpr '^' subExpr { relExpr.v = relExpr1.v != 0 ^ subExpr1.v != 0 ? 1 : 0; }

subExpr<v:int> ::= subExpr0 { subExpr.v = subExpr01.v; }

subExpr0<v:int> ::=
    subExpr1 { subExpr0.v = subExpr11.v; }
    | subExpr0 '+' subExpr1 { subExpr0.v = subExpr01.v + subExpr11.v; }
    | subExpr0 '-' subExpr1 { subExpr0.v = subExpr01.v - subExpr11.v; }

subExpr1<v:int> ::=
    subExpr2 { subExpr1.v = subExpr21.v; }
    | subExpr1 '*' subExpr2 { subExpr1.v = subExpr11.v * subExpr21.v; }
    | subExpr1 '/' subExpr2 { subExpr1.v = subExpr11.v / subExpr21.v; }
    | subExpr1 '%' subExpr2 { subExpr1.v = subExpr11.v % subExpr21.v; }

subExpr2<v:int> ::=
    subExpr3 { subExpr2.v = subExpr31.v; }
    | '-' subExpr2 { subExpr2.v = -subExpr21.v; }
    | '+' subExpr2 { subExpr2.v = subExpr21.v; }
    | '!' subExpr2 { subExpr2.v = subExpr21.v == 0 ? 1 : 0; }

subExpr3<v:int> ::=
    operand { subExpr3.v = operand1.v; }
    | operand '**' subExpr3 { subExpr3.v = (int) Math.pow(operand1.v, subExpr31.v); }

operand<v:int> ::= ID {operand.v = variables.get(ID1.v); }
    | INTEGER {operand.v = INTEGER1.v; }
    | '(' relExpr ')' {operand.v = relExpr1.v; }

JavaFX Plugin

Nothing too interesting about it. Except when creating a new instance of a solid, a entry is added to a HashMap with an ID as key and a reference to the solid as value. Then, this integer ID is returned to the front-end. The front-end can then use this ID to refer to the solid when communnicating with the back-end.