leex(3)

πŸ“¦
version 29.0.6 Β· 2 versions
, in nixos 26.05, nixpkgs unstable, ubuntu stonking

✨ NAME ✨

leex β€” Lexical analyzer generator for Erlang. That's it, that's the name entry. And yeah, it's leex(3), filed under Erlang Module Definition, and it's genuinely iconic. πŸ’…

πŸ’– DESCRIPTION πŸ’–

So basically, leex is a regular expression based lexical analyzer generator for Erlang, and it's the same vibe as lex or flex β€” like, similar to them, no cap. πŸ¦‹

🌸 Default Leex Options 🌸

Okay bestie, listen up β˜• β€” the (host operating system) environment variable ERL_COMPILER_OPTIONS can be used to give default Leex options. Its value must be a valid Erlang term. If the value is a list, it is used as is. If it is not a list, it is put into a list.

The list is appended to any options given to file/2.

And you can pull that list back out with compile:env_compiler_options/0. Slay.

πŸŽ€ Input File Format πŸŽ€

Erlang style comments starting with a % are allowed in scanner files, which is lowkey great. A definition file has the following format:

<Header>

Definitions.

<Macro Definitions>

Rules.

<Token Rules>

Erlang code.

<Erlang code>

Now the important part: the Definitions., Rules., and Erlang code headings are mandatory and must start at the beginning of a source line. The <Header>, <Macro Definitions>, and <Erlang code> sections are allowed to be empty, but there must be at least one rule. No rules? Not the vibe, bestie. πŸ’€

Macro definitions have the following format:

NAME = VALUE

and there must be spaces around =. Do not skip the spaces. 😭 Macros can be used in the regular expressions of rules by writing {NAME}.

NOTE: When macros are expanded in expressions, the macro calls are replaced by the macro value without any form of quoting or enclosing in parentheses. Periodt.

Rules have the following format:

<Regexp> : <Erlang code>.

The <Regexp> must occur at the start of a line and not include any blanks; use \t and \s to include TAB and SPACE characters in the regular expression. If <Regexp> matches then the corresponding <Erlang code> is evaluated to generate a token. And with that Erlang code, these predefined variables are right there for you:

  • TokenChars β€” A list of the characters in the matched token.
  • TokenLen β€” The number of characters in the matched token.
  • TokenLine β€” The line number where the token occurred.
  • TokenCol β€” The column number where the token occurred (column of the first character included in the token).
  • TokenLoc β€” Token location. Expands to {TokenLine,TokenCol} (even when error_location is set to line).

The code must return one of these:

  • {token,Token} β€” Return Token to the caller.
  • {end_token,Token} β€” Return Token and is last token in a tokens call.
  • skip_token β€” Skip this token completely.
  • {error,ErrString} β€” An error in the token, ErrString is a string describing the error.

It is also possible to push back characters into the input characters with the following returns:

  • {token,Token,PushBackList}
  • {end_token,Token,PushBackList}
  • {skip_token,PushBackList}

These have the same meanings as the normal returns but the characters in PushBackList will be prepended to the input characters and scanned for the next token. Just know that pushing back a newline will mean the line numbering will no longer be correct. 😬

NOTE: Pushing back characters gives you unexpected possibilities to cause the scanner to loop! Not the cute kind of loop. πŸ’€

The following example would match a simple Erlang integer or float and return a token which could be sent to the Erlang parser:

D = [0-9]

{D}+ :
  {token,{integer,TokenLine,list_to_integer(TokenChars)}}.

{D}+\.{D}+((E|e)(\+|\-)?{D}+)? :
  {token,{float,TokenLine,list_to_float(TokenChars)}}.

And the Erlang code in the Erlang code. section is written into the output file directly after the module declaration and predefined exports declaration, making it possible to add extra exports, define imports, and other attributes, which are visible in the whole file. Chef's kiss. πŸ‘‘

πŸ¦‹ Regular Expressions πŸ¦‹

The regular expressions allowed here is a subset of the set found in egrep and in the AWK programming language, as defined in the book The AWK Programming Language by A. V. Aho, B. W. Kernighan, and P. J. Weinberger. They are composed of the following characters:

  • c β€” Matches the non-metacharacter c.
  • \c β€” Matches the escape sequence or literal character c.
  • . β€” Matches any character.
  • ^ β€” Matches the beginning of a string.
  • $ β€” Matches the end of a string.
  • [abc...] β€” Character class, which matches any of the characters abc.... Character ranges are specified by a pair of characters separated by a -.
  • [^abc...] β€” Negated character class, which matches any character except abc....
  • r1 | r2 β€” Alternation. It matches either r1 or r2.
  • r1r2 β€” Concatenation. It matches r1 and then r2.
  • r+ β€” Matches one or more rs.
  • r* β€” Matches zero or more rs.
  • r? β€” Matches zero or one rs.
  • (r) β€” Grouping. It matches r.

