lib/metaprogramming.nom is working!

This commit is contained in:
Bruce Hill 2017-09-25 17:02:00 -07:00
parent 8afab37c90
commit 02def0af92
3 changed files with 163 additions and 119 deletions

View File

@ -5,32 +5,74 @@
# Nil # Nil
lua code ".." lua code ".."
|nomsu:defmacro("nil", function(nomsu, vars) return "nil", nil end) |nomsu:defmacro("nil", function(nomsu, vars) return "nil", nil end)
..with value 0
# Macros: # Macros:
lua code ".." lua code ".."
|nomsu:def("parse %shorthand as %longhand", function(nomsu, vars) |local function parse_as(nomsu, vars)
| local alias = nomsu:get_alias(vars.shorthand) | if vars.shorthand.type ~= "Block" then
| nomsu:error("Expected shorthand to be Block, but got "..vars.shorthand.type)
| end
| if vars.longhand.type ~= "Block" then
| nomsu:error("Expected longhand to be Block, but got "..vars.longhand.type)
| end
| local template = vars.longhand | local template = vars.longhand
| nomsu:defmacro(alias, function(nomsu, vars) | local function parsing_as(nomsu, vars)
| return nomsu:tree_to_lua(nomsu:replaced_vars(template, vars)) | local expanded = nomsu:replaced_vars(template, vars)
| end) | local expr,statement = nomsu:tree_to_lua(expanded)
|end) | return expr, statement
..with value (nil) | end
| for _,call in ipairs(vars.shorthand.value) do
| nomsu:defmacro(call, parsing_as)
| end
|end
|nomsu:def("parse nomsu %shorthand as nomsu %longhand", parse_as)
parse nomsu \(parse %shorthand as %longhand) as nomsu \(parse nomsu \%shorthand as nomsu \%longhand)
parse (foo %x) as (baz %x)
parse \(lua code %code) as\: lua code %code with value (nil) lua code ".."
parse \(lua block %block) as\: |nomsu:defmacro("lua expr %code", function(nomsu, vars)
| return nomsu:tree_to_value(vars.code, vars), nil
|end)
# Rule that lets you make new rules
lua code ".."
|nomsu:defmacro("rule %rule_def = %body", function(nomsu, vars)
| if vars.rule_def.type ~= "Block" then
| nomsu:error("Wrong type for rule definition, expected Block, but got "..vars.rule_def.type)
| end
| local thunk = nomsu:tree_to_lua({type="Thunk", value=vars.body, src=vars.body.src})
| local fn_name = "fn_"..nomsu:var_to_lua_identifier(nomsu:get_stub(vars.rule_def.value[1]))
| local lua = ([[
|do
| local %s = %s
| for _,alias in ipairs(%s) do
| nomsu:def(alias, %s)
| end
|end]]):format(fn_name, thunk, nomsu:repr(vars.rule_def.value), fn_name)
| return nil, lua
|end)
parse (rule %rule_name) as: lua expr "nomsu.defs[(nomsu:get_stub(\(%rule_name).value[1]))]"
# Macro helper functions
rule (%tree as value) =:
lua expr "nomsu:tree_to_value(vars.tree, vars)"
rule (%tree as lua) =:
lua expr "nomsu:tree_to_lua(vars.tree)"
parse (lua block %block) as:
lua code ".." lua code ".."
|do |do
| \(%block) | \(%block)
|end |end
..with value (nil)
parse \(lua value %value) as\: lua code (nil) with value %value
parse \(nomsu) as\: lua value "nomsu" parse (nomsu) as: lua expr "nomsu"
parse \(nomsu's %key) as\: parse (nomsu's %key) as:
lua value "nomsu[\(%key as lua)]" lua expr "nomsu[\(%key as lua)]"
parse \(nomsu %method %args) as\: parse (nomsu %method %args) as:
lua block ".." lua block ".."
|local args = {"nomsu"} |local args = {"nomsu"}
|for _,arg in ipairs(vars.args.value) do |for _,arg in ipairs(vars.args.value) do
@ -39,15 +81,7 @@ parse \(nomsu %method %args) as\:
|local method_name = nomsu:repr(nomsu:tree_to_value(vars.method, vars)) |local method_name = nomsu:repr(nomsu:tree_to_value(vars.method, vars))
..with value ".." ..with value ".."
|("nomsu[%s](%s)"):format(method_name, table.concat(args, ", ")) |("nomsu[%s](%s)"):format(method_name, table.concat(args, ", "))
parse \(repr %) as\: nomsu "repr" [%] parse (repr %) as: nomsu "repr" [%]
# Rule that lets you make new rules
lua block ".."
|nomsu:def("rule helper %rule_def = %body", function(nomsu, vars)
| nomsu:def(nomsu:get_alias(vars.rule_def), vars.body)
|end)
parse \(rule %rule_def = %body) as\: rule helper \(%rule_def) = \(%body)
parse \(rule %rule_name) as\: lua value "nomsu.defs[nomsu:get_alias(\(%rule_name))]"
# Get the source code for a function # Get the source code for a function
rule (help %rule) =: rule (help %rule) =:
@ -56,17 +90,10 @@ rule (help %rule) =:
|if not fn_def then |if not fn_def then
| nomsu:writeln("Rule not found: "..nomsu:repr(vars.rule)) | nomsu:writeln("Rule not found: "..nomsu:repr(vars.rule))
|else |else
| nomsu:writeln("rule "..nomsu:repr(nomsu.utils.keys(fn_def.aliases)) | nomsu:writeln("rule "..nomsu:repr(nomsu.utils.keys(fn_def.invocation))
| .." ="..(fn_def.src or ":\\n <unknown source code>")) | .." ="..(fn_def.src or ":\\n <unknown source code>"))
|end |end
# Macro helper functions
rule (%tree as value) =:
lua expr "nomsu:tree_to_value(vars.tree, vars)"
rule (%tree as lua) =:
lua expr "nomsu:tree_to_lua(vars.tree)"
# Compiler tools # Compiler tools
rule (eval %code; run %code) =: nomsu "run" [%code] rule (eval %code; run %code) =: nomsu "run" [%code]
rule (source code from tree %tree) =: rule (source code from tree %tree) =:
@ -80,9 +107,7 @@ rule (source code from tree %tree) =:
| return vars.tree.src:match(":%s*(%S.*)").."\\n" | return vars.tree.src:match(":%s*(%S.*)").."\\n"
|end |end
macro (source code %body) =: parse (source code %body) as: source code from tree \%body
nomsu "repr" [nomsu "call" ["source code from tree %", %body]]
macro (parse tree %code) =: parse (parse tree %code) as: repr (nomsu "tree_to_str" [\%code])
nomsu "repr" [nomsu "stringify_tree" [lua expr "vars.code.value"]]

