Current section
Files
Jump to
Current section
Files
src/otpcl_parse.erl
% @doc OTPCL's parser.
%
% Unlike most Erlang-derived languages, OTPCL's parser is not based on
% leex/yecc; rather, it's written by hand (as a side note: the author has no
% idea exactly what sort of parser OTPCL's parser actually is, though "recursive
% descent" sounds approximately right, given that it's recursive and it
% descends; someone who actually went to college is welcome to try to make sense
% of this module and provide a better explanation of what sort of parser it
% implements). The parser is (as far as the author can surmise) linear and
% relatively efficient, albeit only because it "cheats" by punting some things
% to the interpreter (notably: the parser treats numbers as atoms, so the
% interpreter is required to reparse atoms if it wants to be able to interpret
% them as numbers).
%
% == Syntax ==
%
% A "free" character is a character that is neither escaped (i.e. immediately
% preceded by a backslash character, provided that backslash character is itself
% "free") nor already part of a lower-level construct.
%
% A program is a list of statements separated by contiguous sequences of free
% vertical whitespace characters or semicolons.
%
% A statement is a list of words separated by contiguous sequences of free
% horizontal whitespace characters (escaped vertical whitespace characters are
% considered to be horizontal whitespace characters). Statements may be treated
% as "commands" in certain contexts (e.g. commands are specifically the
% top-level children of a program).
%
% A word is a braced string, double-quoted string, backquoted charlist,
% single-quoted atom, braced variable, unquoted variable, function call, list,
% tuple, comment, pipe, or unquoted atom.
%
% A braced string is a free opening curly brace, followed by zero or more
% characters and/or braced strings, followed by a free closing curly brace.
% That is: a braced string can be inside a braced string (and curly braces not
% intended to begin/end an inner braced string should be escaped with an
% immediately-preceding backslash).
%
% A double-quoted string is a free double-quote, followed by zero or more
% characters, followed by a free double-quote.
%
% A backquoted charlist is a free backquote, followed by zero or more
% characters, followed by a free backquote.
%
% A single-quoted atom is a free single-quote, followed by zero or more
% characters, followed by a free single-quote.
%
% A braced variable is a free dollar-sign, followed by a braced string.
%
% An unquoted variable is a free dollar-sign, followed by a contiguous sequence
% of characters, terminated by the next free whitespace, semicolon, or (when
% expected by the parser) closing parenthesis, square bracket, angle bracket, or
% curly brace. Unquoted variables may not contain free opening parentheses,
% square brackets, angle brackets, or curly braces; if encountered, the parser
% will immediately return an error (this may change in the future).
%
% A function call is a free opening square bracket, followed by a statement,
% followed by a free closing square bracket. It is currently an error for a
% function call to contain more or less than one statement (this may change in
% the future).
%
% A list is a free opening parenthesis, followed by a statement (note: the
% statement is treated purely as a list of words), followed by a free closing
% parenthesis. It is currently an error for a list to contain more than one
% statement (this will change in the future).
%
% A tuple is a free opening angle bracket, followed by a statement (note: the
% statement is treated purely as a list of words), followed by a free closing
% angle bracket. It is currently an error for a tuple to contain more than one
% statement (this will change in the future).
%
% A comment is a free octothorpe, followed by a contiguous sequence of
% characters, terminated by the next vertical whitespace character. A comment
% terminates the statement in which it is encountered.
%
% A pipe is a free pipe character, followed optionally by a contiguous sequence
% of characters, terminated by the next free whitespace. The pipe itself is
% parsed as an unquoted atom, which becomes the first word in a new statement.
%
% An unquoted atom is a contiguous sequence of characters, terminated by the
% next free whitespace, semicolon, or (when expected by the parser) closing
% parenthesis, square bracket, angle bracket, or curly brace. Unquoted atoms
% may not contain free opening parentheses, square brackets, angle brackets, or
% curly braces; if encountered, the parser will immediately return an error
% (this may change in the future).
%
% == Output ==
%
% OTPCL's parser does not emit the same exact structures as Erlang's parser
% (that is: it does not generate Erlang-compatible parse trees). This was
% probably a mistake (and may very well change, notably because it'd presumably
% make OTPCL compilation easier by just piggybacking on the existing
% Erlang-oriented infrastructure), but it works well enough for now.
%
% === Tokens ===
%
% The lexer makes no attempt to actually classify different types of characters
% (unlike Erlang's lexer); thus, each "token" is simply `{Char, Pos={F,L,C}}',
% where `Char' is a character code point and `Pos' is the position of that
% character (that is, `Char' came from column `C' of line `L' of file
% `F').
%
% === Trees ===
%
% The syntax tree the parser emits is a recursive 3-element tuple of the form
% `{parsed, Type, Branches}', where `Type' is an atom and `Branches' is a list
% of either tokens or trees. By default (i.e. when calling parse/1), the root
% of the tree will be a `program', with `command' and/or `comment' branches
% (`pipe's are also parsed at this level, but the parser converts those to
% `command's).
-module(otpcl_parse).
-include("otpcl.hrl").
-export([scan/1, scan/2, parse/1, parse/2, initpos/0, initpos/1]).
-ifdef(DEBUG).
-define(DEBUG_PRINT(Msg, Args), io:format(Msg, Args)).
-else.
-define(DEBUG_PRINT(Msg, Args), ok).
-endif.
%% I don't know if this really counts as "lexing", but it does
%% associate every character with a line/column, which means parse/3
%% doesn't need to care about it. Probably not memory-optimal,
%% though.
-spec scan(str_or_bin()) -> [token()].
% @doc Converts a string into a list of tokens.
scan(Txt) ->
scan(Txt, [], initpos()).
-spec scan(str_or_bin(), position()) -> [token()].
% @doc Converts a string into a list of tokens, starting at the specified
% position.
scan(Txt, Pos) ->
scan(Txt, [], Pos).
-spec scan(str_or_bin(), [token()], position()) -> [token()].
scan(Txt, Acc, Pos) when is_binary(Txt) ->
scan(binary_to_list(Txt), Acc, Pos);
scan([], Acc, _) ->
lists:reverse(Acc);
scan([Char|Rem], Acc, Pos) ->
scan(Rem, [{Char, Pos}|Acc], nextpos(Char, Pos)).
%% This is where the fun begins
-spec parse(str_or_bin()) -> parse_success() | parse_error().
% @doc Like parse/2, but defaulting to `program' as the toplevel parse tree
% element.
parse(Input) ->
parse([program], Input).
-spec parse([level(),...], str_or_bin()) -> parse_success() | parse_error().
% @doc Attempts to parse either a string or token list. Returns either a
% success response `{ok, Tree, Rem}' (where `Tree' is an OTPCL parse tree and
% `Rem' is whatever characters were left over
parse(Lvls, Input) ->
case is_text(Input) of
true -> parse(Lvls, scan(Input), []);
_ -> parse(Lvls, Input, [])
end.
%% I ain't got the fancy schmancy college edumacation to know the
%% *right* terminology for how parsers/lexers work, but here's the
%% terminology I'm using for the conventions below:
%%
%% TOKEN: do something upon matching a specific token
%% TPAIR: do something upon matching a specific pair of tokens
%% ANY: do something upon matching any token
%% EOF: do something upon running out of input tokens
%%
%% DROP: don't store the next token anywhere
%% TAKE: put the token in the current level's accumulator
%% KEEP: put the token back into the current level's remainder
%% EXIT: stop parsing the current level and either...
%% OK: ...return the resulting node or...
%% ERROR: ...return an error with the reason and accumulator
%%
%% SWITCH: replace the current level (useful for dispatching node
%% subtypes)
%% DESCEND: start a new level, then stick the resulting node in the
%% current level's accumulator
%% FLATTEN: append the current level's grandchildren (plus the
%% specified characters at the front and end) to its
%% accumulator
%% ESCAPED: the current token is an unescaped backslash, so drop it
%% and use the next token instead (usually for a TAKE)
-define(TOKEN_DROP(Lvl, Char),
parse(Lvls = [Lvl|_], [{Char,_}|Rem], Acc) ->
?DEBUG_PRINT("~p: dropping token ~p\n", [Lvl, Char]),
parse(Lvls, Rem, Acc)).
-define(TPAIR_DROP(Lvl, First, Second),
parse(Lvls = [Lvl|_], [{First,_}|[{Second,_}|Rem]], Acc) ->
?DEBUG_PRINT("~p: dropping token pair ~p/~p\n",
[Lvl, First, Second]),
parse(Lvls, Rem, Acc)).
-define(TOKEN_DROP_SWITCH(Old, Char, New),
parse([Old|Up], [{Char,_}|Rem], Acc) ->
?DEBUG_PRINT("~p: dropping token ~p and switching to ~p\n",
[Old, Char, New]),
parse([New|Up], Rem, Acc)).
-define(TOKEN_TAKE_DESCEND(Lvl, Char, SubLvl),
parse(Lvls = [Lvl|_], [{Char,_}|Rem], Acc) ->
?DEBUG_PRINT("~p: taking token ~p and descending to ~p\n",
[Lvl, Char, SubLvl]),
case parse([SubLvl|Lvls], Rem) of
{ok, Child, NewRem} ->
parse(Lvls, NewRem, [Child|Acc]);
Error ->
Error
end).
-define(TOKEN_KEEP_DESCEND(Lvl, Char, SubLvl),
parse(Lvls = [Lvl|_], Tokens = [{Char,_}|_], Acc) ->
?DEBUG_PRINT("~p: leaving token ~p for descent to ~p\n",
[Lvl, Char, SubLvl]),
case parse([SubLvl|Lvls], Tokens) of
{ok, Child, NewRem} ->
parse(Lvls, NewRem, [Child|Acc]);
Error ->
Error
end).
-define(TOKEN_TAKE_ESCAPED(Lvl, Char),
parse(Lvls = [Lvl|_], [$\\|[T={Char,_}|Rem]], Acc) ->
?DEBUG_PRINT("~p: taking escaped token ~p\n", [Lvl, Char]),
parse(Lvls, Rem, [T|Acc])).
-define(TOKEN_TAKE_DESCEND_FLATTEN(Lvl, Start, SubLvl, End),
parse(Lvls = [Lvl|_], [SChar = {Start,_}|Rem], Acc) ->
?DEBUG_PRINT("~p: taking token ~p for flat descent into ~p "
++ "with ending token ~p\n",
[Lvl, Start, SubLvl, End]),
case parse([SubLvl|Lvls], Rem) of
{ok, {parsed,_,Inner}, NewRem} ->
case NewRem of
[{_,RemPos}|_] ->
EChar = {End,nextpos(End,RemPos)},
NewAcc = [EChar] ++ lists:reverse(Inner)
++ [SChar] ++ Acc,
parse(Lvls, NewRem, NewAcc);
[] ->
{error, {expected, End}, Lvl, NewRem,
lists:reverse(Inner) ++ [SChar] ++ Acc}
end;
Error ->
Error
end).
-define(TOKEN_EXIT_OK(Lvl, Char),
parse([Lvl|_], [{Char,_}|Rem], Acc) ->
?DEBUG_PRINT("~p: token ~p is a valid exit point\n",
[Lvl, Char]),
{ok, {parsed, Lvl, lists:reverse(Acc)}, Rem}).
-define(TOKEN_EXIT_ERROR(Lvl, Char, Reason),
parse([Lvl|_], Rem = [{Char,_}|_], Acc) ->
?DEBUG_PRINT("~p: token ~p is an invalid exit point\n",
[Lvl, Char]),
{error, Reason, Lvl, Rem, Acc}).
-define(TOKEN_KEEP_EXIT_OK(Lvl, Char),
parse([Lvl|_], Tokens = [{Char,_}|_], Acc) ->
?DEBUG_PRINT("~p: token ~p is a valid exit point; leaving for "
++ "parent\n", [Lvl, Char]),
{ok, {parsed, Lvl, lists:reverse(Acc)}, Tokens}).
-define(TPAIR_EXIT_OK(Lvl, First, Second),
parse([Lvl|_], [{First,_}|[{Second,_}|Rem]], Acc) ->
?DEBUG_PRINT("~p: token pair ~p/~p is a valid exit point\n",
[Lvl, First, Second]),
{ok, {parsed, Lvl, lists:reverse(Acc)}, Rem}).
-define(TPAIR_KEEP_EXIT_OK(Lvl, First, Second),
parse([Lvl|_], Tokens = [{First,_}|[{Second,_}|_]], Acc) ->
?DEBUG_PRINT("~p: token pair ~p/~p is a valid exit point; "
++ "leaving for parent\n", [Lvl, First, Second]),
{ok, {parsed, Lvl, lists:reverse(Acc)}, Tokens}).
-define(TPAIR_DROP_SWITCH(Old, First, Second, New),
parse([Old|Up], [{First,_}|[{Second,_}|Rem]], Acc) ->
?DEBUG_PRINT("~p: dropping token pair ~p/~p and switching to "
++ "~p\n", [Old, First, Second, New]),
parse([New|Up], Rem, Acc)).
-define(EOF_EXIT_OK(Lvl),
parse([Lvl|_], [], Acc) ->
?DEBUG_PRINT("~p: EOF is a valid exit point\n", [Lvl]),
{ok, {parsed, Lvl, lists:reverse(Acc)}, []}).
-define(EOF_EXIT_ERROR(Lvl, Reason),
parse([Lvl|_], [], Acc) ->
?DEBUG_PRINT("~p: EOF is an invalid exit point\n", [Lvl]),
{error, Reason, Lvl, [], Acc}).
-define(ANY_KEEP_DESCEND(Lvl, SubLvl),
parse(Lvls = [Lvl|_], Tokens, Acc) ->
?DEBUG_PRINT("~p: unconditionally descending into ~p\n",
[Lvl, SubLvl]),
case parse([SubLvl|Lvls], Tokens) of
{ok, Child, Rem} ->
parse(Lvls, Rem, [Child|Acc]);
Error ->
Error
end).
-define(ANY_KEEP_SWITCH(Old, New),
parse([Old|Up], Tokens, Acc) ->
?DEBUG_PRINT("~p: unconditionally switching into ~p\n",
[Old, New]),
parse([New|Up], Tokens, Acc)).
-define(ANY_TAKE(Lvl),
parse(Lvls = [Lvl|_], [T|Rem], Acc) ->
?DEBUG_PRINT("~p: unconditionally taking token ~p\n", [Lvl,T]),
parse(Lvls, Rem, [T|Acc])).
-spec parse([level(),...], [token()], [tree()] | [token()]) ->
parse_success() | parse_error().
?EOF_EXIT_OK(program);
?TOKEN_TAKE_DESCEND(program, $#, comment);
?TOKEN_KEEP_DESCEND(program, $|, pipe);
?TOKEN_DROP(program, $\s);
?TOKEN_DROP(program, $\t);
?ANY_KEEP_DESCEND(program, command);
?EOF_EXIT_OK(command);
?TOKEN_EXIT_OK(command, $\n);
?TOKEN_EXIT_OK(command, $;);
?TOKEN_KEEP_EXIT_OK(command, $#);
?TOKEN_KEEP_EXIT_OK(command, $|);
?TPAIR_DROP(command, $\\, $\n);
?TPAIR_DROP(command, $\\, $;);
?TOKEN_DROP(command, $\s);
?TOKEN_DROP(command, $\t);
?ANY_KEEP_DESCEND(command, word);
?TOKEN_EXIT_OK(word, $\s);
?TOKEN_EXIT_OK(word, $\t);
?TOKEN_EXIT_OK(word, $\n);
?TOKEN_EXIT_OK(word, $;);
?TOKEN_KEEP_EXIT_OK(word, $#);
?TOKEN_KEEP_EXIT_OK(word, $|);
?TOKEN_DROP_SWITCH(word, ${, braced);
?TOKEN_EXIT_ERROR(word, $}, {unexpected, $}});
?TOKEN_DROP_SWITCH(word, $", double_quoted);
?TOKEN_DROP_SWITCH(word, $`, backquoted);
?TOKEN_DROP_SWITCH(word, $', single_quoted);
?TPAIR_DROP_SWITCH(word, $$, ${, var_braced);
?TOKEN_DROP_SWITCH(word, $$, var_unquoted);
?TOKEN_DROP_SWITCH(word, $[, funcall);
?TOKEN_EXIT_ERROR(word, $], {unexpected, $]});
?TOKEN_DROP_SWITCH(word, $(, list);
?TOKEN_EXIT_ERROR(word, $), {unexpected, $)});
?TOKEN_DROP_SWITCH(word, $<, tuple);
?TOKEN_EXIT_ERROR(word, $>, {unexpected, $>});
?ANY_KEEP_SWITCH(word, unquoted);
?EOF_EXIT_OK(comment);
?TOKEN_EXIT_OK(comment, $\n);
?ANY_TAKE(comment);
?EOF_EXIT_OK(pipe);
?TOKEN_KEEP_DESCEND(pipe, $|, unquoted);
?ANY_KEEP_SWITCH(pipe, command);
?EOF_EXIT_ERROR(braced, {expected, $}});
?TOKEN_EXIT_OK(braced, $});
?TOKEN_TAKE_DESCEND_FLATTEN(braced, ${, braced, $});
?TOKEN_TAKE_ESCAPED(braced, ${);
?TOKEN_TAKE_ESCAPED(braced, $});
?TOKEN_TAKE_ESCAPED(braced, $\\);
?ANY_TAKE(braced);
?EOF_EXIT_ERROR(double_quoted, {expected, $"});
?TOKEN_EXIT_OK(double_quoted, $");
?TOKEN_TAKE_ESCAPED(double_quoted, $");
?TOKEN_TAKE_ESCAPED(double_quoted, $\\);
?ANY_TAKE(double_quoted);
?EOF_EXIT_ERROR(backquoted, {expected, $`});
?TOKEN_EXIT_OK(backquoted, $`);
?TOKEN_TAKE_ESCAPED(backquoted, $`);
?TOKEN_TAKE_ESCAPED(backquoted, $\\);
?ANY_TAKE(backquoted);
?EOF_EXIT_ERROR(single_quoted, {expected, $'});
?TOKEN_EXIT_OK(single_quoted, $');
?TOKEN_TAKE_ESCAPED(single_quoted, $');
?TOKEN_TAKE_ESCAPED(single_quoted, $\\);
?ANY_TAKE(single_quoted);
?EOF_EXIT_OK(unquoted);
?TOKEN_EXIT_OK(unquoted, $\s);
?TOKEN_EXIT_OK(unquoted, $\t);
?TOKEN_KEEP_EXIT_OK(unquoted, $\n);
?TPAIR_KEEP_EXIT_OK(unquoted, $\\, $\n);
?TOKEN_KEEP_EXIT_OK(unquoted, $;);
?TOKEN_KEEP_EXIT_OK(unquoted, $]);
?TOKEN_KEEP_EXIT_OK(unquoted, $));
?TOKEN_KEEP_EXIT_OK(unquoted, $>);
?TOKEN_EXIT_ERROR(unquoted, $[, {unexpected, $[});
?TOKEN_EXIT_ERROR(unquoted, $(, {unexpected, $(});
?TOKEN_EXIT_ERROR(unquoted, $<, {unexpected, $<});
?TOKEN_TAKE_ESCAPED(unquoted, $\s);
?TOKEN_TAKE_ESCAPED(unquoted, $\t);
?TOKEN_TAKE_ESCAPED(unquoted, $\\);
?TOKEN_TAKE_ESCAPED(unquoted, $;);
?TOKEN_TAKE_ESCAPED(unquoted, $[);
?TOKEN_TAKE_ESCAPED(unquoted, $]);
?TOKEN_TAKE_ESCAPED(unquoted, $();
?TOKEN_TAKE_ESCAPED(unquoted, $));
?TOKEN_TAKE_ESCAPED(unquoted, $<);
?TOKEN_TAKE_ESCAPED(unquoted, $>);
?ANY_TAKE(unquoted);
?EOF_EXIT_OK(var_unquoted);
?TOKEN_EXIT_OK(var_unquoted, $\s);
?TOKEN_EXIT_OK(var_unquoted, $\t);
?TOKEN_KEEP_EXIT_OK(var_unquoted, $\n);
?TPAIR_KEEP_EXIT_OK(var_unquoted, $\\, $\n);
?TOKEN_KEEP_EXIT_OK(var_unquoted, $;);
?TOKEN_KEEP_EXIT_OK(var_unquoted, $]);
?TOKEN_KEEP_EXIT_OK(var_unquoted, $));
?TOKEN_KEEP_EXIT_OK(var_unquoted, $>);
?TOKEN_EXIT_ERROR(var_unquoted, $[, {unexpected, $[});
?TOKEN_EXIT_ERROR(var_unquoted, $(, {unexpected, $(});
?TOKEN_EXIT_ERROR(var_unquoted, $<, {unexpected, $<});
?TOKEN_TAKE_ESCAPED(var_unquoted, $\s);
?TOKEN_TAKE_ESCAPED(var_unquoted, $\t);
?TOKEN_TAKE_ESCAPED(var_unquoted, $\\);
?TOKEN_TAKE_ESCAPED(var_unquoted, $;);
?TOKEN_TAKE_ESCAPED(var_unquoted, $[);
?TOKEN_TAKE_ESCAPED(var_unquoted, $]);
?TOKEN_TAKE_ESCAPED(var_unquoted, $();
?TOKEN_TAKE_ESCAPED(var_unquoted, $));
?TOKEN_TAKE_ESCAPED(var_unquoted, $<);
?TOKEN_TAKE_ESCAPED(var_unquoted, $>);
?ANY_TAKE(var_unquoted);
?EOF_EXIT_ERROR(var_braced, {expected, $'});
?TOKEN_EXIT_OK(var_braced, $});
?TOKEN_TAKE_DESCEND_FLATTEN(var_braced, ${, braced, $});
?TOKEN_TAKE_ESCAPED(var_braced, ${);
?TOKEN_TAKE_ESCAPED(var_braced, $});
?TOKEN_TAKE_ESCAPED(var_braced, $\\);
?ANY_TAKE(var_braced);
?EOF_EXIT_ERROR(funcall, {expected, $]});
?TOKEN_EXIT_OK(funcall, $]);
?TOKEN_EXIT_ERROR(funcall, $\n, {unimplemented, progcalls});
?TPAIR_DROP(funcall, $\\, $\n);
?TOKEN_DROP(funcall, $\s);
?TOKEN_DROP(funcall, $\t);
?ANY_KEEP_DESCEND(funcall, word);
?EOF_EXIT_ERROR(list, {expected, $)});
?TOKEN_EXIT_OK(list, $));
?TOKEN_EXIT_ERROR(list, $\n, {unimplemented, tables});
?TPAIR_DROP(list, $\\, $\n);
?TOKEN_DROP(list, $\s);
?TOKEN_DROP(list, $\t);
?ANY_KEEP_DESCEND(list, word);
?EOF_EXIT_ERROR(tuple, {expected, $>});
?TOKEN_EXIT_OK(tuple, $>);
?TOKEN_EXIT_ERROR(tuple, $\n, {unimplemented, matrices});
?TPAIR_DROP(tuple, $\\, $\n);
?TOKEN_DROP(tuple, $\s);
?TOKEN_DROP(tuple, $\t);
?ANY_KEEP_DESCEND(tuple, word);
parse(Lvls, Rem, Acc) ->
{error, {unexpected, other}, Lvls, Rem, Acc}.
-spec initpos() -> position().
% @doc Column 0 of row 0 of file `nofile'.
initpos() ->
{nofile,0,0}.
-spec initpos(any()) -> position().
% @doc Column 0 of row 0 of file `File'.
initpos(Filename) ->
{Filename,0,0}.
-spec nextpos(char(), position()) -> position().
% @doc Increments the row and sets the column to 0 for vertical whitespace
% characters; else, increments the column only.
nextpos($\n, {F,L,_}) ->
{F,L+1,0};
nextpos(_, {F,L,C}) ->
{F,L,C+1}.
-spec is_text(str_or_bin()) -> boolean().
% @doc True if character list or binary; false otherwise.
is_text(Txt) ->
is_binary(Txt) or io_lib:char_list(Txt).