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