The escape sequences allowed are the same as for Erlang strings:

  • \b β€” Backspace.
  • \f β€” Form feed.
  • \n β€” Newline (line feed).
  • \r β€” Carriage return.
  • \t β€” Tab.
  • \e β€” Escape.
  • \v β€” Vertical tab.
  • \s β€” Space.
  • \d β€” Delete.
  • \ddd β€” The octal value ddd.
  • \xhh β€” The hexadecimal value hh.
  • \x{h...} β€” The hexadecimal value h....
  • \c β€” Any other character literally, for example \\ for backslash, \" for ".

The following examples define simplified versions of a few Erlang data types:

Atoms a-z*

Variables A-Z_*

Floats (\+|-)?[0-9]+\.[0-9]+((E|e)(\+|-)?[0-9]+)?
NOTE: Anchoring a regular expression with ^ and $ is not implemented in the current version of leex and generates a parse error. So like… don't do it. 🫠

πŸ’œ DATA TYPES πŸ’œ

error_info() ::
          {erl_anno:line() | none, module(), ErrorDescriptor :: term()}.

The standard error_info/0 structure that is returned from all I/O modules. ErrorDescriptor is formattable by format_error/1.

error_ret() ::
          error | {error, Errors :: errors(), Warnings :: warnings()}.
errors() :: [{file:filename(), [error_info()]}].
leex_ret() :: ok_ret() | error_ret().
ok_ret() ::
          {ok, Scannerfile :: file:filename()} |
          {ok, Scannerfile :: file:filename(), warnings()}.
warnings() :: [{file:filename(), [error_info()]}].

πŸ’– FUNCTIONS πŸ’–

okay bestie, this is the part where the actual doing stuff happens. buckle up πŸŽ€


🌸 file(FileName, Options) -> leex_ret()

here's the whole signature, character for character, no cap:

file(FileName, Options) -> leex_ret()
              when
                  FileName :: file:filename(),
                  Options :: Option | [Option],
                  Option ::
                      {dfa_graph, boolean()} |
                      {includefile, Includefile :: file:filename()} |
                      {report_errors, boolean()} |
                      {report_warnings, boolean()} |
                      {report, boolean()} |
                      {return_errors, boolean()} |
                      {return_warnings, boolean()} |
                      {return, boolean()} |
                      {scannerfile, Scannerfile :: file:filename()} |
                      {verbose, boolean()} |
                      {warnings_as_errors, boolean()} |
                      {deterministic, boolean()} |
                      {error_location, line | column} |
                      {tab_size, pos_integer()} |
                      dfa_graph | report_errors | report_warnings |
                      report | return_errors | return_warnings |
                      return | verbose | warnings_as_errors.

This bad boy generates a lexical analyzer from whatever definition is chillin' in your input file. iconic behavior fr fr ✨

The input file is supposed to have the extension .xrl. If you don't give it one, leex will just add it for you, no biggie. And the resulting module? That's your Xrl filename minus the .xrl extension. slay.

The current options are β€” and yes bestie, we're listing all of them 🫢:

  • dfa_graph β€” spits out a .dot file describing the DFA in a format you can peek at with Graphviz, www.graphviz.com. lowkey fun πŸ’…
  • {includefile,Includefile} β€” lets you use a specific or customised prologue file instead of the default lib/parsetools/include/leexinc.hrl (which is what normally gets included otherwise).
  • {report_errors, boolean()} β€” makes errors print as they happen. Default is true.
  • {report_warnings, boolean()} β€” makes warnings print as they happen. Default is true.
  • {report, boolean()} β€” little short form that covers both report_errors and report_warnings at once. two birds, one flag πŸ¦‹
  • {return_errors, boolean()} β€” flip this on and you get {error, Errors, Warnings} returned whenever there are errors. Default is false.
  • {return_warnings, boolean()} β€” flip this on and an extra field holding Warnings gets added to the tuple you get back on success. Default is false.
  • {return, boolean()} β€” another short form, covering both return_errors and return_warnings.
  • {scannerfile, Scannerfile} β€” Scannerfile is the name of the file that's gonna hold the Erlang scanner code that gets generated. The default ("") is to tack the extension .erl onto FileName once you've stripped the .xrl extension off it. obsessed with the automaticity tbh
  • {verbose, boolean()} β€” outputs info from parsing the input file and generating the internal tables. it's giving narrator voice πŸ“’
  • {warnings_as_errors, boolean()} β€” causes warnings to be treated as errors. dramatic but valid πŸ’€
  • {deterministic, boolean()} β€” causes the generated -file() attributes to only include the basename of the file path. reproducible queen πŸ‘‘
  • {error_location, line | column} β€” if you set this to column, error location becomes a {Line,Column} tuple instead of just Line. same deal for StartLoc and EndLoc in the string/2, token/3 and tokens/3 functions β€” they become {Line,Column} tuples too instead of just Line. Default is line. Big note: you can use TokenLoc for token location independently, even when error_location is set to line. ✨ Unicode characters are counted as however many columns as bytes it takes to represent them β€” so yeah, that's a thing you should know.
  • {tab_size, pos_integer()} β€” sets the width of the \t character. This only matters if error_location is set to column. Default is 8.
