1166 lines
35 KiB
Elixir
1166 lines
35 KiB
Elixir
Mix.install([:jason])
|
|
|
|
Code.require_file("casex.exs")
|
|
|
|
defmodule Tpl do
|
|
# improved case with multiple patterns and guards
|
|
import Casex
|
|
@newline "\n" |> to_charlist() |> hd()
|
|
@tab "\t" |> to_charlist() |> hd()
|
|
@space " " |> to_charlist() |> hd()
|
|
@lparen "(" |> to_charlist() |> hd()
|
|
@rparen ")" |> to_charlist() |> hd()
|
|
@lbrace "{" |> to_charlist() |> hd()
|
|
@rbrace "}" |> to_charlist() |> hd()
|
|
@dquote "\"" |> to_charlist() |> hd()
|
|
@comma "," |> to_charlist() |> hd()
|
|
@dot "." |> to_charlist() |> hd()
|
|
@semi ";" |> to_charlist() |> hd()
|
|
|
|
def ch(str), do: str |> to_charlist() |> hd()
|
|
|
|
##### DEBUGGING HELPERS #####
|
|
|
|
def print_chunkd(%{program_output: output} = state) do
|
|
print_chunkd(output)
|
|
state
|
|
end
|
|
|
|
def print_chunkd(ast, indent \\ 0) do
|
|
# pretty print state.program_output here, like a tree
|
|
# recursively, with `|` for keeping track of levels
|
|
# like so:
|
|
# S-expression:
|
|
# | Token: a
|
|
# | Token: b
|
|
# Brace block:
|
|
# | Token: x
|
|
# | Token: y
|
|
# | S-expression:
|
|
# | | S-expression:
|
|
# | | | Token: a
|
|
# | Token: b
|
|
#
|
|
# ! note the `|` pipe used for levels
|
|
|
|
Enum.each(ast, fn item ->
|
|
print_item(item, indent)
|
|
end)
|
|
|
|
ast
|
|
end
|
|
|
|
def print_item(%{t: :sexp, list: list}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "S-expression:")
|
|
Enum.each(list, fn item -> print_item(item, indent + 1) end)
|
|
end
|
|
|
|
def print_item(%{t: :brace, list: list, name: %{str: name, t: :token}}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Brace block: (#{name})")
|
|
Enum.each(list, fn item -> print_item(item, indent + 1) end)
|
|
end
|
|
|
|
def print_item(%{t: :brace, list: list}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Brace block:")
|
|
Enum.each(list, fn item -> print_item(item, indent + 1) end)
|
|
end
|
|
|
|
def print_item(%{t: :token, str: str}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Token: #{str}")
|
|
end
|
|
|
|
def print_item(%{t: :string, chunks: chunks, values: values}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "String:")
|
|
max_len = max(length(chunks), length(values))
|
|
|
|
Enum.each(0..(max_len - 1), fn i ->
|
|
if i < length(chunks) do
|
|
chunk = Enum.at(chunks, i)
|
|
IO.puts(String.duplicate("| ", indent + 1) <> "Chunk: \"#{chunk}\"")
|
|
end
|
|
|
|
if i < length(values) do
|
|
value = Enum.at(values, i)
|
|
IO.puts(String.duplicate("| ", indent + 1) <> "Value:")
|
|
print_item(value, indent + 2)
|
|
end
|
|
end)
|
|
end
|
|
|
|
def print_item(%{t: :comment, str: str}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Comment: #{str}")
|
|
end
|
|
|
|
def print_item(%{t: :comma}, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Comma")
|
|
end
|
|
|
|
def print_item(%{t: :newline} = _other, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Newline")
|
|
end
|
|
|
|
def print_item(%{t: :dot} = _other, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Dot access")
|
|
end
|
|
|
|
def print_item(nil, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Unknown (maybe piped?)")
|
|
end
|
|
|
|
def print_item(other, indent) do
|
|
IO.puts(String.duplicate("| ", indent) <> "Unknown item: #{inspect(other)}")
|
|
end
|
|
|
|
##### END DEBUGGING HELPERS #####
|
|
|
|
def add_offset(state, o) do
|
|
%{state | offset: state.offset + o}
|
|
end
|
|
|
|
def drop_input(state, n) do
|
|
%{state | input: Enum.drop(state.input, n)}
|
|
end
|
|
|
|
# Adds a character to the current expression being built
|
|
def add_to_exp(%{exp: exp} = state, char) do
|
|
exp = %{exp | chars: [char | exp.chars]}
|
|
%{state | exp: exp}
|
|
end
|
|
|
|
# Starts a new expression
|
|
def add_to_exp(state, char) do
|
|
Map.put(state, :exp, %{start: state.offset, chars: [char]})
|
|
end
|
|
|
|
# Adds a character to the current string chunk being built
|
|
def add_to_exp(%{exp: %{t: :string} = exp} = state, char, type) when type == :string do
|
|
exp = %{exp | current_chunk_chars: [char | exp.current_chunk_chars]}
|
|
%{state | exp: exp}
|
|
end
|
|
|
|
def add_to_exp(%{exp: %{t: :comment} = exp} = state, char, type) when type == :comment do
|
|
exp = %{exp | chars: [char | exp.chars]}
|
|
%{state | exp: exp}
|
|
end
|
|
|
|
# Starts a new string expression
|
|
def add_to_exp(state, char, type) when type == :comment do
|
|
Map.put(state, :exp, %{
|
|
start: state.offset,
|
|
t: :comment,
|
|
chars: [char]
|
|
})
|
|
end
|
|
|
|
# adds a exp to output
|
|
def add_single_token(state, str, type \\ :token) do
|
|
comma_exp = %{
|
|
start: state.offset,
|
|
end: state.offset + 1,
|
|
t: type,
|
|
str: str
|
|
}
|
|
|
|
Map.update!(state, :output, fn r -> [comma_exp | r] end)
|
|
end
|
|
|
|
# Finalizes the current expression and adds it to the current output list
|
|
def end_exp(%{exp: %{t: :string} = exp} = state) do
|
|
# Finalize the last chunk if it exists
|
|
#
|
|
exp =
|
|
if exp.current_chunk_chars && exp.current_chunk_chars != [] do
|
|
chunk_str = exp.current_chunk_chars |> Enum.reverse() |> to_string
|
|
|
|
%{exp | chunks: [chunk_str | exp.chunks], current_chunk_chars: []}
|
|
else
|
|
exp
|
|
end
|
|
|
|
# Reverse chunks and values to maintain original order
|
|
str_exp = %{exp | chunks: Enum.reverse(exp.chunks), values: Enum.reverse(exp.values)}
|
|
str_exp = str_exp |> Map.put(:end, state.offset) |> Map.delete(:current_chunk_chars)
|
|
|
|
state
|
|
|> Map.update!(:output, fn r -> [str_exp | r] end)
|
|
|> Map.delete(:exp)
|
|
end
|
|
|
|
def end_exp(%{exp: %{t: :comment} = exp} = state) do
|
|
str = exp.chars |> Enum.reverse() |> to_string
|
|
|
|
exp =
|
|
exp
|
|
|> Map.put(:str, str)
|
|
|> Map.put(:t, :comment)
|
|
|> Map.put(:end, state.offset)
|
|
|> Map.delete(:chars)
|
|
|
|
state
|
|
|> Map.update!(:output, fn r -> [exp | r] end)
|
|
|> Map.delete(:exp)
|
|
end
|
|
|
|
def end_exp(%{exp: exp} = state) do
|
|
str = exp.chars |> Enum.reverse() |> to_string
|
|
|
|
exp =
|
|
exp
|
|
|> Map.put(:str, str)
|
|
|> Map.put(:t, :token)
|
|
|> Map.put(:end, state.offset)
|
|
|> Map.delete(:chars)
|
|
|
|
state
|
|
|> Map.update!(:output, fn r -> [exp | r] end)
|
|
|> Map.delete(:exp)
|
|
end
|
|
|
|
# If no expression is active, do nothing.
|
|
def end_exp(state), do: state
|
|
|
|
# Finalizes the current expression if one exists
|
|
def end_exp_if_exists(state) do
|
|
if Map.has_key?(state, :exp), do: end_exp(state), else: state
|
|
end
|
|
|
|
# Reverses the final program_output list
|
|
def end_program_output_list(%{output: output} = state) do
|
|
%{state | program_output: output |> hd() |> Map.get(:list)}
|
|
end
|
|
|
|
# ends a token and puts it as `:name` in the new brace block
|
|
def start_named_brace(state) do
|
|
state = end_exp_if_exists(state)
|
|
[name_token | rest_output] = state.output
|
|
# Push current output onto stack and start a new empty output list for the sublist
|
|
# and put th e name_token as :name in the new brace block
|
|
%{
|
|
state
|
|
| sublist_stack: [%{t: :brace, output: rest_output, name: name_token} | state.sublist_stack],
|
|
output: []
|
|
}
|
|
end
|
|
|
|
# Starts a new sublist (for parentheses or braces)
|
|
def start_sublist(state, type) do
|
|
# Finalize any current token before starting a sublist
|
|
state = end_exp_if_exists(state)
|
|
# Push current output onto stack and start a new empty output list for the sublist
|
|
%{state | sublist_stack: [%{t: type, output: state.output} | state.sublist_stack], output: []}
|
|
end
|
|
|
|
# Ends the current sublist and adds it to the parent list
|
|
# def end_sublist(%{sublist_stack: [%{t: :string_interp} | _sublist_stack]} = _state) do
|
|
# # This function should not be called for :string_interp directly.
|
|
# # String interpolation end is handled specially in the chunk function.
|
|
# raise "end_sublist should not be called for :string_interp directly"
|
|
# end
|
|
|
|
def end_paren(state) do
|
|
state = end_exp_if_exists(state)
|
|
# Pop the parent output list from the stack
|
|
[%{t: :paren, output: parent_output} | sublist_stack] = state.sublist_stack
|
|
# Reverse the current output (which is the paren sublist) and add it to the parent output
|
|
paren_sublist = Enum.reverse(state.output)
|
|
|
|
%{
|
|
state
|
|
| sublist_stack: sublist_stack,
|
|
output: [%{t: :sexp, list: paren_sublist} | parent_output]
|
|
}
|
|
end
|
|
|
|
def end_brace(state) do
|
|
state = end_exp_if_exists(state)
|
|
# Pop the parent output list from the stack
|
|
[%{t: :brace, output: parent_output} = brace | sublist_stack] = state.sublist_stack
|
|
# Reverse the current output (which is the brace sublist) and add it to the parent output
|
|
brace_sublist = Enum.reverse(state.output)
|
|
|
|
brace =
|
|
Map.merge(
|
|
%{t: :brace, list: brace_sublist},
|
|
if(Map.has_key?(brace, :name), do: %{name: brace.name}, else: %{})
|
|
)
|
|
|
|
%{
|
|
state
|
|
| sublist_stack: sublist_stack,
|
|
output: [brace | parent_output]
|
|
}
|
|
end
|
|
|
|
# def end_implicit_sexp(state) do
|
|
# state = end_exp_if_exists(state)
|
|
# # Pop the parent output list from the stack
|
|
# [%{t: :implicit_sexp, output: parent_output} | sublist_stack] = state.sublist_stack
|
|
# # Reverse the current output (which is the implicit sexp) and add it to the parent output
|
|
# sexp = Enum.reverse(state.output)
|
|
# # if the only thing in implicit_sexp is a sexp , we add that directly to parent_output
|
|
# sexp =
|
|
# case sexp do
|
|
# [%{t: :token} = token] -> token
|
|
# [%{t: :string} = string] -> string
|
|
# # [%{t: :sexp, list: [_]} = sexp] -> sexp
|
|
# [%{t: :sexp} = sexp] -> sexp
|
|
# [] -> []
|
|
# values -> %{t: :sexp, list: values}
|
|
# end
|
|
#
|
|
# %{
|
|
# state
|
|
# | sublist_stack: sublist_stack,
|
|
# output:
|
|
# case sexp do
|
|
# [] -> parent_output
|
|
# _ -> [sexp | parent_output]
|
|
# end
|
|
# }
|
|
# end
|
|
#
|
|
# def maybe_end_implicit_sexp(%{sublist_stack: [%{t: :implicit_sexp} | _]} = state) do
|
|
# end_implicit_sexp(state)
|
|
# end
|
|
#
|
|
# def maybe_end_implicit_sexp(state), do: state
|
|
|
|
def end_interpolation(state) do
|
|
state = end_exp_if_exists(state)
|
|
# Pop the parent output list from the stack
|
|
[%{t: :string_interp, exp: string_exp, output: parent_output} | sublist_stack] =
|
|
state.sublist_stack
|
|
|
|
[brace] = state.output
|
|
# The interpolated value is the entire current output list
|
|
# interpolated_value = %{t: :sexp, list: state.output |> Enum.reverse()}
|
|
|
|
# IO.inspect("Interpolated value: #{inspect(interpolated_value)}")
|
|
|
|
updated_string_exp = %{
|
|
string_exp
|
|
| values: [brace | string_exp.values]
|
|
}
|
|
|
|
Map.merge(state, %{
|
|
sublist_stack: sublist_stack,
|
|
exp: updated_string_exp,
|
|
# Restore parent output
|
|
output: parent_output
|
|
})
|
|
end
|
|
|
|
# Helper to finalize a top-level line (current output) and add it to program_output
|
|
defp finalize_top_level_line(state) do
|
|
# Ensure any pending exp is closed
|
|
state = end_exp_if_exists(state)
|
|
|
|
# Reverse the current line's tokens/sublists and add to program_output
|
|
line_content = Enum.reverse(state.output)
|
|
|
|
new_program_output =
|
|
case line_content do
|
|
[] ->
|
|
state.program_output
|
|
|
|
_ ->
|
|
# The line content is already wrapped in an implicit_sexp,
|
|
# which becomes a regular sexp when the sublist is ended.
|
|
# So we just prepend it.
|
|
line_content ++ state.program_output
|
|
end
|
|
|
|
# Reset output for the next line
|
|
%{state | program_output: new_program_output, output: []}
|
|
end
|
|
|
|
def debug_state(state) do
|
|
IO.puts("----- -----")
|
|
|
|
case state.input do
|
|
[] ->
|
|
IO.puts("CONSUMED ALL INPUT")
|
|
IO.puts(state.start_input |> to_string())
|
|
|
|
input ->
|
|
state.start_input
|
|
|> String.trim_trailing(input |> to_string())
|
|
|> IO.inspect(label: "SO FAR")
|
|
end
|
|
|
|
IO.inspect(state.mode |> Enum.reverse(), limit: :infinity, label: "MODE")
|
|
|
|
Enum.at(state.input, 0)
|
|
|> List.wrap()
|
|
|> to_string
|
|
|> IO.inspect(label: "NEXT CHAR", charlists: :as_lists)
|
|
|
|
state
|
|
end
|
|
|
|
# Stage 1 - chunk into lists, tokens, strings, comments, newlines
|
|
def chunk(str) when is_binary(str) do
|
|
input = str |> String.to_charlist()
|
|
|
|
%{
|
|
offset: -1,
|
|
# Accumulates finalized lines
|
|
program_output: [],
|
|
# Accumulates tokens/sublists for the current line/sublist
|
|
output: [],
|
|
sublist_stack: [],
|
|
input: input,
|
|
mode: [],
|
|
start_input: str
|
|
}
|
|
|> start_sublist(:brace)
|
|
|> chunk(input, [:brace])
|
|
# |> chunk
|
|
|> end_brace()
|
|
|> end_program_output_list()
|
|
end
|
|
|
|
def chunk(%{input: input, mode: mode} = state) do
|
|
# debug_state(state)
|
|
if mode == [], do: raise("Mode stack empty!")
|
|
|
|
casex {input, mode} do
|
|
# comments
|
|
{[char | rest], _} when char == @semi ->
|
|
{state, _} =
|
|
Enum.reduce_while(
|
|
rest,
|
|
# state, mode (either :comment or :after_comment)
|
|
{state |> end_exp_if_exists() |> drop_input(1), :comment},
|
|
fn
|
|
@newline, {st, :comment} ->
|
|
st = st |> add_offset(1) |> drop_input(1)
|
|
{:cont, {st, :after_comment}}
|
|
|
|
c, {st, :comment} ->
|
|
st = st |> add_to_exp(c, :comment) |> add_offset(1) |> drop_input(1)
|
|
{:cont, {st, :comment}}
|
|
|
|
c, {st, :after_comment} when c in [@space, @tab] ->
|
|
st = st |> add_offset(1) |> drop_input(1)
|
|
{:cont, {st, :after_comment}}
|
|
|
|
@semi, {st, :after_comment} ->
|
|
st = st |> add_to_exp(@newline, :comment) |> add_offset(1) |> drop_input(1)
|
|
{:cont, {st, :comment}}
|
|
|
|
_c, {st, :after_comment} ->
|
|
# finalize comment exp
|
|
st = st |> end_exp_if_exists()
|
|
{:halt, {st, nil}}
|
|
end
|
|
)
|
|
|
|
state |> chunk(state.input, mode)
|
|
|
|
{[char | rest], [:token | [:paren | prev_mode]]}, {[char | rest], [:paren | prev_mode]}
|
|
when char == @rparen ->
|
|
state
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> end_paren()
|
|
|> chunk(rest, prev_mode)
|
|
|
|
# Handle opening brace inside a string (interpolation)
|
|
{[char | rest], [:string | _prev_mode] = mode} when char == @lbrace ->
|
|
state =
|
|
state
|
|
# Finalize the current string chunk before starting interpolation
|
|
|> (fn s ->
|
|
chunk_str = s.exp.current_chunk_chars |> Enum.reverse() |> to_string
|
|
%{s | exp: %{s.exp | chunks: [chunk_str | s.exp.chunks], current_chunk_chars: []}}
|
|
end).()
|
|
|
|
state
|
|
|> add_offset(1)
|
|
# Push the current string expression AND output onto the stack
|
|
|> Map.update!(:sublist_stack, fn stack ->
|
|
[%{t: :string_interp, exp: state.exp, output: state.output} | stack]
|
|
end)
|
|
# Clear current exp and output to chunk the interpolated expression
|
|
|> Map.put(:output, [])
|
|
|> Map.delete(:exp)
|
|
|> start_sublist(:brace)
|
|
# Enter top mode to chunk the expression
|
|
|> chunk(rest, [:brace | mode])
|
|
|
|
{[char | rest], [m | _] = mode} when char == @dot and m in [:token, :brace, :paren] ->
|
|
state
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> add_single_token(".", :dot)
|
|
|> chunk(rest, mode)
|
|
|
|
{[char | rest], [:token | [:brace | _] = mode]}, {[char | rest], [:brace | _] = mode}
|
|
when char == @comma ->
|
|
# commas separate implicit sexps
|
|
state
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> add_single_token(",", :comma)
|
|
|> chunk(rest, mode)
|
|
|
|
# Handle closing brace inside string interpolation (with active token)
|
|
# Handle closing brace inside string interpolation (no active token)
|
|
{[char | rest], [:token, :brace | [:string | _prev_mode] = string_mode] = mode},
|
|
{[char | rest], [:brace | [:string | _prev_mode] = string_mode]},
|
|
guard([char == @rbrace]) ->
|
|
state
|
|
# Finalize the interpolated expression
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> end_brace()
|
|
# Pop the string expression from the stack
|
|
|> end_interpolation()
|
|
# Return to string mode
|
|
|> chunk(rest, string_mode)
|
|
|
|
# handle closing brace when in :implicit_sexp in brace, token and top
|
|
{[char | rest], [:token | [:brace | prev_mode]]}, {[char | rest], [:brace | prev_mode]}
|
|
when char == @rbrace ->
|
|
state
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> end_brace()
|
|
|> chunk(rest, prev_mode)
|
|
|
|
# Handle opening parenthesis, entering paren mode
|
|
{[char | rest], _} when char == @lparen ->
|
|
state
|
|
# Finalize any current token before starting a sublist
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> start_sublist(:paren)
|
|
|> chunk(rest, [:paren | mode])
|
|
|
|
# Handle NAMED Brace opening
|
|
{[char | rest], [:token | _prev_mode]} when char == @lbrace ->
|
|
state
|
|
# Finalize any current token before starting a sublist
|
|
|> end_exp()
|
|
|> add_offset(1)
|
|
|> start_named_brace()
|
|
|> chunk(rest, [:brace | mode])
|
|
|
|
# Handle opening brace, entering brace mode
|
|
{[char | rest], _} when char == @lbrace ->
|
|
state
|
|
# Finalize any current token before starting a sublist
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> start_sublist(:brace)
|
|
|> chunk(rest, [:brace | mode])
|
|
|
|
# Handle closing double quote, exiting string mode
|
|
{[char | rest], [:string | prev_mode]} when char == @dquote ->
|
|
state
|
|
|> add_offset(1)
|
|
# Finalize the string expression
|
|
|> end_exp()
|
|
|> chunk(rest, prev_mode)
|
|
|
|
# Handle opening double quote, entering string mode
|
|
{[char | rest], _} when char == @dquote ->
|
|
state
|
|
# Finalize any preceding token
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|> Map.put(:exp, %{
|
|
start: state.offset,
|
|
t: :string,
|
|
chunks: [],
|
|
values: [],
|
|
current_chunk_chars: []
|
|
})
|
|
|> chunk(rest, [:string | mode])
|
|
|
|
# # Newline after a token in a top-level implicit sexp. Ends the line.
|
|
# {[@newline | rest], [:token | [:top | _] = prev_mode]} ->
|
|
# state
|
|
# |> end_exp()
|
|
# |> add_offset(1)
|
|
# |> chunk(rest, prev_mode)
|
|
|
|
# Newline after a token in a brace's implicit sexp. Ends the sexp, not the line.
|
|
{[@newline | rest], [:token | [:brace | _] = prev_mode]} ->
|
|
state
|
|
|> end_exp()
|
|
|> add_offset(1)
|
|
|> add_single_token(@newline, :newline)
|
|
|> chunk(rest, prev_mode)
|
|
|
|
# Newline after a token in a paren. Just whitespace.
|
|
{[@newline | rest], [:token | [:paren | _] = prev_mode]} ->
|
|
state
|
|
|> end_exp()
|
|
|> add_offset(1)
|
|
|> add_single_token(@newline, :newline)
|
|
|> chunk(rest, prev_mode)
|
|
|
|
{[@newline | rest], [:brace | _] = mode} ->
|
|
state
|
|
|> end_exp()
|
|
|> add_offset(1)
|
|
|> add_single_token(@newline, :newline)
|
|
|> chunk(rest, mode)
|
|
|
|
# # Newline on its own in a top-level implicit sexp. Ends the line.
|
|
# {[@newline | rest], [:implicit_sexp, :top | _] = mode} ->
|
|
# state
|
|
# |> add_offset(1)
|
|
# |> end_implicit_sexp()
|
|
# |> finalize_top_level_line()
|
|
# |> chunk(rest, Enum.drop(mode, 1))
|
|
#
|
|
# # Newline on its own in a brace's implicit sexp. Ends the sexp.
|
|
# {[@newline | rest], [:implicit_sexp, :brace | _] = mode} ->
|
|
# state
|
|
# |> add_offset(1)
|
|
# |> end_implicit_sexp()
|
|
# |> chunk(rest, Enum.drop(mode, 1))
|
|
|
|
# Newline on an empty top-level line.
|
|
# {[@newline | rest], [:top] = mode} ->
|
|
# state
|
|
# |> add_offset(1)
|
|
# |> finalize_top_level_line()
|
|
# |> chunk(rest, mode)
|
|
|
|
# Newline as whitespace in other contexts (paren, brace).
|
|
{[@newline | rest], _mode} ->
|
|
add_offset(state, 1)
|
|
|> chunk(rest, mode)
|
|
|
|
# Whitespace ending a token
|
|
{[end_token | rest], [:token | prev_mode]} when end_token in [@tab, @space] ->
|
|
state
|
|
|> add_offset(1)
|
|
|> end_exp()
|
|
|> chunk(rest, prev_mode)
|
|
|
|
# Characters into current string chunk (including newlines and some special chars)
|
|
{[char | rest], [:string | _]} ->
|
|
add_offset(state, 1)
|
|
|> add_to_exp(char, :string)
|
|
|> chunk(rest, mode)
|
|
|
|
# Whitespace (not ending a token)
|
|
{[whitespace | rest], _} when whitespace in [@tab, @space] ->
|
|
add_offset(state, 1)
|
|
|> chunk(rest, mode)
|
|
|
|
# Characters into current token
|
|
{[char | rest], [:token | _]} ->
|
|
add_offset(state, 1)
|
|
|> add_to_exp(char)
|
|
|> chunk(rest, mode)
|
|
|
|
# A new token (from :top, :paren, :brace, or :implicit_sexp mode)
|
|
{[char | rest], [current_mode | _] = mode}
|
|
when current_mode in [:paren, :brace] and
|
|
char not in [@tab, @space, @newline, @dquote, @rbrace, @rparen, @lbrace, @lparen] ->
|
|
state =
|
|
state
|
|
|> end_exp_if_exists()
|
|
|> add_offset(1)
|
|
|
|
# If in :top or :brace, start an implicit s-expression for the line
|
|
# {state, mode} =
|
|
# if current_mode in [:top, :brace] do
|
|
# {start_sublist(state, :implicit_sexp), [:implicit_sexp | mode]}
|
|
# else
|
|
# {state, mode}
|
|
# end
|
|
|
|
state
|
|
|> add_to_exp(char)
|
|
|> chunk(rest, [:token | mode])
|
|
|
|
# End of input, with an active token
|
|
# TODO: better way to collapse open sublists
|
|
{[], [:token | _]} ->
|
|
state
|
|
|> end_exp()
|
|
# |> (fn s ->
|
|
# Enum.reduce(s.sublist_stack, s, fn %{t: type}, acc ->
|
|
# if type == :implicit_sexp, do: end_implicit_sexp(acc), else: acc
|
|
# end)
|
|
# end).()
|
|
|> finalize_top_level_line()
|
|
|> (fn s ->
|
|
Enum.reduce(s.sublist_stack, s, fn _, acc ->
|
|
case acc.sublist_stack do
|
|
[] -> acc
|
|
# [%{t: :implicit_sexp} | _] -> end_implicit_sexp(acc)
|
|
[%{t: :paren} | _] -> end_paren(acc)
|
|
[%{t: :brace} | _] -> end_brace(acc)
|
|
end
|
|
end)
|
|
end).()
|
|
|> end_program_output_list()
|
|
|
|
# End of input, no active token
|
|
{[], modes} ->
|
|
Enum.reverse(modes)
|
|
|> Enum.drop(2)
|
|
|> Enum.reverse()
|
|
|> Enum.reduce(state, fn
|
|
# :implicit_sexp, %{output_stack: [%{t: :implicit_sexp} | _]} = acc ->
|
|
# end_implicit_sexp(acc)
|
|
|
|
:paren, %{sublist_stack: [%{t: :paren} | _]} = acc ->
|
|
end_paren(acc)
|
|
|
|
:brace, %{sublist_stack: [%{t: :brace} | _]} = acc ->
|
|
end_brace(acc)
|
|
|
|
_, acc ->
|
|
acc
|
|
end)
|
|
|
|
# Catch-all for unhandled characters
|
|
{[char | rest], mode} ->
|
|
IO.inspect("Unhandled char '#{[char] |> to_string}' in mode #{inspect(mode)}")
|
|
add_offset(state, 1) |> chunk(rest, mode)
|
|
end
|
|
end
|
|
|
|
def chunk(state, input, mode) do
|
|
chunk(%{state | input: input, mode: mode})
|
|
end
|
|
|
|
def run() do
|
|
"""
|
|
|
|
|
|
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
|
|
}
|
|
- - -
|
|
dcg command (cmd Action Target) {
|
|
verb Action
|
|
noun Target
|
|
}
|
|
- - -
|
|
dcg verb (action find) { "find" }
|
|
|
|
dcg noun (obj harry) { "harry" }
|
|
- - -
|
|
|
|
Adults = find P {
|
|
person P
|
|
is_adult P
|
|
}
|
|
- - -
|
|
|
|
A (B
|
|
C)
|
|
D E F
|
|
G H
|
|
- - -
|
|
( f (a (b kek)
|
|
d
|
|
e)
|
|
f )
|
|
- - -
|
|
{ this is { (a) b } of {
|
|
( braces )
|
|
braces 2
|
|
} } asd
|
|
- - -
|
|
"hello
|
|
world"
|
|
- - -
|
|
|
|
"a {n} { nf "{ nn1 } b {nn2 nn3}" } c"
|
|
|
|
- - -
|
|
a b c , b, c
|
|
{ x x c , y x (asd ) , ( z ) }
|
|
( p, q, "r, asdad, ")
|
|
- - -
|
|
dcg asd X Y [ x ] {
|
|
"hello {X
|
|
a b} and {Y}"
|
|
}
|
|
|
|
- - -
|
|
|
|
grandparent G C x{
|
|
parent G P
|
|
parent P C ; and some comment here
|
|
(G = C)
|
|
}
|
|
|
|
|
|
; somecomment here
|
|
m{ x x c , y x (asd ) , z }
|
|
|
|
- - -
|
|
m{a b, b c d , c}
|
|
- - -
|
|
map "n" "<leader>ff" f{vim.lsp.buf.format() } t{ desc "Format code" }
|
|
- - -
|
|
|
|
|
|
arg1 = implicit sexp ending on a newline
|
|
arg2 = another implicit sexp ending on newline
|
|
: hof fn elem {
|
|
transform elem :someatom
|
|
} arg3_to_hof
|
|
|
|
res = somefun arg1 arg2
|
|
: Enum.map fn elem {
|
|
transform elem :someatom
|
|
}
|
|
: length
|
|
: plus 2
|
|
: IO.inspect k{label "Transformed length"}
|
|
: fetch_some_map
|
|
: .mapkey
|
|
|
|
; some comment
|
|
; asd
|
|
- - -
|
|
res : op1 : op2 : op3
|
|
: op4 ; asd
|
|
: asd ; asd
|
|
- - -
|
|
res s d = asd x yy : . q : e
|
|
: map ( fn i (add i 1) )
|
|
: . length, asdklj
|
|
res . length . positive?
|
|
: IO.println
|
|
- - -
|
|
; instead of
|
|
; map("n", "<leader>srf", function()
|
|
; local file_path = vim.api.nvim_buf_get_name(0)
|
|
; print(file_path)
|
|
; require("grug-far").open({
|
|
; transient = true,
|
|
; prefills = {
|
|
; paths = file_path,
|
|
; },
|
|
; })
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
; thats it folks
|
|
|
|
|
|
"""
|
|
|> String.split("- - -\n")
|
|
|> Enum.each(fn input ->
|
|
IO.puts("===================== Input ====")
|
|
IO.puts(input)
|
|
# IO.puts("==== chunkd ====")
|
|
|
|
state =
|
|
input
|
|
|> chunk()
|
|
|
|
# |> IO.inspect()
|
|
# |> print_chunkd()
|
|
|
|
chunks = Map.get(state, :program_output)
|
|
IO.puts("==== AST ====")
|
|
|
|
chunks
|
|
|> chunks_to_ast()
|
|
|> print_chunkd()
|
|
|
|
# |> IO.inspect(label: "AST")
|
|
|
|
IO.puts("\n\n")
|
|
end)
|
|
end
|
|
|
|
def to_ast(str) do
|
|
str
|
|
|> chunk()
|
|
# |> print_chunkd()
|
|
|> chunks_to_ast
|
|
end
|
|
|
|
# ============================================================================
|
|
# Pass 2: Structuring Pass (Chunks -> AST)
|
|
# ============================================================================
|
|
# chunks are currently just lists of tokens / other lists
|
|
# among these tokens are special tokens like :comma , :newline, and infix tokens ":", ".", etc
|
|
# theses , along with the type of list that contains them, will determine the AST structure
|
|
# 1. sexp lists with parens ()
|
|
# , / spaces / nelines delimit expressions, expressions are single tokens or sublists, OR binary operations (: . || etc )
|
|
# (a b c : d e, g . f (h i) ) -> AST representing function call with arguments and method calls
|
|
# elements:
|
|
# - a (separated by space from the next element)
|
|
# - b (separated by space from the next element)
|
|
# - c: d e (binary operation with left = c , operator = : , right = (d e) ) (separated by comma / newline, from the next element)
|
|
# - g . f (binary operation with left = g , operator = . , right = f )
|
|
# - (h i) (separated by space from the previous element)
|
|
# 2. brackets with {}
|
|
# spaces dont delimit new elements, only commas and newlines do
|
|
# , / spaces / newlines delimit expressions, expressions are single tokens or sublists, OR binary operations (: . || etc )
|
|
# { a b
|
|
# c : d e
|
|
# : x, g . f (h i) }
|
|
# - a b (separated by newline from the next element)
|
|
# - c piped into d e and then into x (binary operation with left = (c) , operator = : , right = (d e : x ) ) (separated by comma / newline, from the next element)
|
|
# - g . f (binary operation with left = g , operator = . , right = f )
|
|
#
|
|
# also handle infix operations here
|
|
|
|
# This pass takes the raw nodes from the chunker and applies grammatical rules
|
|
# (like operator precedence) to build a meaningful AST.
|
|
|
|
@operator_table %{
|
|
# "token_string" => {precedence, :left | :right}
|
|
# Lower precedence number binds less tightly.
|
|
# assignment
|
|
"=" => {10, :right},
|
|
# pipe operator
|
|
":" => {50, :right},
|
|
# method call / property access
|
|
"." => {80, :right}
|
|
}
|
|
|
|
def chunks_to_ast(%{program_output: chunks}) do
|
|
chunks_to_ast(chunks)
|
|
end
|
|
|
|
def chunks_to_ast(chunks) do
|
|
# The top-level program is a list of expressions delimited by newlines, like a brace block.
|
|
structure_expressions(chunks, :brace)
|
|
end
|
|
|
|
defp structure_node(%{t: :brace, list: raw_nodes} = node) do
|
|
# For brace blocks, group by newline/comma and process each expression.
|
|
structured_list = structure_expressions(raw_nodes, :brace)
|
|
%{node | list: structured_list}
|
|
end
|
|
|
|
defp structure_node(%{t: :sexp, list: raw_nodes} = node) do
|
|
# For paren s-expressions, the contents are comma-separated expressions.
|
|
# We process the groups, but the result is a flat list, not another s-exp.
|
|
structured_list =
|
|
raw_nodes
|
|
# Recurse on sub-nodes first
|
|
|> Enum.map(&structure_node/1)
|
|
# Group by comma only
|
|
|> group_expressions(:paren)
|
|
|> Enum.map(&process_expression/1)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
# If there's only one resulting expression and it's an sexp, flatten it.
|
|
case structured_list do
|
|
[%{t: :sexp, list: inner_list}] -> %{node | list: inner_list}
|
|
_ -> %{node | list: structured_list}
|
|
end
|
|
end
|
|
|
|
defp structure_node(%{t: :string, values: values} = node) do
|
|
# Also recurse into string interpolations
|
|
structured_values = Enum.map(values, &structure_node/1)
|
|
%{node | values: structured_values}
|
|
end
|
|
|
|
defp structure_node(node) do
|
|
# It's a simple token, string, comment, or already-processed node. Leave it as is.
|
|
node
|
|
end
|
|
|
|
defp structure_expressions(raw_nodes, type) do
|
|
raw_nodes
|
|
# 1. Recursively structure any sub-blocks first.
|
|
|> Enum.map(&structure_node/1)
|
|
# 2. Group nodes into expressions based on delimiters.
|
|
|> group_expressions(type)
|
|
# 3. Process each individual expression group.
|
|
|> Enum.map(&process_expression/1)
|
|
# 4. Filter out any nil results from empty expressions
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
# |> dbg()
|
|
end
|
|
|
|
defp group_expressions(nodes, type) do
|
|
# Manually group expressions, handling line continuations for pipes.
|
|
{groups, last_group} =
|
|
Enum.reduce(nodes, {[], []}, fn node, {acc_groups, current_group} ->
|
|
case node do
|
|
%{t: :comma} ->
|
|
# A comma always finishes the current expression.
|
|
{[Enum.reverse(current_group) | acc_groups], []}
|
|
|
|
%{t: :newline} when type == :brace ->
|
|
# A newline might finish the expression. Look ahead.
|
|
next_significant = find_next_significant(nodes, node)
|
|
|
|
is_continuation =
|
|
case next_significant do
|
|
%{t: :token, str: ":"} -> true
|
|
%{t: :token, str: "."} -> true
|
|
_ -> false
|
|
end
|
|
|
|
if is_continuation do
|
|
# It's a line continuation, add newline to current group (as whitespace) and continue.
|
|
{acc_groups, [node | current_group]}
|
|
else
|
|
# It's a delimiter, finish the current expression.
|
|
{[Enum.reverse(current_group) | acc_groups], []}
|
|
end
|
|
|
|
_ ->
|
|
# Any other token is part of the current expression.
|
|
{acc_groups, [node | current_group]}
|
|
end
|
|
end)
|
|
|
|
# Add the last group if it's not empty.
|
|
final_groups =
|
|
if last_group != [], do: [Enum.reverse(last_group) | groups], else: groups
|
|
|
|
final_groups
|
|
|> Enum.reverse()
|
|
|> Enum.reject(&(&1 == []))
|
|
end
|
|
|
|
defp find_next_significant(nodes, current_node) do
|
|
# This is a simplified lookahead. A more robust implementation might need the index.
|
|
# For now, we find the index of the current node and look from there.
|
|
case Enum.find_index(nodes, &(&1 == current_node)) do
|
|
nil ->
|
|
nil
|
|
|
|
index ->
|
|
nodes
|
|
|> Enum.drop(index + 1)
|
|
|> Enum.find(fn
|
|
%{t: :comment} -> false
|
|
%{t: :newline} -> false
|
|
_ -> true
|
|
end)
|
|
end
|
|
end
|
|
|
|
# Base case: A single token/item becomes itself.
|
|
defp process_expression([item]), do: item
|
|
|
|
# Base case: An empty list of tokens results in nil.
|
|
defp process_expression([]), do: nil
|
|
|
|
# Updated recursive case without exceptions
|
|
defp process_expression(tokens) do
|
|
# IO.inspect(tokens, label: "Processing expression")
|
|
# 1. Find all potential operators and their indices.
|
|
operators_with_indices =
|
|
tokens
|
|
|> Enum.with_index()
|
|
|> Enum.filter(fn {token, _idx} ->
|
|
# Ensure token is a map with a `str` key before checking the table
|
|
is_map(token) && Map.has_key?(@operator_table, token[:str])
|
|
end)
|
|
|
|
case operators_with_indices do
|
|
# Case 1: No operators found. This is a simple s-expression (e.g., function call).
|
|
[] ->
|
|
%{t: :sexp, list: tokens}
|
|
|
|
# |> IO.inspect(label: "Simple s-expression (no operators)")
|
|
|
|
# Case 2: Operators were found. Proceed with precedence logic.
|
|
ops ->
|
|
# 2. Find the operator with the lowest precedence (the pivot).
|
|
{_operator, pivot_index} =
|
|
Enum.min(ops, fn {token1, _idx1}, {token2, _idx2} ->
|
|
{prec1, assoc1} = @operator_table[token1.str]
|
|
{prec2, assoc2} = @operator_table[token2.str]
|
|
|
|
cond do
|
|
prec1 < prec2 ->
|
|
true
|
|
|
|
prec1 > prec2 ->
|
|
false
|
|
|
|
# Precedences are equal, use associativity to break the tie.
|
|
# For left-associative, the leftmost (first) operator wins.
|
|
# For right-associative, the rightmost (last) operator wins.
|
|
assoc1 == :left ->
|
|
true
|
|
|
|
assoc1 == :right && assoc2 == :right ->
|
|
false
|
|
|
|
true ->
|
|
false
|
|
end
|
|
end)
|
|
|
|
# |> IO.inspect(label: "Chosen pivot operator")
|
|
|
|
operator = Enum.at(tokens, pivot_index)
|
|
|
|
# 3. Split the token list into left/right sides around the pivot.
|
|
left_tokens = Enum.slice(tokens, 0, pivot_index)
|
|
right_tokens = Enum.slice(tokens, pivot_index + 1, length(tokens))
|
|
|
|
# 4. Recursively process the left and right sides.
|
|
# Special case for dot operator: RHS is a literal, not an expression.
|
|
{lhs, rhs} =
|
|
{process_expression(left_tokens), process_expression(right_tokens)}
|
|
|
|
# if operator.str == "." do
|
|
# {process_expression(left_tokens), List.first(right_tokens)}
|
|
# else
|
|
# {process_expression(left_tokens), process_expression(right_tokens)}
|
|
# end
|
|
|
|
# 5. Build the final s-expression.
|
|
%{t: :sexp, list: [operator, lhs, rhs]}
|
|
end
|
|
end
|
|
end
|
|
|
|
Tpl.run()
|