add new parser script
This commit is contained in:
parent
dc34827847
commit
8d33ae0172
66
tpl_json.exs
Normal file
66
tpl_json.exs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
Code.require_file("tpl.exs")
|
||||||
|
|
||||||
|
defmodule TplJson do
|
||||||
|
# --- JSON Transpiler ---
|
||||||
|
# transforms sexps into lists
|
||||||
|
# braces into objects
|
||||||
|
# tokens and strings into strings
|
||||||
|
def to_json(ast) do
|
||||||
|
ast
|
||||||
|
|> Enum.map(&to_json_item/1)
|
||||||
|
|> IO.inspect(label: "AST for JSON")
|
||||||
|
# if the top level is a single elem, we output that directly
|
||||||
|
|> case do
|
||||||
|
[[single]] -> single
|
||||||
|
[single] -> single
|
||||||
|
multiple -> multiple
|
||||||
|
end
|
||||||
|
|> Jason.encode!(pretty: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp to_json_item(%{t: :sexp, list: list}) do
|
||||||
|
list |> Enum.map(&to_json_item/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp to_json_item(%{t: :brace, list: list}) do
|
||||||
|
list
|
||||||
|
|> Enum.map(fn
|
||||||
|
%{t: :sexp, list: [key_item | value_list]} ->
|
||||||
|
key = to_json_item(key_item)
|
||||||
|
|
||||||
|
value =
|
||||||
|
case value_list do
|
||||||
|
[ex] -> to_json_item(ex)
|
||||||
|
_ -> value_list |> Enum.map(&to_json_item/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
{key, value}
|
||||||
|
end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp to_json_item(%{t: :token, str: str}), do: str
|
||||||
|
|
||||||
|
defp to_json_item(%{t: :string, chunks: chunks, values: values}) do
|
||||||
|
# For simplicity, concatenate chunks and ignore interpolations
|
||||||
|
Enum.join(chunks)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json_output =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
asd "something else "
|
||||||
|
nested {
|
||||||
|
a ( b c asd asd asd)
|
||||||
|
d e f
|
||||||
|
inner {
|
||||||
|
x y z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> Tpl.to_ast()
|
||||||
|
|> TplJson.to_json()
|
||||||
|
|
||||||
|
IO.puts("\n--- Generated JSON Output ---")
|
||||||
|
IO.puts(json_output)
|
||||||
466
tpl_lua.exs
Normal file
466
tpl_lua.exs
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
Code.require_file("tpl.exs")
|
||||||
|
|
||||||
|
defmodule TplLua do
|
||||||
|
def transpile(ast_nodes) do
|
||||||
|
ast_nodes
|
||||||
|
|> Enum.map(&transpile_node(&1, 0))
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|> Enum.join("\n\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Filters out noise tokens that should not appear as arguments or statements
|
||||||
|
defp relevant?(nil), do: false
|
||||||
|
defp relevant?(%{t: t}) when t in [:newline, :comment], do: false
|
||||||
|
defp relevant?(_), do: true
|
||||||
|
|
||||||
|
# Coalesces `fn arg... brace` sequences in a flat token list into fn sexp nodes.
|
||||||
|
# Needed when `fn` appears as an argument inside a larger call, e.g. `map "k" fn { ... }`.
|
||||||
|
defp coalesce_fns([]), do: []
|
||||||
|
|
||||||
|
defp coalesce_fns([%{t: :token, str: "fn"} | rest]) do
|
||||||
|
{fn_args, remainder} = Enum.split_while(rest, fn n -> n[:t] not in [:brace] end)
|
||||||
|
case remainder do
|
||||||
|
[brace | after_brace] ->
|
||||||
|
fn_node = %{t: :sexp, list: [%{t: :token, str: "fn"} | fn_args ++ [brace]]}
|
||||||
|
[fn_node | coalesce_fns(after_brace)]
|
||||||
|
[] ->
|
||||||
|
[%{t: :token, str: "fn"} | coalesce_fns(rest)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp coalesce_fns([head | rest]), do: [head | coalesce_fns(rest)]
|
||||||
|
|
||||||
|
defp transpile_node(node, indent) do
|
||||||
|
indent_str = String.duplicate(" ", indent)
|
||||||
|
|
||||||
|
case node do
|
||||||
|
# Bug 9: comment nodes → Lua -- comments
|
||||||
|
%{t: :comment, str: str} ->
|
||||||
|
str
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map(&"#{indent_str}-- #{String.trim(&1)}")
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
# Bug 2 & 6: nil / noise / empty brace
|
||||||
|
nil -> ""
|
||||||
|
%{t: t} when t in [:newline, :dot] -> ""
|
||||||
|
|
||||||
|
# Bug 6: empty unnamed brace → empty Lua table
|
||||||
|
%{t: :brace, list: []} -> "{}"
|
||||||
|
|
||||||
|
%{t: :sexp, list: list} ->
|
||||||
|
transpile_sexp(list, indent)
|
||||||
|
|
||||||
|
%{t: :brace, list: list, name: %{str: "t"}} ->
|
||||||
|
transpile_table(list, indent)
|
||||||
|
|
||||||
|
%{t: :brace, list: list, name: %{str: "l"}} ->
|
||||||
|
transpile_list(list, indent)
|
||||||
|
|
||||||
|
%{t: :brace, list: list} ->
|
||||||
|
transpile_block(list, indent, :block)
|
||||||
|
|
||||||
|
%{t: :token, str: str} ->
|
||||||
|
str
|
||||||
|
|
||||||
|
%{t: :string, chunks: chunks, values: values} ->
|
||||||
|
transpile_string(chunks, values, indent)
|
||||||
|
|
||||||
|
other ->
|
||||||
|
" --[[ UNHANDLED NODE: #{inspect(other, pretty: true)} ]]--"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bug 7: non-brace fn body gets `return` + inner indentation
|
||||||
|
defp transpile_node(node, indent, context) do
|
||||||
|
case node do
|
||||||
|
%{t: :brace, list: list} ->
|
||||||
|
transpile_block(list, indent, context)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
body_str = transpile_node(node, indent)
|
||||||
|
|
||||||
|
case context do
|
||||||
|
:function_body ->
|
||||||
|
inner = String.duplicate(" ", indent + 1)
|
||||||
|
"#{inner}return #{body_str}"
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
body_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transpile_sexp(list, indent) do
|
||||||
|
indent_str = String.duplicate(" ", indent)
|
||||||
|
|
||||||
|
case list do
|
||||||
|
# Assignment: = lhs rhs
|
||||||
|
[%{str: "="}, %{t: :sexp, list: [%{str: "l"}, %{str: name}]}, rhs] ->
|
||||||
|
"local #{name} = #{transpile_node(rhs, indent)}"
|
||||||
|
|
||||||
|
[%{str: "="}, lhs, rhs] ->
|
||||||
|
"#{transpile_node(lhs, indent)} = #{transpile_node(rhs, indent)}"
|
||||||
|
|
||||||
|
# Bug 4: if form → if/then/end
|
||||||
|
[%{str: "if"} | rest] ->
|
||||||
|
filtered = Enum.filter(rest, &relevant?/1)
|
||||||
|
{cond_parts, [body]} = Enum.split(filtered, -1)
|
||||||
|
cond_str = cond_parts |> Enum.map(&transpile_node(&1, indent)) |> Enum.join(" ")
|
||||||
|
body_str = transpile_node(body, indent, :block)
|
||||||
|
"if #{cond_str} then\n#{body_str}\n#{indent_str}end"
|
||||||
|
|
||||||
|
# Bug 3: a : .method args → a.method(args)
|
||||||
|
[%{str: ":"}, lhs, %{t: :sexp, list: [%{str: "."}, nil, rhs]}] ->
|
||||||
|
lhs_str = transpile_node(lhs, indent)
|
||||||
|
|
||||||
|
case rhs do
|
||||||
|
%{t: :sexp, list: [method | args]} ->
|
||||||
|
method_str = transpile_node(method, indent)
|
||||||
|
arg_list = args |> Enum.filter(&relevant?/1) |> Enum.map(&transpile_node(&1, indent)) |> Enum.join(", ")
|
||||||
|
"#{lhs_str}.#{method_str}(#{arg_list})"
|
||||||
|
|
||||||
|
prop ->
|
||||||
|
"#{lhs_str}.#{transpile_node(prop, indent)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pipe: a : b c → a:b(c)
|
||||||
|
[%{str: ":"}, lhs, %{t: :sexp, list: [func | args]}] ->
|
||||||
|
lhs_str = transpile_node(lhs, indent)
|
||||||
|
func_str = transpile_node(func, indent)
|
||||||
|
# Bug 2: filter noise from args
|
||||||
|
arg_list = args |> Enum.filter(&relevant?/1) |> Enum.map(&transpile_node(&1, indent)) |> Enum.join(", ")
|
||||||
|
"#{lhs_str}:#{func_str}(#{arg_list})"
|
||||||
|
|
||||||
|
# Pipe: a : b → a:b()
|
||||||
|
[%{str: ":"}, lhs, rhs] ->
|
||||||
|
"#{transpile_node(lhs, indent)}:#{transpile_node(rhs, indent)}()"
|
||||||
|
|
||||||
|
# Dot access: . lhs rhs
|
||||||
|
[%{str: "."}, lhs, rhs] ->
|
||||||
|
"#{transpile_node(lhs, indent)}.#{transpile_node(rhs, indent)}"
|
||||||
|
|
||||||
|
# Anonymous function: fn args... body
|
||||||
|
[%{str: "fn"} | rest] ->
|
||||||
|
filtered = Enum.filter(rest, &relevant?/1)
|
||||||
|
{args, [body]} = Enum.split(filtered, -1)
|
||||||
|
arg_list = args |> Enum.map(&transpile_node(&1, indent)) |> Enum.join(", ")
|
||||||
|
body_str = transpile_node(body, indent, :function_body)
|
||||||
|
"function(#{arg_list})\n#{body_str}\n#{indent_str}end"
|
||||||
|
|
||||||
|
# For loop: for i v in (ipairs arr) { body }
|
||||||
|
[%{str: "for"} | rest] ->
|
||||||
|
filtered = Enum.filter(rest, &relevant?/1)
|
||||||
|
{vars_and_in, [body_node]} = Enum.split(filtered, -1)
|
||||||
|
{vars, [%{str: "in"}, call_node]} = Enum.split(vars_and_in, -2)
|
||||||
|
var_list = vars |> Enum.map(&transpile_node(&1, indent)) |> Enum.join(", ")
|
||||||
|
body_str = transpile_node(body_node, indent, :block)
|
||||||
|
"for #{var_list} in #{transpile_node(call_node, indent)} do\n#{body_str}\n#{indent_str}end"
|
||||||
|
|
||||||
|
# Function call: func arg1 arg2 ...
|
||||||
|
[func | args] ->
|
||||||
|
func_str = transpile_node(func, indent)
|
||||||
|
arg_list =
|
||||||
|
args
|
||||||
|
|> Enum.filter(&relevant?/1)
|
||||||
|
|> coalesce_fns()
|
||||||
|
|> Enum.map(&transpile_node(&1, indent))
|
||||||
|
|> Enum.join(", ")
|
||||||
|
"#{func_str}(#{arg_list})"
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
"()"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# True when a node is a block-level statement (if/for/while) that cannot be returned
|
||||||
|
defp block_stmt?(%{t: :sexp, list: [%{str: kw} | _]}) when kw in ["if", "for", "while"],
|
||||||
|
do: true
|
||||||
|
|
||||||
|
defp block_stmt?(_), do: false
|
||||||
|
|
||||||
|
defp transpile_block(list, indent, context) do
|
||||||
|
inner = String.duplicate(" ", indent + 1)
|
||||||
|
|
||||||
|
statements =
|
||||||
|
list
|
||||||
|
|> Enum.map(&transpile_node(&1, indent + 1))
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|> Enum.map(&"#{inner}#{&1}")
|
||||||
|
|
||||||
|
case context do
|
||||||
|
# Bug 8: don't prepend `return` to if/for/while
|
||||||
|
:function_body ->
|
||||||
|
last_node = list |> Enum.filter(&relevant?/1) |> List.last()
|
||||||
|
|
||||||
|
if last_node == nil or block_stmt?(last_node) do
|
||||||
|
statements |> Enum.join("\n")
|
||||||
|
else
|
||||||
|
case statements do
|
||||||
|
[] ->
|
||||||
|
""
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{body, [last]} = Enum.split(statements, -1)
|
||||||
|
return_last = String.replace(last, ~r/^\s*/, "#{inner}return ", global: false)
|
||||||
|
(body ++ [return_last]) |> Enum.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
statements |> Enum.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transpile_list(list, indent) do
|
||||||
|
if list == [] do
|
||||||
|
"{}"
|
||||||
|
else
|
||||||
|
inner = String.duplicate(" ", indent + 1)
|
||||||
|
elements = list |> Enum.map(&"#{inner}#{transpile_node(&1, indent + 1)}") |> Enum.join(",\n")
|
||||||
|
"{\n#{elements}\n#{String.duplicate(" ", indent)}}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transpile_table(list, indent) do
|
||||||
|
if list == [] do
|
||||||
|
"{}"
|
||||||
|
else
|
||||||
|
inner = String.duplicate(" ", indent + 1)
|
||||||
|
|
||||||
|
pairs =
|
||||||
|
list
|
||||||
|
|> Enum.flat_map(fn
|
||||||
|
%{t: :sexp, list: kv_list} ->
|
||||||
|
kv_list
|
||||||
|
|> Enum.filter(&relevant?/1)
|
||||||
|
|> Enum.chunk_every(2)
|
||||||
|
|> Enum.map(fn
|
||||||
|
[k, v] -> "#{inner}#{transpile_node(k, indent + 1)} = #{transpile_node(v, indent + 1)}"
|
||||||
|
[k] -> "#{inner}#{transpile_node(k, indent + 1)} = true"
|
||||||
|
end)
|
||||||
|
|
||||||
|
other ->
|
||||||
|
["#{inner}--[[ INVALID TABLE ENTRY: #{inspect(other)} ]]--"]
|
||||||
|
end)
|
||||||
|
|> Enum.join(",\n")
|
||||||
|
|
||||||
|
"{\n#{pairs}\n#{String.duplicate(" ", indent)}}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bug 5: unwrap brace so interpolated values aren't treated as indented blocks
|
||||||
|
defp transpile_interp_value(%{t: :brace, list: [single]}, indent),
|
||||||
|
do: transpile_node(single, indent)
|
||||||
|
|
||||||
|
defp transpile_interp_value(%{t: :brace, list: list}, indent) when list != [],
|
||||||
|
do: transpile_node(%{t: :sexp, list: list}, indent)
|
||||||
|
|
||||||
|
defp transpile_interp_value(other, indent),
|
||||||
|
do: transpile_node(other, indent)
|
||||||
|
|
||||||
|
defp transpile_string(chunks, values, indent) do
|
||||||
|
if values == [] do
|
||||||
|
"\"#{Enum.at(chunks, 0, "")}\""
|
||||||
|
else
|
||||||
|
parts =
|
||||||
|
Enum.zip(chunks, values)
|
||||||
|
|> Enum.flat_map(fn {chunk, value} ->
|
||||||
|
["\"#{chunk}\"", "tostring(#{transpile_interp_value(value, indent)})"]
|
||||||
|
end)
|
||||||
|
|
||||||
|
final_parts =
|
||||||
|
if length(chunks) > length(values),
|
||||||
|
do: parts ++ ["\"#{List.last(chunks)}\""],
|
||||||
|
else: parts
|
||||||
|
|
||||||
|
final_parts
|
||||||
|
|> Enum.reject(&(&1 == "\"\""))
|
||||||
|
|> Enum.join(" .. ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule TplLua.CLI do
|
||||||
|
@moduledoc """
|
||||||
|
Provides a command-line interface for transpiling `.lua.tpl` files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Transpiles `.lua.tpl` source files or directories into `.lua` files.
|
||||||
|
|
||||||
|
This function can be used in two ways:
|
||||||
|
|
||||||
|
1. **Directory to Directory**:
|
||||||
|
`run(input_dir, output_dir)`
|
||||||
|
Recursively finds all `*.lua.tpl` files in `input_dir`, transpiles them,
|
||||||
|
and writes the output `.lua` files to `output_dir`, preserving the
|
||||||
|
directory structure.
|
||||||
|
|
||||||
|
2. **File to File**:
|
||||||
|
`run(input_file, output_file)`
|
||||||
|
Transpiles a single `input_file` and writes the result to `output_file`.
|
||||||
|
|
||||||
|
Returns `:ok` on success, or `{:error, reason}` on failure.
|
||||||
|
"""
|
||||||
|
def run(input_path, output_path) do
|
||||||
|
cond do
|
||||||
|
File.dir?(input_path) ->
|
||||||
|
transpile_directory(input_path, output_path)
|
||||||
|
|
||||||
|
File.regular?(input_path) ->
|
||||||
|
transpile_single_file(input_path, output_path)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:error, "Input path not found: #{input_path}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transpile_directory(input_dir, output_dir) do
|
||||||
|
input_dir = Path.expand(input_dir)
|
||||||
|
output_dir = Path.expand(output_dir)
|
||||||
|
|
||||||
|
File.mkdir_p!(output_dir)
|
||||||
|
|
||||||
|
input_dir
|
||||||
|
|> Path.join("**/*.lua.tpl")
|
||||||
|
|> Path.wildcard()
|
||||||
|
|> Enum.each(fn source_file ->
|
||||||
|
relative_path = String.replace(source_file, input_dir <> "/", "")
|
||||||
|
dest_file = Path.join(output_dir, relative_path) |> String.replace(".lua.tpl", ".lua")
|
||||||
|
File.mkdir_p!(Path.dirname(dest_file))
|
||||||
|
transpile_file(source_file, dest_file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transpile_single_file(input_file, output_path) do
|
||||||
|
dest_file =
|
||||||
|
if File.dir?(output_path) do
|
||||||
|
# If output is a directory, create file with same name inside it
|
||||||
|
Path.join(output_path, Path.basename(input_file)) |> String.replace(".lua.tpl", ".lua")
|
||||||
|
else
|
||||||
|
# Otherwise, use the exact output path
|
||||||
|
output_path
|
||||||
|
end
|
||||||
|
|
||||||
|
File.mkdir_p!(Path.dirname(dest_file))
|
||||||
|
transpile_file(input_file, dest_file)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transpile_file(source, dest) do
|
||||||
|
IO.puts("Transpiling #{source} -> #{dest}")
|
||||||
|
|
||||||
|
source
|
||||||
|
|> File.read!()
|
||||||
|
|> Tpl.to_ast()
|
||||||
|
|> TplLua.transpile()
|
||||||
|
|> then(&File.write!(dest, &1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
example =
|
||||||
|
"""
|
||||||
|
---
|
||||||
|
|
||||||
|
on_ff = fn a b c (vim.lsp.buf.format)
|
||||||
|
on_ff = fn a b c {
|
||||||
|
(vim.lsp.buf.format)
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
map "n" "<leader>ff" (fn a b c (vim.lsp.buf.format)) t{ desc "Format code" }
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
l asd = fn x y z {
|
||||||
|
print "Hello, World!"
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
player.add_hook = fn self event data {
|
||||||
|
self:process_event event data
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
arr_map = fn arr fun {
|
||||||
|
l result = {}
|
||||||
|
for i v in (ipairs arr) {
|
||||||
|
table.insert result (fun v)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
---
|
||||||
|
day = fetch_day db
|
||||||
|
name = fetch_name user_id
|
||||||
|
greeting = "Hello, {name}! Today is {day}."
|
||||||
|
---
|
||||||
|
asd = fn {print "Hello"}
|
||||||
|
---
|
||||||
|
asd = t{ key1 "value1" key2 "value2" }
|
||||||
|
---
|
||||||
|
|
||||||
|
map "n" "<leader>stf" fn {
|
||||||
|
l timer_minutes = (choose_timer)
|
||||||
|
if not timer_minutes {
|
||||||
|
print "No timer selected"
|
||||||
|
}
|
||||||
|
require "grug-far"
|
||||||
|
: .open t{
|
||||||
|
transient true
|
||||||
|
prefills t{
|
||||||
|
timers (timer_minutes : at 1 : tostring)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Keep the interactive example runner for development
|
||||||
|
example
|
||||||
|
|> String.split("---\n")
|
||||||
|
|> Enum.each(fn input ->
|
||||||
|
IO.puts("===================== Input ====")
|
||||||
|
IO.puts(input)
|
||||||
|
IO.puts("==== Parsed ====")
|
||||||
|
|
||||||
|
state =
|
||||||
|
input
|
||||||
|
|> Tpl.chunk()
|
||||||
|
|
||||||
|
# |> IO.inspect()
|
||||||
|
# |> print_chunkd()
|
||||||
|
|
||||||
|
chunks = Map.get(state, :program_output)
|
||||||
|
IO.puts("==== AST ====")
|
||||||
|
|
||||||
|
lua_code =
|
||||||
|
chunks
|
||||||
|
|> Tpl.chunks_to_ast()
|
||||||
|
|> TplLua.transpile()
|
||||||
|
|
||||||
|
IO.puts("\n==== Transpiled Lua ====")
|
||||||
|
|
||||||
|
lua_code
|
||||||
|
|> IO.puts()
|
||||||
|
|
||||||
|
IO.puts("\n\n")
|
||||||
|
end)
|
||||||
|
|
||||||
|
# --- Example usage for the new CLI ---
|
||||||
|
#
|
||||||
|
# To run this, you could create some directories and files:
|
||||||
|
#
|
||||||
|
# mkdir -p lua_tpl/plugins
|
||||||
|
# echo 'l my_plugin = fn { print "plugin loaded" }' > lua_tpl/plugins/my_plugin.lua.tpl
|
||||||
|
# echo 'l main = fn { print "init file" }' > lua_tpl/init.lua.tpl
|
||||||
|
#
|
||||||
|
# Then, you could uncomment and run the following line in iex or from the script:
|
||||||
|
#
|
||||||
|
# TplLua.CLI.run("lua_tpl", "lua_output")
|
||||||
|
#
|
||||||
|
# This would produce:
|
||||||
|
# lua_output/plugins/my_plugin.lua
|
||||||
|
# lua_output/init.lua
|
||||||
|
TplLua.CLI.run("lua_tpl", "lua_output")
|
||||||
251
tpl_prolog.exs
Normal file
251
tpl_prolog.exs
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
Code.require_file("tpl.exs")
|
||||||
|
|
||||||
|
defmodule TplProlog do
|
||||||
|
# Prolog comparison/arithmetic operators not in the TPL precedence table.
|
||||||
|
# These remain in natural order [lhs, op, rhs] in the AST.
|
||||||
|
@infix_ops ["\\=", ">=", "=<", ">", "<", "is", "==", "\\==", "=:=", "=\\="]
|
||||||
|
|
||||||
|
# --- Public API ---
|
||||||
|
|
||||||
|
def to_prolog(ast_nodes) do
|
||||||
|
ast_nodes
|
||||||
|
|> Enum.reject(&skip?/1)
|
||||||
|
|> Enum.map(&emit(&1, :top))
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Enum.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Node emitters ---
|
||||||
|
|
||||||
|
# emit/2: convert one AST node to a Prolog string in the given context.
|
||||||
|
# context is :top (clause level), :body (goal), or :arg (term inside a call).
|
||||||
|
|
||||||
|
defp emit(%{t: :sexp, list: list}, ctx), do: sexp(clean(list), ctx)
|
||||||
|
defp emit(%{t: :brace, list: list}, _ctx), do: body(list)
|
||||||
|
defp emit(%{t: :token, str: s}, _ctx), do: s
|
||||||
|
defp emit(%{t: :string, chunks: chunks, values: []}, _ctx), do: ~s("#{Enum.join(chunks)}")
|
||||||
|
defp emit(%{t: :string, chunks: chunks, values: values}, _ctx) do
|
||||||
|
# Best-effort: interleave chunks and values
|
||||||
|
max = max(length(chunks), length(values))
|
||||||
|
|
||||||
|
Enum.flat_map(0..(max - 1), fn i ->
|
||||||
|
chunk = if i < length(chunks), do: [Enum.at(chunks, i)], else: []
|
||||||
|
value = if i < length(values), do: ["#{emit(Enum.at(values, i), :arg)}"], else: []
|
||||||
|
chunk ++ value
|
||||||
|
end)
|
||||||
|
|> Enum.join()
|
||||||
|
|> then(&~s("#{&1}"))
|
||||||
|
end
|
||||||
|
defp emit(%{t: t}, _ctx) when t in [:newline, :comment, :dot, :comma], do: nil
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
defp skip?(%{t: t}) when t in [:newline, :comment], do: true
|
||||||
|
defp skip?(_), do: false
|
||||||
|
|
||||||
|
defp clean(list), do: Enum.reject(list, &skip?/1)
|
||||||
|
|
||||||
|
defp dot(s), do: s <> "."
|
||||||
|
defp maybe_dot(s, :top), do: dot(s)
|
||||||
|
defp maybe_dot(s, _), do: s
|
||||||
|
|
||||||
|
# term/1: list of head nodes -> Prolog term (functor with args, or atom)
|
||||||
|
defp term([%{t: :token, str: f}]), do: f
|
||||||
|
defp term([%{t: :token, str: f} | args]) do
|
||||||
|
args_s = args |> Enum.map(&emit(&1, :arg)) |> Enum.reject(&is_nil/1) |> Enum.join(", ")
|
||||||
|
if args_s == "", do: f, else: "#{f}(#{args_s})"
|
||||||
|
end
|
||||||
|
defp term([single]), do: emit(single, :arg)
|
||||||
|
defp term(nodes), do: nodes |> Enum.map(&emit(&1, :arg)) |> Enum.reject(&is_nil/1) |> Enum.join(", ")
|
||||||
|
|
||||||
|
# body/1: goal list from a brace block -> comma-joined conjunction string
|
||||||
|
defp body(list) do
|
||||||
|
goals =
|
||||||
|
list
|
||||||
|
|> Enum.reject(&skip?/1)
|
||||||
|
|> Enum.map(&emit(&1, :body))
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
|
case goals do
|
||||||
|
[] -> "true"
|
||||||
|
_ -> Enum.join(goals, ",\n ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# findall_body: inline conjunction with parens for multi-goal bodies.
|
||||||
|
# Avoids findall(T, goal1,\n goal2, R) misparse.
|
||||||
|
defp findall_body(list) do
|
||||||
|
goals = list |> Enum.reject(&skip?/1) |> Enum.map(&emit(&1, :body)) |> Enum.reject(&is_nil/1)
|
||||||
|
case goals do
|
||||||
|
[] -> "true"
|
||||||
|
[single] -> single
|
||||||
|
multiple -> "(#{Enum.join(multiple, ", ")})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp brace?(%{t: :brace}), do: true
|
||||||
|
defp brace?(_), do: false
|
||||||
|
|
||||||
|
# --- S-expression dispatch ---
|
||||||
|
|
||||||
|
# Any sexp whose last element is a brace is a rule (or DCG rule).
|
||||||
|
# e.g. ancestor X Y { parent X Z, ancestor Z Y }
|
||||||
|
# dcg command (cmd A T) { verb A, noun T }
|
||||||
|
defp sexp(list, ctx) when length(list) >= 2 do
|
||||||
|
head = Enum.drop(list, -1)
|
||||||
|
last = List.last(list)
|
||||||
|
|
||||||
|
# A rule has exactly one trailing brace (the body) and no braces in the head.
|
||||||
|
# This excludes `or { A } { B }` where the head itself contains braces.
|
||||||
|
if brace?(last) and not Enum.any?(head, &brace?/1) do
|
||||||
|
body_s = body(last.list)
|
||||||
|
|
||||||
|
case head do
|
||||||
|
[%{t: :token, str: "dcg"} | rest] ->
|
||||||
|
"#{term(rest)} --> #{body_s}" |> maybe_dot(ctx)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
"#{term(head)} :- #{body_s}" |> maybe_dot(ctx)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
sexp_no_rule(list, ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sexp(list, ctx), do: sexp_no_rule(list, ctx)
|
||||||
|
|
||||||
|
defp sexp_no_rule(list, ctx) do
|
||||||
|
case list do
|
||||||
|
# ── Findall ──────────────────────────────────────────────────────────────
|
||||||
|
# Var = find Template { Goals }
|
||||||
|
# AST (after precedence pass): [token:=, token:Var, sexp:[find, Template, brace:Goals]]
|
||||||
|
[%{t: :token, str: "="}, %{t: :token, str: var}, %{t: :sexp, list: rhs}] ->
|
||||||
|
rhs_clean = clean(rhs)
|
||||||
|
|
||||||
|
if match?([%{t: :token, str: "find"} | _], rhs_clean) and
|
||||||
|
length(rhs_clean) >= 3 and brace?(List.last(rhs_clean)) do
|
||||||
|
[_ | rest] = rhs_clean
|
||||||
|
{tmpl_nodes, [brace]} = Enum.split(rest, length(rest) - 1)
|
||||||
|
tmpl_s = term(tmpl_nodes)
|
||||||
|
goal_s = findall_body(brace.list)
|
||||||
|
"findall(#{tmpl_s}, #{goal_s}, #{var})" |> maybe_dot(ctx)
|
||||||
|
else
|
||||||
|
rhs_s = sexp(rhs_clean, :arg)
|
||||||
|
"#{var} = #{rhs_s}" |> maybe_dot(ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Assignment / unification ─────────────────────────────────────────────
|
||||||
|
# = lhs rhs (operator-first because = is in the TPL precedence table)
|
||||||
|
[%{t: :token, str: "="}, lhs, rhs] ->
|
||||||
|
"#{emit(lhs, :body)} = #{emit(rhs, :body)}" |> maybe_dot(ctx)
|
||||||
|
|
||||||
|
# ── Infix comparison operators (not in TPL table, stay in natural order) ──
|
||||||
|
# e.g. A >= 18 , X \= Y
|
||||||
|
[lhs, %{t: :token, str: op}, rhs] when op in @infix_ops ->
|
||||||
|
"#{emit(lhs, :body)} #{op} #{emit(rhs, :body)}"
|
||||||
|
|
||||||
|
# ── Disjunction ──────────────────────────────────────────────────────────
|
||||||
|
# or { goalA } { goalB } ...
|
||||||
|
[%{t: :token, str: "or"} | branches] ->
|
||||||
|
branches
|
||||||
|
|> Enum.filter(&match?(%{t: :brace}, &1))
|
||||||
|
|> Enum.map(&emit(&1, :body))
|
||||||
|
|> Enum.join(" ; ")
|
||||||
|
|> then(&"(#{&1})")
|
||||||
|
|
||||||
|
# ── Negation ─────────────────────────────────────────────────────────────
|
||||||
|
# not goal...
|
||||||
|
[%{t: :token, str: "not"} | goals] ->
|
||||||
|
"\\+ #{term(goals)}"
|
||||||
|
|
||||||
|
# ── General functor call / fact ──────────────────────────────────────────
|
||||||
|
[%{t: :token, str: f} | args] ->
|
||||||
|
args_s = args |> Enum.map(&emit(&1, :arg)) |> Enum.reject(&is_nil/1) |> Enum.join(", ")
|
||||||
|
(if args_s == "", do: f, else: "#{f}(#{args_s})") |> maybe_dot(ctx)
|
||||||
|
|
||||||
|
# ── Fallback ─────────────────────────────────────────────────────────────
|
||||||
|
_ ->
|
||||||
|
list
|
||||||
|
|> Enum.map(&emit(&1, :body))
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Enum.join(" ")
|
||||||
|
|> maybe_dot(ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# ── Demo ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
prolog_code =
|
||||||
|
"""
|
||||||
|
parent elizabeth charles
|
||||||
|
parent charles william
|
||||||
|
parent charles harry
|
||||||
|
|
||||||
|
male charles
|
||||||
|
male william
|
||||||
|
male harry
|
||||||
|
female elizabeth
|
||||||
|
|
||||||
|
age charles 75
|
||||||
|
age william 42
|
||||||
|
age harry 40
|
||||||
|
|
||||||
|
person X {
|
||||||
|
male X
|
||||||
|
}
|
||||||
|
|
||||||
|
grandparent G C {
|
||||||
|
parent G P
|
||||||
|
parent P C
|
||||||
|
(G \= C)
|
||||||
|
}
|
||||||
|
|
||||||
|
is_adult Person {
|
||||||
|
age Person A
|
||||||
|
A >= 18
|
||||||
|
}
|
||||||
|
|
||||||
|
ancestor X Y {
|
||||||
|
parent X Z
|
||||||
|
ancestor Z Y
|
||||||
|
}
|
||||||
|
|
||||||
|
child_or_pet_owner X {
|
||||||
|
or {
|
||||||
|
parent X _
|
||||||
|
} {
|
||||||
|
owns_pet X _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_childless X {
|
||||||
|
person X
|
||||||
|
not parent X _
|
||||||
|
}
|
||||||
|
|
||||||
|
Adults = find P {
|
||||||
|
person P
|
||||||
|
is_adult P
|
||||||
|
}
|
||||||
|
|
||||||
|
dcg command (cmd Action Target) {
|
||||||
|
verb Action
|
||||||
|
noun Target
|
||||||
|
}
|
||||||
|
|
||||||
|
dcg verb (action find) { "find" }
|
||||||
|
dcg noun (obj harry) { "harry" }
|
||||||
|
|
||||||
|
dcg sentence {
|
||||||
|
noun_phrase
|
||||||
|
verb_phrase
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> Tpl.to_ast()
|
||||||
|
|> TplProlog.to_prolog()
|
||||||
|
|
||||||
|
IO.puts("\n--- Generated Prolog ---")
|
||||||
|
IO.puts(prolog_code)
|
||||||
|
IO.puts("------------------------\n")
|
||||||
Loading…
x
Reference in New Issue
Block a user