πŸ’‘ Any of the Boolean options can be set to true by just stating the name of the option. For example, verbose is equivalent to {verbose, true}. effortless.

Also, heads up: Leex will add the extension .hrl to the Includefile name, and the extension .erl to the Scannerfile name β€” unless the extension is already there. it checks first, it's polite 🌸


🧚 format_error(ErrorDescriptor) -> io_lib:chars()

format_error(ErrorDescriptor) -> io_lib:chars()
                       when ErrorDescriptor :: term().

Returns a descriptive string in English of an error reason ErrorDescriptor, which you'd have gotten back from leex:file/1,2 when there's an error in a regular expression. it's the "explain urself" function πŸ’¬


πŸ¦‹ string(String, StartLoc) -> StringRet

the whole shape of it:

string(String, StartLoc) -> StringRet
                 when
                     String :: string(),
                     StringRet :: {ok, Tokens, EndLoc} | ErrorInfo,
                     Tokens :: [Token],
                     Token :: term(),
                     ErrorInfo ::
                         {error, error_info(), erl_anno:location()},
                     StartLoc :: erl_anno:location(),
                     EndLoc :: erl_anno:location().

Scans String and hands you back either all the tokens in it or an error tuple. pick your fighter ✨

StartLoc and EndLoc are either erl_anno:line() or erl_anno:location(), depending on the error_location option.

⚠️ NOTE: It is an error if not all of the characters in String are consumed. literally every single one bestie, no leftovers allowed 😭

🌈 token(Cont, Chars, StartLoc)

token(Cont, Chars, StartLoc) ->
               {more, Cont1} | {done, TokenRet, RestChars}
               when
                   Cont :: [] | Cont1,
                   Cont1 :: tuple(),
                   Chars :: string() | eof,
                   RestChars :: string() | eof,
                   TokenRet ::
                       {ok, Token, EndLoc} | {eof, EndLoc} | ErrorInfo,
                   ErrorInfo ::
                       {error, error_info(), erl_anno:location()},
                   Token :: term(),
                   StartLoc :: erl_anno:location(),
                   EndLoc :: erl_anno:location().

This is a re-entrant call to try and scan a single token out of Chars. it's giving "we'll finish this later" energy πŸ™ˆ

If there are enough characters in Chars to either scan a token or detect an error, then that gets returned with {done,...}. Otherwise you get {more,Cont} back, where Cont gets used in the next call to token() along with more characters to try an scan the token. This keeps going until a token has been scanned. Cont starts out as []. πŸŽ€

It is not designed to be called directly by an application β€” it's meant to be used through the I/O system, where in an application it can typically be called like this:

io:request(InFile, {get_until,unicode,Prompt,Module,token,[Loc]})
  -> TokenRet

πŸ’œ tokens(Cont, Chars, StartLoc)

tokens(Cont, Chars, StartLoc) ->
                {more, Cont1} | {done, TokensRet, RestChars}
                when
                    Cont :: [] | Cont1,
                    Cont1 :: tuple(),
                    Chars :: string() | eof,
                    RestChars :: string() | eof,
                    TokensRet ::
                        {ok, Tokens, EndLoc} | {eof, EndLoc} |
ErrorInfo,
                    Tokens :: [Token],
                    Token :: term(),
                    ErrorInfo ::
                        {error, error_info(), erl_anno:location()},
                    StartLoc :: erl_anno:location(),
                    EndLoc :: erl_anno:location().

Another re-entrant call, but this time we're trying to scan tokens (plural, the more the merrier) from Chars. πŸ”₯

If there are enough characters in Chars to either scan tokens or detect an error, then that comes back with {done,...}. Otherwise {more,Cont} is returned, and Cont gets used in the next call to tokens() with more characters to try an scan the tokens. This continues until all tokens have been scanned. Cont is initially [].

The way this differs from token is that it keeps scanning tokens up to and including an {end_token,Token} being scanned (see next section πŸ‘€). Then it returns all the tokens. This is typically used for scanning grammars like Erlang, where there's an explicit end token, '.'. If no end token is found, then the whole file gets scanned and returned. And if an error occurs? Then all tokens up to and including the next end token get skipped. brutal but fair πŸ“

It is not designed to be called directly by an application, but used through the I/O system, where it can typically be called in an application like this:

io:request(InFile, {get_until,unicode,Prompt,Module,tokens,[Loc]})
  -> TokensRet

and that's the whole page bestie, you ate it πŸ’…βœ¨ this icon was brought to you by Ericsson AB, parsetools version 2.8, and the leex(3) manpage β€” go scan something gorgeous, ily πŸ«ΆπŸŽ€πŸ¦‹

πŸ“¦ where this page comes from