aboutsummaryrefslogtreecommitdiff
path: root/lib/core/metaprogramming.nom
diff options
context:
space:
mode:
Diffstat (limited to 'lib/core/metaprogramming.nom')
-rw-r--r--lib/core/metaprogramming.nom459
1 files changed, 459 insertions, 0 deletions
diff --git a/lib/core/metaprogramming.nom b/lib/core/metaprogramming.nom
new file mode 100644
index 0000000..936f9bd
--- /dev/null
+++ b/lib/core/metaprogramming.nom
@@ -0,0 +1,459 @@
+#!/usr/bin/env nomsu -V6.14
+#
+ This File contains actions for making actions and compile-time actions and some helper
+ functions to make that easier.
+
+lua> "NOMSU_CORE_VERSION = 14"
+lua> "NOMSU_LIB_VERSION = 8"
+lua> ("
+ do
+ local mangle_index = 0
+ function mangler()
+ local my_mangle_index = mangle_index
+ mangle_index = mangle_index + 1
+ return function(varname)
+ return (varname..(("\\3%X"):format(my_mangle_index))):as_lua_id()
+ end
+ end
+ end
+ COMPILE_RULES["define mangler"] = function(\(nomsu environment))
+ return LuaCode("local mangle = mangler()")
+ end
+")
+
+lua> ("
+ COMPILE_RULES["1 ->"] = function(\(nomsu environment), \$args, \$body)
+ if \$args and not \$body then \$args, \$body = {}, \$args end
+ local body_lua = SyntaxTree:is_instance(\$body) and \(nomsu environment):compile(\$body) or \$body
+ if SyntaxTree:is_instance(\$body) and \$body.type ~= "Block" then body_lua:prepend("\
+ ..return ") end
+ local lua = LuaCode("(function(")
+ if SyntaxTree:is_instance(\$args) and (\$args.type == "Action" or \$args.type == "MethodCall") then
+ \$args = \$args:get_args()
+ elseif SyntaxTree:is_instance(\$args) and \$args.type == "Var" then \$args = {\$args} end
+ for i, arg in ipairs(\$args) do
+ local arg_lua = SyntaxTree:is_instance(arg) and \(nomsu environment):compile(arg):text() or arg
+ if arg_lua == "..." then
+ if i < #\$args then
+ compile_error_at(SyntaxTree:is_instance(arg) and arg or nil,
+ "Extra arguments must come last.", "Try removing any arguments after \
+ ..(*extra arguments*)")
+ end
+ elseif not arg_lua:is_lua_id() then
+ compile_error_at(SyntaxTree:is_instance(arg) and arg or nil,
+ "This does not compile to a Lua identifier, so it can't be used as a function \
+ ..argument.",
+ "This should probably be a Nomsu variable instead (like $x).")
+ end
+ lua:add(i > 1 and ", " or "", arg_lua)
+ body_lua:remove_free_vars({arg_lua})
+ end
+ body_lua:declare_locals()
+ lua:add(")\\n ", body_lua, "\\nend)")
+ return lua
+ end
+ COMPILE_RULES["->"] = COMPILE_RULES["1 ->"]
+ COMPILE_RULES["for"] = COMPILE_RULES["1 ->"]
+")
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+test:
+ (five) compiles to "5"
+
+test:
+ unless ((five) == 5):
+ fail "Compile to expression failed."
+ (loc x) compiles to "local x = 99;"
+
+test:
+ lua> "do"
+ loc x
+ unless ($x is 99):
+ fail "Compile to statements with locals failed."
+ lua> "end"
+ unless ($x is (nil)):
+ fail "Failed to properly localize a variable."
+
+ (asdf) compiles to:
+ $tmp = ""
+ return (Lua $tmp)
+
+test:
+ asdf
+ unless ($tmp is (nil)):
+ fail "compile to is leaking variables"
+
+lua> ("
+ COMPILE_RULES["1 compiles to"] = function(\(nomsu environment), \$action, \$body)
+ local \$args = List{"\(nomsu environment)", unpack(\$action:get_args())}
+ if \$body.type == "Text" then
+ \$body = SyntaxTree{source=\$body.source, type="Action", "Lua", \$body}
+ end
+ return LuaCode("COMPILE_RULES[", \$action:get_stub():as_lua(),
+ "] = ", \(\($args -> $body) as lua))
+ end
+")
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+($actions all compile to $body) compiles to:
+ lua> ("
+ if \$actions.type ~= "List" then
+ compile_error(\$actions, "This should be a list of actions.")
+ end
+ local lua = \(\($actions.1 compiles to $body) as lua)
+ local \$args = List{"\(nomsu environment)", unpack(\$actions[1]:get_args())}
+ local \$compiled_args = List{"\(nomsu environment)"};
+ for i=2,#\$args do \$compiled_args[i] = \(nomsu environment):compile(\$args[i]) end
+ for i=2,#\$actions do
+ local alias = \$actions[i]
+ local \$alias_args = List{"\(nomsu environment)", unpack(alias:get_args())}
+ lua:add("\\nCOMPILE_RULES[", alias:get_stub():as_lua(), "] = ")
+ if \$alias_args == \$args then
+ lua:add("COMPILE_RULES[", \$actions[1]:get_stub():as_lua(), "]")
+ else
+ lua:add("function(")
+ local \$compiled_alias_args = List{"\(nomsu environment)"};
+ for i=2,#\$alias_args do \$compiled_alias_args[i] = \(nomsu environment):compile(\$alias_args[i]) end
+ lua:concat_add(\$compiled_alias_args, ", ")
+ lua:add(") return COMPILE_RULES[", \$actions[1]:get_stub():as_lua(), "](")
+ lua:concat_add(\$compiled_args, ", ")
+ lua:add(") end")
+ end
+ end
+ return lua
+ ")
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+test:
+ (foo $x) means "outer"
+ with [$(foo $)]:
+ (foo $x) means:
+ $y = ($x + 1)
+ return $y
+
+ unless ((foo 10) == 11):
+ fail "Action didn't work."
+
+ unless ($y is (nil)):
+ fail "Action leaked a local into globals."
+
+ (baz $) parses as (foo $)
+ assume ((foo 1) == "outer")
+
+($action means $body) compiles to:
+ lua> ("
+
+ local lua = LuaCode()
+ if \$action.type == "MethodCall" then
+ lua:add(\(nomsu environment):compile(\$action[1]), ".", \$action[2]:get_stub():as_lua_id())
+ elseif \$action.type == "Action" then
+ lua:add(\$action:get_stub():as_lua_id())
+ lua:add_free_vars({\$action:get_stub():as_lua_id()})
+ else
+ compile_error_at(\$action, "Expected an action or method call here")
+ end
+ lua:add(" = ", \(\($action -> $body) as lua), ";")
+ return lua
+ ")
+
+($actions all mean $body) compiles to:
+ lua> ("
+ local lua = \(\($actions.1 means $body) as lua)
+ local first_def = (\$actions[1].type == "MethodCall"
+ and LuaCode(\(nomsu environment):compile(\$actions[1][1]), ".", \$actions[1]:get_stub():as_lua_id())
+ or LuaCode(\$actions[1]:get_stub():as_lua_id()))
+ local \$args = List(\$actions[1]:get_args())
+ for i=2,#\$actions do
+ local alias = \$actions[i]
+ local \$alias_args = List(alias:get_args())
+ lua:add("\\n")
+ if alias.type == "MethodCall" then
+ lua:add(\(nomsu environment):compile(alias[1]), ".", alias:get_stub():as_lua_id())
+ else
+ lua:add(alias:get_stub():as_lua_id())
+ lua:add_free_vars({alias_name})
+ end
+ if \$args == \$alias_args then
+ lua:add(" = ", first_def, ";")
+ else
+ lua:add(" = ", \(\($alias_args -> $actions.1) as lua), ";")
+ end
+ end
+ return lua
+ ")
+
+test:
+ externally (baz1) means:
+ return "baz1"
+ externally (baz2) means "baz2"
+
+test:
+ assume ((baz1) == "baz1")
+ assume ((baz2) == "baz2")
+
+(externally $action means $body) compiles to:
+ lua> ("
+ local lua = \(\($action means $body) as lua)
+ lua:remove_free_vars({\$action:get_stub():as_lua_id()})
+ return lua
+ ")
+
+(externally $actions all mean $body) compiles to:
+ lua> ("
+ local lua = \(\($actions all mean $body) as lua)
+ lua:remove_free_vars(table.map(\$actions, function(a) return a:get_stub():as_lua_id() end))
+ return lua
+ ")
+
+test:
+ (swap $x and $y) parses as
+ do:
+ $tmp = $x
+ $x = $y
+ $y = $tmp
+
+test:
+ [$1, $2] = [1, 2]
+ swap $1 and $2
+ unless (($1 == 2) and ($2 == 1)):
+ fail "'parse $ as $' failed on 'swap $ and $'"
+ [$tmp, $tmp2] = [1, 2]
+ swap $tmp and $tmp2
+ unless (($tmp == 2) and ($tmp2 == 1)):
+ fail "'parse $ as $' variable mangling failed."
+
+($actions all parse as $body) compiles to:
+ lua> ("
+ local replacements = {}
+ if \$actions.type ~= "List" then
+ compile_error(\$actions, "This should be a list.")
+ end
+ for i,arg in ipairs(\$actions[1]:get_args()) do
+ replacements[arg[1]] = \(nomsu environment):compile(arg):text()
+ end
+ local function make_tree(t)
+ if SyntaxTree:is_instance(t) and t.type == "Var" then
+ if replacements[t:as_var()] then
+ return replacements[t:as_var()]
+ else
+ return "SyntaxTree{mangle("..t:as_var():as_lua().."), type="..t.type:as_lua()..", \
+ ..source="..tostring(t.source):as_lua().."}"
+ end
+ elseif SyntaxTree:is_instance(t) then
+ local ret = {}
+ local i = 1
+ for k, v in pairs(t) do
+ if k == i then
+ ret[#ret+1] = make_tree(t[i])
+ i = i + 1
+ elseif k == "source" then
+ ret[#ret+1] = k.."= "..tostring(v):as_lua()
+ elseif lua_type_of(k) == 'string' and k:is_a_lua_id() then
+ ret[#ret+1] = k.."= "..make_tree(v)
+ else
+ ret[#ret+1] = "["..make_tree(k).."]= "..make_tree(v)
+ end
+ end
+ return "SyntaxTree{"..table.concat(ret, ", ").."}"
+ elseif lua_type_of(t) == 'number' then
+ return tostring(t)
+ else
+ return t:as_lua()
+ end
+ end
+ local \$new_body = LuaCode:from(\$body.source,
+ "local mangle = mangler()",
+ "\\nreturn ", make_tree(\$body))
+ local ret = \(\($actions all compile to $new_body) as lua)
+ return ret
+ ")
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[$action parses as $body] all parse as ([$action] all parse as $body)
+externally (in (nomsu environment) $tree as lua expr) means:
+ lua> ("
+ local tree_lua = \(nomsu environment):compile(\$tree)
+ if \$tree.type == 'Block' then
+ tree_lua = LuaCode:from(\$tree.source, '(function()\\n ', tree_lua, '\\nend)()')
+ elseif \$tree.type == 'MethodCall' and #\$tree > 2 then
+ compile_error_at(\$tree, "This must be a single value instead of "..(#\$tree - 1).."\
+ .. method calls.",
+ "Replace this with a single method call.")
+ end
+ return tree_lua
+ ")
+
+# Need to make sure the proper environment is used for compilation (i.e. the caller's environment)
+($tree as lua expr) compiles to (\(in \(nomsu environment) $tree as lua expr) as lua)
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+externally [$var as lua identifier, $var as lua id] all mean:
+ lua> ("
+ local lua = \($var as lua)
+ if not lua:text():is_a_lua_id() then
+ compile_error(\$var,
+ "This is supposed to be something that compiles to a valid Lua identifier.",
+ "This should probably be a variable.")
+ end
+ return lua
+ ")
+
+test:
+ (num args (*extra arguments*)) means (select "#" (*extra arguments*))
+ assume (num args 1 2 3) == 3
+ (extra args (*extra arguments*)) means [*extra arguments*]
+ assume (extra args 1 2 3) == [1, 2, 3]
+ (third arg (*extra arguments*)) means (select 3 (*extra arguments*))
+ assume (third arg 5 6 7 8) == 7
+
+(*extra arguments*) compiles to "..."
+
+($ is syntax tree) compiles to "SyntaxTree:is_instance(\($ as lua expr))"
+
+externally ($ is $kind syntax tree) means
+ =lua "SyntaxTree:is_instance(\$) and \$.type == \$kind"
+
+($tree with $t -> $replacement) compiles to ("
+ \($tree as lua expr):map(function(\($t as lua expr))
+ \(
+ =lua ("
+ \$replacement.type == 'Block' and \($replacement as lua) or 'return '..\
+ ..\($replacement as lua expr):text()
+ ")
+ )
+ end)
+")
+
+externally ($tree with vars $replacements) means
+ =lua ("
+ \$tree:map(function(\$t)
+ if \$t.type == "Var" then
+ return \$replacements[\$t:as_var()]
+ end
+ end)
+ ")
+
+(tree $tree with vars $replacements) compiles to ("
+ \(=lua "(\$tree):as_lua()"):map(function(t)
+ if t.type == "Var" then
+ return \($replacements as lua expr)[t:as_var()]
+ end
+ end)
+")
+
+($tree has subtree $match_tree) compiles to ("
+ (function()
+ local match_tree = \($match_tree as lua expr)
+ for subtree in coroutine_wrap(function() \($tree as lua expr):map(yield) end) do
+ if subtree == match_tree then return true end
+ end
+ end)()
+")
+
+externally (match $tree with $patt) means:
+ lua> ("
+ if \$patt.type == "Var" then return Dict{[\$patt:as_var()]=\$tree} end
+ if \$patt.type == "Action" and \$patt:get_stub() ~= \$tree:get_stub() then return nil end
+ if #\$patt ~= #\$tree then return nil end
+ local matches = Dict{}
+ for \($i)=1,#\$patt do
+ if SyntaxTree:is_instance(\$tree[\$i]) then
+ local submatch = \(match $tree.$i with $patt.$i)
+ if not submatch then return nil end
+ for k,v in pairs(submatch) do
+ if matches[k] and matches[k] ~= v then return nil end
+ matches[k] = v
+ end
+ end
+ end
+ return matches
+ ")
+
+test:
+ assume
+ (
+ quote ("
+ one
+ "two"
+ ")
+ ) == "\"one\\n\\\"two\\\"\""
+
+(quote $s) compiles to "tostring(\($s as lua expr)):as_lua()"
+test:
+ assume (lua type of {}) == "table"
+ assume (type of {}) == "Dict"
+ assume ({} is a "Dict")
+ assume ("" is text)
+ assume ("" isn't a "Dict")
+externally ($ is text) means (=lua "\(lua type of $) == 'string'")
+externally [$ is not text, $ isn't text] all mean
+ =lua "\(lua type of $) ~= 'string'"
+
+externally (type of $) means:
+ lua> ("
+ local lua_type = \(lua type of $)
+ if lua_type == 'string' then return 'Text'
+ elseif lua_type == 'table' or lua_type == 'userdata' then
+ local mt = getmetatable(\$)
+ if mt and mt.__type then return mt.__type end
+ end
+ return lua_type
+ ")
+
+[$ is a $type, $ is an $type] all parse as ((type of $) == $type)
+[$ isn't a $type, $ isn't an $type, $ is not a $type, $ is not an $type]
+..all parse as ((type of $) != $type)
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+test:
+ (foo) means:
+ return 100 200 300
+ assume (select 2 (foo)) == 200
+
+# Return statement is wrapped in a do..end block because Lua is unhappy if you
+ put code after a return statement, unless you wrap it in a block.
+(return (*extra arguments*)) compiles to:
+ lua> ("
+ local lua = \(Lua "do return ")
+ for i=1,select('#',...) do
+ if i > 1 then lua:add(", ") end
+ lua:add(\(nomsu environment):compile((select(i, ...))))
+ end
+ lua:add(" end")
+ return lua
+ ")
+
+# Literals
+(yes) compiles to "true"
+(no) compiles to "false"
+[nothing, nil, null] all compile to "nil"
+(Nomsu syntax version) compiles to "NOMSU_SYNTAX_VERSION"
+(Nomsu compiler version) compiles to "NOMSU_COMPILER_VERSION"
+(core version) compiles to "NOMSU_CORE_VERSION"
+(lib version) compiles to "NOMSU_LIB_VERSION"
+(command line args) compiles to "COMMAND_LINE_ARGS"
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+#
+ (with local compile actions $body) compiles to ("
+ do
+ local OLD_RULES = COMPILE_RULES
+ local OLD_ENV = \(nomsu environment)
+ local \(nomsu environment) = setmetatable({
+ COMPILE_RULES=setmetatable({}, {__index=OLD_RULES})
+ }, {__index=OLD_ENV})
+ \($body as lua)
+ end
+ ")
+
+externally (Nomsu version) means:
+ return ("
+ \(Nomsu syntax version).\(core version).\(Nomsu compiler version).\(lib version)
+ ")