From afc0738a31c59200d5b23f0dce2d9f5f78b52ce9 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Fri, 15 Nov 2019 00:51:27 -0600 Subject: [PATCH] WIP STUFF --- .gitignore | 2 +- README.md | 4 +- lib/lytlang.ex | 102 +++++++++++++----- lib/parser.ex | 10 -- lib/transpiler.ex | 95 ---------------- src/lytlang.xrl | 16 --- src/lytlang_lexer.xrl | 44 ++++++++ src/lytlang_parser.yrl | 90 ++++++++++++++++ .../example_projects/simple_app/app.lyt | 34 +++--- test/lytlang_test.exs | 18 +++- 10 files changed, 247 insertions(+), 168 deletions(-) delete mode 100644 lib/parser.ex delete mode 100644 lib/transpiler.ex delete mode 100644 src/lytlang.xrl create mode 100644 src/lytlang_lexer.xrl create mode 100644 src/lytlang_parser.yrl diff --git a/.gitignore b/.gitignore index ad78134..215bdf2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,4 @@ lytlang-*.tar *.swp # Ignore compiled Erlang files -/src/lytlang.erl +/src/*.erl diff --git a/README.md b/README.md index f107cfd..da253b0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Lytlang -Lytlang's goal is to enable you to write Elixir code more tersely and is itself -written in Elixir. Its syntax is a terrible merging of Elm, CoffeeScript, -Elixir, a sprinkling of Rust, and some made-up crap. Here's a rough preview: +Lytlang is an opinionated way to write more readable Elixir code. ``` !module Leetcode diff --git a/lib/lytlang.ex b/lib/lytlang.ex index 096e138..c6dce4f 100644 --- a/lib/lytlang.ex +++ b/lib/lytlang.ex @@ -1,31 +1,83 @@ defmodule Lytlang do - @moduledoc """ - Documentation for Lytlang. - """ + def eval(string) do + {value, _} = + string + |> from_lytlang() + |> IO.inspect() + |> Code.eval_quoted() + |> IO.inspect() - @doc """ - Hello world. - - ## Examples - - iex> Lytlang.hello() - :world - - """ - def hello do - :world + value end - @doc """ - Echo. - - ## Examples - - iex> Lytlang.echo("world") - "world" - - """ - def echo(s) do - s + def from_lytlang(string, binding \\ [], opts \\ []) do + string |> String.to_charlist() |> tokenize() |> parse() |> transform(binding, opts) end + + def tokenize(string) do + {:ok, tokens, _} = + :lytlang_lexer.string(string) + |> IO.inspect() + + tokens + end + + def parse(tokens) do + {:ok, tree} = + :lytlang_parser.parse(tokens) + |> IO.inspect() + + tree + end + + def transform(ast, binding \\ [], opts \\ []) + + def transform([] = _expr_list, _binding, _opts), do: [] + + def transform([expr | rest] = _expr_list, binding, opts) do + [transform(expr, binding, opts) | transform(rest, binding, opts)] + end + + def transform({:binary_op, _line, op, left, right}, binding, _) do + {op, binding, [transform(left), transform(right)]} + end + + def transform({:unary_op, line, op, left}, _, _) do + {:op, line, op, transform(left)} + end + + def transform({:integer, _, n} = _expr, _, _), do: n end + +""" +-module(elixir). +-export([eval/1, from_elixir/1, from_erlang/1]). + +eval(String) -> + {value, Value, _} = erl_eval:expr(from_elixir(String), []), + Value. + +% Temporary to aid debugging +from_elixir(String) -> + transform(parse(String)). + +% Temporary to aid debugging +from_erlang(String) -> + {ok, Tokens, _} = erl_scan:string(String), + {ok, [Form]} = erl_parse:parse_exprs(Tokens), + Form. + +parse(String) -> + {ok, Tokens, _} = elixir_lexer:string(String), + {ok, ParseTree} = elixir_parser:parse(Tokens), + ParseTree. + +transform({ binary_op, Line, Op, Left, Right }) -> + {op, Line, Op, transform(Left), transform(Right)}; + +transform({ unary_op, Line, Op, Right }) -> + {op, Line, Op, transform(Right)}; + + +transform({ integer, _, _ } = Expr) -> Expr. +""" diff --git a/lib/parser.ex b/lib/parser.ex deleted file mode 100644 index f24871c..0000000 --- a/lib/parser.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule Lytlang.Parser do - @doc """ - Attempts to tokenize an input string to start_tag, end_tag, and char - """ - @spec parse(binary) :: list - def parse(input) do - {:ok, tokens, _} = input |> to_char_list |> :lytlang.string() - tokens - end -end diff --git a/lib/transpiler.ex b/lib/transpiler.ex deleted file mode 100644 index 563f1f9..0000000 --- a/lib/transpiler.ex +++ /dev/null @@ -1,95 +0,0 @@ -defmodule Lytlang.Transpiler do - @moduledoc """ - Documentation for Lytlang. - """ - - @spec transpile_file(String.t()) :: String.t() - def transpile_file(file_path) do - file_path - |> File.read!() - |> transpile_block() - end - - @doc """ - Transpiler! - - ## Examples - - iex> Lytlang.Transpiler.transpile_block("!mod EmptyModule") - "defmodule EmptyModule do\\nend" - - iex> Lytlang.Transpiler.transpile_block("!mod EmptyModule\\n\\tfn hello\\n\\t\\t:world") - "defmodule EmptyModule\\n\\tdef hello() do\\n\\t\\t:world\\n\\tend\\nend" - - """ - @spec transpile_block(String.t(), Keyword.t()) :: String.t() - def transpile_block(s, _opts \\ []) do - s - |> split_lines() - |> Enum.map(&tokenize/1) - |> IO.inspect(label: "Tokens") - |> build_ast() - |> IO.inspect(label: "AST") - |> ast_to_elixir() - end - - def initial_ast_state() do - %{ - ast: {:__global, []} - } - end - - def build_ast(token_lines, state \\ nil) - - def build_ast([], state), do: state.ast |> Enum.reverse() - - def build_ast([line_tokens | rest], state) do - state = - if !state do - initial_ast_state() - else - state - end - - ast = state.ast - - state = - case line_tokens do - ["!mod", module_name | rest] -> - %{state | ast: [{:module, module_name, []} | ast]} - - ["fn", fn_name | rest] -> - %{state | ast: [{:function, fn_name, []} | ast]} - - expr -> - expr |> IO.inspect(label: "lyt expression") - %{state | ast: [expr |> Enum.join(" ") | ast]} - end - - build_ast(rest, state) - end - - def ast_to_elixir(s, state \\ %{elixir_code: []}) - - def ast_to_elixir([], state), - do: state.elixir_code |> Enum.filter(&(String.trim(&1) != "")) |> Enum.join("\n") - - def ast_to_elixir([leaf | rest], state) do - child = fn {head, tail}, state -> [head | ast_to_elixir(rest, state)] ++ [tail] end - - case leaf do - :global -> child.({"", ""}, state) - {:module, mod, []} -> child.({"defmodule #{mod} do", "end"}, state) - {:fn, fname, []} -> child.({"def #{fname} do", "end"}, state) - expr when is_binary(expr) -> [expr] - end - end - - def split_lines(s) do - String.split(s, "\n", trim: true) - end - - def tokenize(s) do - Regex.split(~r/\s/, s) - end -end diff --git a/src/lytlang.xrl b/src/lytlang.xrl deleted file mode 100644 index fbfc248..0000000 --- a/src/lytlang.xrl +++ /dev/null @@ -1,16 +0,0 @@ -Definitions. - -I = \t -MODULE_DECL = module\s[a-zA-Z][a-zA-Z0-9_]* -FILE_LEVEL_MODULE_DECL = !module\s([a-zA-Z][a-zA-Z0-9_]*) -NUMBER = [0-9]+ - -Rules. - -%% number -{NUMBER} : {token, { number, TokenLine, list_to_integer(TokenChars) } }. -{MODULE_DECL} : {token, { module_decl, TokenLine, TokenChars } }. -{FILE_LEVEL_MODULE_DECL} : {token, { file_level_module_decl, TokenLine, TokenChars } }. -[\s\n\r\t]+ : skip_token. - -Erlang code. diff --git a/src/lytlang_lexer.xrl b/src/lytlang_lexer.xrl new file mode 100644 index 0000000..da8c05f --- /dev/null +++ b/src/lytlang_lexer.xrl @@ -0,0 +1,44 @@ +% Lexer syntax for the Elixir language done with leex +% Copyright (C) 2011 Jose Valim + +Definitions. + +D = [0-9] +U = [A-Z] +L = [a-z] +WS = [\s] + +Rules. + +{D}+\.{D}+ : { token, { float, TokenLine, list_to_float(TokenChars) } }. +{D}+ : { token, { integer, TokenLine, list_to_integer(TokenChars) } }. +({L}|_)({U}{L}{D}|_)* : { token, var(TokenChars, TokenLine) }. + +\+ : { token, { '+', TokenLine } }. +- : { token, { '-', TokenLine } }. +\* : { token, { '*', TokenLine } }. +/ : { token, { '/', TokenLine } }. +\( : { token, { '(', TokenLine } }. +\) : { token, { ')', TokenLine } }. += : { token, { '=', TokenLine } }. +-> : { token, { '->', TokenLine } }. +; : { token, { ';', TokenLine } }. + +{Comment} : skip_token. +{WS}+ : skip_token. +% ({Comment}|{Whitespace})*(\n({Comment}|{Whitespace})*)+ : { token, { eol, TokenLine } }. + +Erlang code. + +var(Chars, Line) -> + Atom = list_to_atom(Chars), + case reserved_word(Atom) of + true -> {Atom, Line}; + false -> {var, Line, Atom} + end. + +reserved_word('nil') -> true; +reserved_word('true') -> true; +reserved_word('false') -> true; +reserved_word('module') -> true; +reserved_word(_) -> false. diff --git a/src/lytlang_parser.yrl b/src/lytlang_parser.yrl new file mode 100644 index 0000000..22cbc9e --- /dev/null +++ b/src/lytlang_parser.yrl @@ -0,0 +1,90 @@ +Nonterminals + grammar + expr_list + expr + assign_expr + add_expr + mult_expr + unary_expr + fun_expr + body + stabber + max_expr + number + unary_op + add_op + mult_op + . + +Terminals + var float integer eol + '+' '-' '*' '/' '(' ')' '=' '->' + . + +Rootsymbol grammar. + +grammar -> expr_list : '$1'. +grammar -> '$empty' : []. + +expr_list -> eol : []. +expr_list -> expr : ['$1']. +expr_list -> expr eol : ['$1']. +expr_list -> eol expr_list : '$2'. +expr_list -> expr eol expr_list : ['$1'|'$3']. + +expr -> assign_expr : '$1'. + +assign_expr -> add_expr '=' assign_expr : + { match, ?line('$2'), '$1', '$3' }. + +assign_expr -> add_expr : '$1'. + +%% Arithmetic operations +add_expr -> add_expr add_op mult_expr : + { binary_op, ?line('$1'), ?op('$2'), '$1', '$3' }. + +add_expr -> mult_expr : '$1'. + +mult_expr -> mult_expr mult_op unary_expr : + { binary_op, ?line('$1'), ?op('$2'), '$1', '$3' }. + +mult_expr -> unary_expr : '$1'. + +unary_expr -> unary_op max_expr : + { unary_op, ?line('$1'), ?op('$1'), '$2' }. + +unary_expr -> max_expr : '$1'. + +fun_expr -> stabber eol body : + { 'fn', ?line('$1') + , { clauses, [ { clause, ?line('$1'), [], [], '$3' } ] } + }. + +fun_expr -> max_expr : '$1'. + +%% Minimum expressions +max_expr -> number : '$1'. +max_expr -> '(' expr ')' : '$2'. + +%% Numbers +number -> float : '$1'. +number -> integer : '$1'. + +%% Unary operator +unary_op -> '+' : '$1'. +unary_op -> '-' : '$1'. + +%% Addition operators +add_op -> '+' : '$1'. +add_op -> '-' : '$1'. + +%% Multiplication operators +mult_op -> '*' : '$1'. +mult_op -> '/' : '$1'. + +Erlang code. + +-define(op(Node), element(1, Node)). +-define(line(Node), element(2, Node)). +-define(char(Node), element(3, Node)). + diff --git a/test/fixtures/example_projects/simple_app/app.lyt b/test/fixtures/example_projects/simple_app/app.lyt index 392acbc..497fe3a 100644 --- a/test/fixtures/example_projects/simple_app/app.lyt +++ b/test/fixtures/example_projects/simple_app/app.lyt @@ -1,24 +1,28 @@ -mod SimpleApp -entry - :project - deps: - {:cowboy, "~> 2.6"} - {:plug, "~> 1.8"} - {:plug_cowboy, "~> 2.0"} - :app - [extra_applications: [:logger]] +!project SimpleApp simple_app + # should generate a SimpleApp.MixProject_ module + version: "0.1.0" + elixir: "~> 1.8" + lytl: "~> 0.1" + start_permanent: Mix.env() == prod + extra_applications: [logger] + deps: [] -import Plug.Conn +!module + # uses project's name as module if not module name specified + import Plug.Conn -fn init default_options - IO.puts "initializing plug" - default_options +init = opts -> IO.puts "initializing plug"; opts -fn call conn:Plug.Conn.t() _options = [] +call = conn _options = [] -> IO.puts "calling plug" - conn |> put_resp_content_type "text/plain" |> send_resp 200 "Hello world" +long_fn = + arg1 + arg2 + arg3 + -> + # in lytx run `Plug.Adapters.Cowboy.http SimpleApp []` diff --git a/test/lytlang_test.exs b/test/lytlang_test.exs index d376b88..6d3ceda 100644 --- a/test/lytlang_test.exs +++ b/test/lytlang_test.exs @@ -1,9 +1,21 @@ defmodule LytlangTest do use ExUnit.Case doctest Lytlang - doctest Lytlang.Transpiler - test "greets the world" do - assert Lytlang.hello() == :world + test "arithmetic" do + [ + {"2 + 3 + 8", 13}, + {"2 * 3 + 8", 14}, + {"2 + 3 * 8", 26}, + {"(2 + 3) * 8", 40}, + {"2 * (3 + 8)", 22}, + {"2 + (3 * 8)", 26}, + {"8 - 3", 5}, + {"8 / 4", 2.0}, + {"2 + 3", 5} + ] + |> Enum.map(fn {string, val} -> + assert Lytlang.eval(string) == val + end) end end