-
Notifications
You must be signed in to change notification settings - Fork 3
MetaProgramming
- Extend NPL syntax with a LISP like AST structure.
- Compile *.npl source code into standard lua-compatible code (NOT byte code to support JIT) using AST (Compiler code needs to be written in NPL, please refer to metalua project)
- Add new NPL syntax to make NPL CAD language more expressive as a DSL (domain specific language)
Our goal is to support syntax like below in NPL cad.
translate(x,y,z){
for i=1, 4 do
rotate(i*45){ cube() }
end
}
The above *.npl code will be compiled to following lua-compatible code via meta programming
push()
translate(x,y,z)
for i=1, 4 do
push()
rotate(i*45);
cube()
pop()
end
pop()
Introduce a New LISP-like NPL AST syntax (called Func-expression)
symbol ( expressions ) {
anything here is subject to the macro of symbol
}
Func-expression is very similar to LISP S-expression, except that it does not break the original lua syntax very much. Yet it allows the same functional programming like in LISP. expressions in above code is just regular lua expression, so Func-expression has two part of inputs, one is regular expressions in brackets (), the others are anything else inside curly brackets {}. At dynamic compile time, code inside {} can be translated in arbitrary ways according to macro definition code usually via simple code replacement or AST parsing.
If no macro is defined for symbol, then above code is equivalent to calling a function of name symbol with expressions. i.e.
symbol ( expressions ){
statements
}
If a macro is defined for symbol, then above code may compile to a different AST according to the macro. We can dynamically define a new macro in *.npl using the def symbol.
def (symbol_names, expressions){
-- generate template code here, we can support metalua syntax here ...
}
NPL CAD utilizes above N-expression to create translate|rotate|... syntax with curly brackets, such as
def ({"translate, rotate"}, ... ) {
push();
_G[ -{ symbol_name} ](...);
-{nplp.emit(nplp.ast)}
pop();
}
def ("scale", p1 ) {
push();
scale(p1);
-{nplp.emit(nplp.getsource())}
pop();
}
Additional requirement:
- compiled source code should try to match source code line by line to make debugging possible
Suppose we have following function expression definition.
def("scale", p1, p2) {
push()
scale( +{=params("p1")}, p3)
+{ emit(); -- emit all code }
pop()
}
It will translate to following code after compilation using AST.
do
local f = nplp.FuncExpression:new():init("scale");
nplp.register(f);
-- generate compiled code
f.CompileCode = function(input)
local compiledCode = {};
local f_scope = {
emit = function(code)
if(code) then
compiledCode[#compiledCode+1] = code;
else
compiledCode[#compiledCode+1] = input:GetCodeAsString();
end
end,
-- helper function to get input params
params = function(name) return input:GetParams(name) end,
p1 = input:GetParams("p1"),
p2 = input:GetParams("p2"),
};
local function compile(input)
emit("push()\n")
emit("scale( ")
emit(params("p1"))
emit("p3)\n")
emit(); -- emit all code
emit("pop()\n")
end
setfenv(compile, f_scope);
compile();
return compiledCode;
end
end-- base class compile method
function FuncExpression:Compile(ast)
local lines = self:CompileCode(ast);
local sourceCode = table.concat(lines);
if(sourceCode) then
local final_ast = self:LoadAstFromString(sourceCode, self.filename, self.nLineOffset)
--recursively define if there is any custom functions in the ast that needs translation.
for any custom function in final_ast do
call inner custom function's compile recursively.
end
return final_ast;
end
end
-- virtual function: just return as it is.
function FuncExpression:CompileCode(ast)
return {ast:tostring()};
end
-- def function implementation
function FuncExpressionDef:CompileCode(ast)
local mode = self:GetModeFromAst(ast);
local f = nplp.FuncExpression:new():init(ast:getparam(1));
f:setmode(mode); -- mode can be strict, line, tokens, etc.
nplp.register(f);
local compiledCode = {};
local function compile()
for traverse ast do
if ast is not +{},
generate emit (ast)
else
genterate ast to string to compiledCode
end
end
end
setfenv(compile, f_scope);
compiledCode[#compiledCode+1] = {[[return function(ast)
local compiledCode = {};
local f_scope = {
emit = function(code)
if(code) then
compiledCode[#compiledCode+1] = code;
else
compiledCode[#compiledCode+1] = input:GetCodeAsString();
end
end,
-- helper function to get input params
params = function(name) return input:GetParams(name) end,
p1 = input:GetParams("p1"),
p2 = input:GetParams("p2"),
};
local function compile()
]]}
compile();
compiledCode[#compiledCode+1] = {[[
end
setfenv(compile, f_scope);
compile();
return compiledCode;
end ]]}
f.CompileCode = pcall(loadstring(compiledCode))
return {nil};
end- [dropped]namespaces:
A.B.C(){}
def("A.B.C")
- test case with lua syntax: such as
A({}){ {} } - generate and print errors: def("aaa"){ }
-- [string "?"]:1: ')' expected near '<eof>' <Syntax error>
log("<syntax error>[string "echo('hello'"]:1: ')' expected near '<eof>'")
- test line numbers
assert(debug.getinfo(1, "nSl").currentline == 1)
- invoke custom functions without
()likeA.B.C{} - replace code
func "" with func("") - add ast manipulation functions in side
+{}- emit(code, linenumber):
- @param linenumber: if nil, insert at current line. if -1, append to front. if [1,oo], try to emit at given line.
- emit(code, linenumber):
def("linenumber"){
local a=1;
a=a+1;
+{emit("a=a+1")}
+{emit("a=a+2", 4)}
+{emit(nil, 0)}
}
linenumber{ -- assert(line==1)
a=a+8
a=a+9 -- assert(line==3)
a=a+10
}
local a=1;a=a+1;a=a+1;\n
a=a+8\n
a=a+9\n
a=a+2;a=a+10\n
- helper functions on ast node that can be used inside +{}
def("circle"){
-- mode: unstrict
+{
local lines = ast:tolines();
for i=1, #lines do
local line = lines[i];
local radius = line:match("radius[%D]*([%d%.]+)")
if(radius) then
ast:replaceLine(i, format("drawingapi.drawcircle(%d)", radius));
end
end
}
}
circle { I want a circle with radius 2.0 }
circle { I want a circle with radius 3.0 }
def("PlusEqual"){
+{
nplp.addstatement("+","=", ...})
emit()
}
}
def("CodeSlice"){
+{ for i=1, 10 do } --
circle(i); -- emit("circle(i), 2)")
+{ end } --
}
-- support (...)
References:
- https://github.com/fab13n/metalua (for lexer, compiler and meta-language syntax)
- http://metalua.luaforge.net/src/index.html#lib
- http://metalua.luaforge.net/manual006.html#text16
Music board is very interesting tutorial for 3d printing http://www.thingiverse.com/thing:53235