Current section
Files
Jump to
Current section
Files
src/lfe_scan.erl
%% -*- mode: erlang; indent-tabs-mode: nil -*-
%% Copyright (c) 2024 Robert Virding
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% File : lfe_scan.erl
%% Author : Robert Virding
%% Purpose : Token scanner for the Lisp Flavoured Erlang language.
%%% Much of the basic structure has been taken from erl_scan. A core
%%% difference is that we need to handle both the cases of only one
%%% token and many tokens. For this reason we have added the 'none'
%%% return.
%%%
%%% We chain a state, #lfe_scan{}, through the scanner but we never
%%% use it for anything. It was inherited from erl_scan and we left it
%%% just in case we might need it.
-module(lfe_scan).
-export([string/1,string/2,string/3,token/2,token/3,tokens/2,tokens/3,
format_error/1]).
-export([start_symbol_char/1,symbol_char/1]).
-export([token_test/1,token_test/2,tokens_test/1,tokens_test/2]).
format_error({illegal,S}) ->
io_lib:format(<<"illegal ~s">>, [S]);
format_error({illegal_token,S}) ->
io_lib:format(<<"illegal token ~s">>, [S]);
format_error({illegal_chars,S}) ->
io_lib:format(<<"illegal characters in ~s">>, [S]);
format_error({bad_format,S}) ->
io_lib:format(<<"bad of format of ~s">>, [S]);
format_error(bad_tq_string) ->
"bad format of triple-quoted string";
format_error({user,S}) -> S;
format_error(Other) ->
lists:flatten(io_lib:write(Other)).
%% Nothing in here yet, but who knows.
-record(lfe_scan, {}).
%% LFE definitions of these types.
-define(WHITE_SPACE(C), (C >= $\000 andalso C =< $\s)).
-define(DIGIT(C), (C >= $0 andalso C =< $9)).
-define(CHAR(C), (C >= O andalso C < 16#110000)).
-define(UNICODE(C),
(is_integer(C) andalso
(C >= 0 andalso C < 16#D800 orelse
C > 16#DFFF andalso C < 16#FFFE orelse
C > 16#FFFF andalso C =< 16#10FFFF))).
-define(UNI255(C), (is_integer(C) andalso 0 =< C andalso C =< 16#ff)).
%% string(String) ->
%% string(String, StartLine) ->
%% string(String, StartLine, Options) ->
%% {ok,Tokens,LastLine} | {error,Error,LastLine}.
%% Scan a string for the tokens in it.
string(String) ->
string(String, 1, []).
string(String, StartLocation) ->
string(String, StartLocation, []).
string(String, StartLocation, _Options) ->
string1(String, StartLocation, 1, [], #lfe_scan{}).
%% string1(Chars, Line, Column, Tokens, State) ->
%% {ok,Tokens,LastLine} | {error,Error,LastLine}.
string1(Cs, Line, _Col, Toks, _St) when Cs =:= [] orelse Cs =:= eof ->
%% No more chars left in the string.
{ok,lists:reverse(Toks),anno(Line)};
string1(Cs0, Line0, Col0, Toks0, St0) ->
%% Keep going!
case scan1(Cs0, Line0, Col0, St0) of
{more,{Cs1,Line1,Col1,St1,Extra,Fun}} ->
%% Needs more, try again by appending eof.
case Fun(Cs1 ++ eof, Line1, Col1, St1, Extra) of
%% What to do when we get more here.
{more,{Cs2,Line2,Col2,St2,Extra2,Fun2}} ->
Fun2(Cs2, Line2, Col2, St2, Extra2);
{none,Rest,Line2,Col2,St2} ->
string1(Rest, Line2, Col2, Toks0, St2);
{ok,Tok,_Rest,Line2,_Col2, _St2} ->
%% It worked and all is done.
{ok,lists:reverse([Tok|Toks0]),anno(Line2)};
{{error,_,_}=Error,_Rest} ->
%% Really bad!
Error
end;
{none,Rest,Line1,Col1,St1} ->
%% Nothing from this call so try again.
string1(Rest, Line1, Col1, Toks0, St1);
{ok,Tok,Rest,Line1,Col1,St1} ->
%% It worked but still has chars left, try agiain.
string1(Rest, Line1, Col1, [Tok|Toks0], St1);
{{error,_,_}=Error,_Rest} ->
%% Bad!
Error
end.
%% token(Continuation, String) ->
%% token(Continuation, String, StartLine) ->
%% {more,Continuation} | {done,ReturnVal,RestChars}.
%% Scan all tokens in the repeated calls threading the continuation
%% through the calls.
token(Cont, Chars) ->
token(Cont, Chars, 1, []).
token(Cont, Chars, StartLine) ->
token(Cont, Chars, StartLine, []).
token([], Chars, Line, _Options) ->
%% io:format("t4 ~p\n", [{[],Chars,Line,_Options}]),
token1(Chars, Line, 1, #lfe_scan{}, [], fun scan/5);
token({lfe_scan_token,Cs,Line,Col,St,Extra,Fun},
Chars, _Line, _Options) ->
%% io:format("t4 ~p\n", [{lfe_scan_token,Chars,_Line,_Options}]),
token1(Cs ++ Chars, Line, Col, St, Extra, Fun).
%% token1(Chars, Line, Column, State, Extra, Fun) ->
%% {done,{ok,Token,Line},Cs} | {done,Error,Cs} | {more,Continuation}.
token1(eof, Line, Col, St, Extra, Fun) ->
Fun(eof, Line, Col, St, Extra);
token1([], Line, Col, St, Extra, Fun) ->
{more,{lfe_scan_token,[],Line,Col,St,Extra,Fun}};
token1(Cs0, Line0, Col0, St0, Extra0, Fun0) ->
%% io:format("t11 ~p\n", [{Cs0,Fun0,fun scan/5]),
case Fun0(Cs0, Line0, Col0, St0, Extra0) of
{more,{Cs1,Line1,Col1,St1,Extra1,Fun1}} ->
Cont = {lfe_scan_token,Cs1,Line1,Col1,St1,Extra1,Fun1},
%% io:format("tf1 ~p\n", [{Cs1,Line1,Col1,Fun1,fun scan/5}]),
{more,Cont};
{none,Rest,Line1,Col1,St1} ->
%% Nothing from this call so try again.
token1(Rest, Line1, Col1, St1, Extra0, fun scan/5);
{ok,Token,Rest,Line1,_Col1,_St1} ->
{done,{ok,Token,Line1},Rest};
{{error,_,_}=Error,Rest} ->
{done,Error,Rest}
end.
%% tokens(Continuation, String) ->
%% tokens(Continuation, String, StartLine) ->
%% tokens(Continuation, String, StartLine, Options) ->
%% {more,Continuation} | {done,ReturnVal,RestChars}.
%% Scan all tokens in the repeated calls threading the continuation
%% through the calls.
tokens(Cont, Chars) ->
%% io:format("ts2 ~p\n", [{Cont,Chars}]),
tokens(Cont, Chars, 1, []).
tokens(Cont, Chars, StartLine) ->
%% io:format("ts3 ~p\n", [{Cont,Chars,StartLine}]),
tokens(Cont, Chars, StartLine, []).
tokens([], Chars, Line, _Options) ->
%% io:format("ts4 ~p\n", [{[],Chars}]),
tokens1(Chars, Line, 1, [], #lfe_scan{}, [], fun scan/5);
tokens({lfe_scan_tokens,Cs,Line,Col,Toks,St,Extra,Fun},
Chars, _Line, _Options) ->
%% io:format("ts4 ~p\n", [{Cont,Chars}]),
tokens1(Cs ++ Chars, Line, Col, Toks, St, Extra, Fun).
%% tokens1(Chars, Line, Column, State, Extra, Fun) ->
%% {done,{ok,Token,Line},Cs} | {done,Error,Cs} | {more,Continuation}.
%% We loop inside this function for as long as we can until we need
%% more characters or there is an error.
tokens1(Cs, Line0, Col0, Toks, St0, Extra0, Fun0) ->
case Fun0(Cs, Line0, Col0, St0, Extra0) of
{more,{Cs1,Line1,Col1,St1,Extra1,Fun1}} ->
Cont = {lfe_scan_tokens,Cs1,Line1,Col1,Toks,St1,Extra1,Fun1},
{more,Cont};
{none,Rest,Line1,Col1,St1} ->
if Rest =:= eof ->
{done,{ok,lists:reverse(Toks),anno(Line1)},eof};
true ->
tokens1(Rest, Line1, Col1, Toks, St1, Extra0, fun scan/5)
end;
{ok,Tok,Rest,Line1,Col1,St1} ->
if Rest =:= eof ->
{done,{ok,lists:reverse([Tok|Toks]),anno(Line1)},eof};
true ->
tokens1(Rest, Line1, Col1, [Tok|Toks], St1, [], fun scan/5)
end;
{{error,_,_}=Error,Rest} ->
{done,Error,Rest}
end.
%%
%% Now to the actual scanning and collecting tokens.
%%
%% scan(Chars, Line, Column, State, Extra) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan one token.
scan(Chars, Line, Col, St, _Extra) ->
scan1(Chars, Line, Col, St).
%% scan1(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | None| ScanError.
scan1_fun(Cs, Line, Col, St, _Extra) ->
scan1(Cs, Line, Col, St).
%% Strings
scan1([$"|Cs], Line, Col, St) ->
%% We handle both normal strings and triple quote strings here.
scan_string(Cs, Line, Col, St);
%% Newlines and white space.
scan1([$\n|Cs], Line, _Col, St) ->
scan1(Cs, Line+1, 1, St);
scan1([C|Cs], Line, Col, St) when ?WHITE_SPACE(C) ->
scan1(Cs, Line, Col+1, St);
%% Comments.
scan1([$;|Cs], Line, Col, St) ->
scan_line_comment(Cs, Line, Col+1, St);
%% These start symbol chars must be handled specially.
scan1([$||Cs], Line, Col, St) ->
scan_qsymbol(Cs, Line, Col, St);
scan1([$#|Cs], Line, Col, St) ->
scan_hash(Cs, Line, Col+1, St);
%% Non-existent test tokens which can be useful for testing the parser.
%% scan1([$^|Cs], Line, Col, St) ->
%% {ok,{'^',anno(Line)},Cs,Line,Col+1,St};
%% scan1([$&|Cs], Line, Col, St) ->
%% {ok,{'&',anno(Line)},Cs,Line,Col+1,St};
%% We do the one character separators which are also start symbol chars.
scan1([$'|Cs], Line, Col, St) ->
{ok,{'\'',anno(Line)},Cs,Line,Col+1,St};
scan1([$`|Cs], Line, Col, St) ->
{ok,{'\`',anno(Line)},Cs,Line,Col+1,St};
scan1([$.|Cs], Line, Col, St) ->
{ok,{'.',anno(Line)},Cs,Line,Col+1,St};
scan1([$,,$@|Cs], Line, Col, St) ->
{ok,{',@',anno(Line)},Cs,Line,Col+2,St};
scan1([$,|Cs], Line, Col, St) ->
{ok,{',',anno(Line)},Cs,Line,Col+1,St};
%% Now it gets a little tricky, everything starts with a start symbol
%% char and is a symbol or single char or a NUMBER!
scan1([C|Cs], Line, Col, St) ->
case start_symbol_char(C) of
true ->
scan_symbol([C|Cs], Line, Col, St);
false ->
Tok = {list_to_atom([C]),anno(Line)},
{ok,Tok,Cs,Line,Col,St}
end;
scan1([], Line, Col, St) ->
%% Need more here.
{more,{[],Line,Col,St,[],fun scan1_fun/5}};
scan1(eof, Line, Col, St) ->
%% We didn't get anything and nothing left.
{none,eof,Line,Col,St}.
%% scan_line_comment(Chars, Line, Column, State) ->
%% {ok,Tokens,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Skip to the end of the line.
scan_line_comment_fun(Cs, Line, Col, St, _Extra) ->
scan_line_comment(Cs, Line, Col, St).
scan_line_comment([$\n|Cs], Line, _Col, St) ->
{none,Cs,Line+1,0,St};
scan_line_comment([_C|Cs], Line, Col, St) ->
scan_line_comment(Cs, Line, Col+1, St);
scan_line_comment([]=Cs, Line, Col, St) ->
{more,{Cs,Line,Col,St,[],fun scan_line_comment_fun/5}};
scan_line_comment(eof=Cs, Line, Col, St) ->
{none,Cs,Line,Col,St}.
%% scan_block_comment(Chars, Line, Column, State) ->
%% {ok,Tokens,Chars,Line,Column} | {more,Continuation} | | ScanError.
%% Skip to the end of the block comment. The first # has already been
%% scanned.
scan_block_comment_fun(Cs, Line, Col, St, _Extra) ->
scan_block_comment(Cs, Line, Col, St).
scan_block_comment("|#" ++ Cs, Line, Col, St) ->
{none,Cs,Line,Col,St};
scan_block_comment("#|" ++ Cs, Line, Col, _St) ->
scan_error({illegal,"nested block comment"}, Line, Col, Line, Col, Cs);
scan_block_comment([C]=Cs, Line, Col, St) when C =:= $|; C =:= $# ->
%% Need next character to work these out.
{more,{Cs,Line,Col,St,[],fun scan_block_comment_fun/5}};
scan_block_comment([$\n|Cs], Line, _Col, St) ->
scan_block_comment(Cs, Line+1, 0, St);
scan_block_comment([_C|Cs], Line, Col, St) ->
scan_block_comment(Cs, Line, Col+1, St);
%% [], we need more to know.
scan_block_comment([]=Cs, Line, Col, St) ->
{more,{Cs,Line,Col,St,[],fun scan_block_comment_fun/5}};
%% Eof so we are really done!
scan_block_comment(eof=Cs, Line, Col, _St) ->
scan_error({bad_format,"block comment"}, Line, Col, Line, Col, Cs).
%% scan_hash(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan a hash character symbol. We collect digits before the command
%% character as some forms need this.
scan_hash(Cs, Line, Col, St) ->
scan_hash_digits(Cs, Line, Col, [], St).
scan_hash_digits_fun(Cs, Line, Col, St, Digits) ->
scan_hash_digits(Cs, Line, Col, Digits, St).
scan_hash_digits([C|Cs], Line, Col, Digits, St) when ?DIGIT(C) ->
scan_hash_digits(Cs, Line, Col+1, Digits ++ [C], St);
%% [], we need more to know.
scan_hash_digits([]=Cs, Line, Col, Digits, St) ->
{more,{Cs,Line,Col,St,Digits,fun scan_hash_digits_fun/5}};
scan_hash_digits(Cs, Line, Col, Digits, St) ->
%% We know there is
scan_hash1(Cs, Line, Col, Digits, St).
%% scan_hash1_fun(Cs, Line, Col, St, Digits) ->
%% scan_hash1(Cs, Line, Col, Digits, St).
%% First the tokens which only need one character.
scan_hash1([$\(|Cs], Line, Col, [], St) ->
{ok,{'#(',Line},Cs,Line,Col,St};
scan_hash1([$.|Cs], Line, Col, [], St) ->
{ok,{'#.',Line},Cs,Line,Col,St};
scan_hash1([$`|Cs], Line, Col, [], St) ->
{ok,{'#`',Line},Cs,Line,Col,St};
scan_hash1([$;|Cs], Line, Col, [], St) ->
{ok,{'#;',Line},Cs,Line,Col,St};
scan_hash1([$||Cs], Line, Col, [], St) ->
scan_block_comment(Cs, Line, Col+1, St);
scan_hash1([$"|Cs], Line, Col, [], St) ->
scan_binary_string(Cs, Line, Col+1, St);
scan_hash1([$'|Cs], Line, Col, [], St) ->
scan_fun(Cs, Line, Col+1, St);
scan_hash1([$*|Cs], Line, Col, [], St) ->
scan_bnumber(Cs, 2, Line, Col+1, St);
%% The #B binary number needs to be checked in hash2.
scan_hash1([C|Cs], Line, Col, [], St) when (C =:= $o) or (C =:= $O) ->
scan_bnumber(Cs, 8, Line, Col+1, St);
scan_hash1([C|Cs], Line, Col, [], St) when (C =:= $d) or (C =:= $D) ->
scan_bnumber(Cs, 10, Line, Col+1, St);
scan_hash1([C|Cs], Line, Col, [], St) when (C =:= $x) or (C =:= $X) ->
scan_bnumber(Cs, 16, Line, Col+1, St);
scan_hash1([C|Cs], Line, Col, Digits, St) when (C =:= $r) or (C =:= $R) ->
Base = list_to_integer([$0|Digits]),
if Base >=2, Base =< 36 ->
scan_bnumber(Cs, Base, Line, Col+1, St);
true ->
scan_error({bad_format,"#r"}, Line, Col, Line, Col+1, Cs)
end;
scan_hash1(Cs, Line, Col, Digits, St) ->
%% Pass the buck!
scan_hash2(Cs, Line, Col, Digits, St).
%% scan_hash1([_,_|_]=Cs, Line, Col, Digits, St) ->
%% %% We have two characters and can now go on.
%% scan_hash2(Cs, Line, Col, Digits, St);
%% scan_hash1([_]=Cs, Line, Col, Digits, St) ->
%% %% We now need more characters to go on.
%% {more,{Cs,Line,Col,St,Digits,fun scan_hash2_fun/5}};
%% scan_hash1(eof=Cs, Line, Col, _Digits, _St) ->
%% scan_error({illegal_chars,"#"}, Line, Col, Line, Col+1, Cs).
scan_hash2_fun(Cs, Line, Col, St, Digits) ->
scan_hash2(Cs, Line, Col, Digits, St).
%% Check that we don't just have one character.
scan_hash2([_]=Cs, Line, Col, Digits, St) ->
{more,{Cs,Line,Col,St,Digits,fun scan_hash2_fun/5}};
%% The tokens where we need two characters.
scan_hash2([$\\,C|Cs], Line, Col, [], St) ->
{ok,{number,Line,C},Cs,Line,Col+2,St};
scan_hash2([$,,$@|Cs], Line, Col, [], St) -> %Get this before #,
{ok,{'#,@',Line},Cs,Line,Col+2,St};
scan_hash2([$,|Cs], Line, Col, [], St) ->
{ok,{'#,',Line},Cs,Line,Col+1,St};
scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $m) or (C =:= $M) ->
{ok,{'#M(',Line},Cs,Line,Col+2,St};
scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $s) or (C =:= $S) ->
{ok,{'#S(',Line},Cs,Line,Col+2,St};
%% scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $f) or (C =:= $F) ->
%% {ok,{'#F(',Line},Cs,Line,Col+2,St};
%% Scan binary tokens, these must come before the based number.
scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $b) or (C =:= $B) ->
{ok,{'#B(',Line},Cs,Line,Col,St};
%% Scan based numbers, these must come after binary tokens.
scan_hash2([C,C1|Cs], Line, Col, [], St) when
(C =:= $b) or (C =:= $B) andalso C1 =/= $( ->
%% Must not clash with #b(
scan_bnumber([C1|Cs], 2, Line, Col+1, St);
%% Eof, other chars or illegal digits, so we are really done!
scan_hash2([C|Cs], Line, Col, _Digits, _St) ->
scan_error({illegal_token,[$#,C]}, Line, Col, Line, Col+1, Cs);
scan_hash2(eof, Line, Col, _Digits, _St) ->
scan_error({illegal_token,"#"}, Line, Col, Line, Col+1, eof).
%% scan_fun(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan a fun symbol.
scan_fun(Cs, Line, Col, St) ->
scan_fun1(Cs, Line, Col, [], St).
scan_fun1_fun(Cs, Line, Col, St, Symcs) ->
scan_fun1(Cs, Line, Col, Symcs, St).
scan_fun1([C|Cs], Line, Col, Symcs, St) ->
case symbol_char(C) of
true ->
scan_fun1(Cs, Line, Col+1, Symcs ++ [C], St);
false ->
scan_fun_ret(Symcs, [C|Cs], Line, Col, St)
end;
scan_fun1([]=Cs, Line, Col, Symcs, St) ->
{more,{Cs,Line,Col,St,Symcs,fun scan_fun1_fun/5}};
scan_fun1(eof, Line, Col, Symcs, St) ->
scan_fun_ret(Symcs, eof, Line, Col, St).
scan_fun_ret(Symcs, Cs, Line, Col, St) ->
case lists:splitwith(fun (C) -> C =/= $/ end, Symcs) of
{_Pre,[]} ->
scan_error({bad_format,[$#,$'|Symcs]}, Line, Col, Line, Col, Cs);
{Pre,[$/|Ds]=After} ->
Field = Pre ++ After,
%% io:format("sfr ~p\n", [{Symcs,Field}]),
case lists:any(fun (C) -> not ?DIGIT(C) end, Ds) of
true ->
scan_error({bad_format,[$#,$'|Symcs]},
Line, Col, Line, Col, Cs);
false ->
Token = {'#\'',Line,Field},
{ok,Token,Cs,Line,Col,St}
end
end.
%% scan_bnumber(Chars, Base, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan a based number. We stay compatible and scan all symbol
%% characters then check then build and check.
scan_bnumber(Cs, Base, Line, Col, St) ->
scan_bnumber_sign(Cs, Line, Col, Base, St).
scan_bnumber_sign_fun(Cs, Line, Col, St, Base) ->
scan_bnumber_sign(Cs, Line, Col, Base, St).
scan_bnumber_sign([$+|Cs], Line, Col, Base, St) ->
scan_bnumber_digits(Cs, Line, Col, [], Base, +1, St);
scan_bnumber_sign([$-|Cs], Line, Col, Base, St) ->
scan_bnumber_digits(Cs, Line, Col, [], Base, -1, St);
scan_bnumber_sign([], Line, Col, Base, St) ->
{more,{[],Line,Col,St,Base,fun scan_bnumber_sign_fun/5}};
scan_bnumber_sign(Cs, Line, Col, Base, St) ->
scan_bnumber_digits(Cs, Line, Col, [], Base, +1, St).
scan_bnumber_digits_fun(Cs, Line, Col, St, {Digits,Base,Sign}) ->
scan_bnumber_digits(Cs, Line, Col, Digits, Base, Sign, St).
scan_bnumber_digits([C|Cs], Line, Col, Digits, Base, Sign, St) ->
case symbol_char(C) of
true ->
scan_bnumber_digits(Cs, Line, Col+1, Digits++[C], Base, Sign, St);
false ->
scan_bnumber_check([C|Cs], Line, Col, Digits, Base, Sign, St)
end;
scan_bnumber_digits([]=Cs, Line, Col, Digits, Base, Sign, St) ->
{more,{Cs,Line,Col,St,{Digits,Base,Sign},fun scan_bnumber_digits_fun/5}};
scan_bnumber_digits(eof=Cs, Line, Col, Digits, Base, Sign, St) ->
scan_bnumber_check(Cs, Line, Col, Digits, Base, Sign, St).
scan_bnumber_check(Cs, Line, Col, [], _Base, _Sign, _St) ->
scan_error({bad_format,"based number"}, Line, Col, Line, Col, Cs);
scan_bnumber_check(Cs, Line, Col, Digits, Base, Sign, St) ->
case base_collect_chars(Digits, Base, 0) of
{yes,Number} ->
Token = {number,Line,Sign*Number},
{ok,Token,Cs,Line,Col,St};
no ->
scan_error({illegal_chars,"based number"}, Line, Col, Line, Col, Cs)
end.
%% base_collect_chars(Chars, Base, NumberSoFar) -> {yes,Number} | no.
%% base_collect_char(Char, Base, NumberSoFar) -> {yes,Number} | no.
%% Check if Char is number of valid base and if so build new number
%% with SoFar.
base_collect_chars([C|Cs], Base, SoFar) ->
if C >= $0, C =< $9, C < Base + $0 ->
Next = SoFar * Base + (C - $0),
base_collect_chars(Cs, Base, Next);
C >= $a, C =< $z, C < Base + $a - 10 ->
Next = SoFar * Base + (C - $a + 10),
base_collect_chars(Cs, Base, Next);
C >= $A, C =< $Z, C < Base + $A - 10 ->
Next = SoFar * Base + (C - $A + 10),
base_collect_chars(Cs, Base, Next);
true -> no
end;
base_collect_chars([], _Base, SoFar) ->
{yes,SoFar}.
%% scan_symbol(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan unquoted "symbols". This is a little tricky as numbers will
%% also match here. We try to solve this by scanning in all the
%% matching characters and then first checking whether it is a
%% number, otherwise it is a symbol.
scan_symbol(Cs, Line, Col, St) ->
scan_symbol1(Cs, Line, Col, [], St).
scan_symbol1_fun(Cs, Line, Col, St, Symcs) ->
scan_symbol1(Cs, Line, Col, Symcs, St).
scan_symbol1([C|Cs], Line, Col, Symcs, St) ->
case symbol_char(C) of
true ->
scan_symbol1(Cs, Line, Col+1, Symcs ++ [C], St);
false ->
%% Check whether we have an integer, a float or a symbol.
Token = make_symbol_token(Symcs, Line),
{ok,Token,[C|Cs],Line,Col+1,St}
end;
scan_symbol1([]=Cs, Line, Col, Symcs, St) ->
{more,{Cs,Line,Col,St,Symcs,fun scan_symbol1_fun/5}};
scan_symbol1(eof=Cs, Line, Col, Symcs, St) ->
Token = make_symbol_token(Symcs, Line),
{ok,Token,Cs,Line,Col,St}.
make_symbol_token(Chars, Line) ->
try
Integer = list_to_integer(Chars),
{number,Line,Integer}
catch
_:_ ->
try
Float = list_to_float(Chars),
{number,Line,Float}
catch
_:_ ->
Symbol = list_to_atom(Chars),
{symbol,Line,Symbol}
end
end.
%% start_symbol_char(Char) -> true | false.
%% symbol_char(Char) -> true | false.
%% Define start symbol chars and symbol chars.
%% start_symbol_char($^) -> false; %These 2 are for test cases
%% start_symbol_char($&) -> false;
start_symbol_char($#) -> false;
start_symbol_char($`) -> false;
start_symbol_char($') -> false; %'
start_symbol_char($,) -> false;
start_symbol_char($|) -> false; %Symbol quote character
start_symbol_char(C) -> symbol_char(C).
symbol_char($() -> false;
symbol_char($)) -> false;
symbol_char($[) -> false;
symbol_char($]) -> false;
symbol_char(${) -> false;
symbol_char($}) -> false;
symbol_char($") -> false;
symbol_char($;) -> false;
symbol_char(C) -> ((C > $\s) and (C =< $~)) orelse (C > $\240).
%% scan_qsymbol(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan quoted symbols.
scan_qsymbol(Cs, Line, Col, St) ->
scan_qsymbol1(Cs, Line, Col, Line, Col, [], St).
scan_qsymbol1_fun(Cs, Line, Col, St, {Sline,Scol,Symcs}) ->
scan_qsymbol1(Cs, Line, Col, Sline, Scol, Symcs, St).
scan_qsymbol1([$\\,C|Cs], Line, Col, Sline, Scol, Symcs, St) ->
%% Take the quoted character as is.
scan_qsymbol1(Cs, Line, Col+2, Sline, Scol, Symcs ++ [C], St);
scan_qsymbol1([$||Cs], Line, Col, Sline, _Scol, Symcs, St) ->
Qsymb = list_to_atom(Symcs),
{ok,{symbol,Sline,Qsymb},Cs,Line,Col+1,St};
scan_qsymbol1([$\n|Cs], Line, _Col, Sline, Scol, Symcs, St) ->
scan_qsymbol1(Cs, Line+1, 0, Sline, Scol, Symcs ++ [$\n], St);
scan_qsymbol1([C|Cs], Line, Col, Sline, Scol, Symcs, St) when C =/= $\\ ->
scan_qsymbol1(Cs, Line, Col+1, Sline, Scol, Symcs ++ [C], St);
scan_qsymbol1(Cs, Line, Col, Sline, Scol, Symcs, St) when Cs =/= eof ->
{more,{Cs,Line,Col,St,{Sline,Scol,Symcs},fun scan_qsymbol1_fun/5}};
scan_qsymbol1(eof, Line, Col, Sline, Scol, Symcs, _St) ->
scan_error({illegal_chars,[$| | Symcs]}, Line, Col, Sline, Scol, eof).
%% scan_string(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% scan_binary_string(Chars, Line, Column, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Scan strings. We have not incremented Col.
scan_string(Cs, Line, Col, St) ->
scan_string(Cs, Line, Col, string, St).
scan_binary_string(Cs, Line, Col, St) ->
scan_string(Cs, Line, Col, binary, St).
%% scan_string(Chars, Line, Column, Type, State) ->
%% {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% Find which string type to use. Note the first " has already been
%% scanned. Here we work out if is a normal string or a triple quote
%% string and then pass the buck.
scan_string(Cs, Line, Col, Type, St) ->
scan_string1(Cs, Line, Col, Type, St).
scan_string1_fun(Cs, Line, Col, St, Type) ->
scan_string1(Cs, Line, Col, Type, St).
scan_string1([C|_]=Cs, Line, Col, Type, St) when C =/= $" ->
%% Scan a normal single quote string.
scan_sq_string(Cs, Line, Col, Type, St);
scan_string1([$",$"|Cs], Line, Col, Type, St) ->
%% This is a triple quoted string.
scan_tq_string(Cs, Line, Col+3, Line, Col, Type, St);
scan_string1([$",C|_]=Cs, Line, Col, Type, St) when C =/= $" ->
%% This is a empty normal string which we pass on.
scan_sq_string(Cs, Line, Col, Type, St);
scan_string1([$"]=Cs, Line, Col, Type, St) ->
%% We don't know yet.
{more,{Cs,Line,Col,St,Type,fun scan_string1_fun/5}};
scan_string1([], Line, Col, Type, St) ->
%% We need more to decide what to do.
{more,{[],Line,Col,St,Type,fun scan_string1_fun/5}};
scan_string1(Cs, Line, Col, _Type, _St) ->
scan_error({illegal_chars,[$"]}, Line, Col, Line, Col, Cs).
%% This is a normal string which we pass on.
%% scan_sq_string(Cs, Line, Col, Type, St).
%% scan_sq_string(Chars, Line, Column, Type, State) ->
%% {ok,Token,Chars,Line,Column} | ScanError | ScanMore.
%% Single quote "normal" strings. Note that the first " has already
%% been scanned.
scan_sq_string(Chars, Line, Col, Type, St) ->
scan_sq_string1(Chars, Line, Col+1, Line, Col, [], Type, St).
scan_sq_string1_fun(Cs, Line, Col, St, {Sline,Scol,Symcs,Type}) ->
scan_sq_string1(Cs, Line, Col, Sline, Scol, Symcs, Type, St).
scan_sq_string1([$\\,C|Cs], Line, Col, Sline, Scol, Symcs, Type, St) ->
scan_sq_string1(Cs, Line, Col+2, Sline, Scol,
Symcs ++ [escape_char(C)], Type, St);
scan_sq_string1([$\\]=Cs, Line, Col, Sline, Scol, Symcs, Type, St) ->
{more,{Cs,Line,Col,St,{Sline,Scol,Symcs,Type},fun scan_sq_string1_fun/5}};
scan_sq_string1([$"|Cs], Line, Col, Sline, _Scol, Symcs, Type, St) ->
%% We have the string.
Token = string_token(Symcs, Sline, Type),
{ok,Token,Cs,Line,Col+1,St};
scan_sq_string1([$\n|Cs], Line, _Col, Sline, Scol, Symcs, Type, St) ->
scan_sq_string1(Cs, Line+1, 1, Sline, Scol, Symcs ++ [$\n], Type, St);
scan_sq_string1([C|Cs], Line, Col, Sline, Scol, Symcs, Type, St) ->
scan_sq_string1(Cs, Line, Col+1, Sline, Scol, Symcs ++ [C], Type, St);
scan_sq_string1([]=Cs, Line, Col, Sline, Scol, Symcs, Type, St) ->
{more,{Cs,Line,Col,St,{Sline,Scol,Symcs,Type},fun scan_sq_string1_fun/5}};
scan_sq_string1(eof=Cs, Line, Col, Sline, Scol, Symcs, _Type, _St) ->
scan_error({illegal_chars,[$" | Symcs]}, Line, Col, Sline, Scol, Cs).
escape_char($b) -> $\b; %\b = BS
escape_char($t) -> $\t; %\t = TAB
escape_char($n) -> $\n; %\n = LF
escape_char($v) -> $\v; %\v = VT
escape_char($f) -> $\f; %\f = FF
escape_char($r) -> $\r; %\r = CR
escape_char($e) -> $\e; %\e = ESC
escape_char($s) -> $\s; %\s = SPC
escape_char($d) -> $\d; %\d = DEL
escape_char(C) -> C. %\Other = Other
string_token(String, StartLine, Type) ->
case Type of
string -> {string,StartLine,String};
binary ->
Binary = unicode:characters_to_binary(String, utf8, utf8),
{binary,StartLine,Binary}
end.
%% scan_tq_string(Chars, Line, Column, StartLine, StartCol, Type, State) ->
%% {ok,Token,Char,Line,Column} | {more,Continuation} | ScanError.
%% Scan triple quoted strings. Note that the first """ has already
%% been scanned.
scan_tq_string(Chars, Line, Col, StartLine, StartCol, Type, State) ->
scan_tq_string_1(Chars, Line, Col, StartLine, StartCol, Type, State).
%% scan_tq_string_1(Chars, Line, Column, StartLine, StartCol, Type, State) ->
%% {ok,Token,Char,Line,Column} | {more,Continuation} | ScanError.
%% Scan the first line and check the format.
scan_tq_string_1_fun(Cs, Line, Col, St, {Sline,Scol,Type}) ->
scan_tq_string_1(Cs, Line, Col, Sline, Scol, Type, St).
scan_tq_string_1([$\s|Cs], Line, Col, Sline, Scol, Type, St) ->
scan_tq_string_1(Cs, Line, Col, Sline, Scol, Type, St);
scan_tq_string_1([$\n|Cs], Line, _Col, Sline, Scol, Type, St) ->
scan_tq_string_lines(Cs, Line+1, 1, Sline, Scol, [], [], Type, St);
scan_tq_string_1([]=Cs, Line, Col, Sline, Scol, Type, St) ->
{more,{Cs,Line,Col,St,{Sline,Scol,Type},fun scan_tq_string_1_fun/5}};
scan_tq_string_1(Cs, Line, Col, Sline, Scol, _Type, _St) ->
%% An error for illegal character or eof.
scan_error(bad_tq_string, Line, Col, Sline, Scol, Cs).
%% scan_tq_string_lines(Chars, Line, Col, StartLine, StartCol, LineChars, Lines,
%% Type, State) ->
%% {ok,Token,Char,Line,Column,State} | {more,Continuation} | ScanError.
%% Scan and collect the following lines up to a valid end """.
scan_tq_string_lines_fun(Chars, Line, Col, St, {Sline,Scol,Lcs,Lines,Type}) ->
scan_tq_string_lines(Chars, Line, Col, Sline, Scol, Lcs, Lines, Type, St).
scan_tq_string_lines([$\n|Cs], Line, _Col, Sline, Scol, Lcs, Lines, Type, St) ->
scan_tq_string_lines(Cs, Line+1, 0, Sline, Scol,
[], Lines ++ [Lcs], Type, St);
scan_tq_string_lines([$"|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
scan_tq_string_tq(Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St);
scan_tq_string_lines([C|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
scan_tq_string_lines(Cs, Line, Col, Sline, Scol,
Lcs ++ [C], Lines, Type, St);
scan_tq_string_lines([]=Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
{more,{Cs,Line,Col,St,{Sline,Scol,Lcs,Lines,Type},
fun scan_tq_string_lines_fun/5}};
scan_tq_string_lines(eof=Cs, Line, Col, Sline, Scol,
_Lcs, _Lines, _Type, _St) ->
scan_error(bad_tq_string, Line, Col, Sline, Scol, Cs).
%% scan_tq_string_tq(Chars, Line, Col, StartLine, StartCol, LineChars, Lines,
%% Type, State) ->
%% {ok,Token,Char,Line,Column,State} | {more,Continuation} | ScanError.
%% Check if we have valid end of the """ or whether we must go
%% on. Note that the first " has already been scanned.
scan_tq_string_tq_fun(Cs, Line, Col, St, {Sline,Scol,Lcs,Lines,Type}) ->
scan_tq_string_tq(Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St).
scan_tq_string_tq([$",$"|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
%% This is a triple quote, check if this is a valid end line.
case blank_line(Lcs) of
true ->
scan_tq_string_end(Cs, Line, Col+3, Sline, Scol,
Lcs, Lines, Type, St);
false ->
scan_tq_string_lines(Cs, Line, Col+3, Sline, Scol,
Lcs ++ [$",$",$"], Lines, Type, St)
end;
scan_tq_string_tq([$",C|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) when
C =/= $" ->
%% This is not a triple quote here, it is a normal line. So we
%% pass the buck, but don't forget the ".
scan_tq_string_lines([C|Cs], Line, Col, Sline, Scol,
Lcs ++ [$"], Lines, Type, St);
scan_tq_string_tq([$"]=Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
{more,{Cs,Line,Col,St,{Sline,Scol,Lcs,Lines,Type},
fun scan_tq_string_tq_fun/5}};
scan_tq_string_tq([]=Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
{more,{Cs,Line,Col,St,{Sline,Scol,Lcs,Lines,Type},
fun scan_tq_string_tq_fun/5}};
scan_tq_string_tq(eof, Line, Col, Sline, Scol, _Lcs, _Lines, _Type, _St) ->
scan_error(bad_tq_string, Line, Col, Sline, Scol, eof);
scan_tq_string_tq(Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
%% This is not a triple quote here, it is a normal line. So we
%% pass the buck, but don't forget the ".
scan_tq_string_lines(Cs, Line, Col, Sline, Scol,
Lcs ++ [$"], Lines, Type, St).
%% scan_tq_string_end(Chars, Line, Col, StartLine, StartCol, Prefix, Lines,
%% Type, State) ->
%% {ok,Token,Char,Line,Column,State} | ScanError.
%% Check if we have a valid end line.
scan_tq_string_end(Cs, Line, Col, Sline, _Scol, _Prefix, [], Type, St) ->
%% No lines so it just is empty.
Token = string_token([], Sline, Type),
{ok,Token,Cs,Line,Col,St};
scan_tq_string_end(Cs, Line, Col, Sline, Scol, Prefix, Lines, Type, St) ->
%% Here we will check what we have.
case collect_tqstring_lines(Lines, Prefix, []) of
{yes,CheckedLines} ->
%% Skip the leading newline added by the fold.
[_Lc|String] = lists:foldr(fun (Lcs, Scs) -> [$\n|Lcs] ++ Scs end,
[], CheckedLines),
Token = string_token(String, Sline, Type),
{ok,Token,Cs,Line,Col,St};
no ->
%% io:format("ls ~p\n", [{ste,Cs,Sline,Line,Lines,Prefix}]),
scan_error(bad_tq_string, Line, Col, Sline, Scol, Cs)
end.
blank_line(Cs) ->
lists:all(fun (C) -> C =:= $\s end, Cs).
collect_tqstring_lines([Lcs|Lines], Prefix, Plines) ->
case check_tqstring_prefix(Prefix, Lcs) of
{yes,Lcs1} ->
collect_tqstring_lines(Lines, Prefix, Plines ++ [Lcs1]);
no -> no
end;
collect_tqstring_lines([], _Prefix, Plines) ->
{yes,Plines}.
check_tqstring_prefix([P|Ps], [C|Lcs]) ->
if P =:= C ->
check_tqstring_prefix(Ps, Lcs);
true -> no
end;
check_tqstring_prefix([], Lcs) ->
{yes,Lcs}.
%% scan_error(Error, StartLine, StartCol, EndLine, EndCol, RestChars) ->
%% {Error,Rest}.
scan_error(Error, Line, _Col, EndLine, _EndCol, Rest) ->
Loc = Line, %location(Line, Col)
EndLoc = EndLine, %location(EndLine, EndCol)
scan_error(Error, Loc, EndLoc, Rest).
scan_error(Error, ErrorLoc, EndLoc, Rest) ->
{{error,{ErrorLoc,?MODULE,Error},EndLoc},Rest}.
%% location(Line, no_col) ->
%% Line;
%% location(Line, Col) when is_integer(Col) ->
%% {Line,Col}.
%% -compile({inline,[anno/1,incr_column/2,new_column/2,int_column/1]}).
anno(Location) ->
erl_anno:new(Location).
%% token_test(Chars) -> {done,Done,Rest}.
%% token_test(Continuation, Chars) -> {done,Done,Rest}.
%% tokens_test(Chars) -> {done,Done,Rest}.
%% tokens_test(Continuation, Chars) -> {done,Done,Rest}.
%% Carefully test the token(s) by steping over them one character at
%% a time. This guarantees that they can handle the input string
%% safely and correctly.
token_test(Cs) ->
token_test([], Cs).
token_test(Cont0, [C|Cs]) ->
case lfe_scan:token(Cont0, [C]) of
{more,Cont1} ->
token_test(Cont1, Cs);
{done,_Done,_Rest}=Done ->
Done
end;
token_test(Cont, []) ->
io:format("tt ~p\n", [Cont]),
lfe_scan:token(Cont, eof).
tokens_test(Cs) ->
tokens_test([], Cs).
tokens_test(Cont0, [C|Cs]) ->
case lfe_scan:tokens(Cont0, [C]) of
{more,Cont1} ->
tokens_test(Cont1, Cs);
{done,_Done,_Rest}=Done ->
Done
end;
tokens_test(Cont, []) ->
io:format("tst ~p\n", [Cont]),
lfe_scan:tokens(Cont, eof).