View File

@ -16,7 +16,7 @@ lpeg = require 'lpeg'
utils = require 'utils' utils = require 'utils'
repr = utils.repr repr = utils.repr
{:insert, :remove, :concat} = table {:insert, :remove, :concat} = table
pcall = (fn,...)-> true, fn(...) --pcall = (fn,...)-> true, fn(...)
-- TODO: -- TODO:
-- use actual variables instead of a vars table -- use actual variables instead of a vars table
@ -74,9 +74,9 @@ nomsu = [=[
(dedent / (({.+} ("" -> "Error while parsing block")) => error)) (dedent / (({.+} ("" -> "Error while parsing block")) => error))
|} }) -> Block |} }) -> Block
inline_nomsu <- ({ ("\" inline_block ) }) -> Nomsu inline_nomsu <- ({ ("\" inline_expression) }) -> Nomsu
eol_nomsu <- ({ ("\" eol_block ) }) -> Nomsu eol_nomsu <- ({ ("\" noeol_expression) }) -> Nomsu
indented_nomsu <- ({ ("\" {indented_block} ) }) -> Nomsu indented_nomsu <- ({ ("\" expression) }) -> Nomsu
inline_expression <- number / variable / inline_string / inline_list / inline_block / inline_nomsu inline_expression <- number / variable / inline_string / inline_list / inline_block / inline_nomsu
noeol_expression <- indented_string / indented_block / indented_nomsu / indented_list / inline_expression noeol_expression <- indented_string / indented_block / indented_nomsu / indented_list / inline_expression
@ -187,42 +187,40 @@ class NomsuCompiler
@write("\n") @write("\n")
def: (invocation, thunk, src)=> def: (invocation, thunk, src)=>
if type(invocation) != 'string' then @error "Invocation should be string, not: #{repr invocation}" stub, arg_names = @get_stub invocation
if @debug then @writeln "Defining rule: #{repr invocation}" assert stub, "NO STUB FOUND: #{repr invocation}"
stub = invocation\gsub("'"," '")\gsub("%%%S+","%%")\gsub("%s+"," ") if @debug then @writeln "Defining rule: #{repr stub} with args #{repr arg_names}"
args = [arg for arg in invocation\gmatch("%%(%S[^%s']*)")] for i=1,#arg_names-1 do for j=i+1,#arg_names
for i=1,#args-1 do for j=i+1,#args if arg_names[i] == arg_names[j] then @error "Duplicate argument in function #{stub}: '#{arg_names[i]}'"
if args[i] == args[j] then @error "Duplicate argument in function def: #{args[i]}" with @defs[stub] = {:thunk, :invocation, :arg_names, :src, is_macro:false} do nil
with @defs[invocation] = {:thunk, :invocation, :args, :src, is_macro:false} do nil
defmacro: (invocation, thunk, src)=> defmacro: (invocation, thunk, src)=>
with @def(invocation, thunk, src) do .is_macro = true with @def(invocation, thunk, src) do .is_macro = true
call: (alias,...)=> call: (stub,...)=>
def = @defs[alias] def = @defs[stub]
if def == nil if def == nil
@error "Attempt to call undefined function: #{alias}" @error "Attempt to call undefined function: #{stub}"
-- This is a little bit hacky, but having this check is handy for catching mistakes -- This is a little bit hacky, but having this check is handy for catching mistakes
-- I use a hash sign in "#macro" so it's guaranteed to not be a valid function name -- I use a hash sign in "#macro" so it's guaranteed to not be a valid function name
if def.is_macro and @callstack[#@callstack] != "#macro" if def.is_macro and @callstack[#@callstack] != "#macro"
@error "Attempt to call macro at runtime: #{alias}\nThis can be caused by using a macro in a function that is defined before the macro." @error "Attempt to call macro at runtime: #{stub}\nThis can be caused by using a macro in a function that is defined before the macro."
unless @check_permission(def) unless @check_permission(def)
@error "You do not have the authority to call: #{alias}" @error "You do not have the authority to call: #{stub}"
{:thunk, :args} = def {:thunk, :arg_names} = def
args = {name, select(i,...) for i,name in ipairs(args)} args = {name, select(i,...) for i,name in ipairs(arg_names)}
if @debug if @debug
@writeln "Calling #{repr alias} with args: #{repr(args)}" @writeln "Calling #{repr stub} with args: #{repr(args)}"
insert @callstack, alias insert @callstack, stub
-- TODO: optimize, but still allow multiple return values? -- TODO: optimize, but still allow multiple return values?
rets = {thunk(self,args)} rets = {thunk(self,args)}
remove @callstack remove @callstack
return unpack(rets) return unpack(rets)
run_macro: (tree, kind="Expression")=> run_macro: (tree, kind="Expression")=>
local args, alias stub,args = @get_stub tree
alias,args = @get_alias tree
insert @callstack, "#macro" insert @callstack, "#macro"
expr, statement = @call(alias, unpack(args)) expr, statement = @call(stub, unpack(args))
remove @callstack remove @callstack
return expr, statement return expr, statement
@ -257,12 +255,15 @@ class NomsuCompiler
run: (src, filename)=> run: (src, filename)=>
tree = @parse(src, filename) tree = @parse(src, filename)
assert tree, "Tree failed to compile: #{src}" assert tree, "Tree failed to compile: #{src}"
assert tree.type == "File" assert tree.type == "File", "Attempt to run non-file: #{tree.type}"
buffer = {} buffer = {}
vars = {} vars = {}
return_value = nil return_value = nil
for statement in *tree.value for statement in *tree.value
if @debug
@writeln "RUNNING TREE:"
@print_tree statement
ok,expr,statements = pcall(@tree_to_lua, self, statement) ok,expr,statements = pcall(@tree_to_lua, self, statement)
if not ok if not ok
@writeln "Error occurred in statement:\n#{statement.src}" @writeln "Error occurred in statement:\n#{statement.src}"
@ -282,7 +283,7 @@ class NomsuCompiler
if expr then return_value = ret if expr then return_value = ret
if not ok if not ok
@writeln "Error occurred in statement:\n#{statement.src}" @writeln "Error occurred in statement:\n#{statement.src}"
@error(return_value) @error(repr return_value)
insert buffer, "#{statements or ''}\n#{expr and "ret = #{expr}" or ''}" insert buffer, "#{statements or ''}\n#{expr and "ret = #{expr}" or ''}"
lua_code = ([[ lua_code = ([[
@ -305,6 +306,7 @@ class NomsuCompiler
-- Return <lua code for value>, <additional lua code> -- Return <lua code for value>, <additional lua code>
assert tree, "No tree provided." assert tree, "No tree provided."
if not tree.type if not tree.type
@writeln debug.traceback()
@error "Invalid tree: #{repr(tree)}" @error "Invalid tree: #{repr(tree)}"
switch tree.type switch tree.type
when "File" when "File"
@ -313,27 +315,32 @@ class NomsuCompiler
when "Nomsu" when "Nomsu"
return repr(tree.value), nil return repr(tree.value), nil
when "Block" when "Thunk" -- This is not created by the parser, it's just a helper
lua_bits = {} lua_bits = {}
for arg in *tree.value for arg in *tree.value.value
expr,statement = @tree_to_lua arg expr,statement = @tree_to_lua arg
-- Optimization for common case
if expr and not statement and #tree.value == 1
return expr, nil
if statement then insert lua_bits, statement if statement then insert lua_bits, statement
if expr then insert lua_bits, "ret = #{expr}" if expr then insert lua_bits, "ret = #{expr}"
return ([[ return ([[
function(nomsu, vars) (function(nomsu, vars)
local ret local ret
%s %s
return ret return ret
end]])\format(concat lua_bits, "\n") end)]])\format(concat lua_bits, "\n")
when "Block"
if #tree.value == 1
expr,statement = @tree_to_lua tree.value[1]
if not statement
return expr, nil
thunk_lua = @tree_to_lua {type:"Thunk", value:tree, src:tree.src}
return ("%s(nomsu, vars)")\format(thunk_lua), nil
when "FunctionCall" when "FunctionCall"
alias = @get_alias(tree) stub = @get_stub(tree)
if @defs[alias] and @defs[alias].is_macro if @defs[stub] and @defs[stub].is_macro
return @run_macro(tree, "Expression") return @run_macro(tree, "Expression")
args = {repr(alias)} args = {repr(stub)}
for arg in *tree.value for arg in *tree.value
if arg.type == 'Word' then continue if arg.type == 'Word' then continue
expr,statement = @tree_to_lua arg expr,statement = @tree_to_lua arg
@ -380,16 +387,32 @@ class NomsuCompiler
else else
@error("Unknown/unimplemented thingy: #{tree.type}") @error("Unknown/unimplemented thingy: #{tree.type}")
print_tree: (tree, ind="")=> walk_tree: (tree, depth=0)=>
if type(tree) ~= 'table' or not tree.type coroutine.yield(tree, depth)
@writeln "#{ind}#{repr tree}" if type(tree) != 'table' or not tree.type
return return
@writeln "#{ind}#{tree.type}:"
switch tree.type switch tree.type
when "List", "File", "Block", "FunctionCall", "String" when "List", "File", "Nomsu", "Block", "FunctionCall", "String"
for v in *tree.value for v in *tree.value
@print_tree(v, ind.." ") @walk_tree(v, depth+1)
else @print_tree(tree.value, ind.." ") else @walk_tree(tree.value, depth+1)
return nil
print_tree: (tree)=>
for node,depth in coroutine.wrap(-> @walk_tree tree)
if type(node) != 'table' or not node.type
@writeln((" ")\rep(depth)..repr(node))
else
@writeln("#{(" ")\rep(depth)}#{node.type}:")
tree_to_str: (tree)=>
bits = {}
for node,depth in coroutine.wrap(-> @walk_tree tree)
if type(node) != 'table' or not node.type
insert bits, ((" ")\rep(depth)..repr(node))
else
insert bits, ("#{(" ")\rep(depth)}#{node.type}:")
return concat(bits, "\n")
@unescape_string: (str)=> @unescape_string: (str)=>
str\gsub("\\(.)", ((c)-> STRING_ESCAPES[c] or c)) str\gsub("\\(.)", ((c)-> STRING_ESCAPES[c] or c))
@ -414,8 +437,8 @@ class NomsuCompiler
when "Var" when "Var"
if vars[tree.value] if vars[tree.value]
tree = vars[tree.value] tree = vars[tree.value]
when "File", "Thunk", "Statement", "Block", "List", "FunctionCall", "String" when "File", "Nomsu", "Thunk", "Block", "List", "FunctionCall", "String"
new_value = @replaced_vars tree.value new_value = @replaced_vars tree.value, vars
if new_value != tree.value if new_value != tree.value
tree = {k,v for k,v in pairs(tree)} tree = {k,v for k,v in pairs(tree)}
tree.value = new_value tree.value = new_value
@ -423,52 +446,39 @@ class NomsuCompiler
new_values = {} new_values = {}
any_different = false any_different = false
for k,v in pairs tree for k,v in pairs tree
new_values[k] = @replaced_vars v new_values[k] = @replaced_vars v, vars
any_different or= (new_values[k] != tree[k]) any_different or= (new_values[k] != tree[k])
if any_different if any_different
tree = new_values tree = new_values
return tree return tree
get_alias: (x)=> get_stub: (x)=>
if not x then @error "Nothing to get alias from" if not x
-- Returns a single alias ("say %"), and list of args ({msg}) from a single rule def @error "Nothing to get stub from"
-- Returns a single stub ("say %"), and list of args ({msg}) from a single rule def
-- (e.g. "say %msg") or function call (e.g. FunctionCall({Word("say"), Var("msg"))) -- (e.g. "say %msg") or function call (e.g. FunctionCall({Word("say"), Var("msg")))
if type(x) == 'string' if type(x) == 'string'
alias = x\gsub("'"," '")\gsub("%%%S+","%%")\gsub("%s+"," ") stub = x\gsub("'"," '")\gsub("%%%S+","%%")\gsub("%s+"," ")
args = [arg for arg in x\gmatch("%%(%S[^%s']*)")] args = [arg for arg in x\gmatch("%%([^%s']*)")]
return alias, args return stub, args
switch x.type switch x.type
when "String" then return @get_alias(x.value) when "String" then return @get_stub(x.value)
when "Statement" then return @get_alias(x.value)
when "FunctionCall" when "FunctionCall"
alias, args = {}, {}, {} stub, args = {}, {}, {}
for token in *x.value for token in *x.value
switch token.type switch token.type
when "Word" when "Word"
insert alias, token.value insert stub, token.value
when "Var" when "Var"
insert alias, "%" insert stub, "%"
insert args, token.value insert args, token.value
else else
insert alias, "%" insert stub, "%"
insert args, token insert args, token
return concat(alias," "), args return concat(stub," "), args
when "Block"
get_aliases:(x)=> @writeln debug.traceback!
if not x then @error "Nothing to get aliases from" @error "Please pass in a single line from a block, not the whole thing:\n#{@tree_to_str x}"
if type(x) == 'string'
alias, args = @get_alias(x)
return {[alias]: args}
switch x.type
when "String" then return @get_aliases({x.value})
when "Statement" then return @get_aliases({x.value})
when "FunctionCall" then return @get_aliases({x})
when "List" then x = x.value
when "Block" then x = x.value
with {}
for y in *x
alias,args = @get_alias(y)
[alias] = args
var_to_lua_identifier: (var)=> var_to_lua_identifier: (var)=>
-- Converts arbitrary nomsu vars to valid lua identifiers by replacing illegal -- Converts arbitrary nomsu vars to valid lua identifiers by replacing illegal
@ -480,7 +490,8 @@ class NomsuCompiler
error: (...)=> error: (...)=>
@writeln "ERROR!" @writeln "ERROR!"
@writeln(...) if select(1, ...)
@writeln(...)
@writeln("Callstack:") @writeln("Callstack:")
for i=#@callstack,1,-1 for i=#@callstack,1,-1
@writeln " #{@callstack[i]}" @writeln " #{@callstack[i]}"
@ -490,32 +501,39 @@ class NomsuCompiler
initialize_core: => initialize_core: =>
-- Sets up some core functionality -- Sets up some core functionality
@defmacro "lua code %statements with value %value", (vars)=> lua_code = (vars)=>
inner_vars = setmetatable({}, {__index:(_,key)-> "vars[#{repr(key)}]"}) inner_vars = setmetatable({}, {__index:(_,key)-> "vars[#{repr(key)}]"})
statements = @tree_to_value(vars.statements, inner_vars) lua = @tree_to_value(vars.code, inner_vars)
value = @tree_to_value(vars.value, inner_vars) return nil, lua
return value, statements @defmacro "lua code %code", lua_code
lua_value = (vars)=>
inner_vars = setmetatable({}, {__index:(_,key)-> "vars[#{repr(key)}]"})
lua = @tree_to_value(vars.code, inner_vars)
return lua, nil
@defmacro "lua value %code", lua_value
@def "require %filename", (vars)=> _require = (vars)=>
if not @loaded_files[vars.filename] if not @loaded_files[vars.filename]
file = io.open(vars.filename) file = io.open(vars.filename)
if not file if not file
@error "File does not exist: #{vars.filename}" @error "File does not exist: #{vars.filename}"
@loaded_files[vars.filename] = (@run(file\read('*a'), vars.filename)) or true @loaded_files[vars.filename] = (@run(file\read('*a'), vars.filename)) or true
return @loaded_files[vars.filename] return @loaded_files[vars.filename]
@def "require %filename", _require
@def "run file %filename", (vars)=> run_file = (vars)=>
file = io.open(vars.filename) file = io.open(vars.filename)
if not file if not file
@error "File does not exist: #{vars.filename}" @error "File does not exist: #{vars.filename}"
return @run(file\read('*a'), vars.filename) return @run(file\read('*a'), vars.filename)
@def "run file %filename", run_file
if arg and arg[1] if arg and arg[1]
--ProFi = require 'ProFi' --ProFi = require 'ProFi'
--ProFi\start() --ProFi\start()
c = NomsuCompiler() c = NomsuCompiler()
c.debug = true
input = io.open(arg[1])\read("*a") input = io.open(arg[1])\read("*a")
-- If run via "./nomsu.moon file.nom -", then silence output and print generated -- If run via "./nomsu.moon file.nom -", then silence output and print generated
-- source code instead. -- source code instead.

View File

@ -1,6 +1,7 @@
local utils local utils
utils = { utils = {
is_list: (t)-> is_list: (t)->
if type(t) != 'table' then return false
i = 1 i = 1
for _ in pairs(t) for _ in pairs(t)
if t[i] == nil then return false if t[i] == nil then return false