Skip to main content

A lightweight lexical analyzer

Project description

alex

alex is a lexical analyzer tool.

It reads an input string and converts it into a list of Token objects.

A Token contains:

  • a name (token type)
  • the lexeme (the exact matched text)
  • the position in the input where it was found

A lexeme is a sequence of input characters that together form a single token.

alex provides a clean, predictable, and idiomatic Python API for building lexers that are easy to understand, test, and extend.

With the right composition of input rules, it can scan any text file and extract the tokens you define.


Key Features

  • Straightforward token model
    Every token carries its type, matched text, and position.

  • Composable rule system
    Define patterns using regular expressions and map them to token classes.

  • Deterministic lexer pipeline
    Input is processed left‑to‑right with clear, explicit behavior.

  • Domain‑specific exceptions
    Errors are raised with meaningful context to simplify debugging.

  • Minimal dependencies
    Lightweight and suitable for both small utilities and large applications.


🚀 Installation

Install directly from PyPI:

pip install alex-lexer

Usage

Instantiate the class Alex and call one of the functions

  • scan(text)
  • scan_file(path)
  • generate(text)
  • generate_file(path)

The scan functions returns a list of Tokens while the generate functions creates a generator that returns one Token at a time.

When instantiating the class Alex you must provide definitions of the tokens that can appear in your input text.

The definitions to be defined are:

  • operators (A list/tuple of tuples)
  • regexps (A list/tuple of tuples)
  • keywords (A list of strings)

Example - simple.py

import alex


OPERATORS = (
    ("ADD", "+"),
    ("SUB", "-"),
)

REGEXPS = (
    ("NUM", '^["0123456789"]*'),
)

lexer = alex.Alex(
        operators=OPERATORS,
        regexps=REGEXPS)

lexer.scan('1 + 2 - 123')

print('----(Tokens found)----------')
for token in lexer.tokens:
    print(token)

will output the following:

----(Tokens found)----------
    1:  1  NUM          1           
    1:  3  ADD          +           
    1:  5  NUM          2           
    1:  7  SUB          -           
    1:  9  NUM          123  

The first column contains the line number and the second column contains the column number in witch the lexical token was found.

The third column contains the name of the token given in the definitions and the last column contains the lexeme of the token.

Example - keywords.py

import alex

OPERATORS = (
    ("EQ", "="),
    ("EQEQ", "=="),
)
REGEXPS = (
    ("ID", f"^[a-zA-Z_]*"),
    ("NUM", '^["0123456789"]*'),
)
KEYWORDS = [
    'if',
    'then',
]

lexer = alex.Alex(
    operators=OPERATORS,
    regexps=REGEXPS,
    keywords=KEYWORDS)

lexer.scan('''if foo == bar 
then x = 17''')

print('----(Tokens found)----------')
for token in lexer.tokens:
    print(token)

In this example we have added a KEYWORDS definition and added a REGEXP to find identifiers.

The output is:

----(Tokens found)----------
    1:  1  KEYWORD      if          
    1:  4  ID           foo         
    1:  8  EQEQ         ==          
    1: 11  ID           bar         
    2:  1  KEYWORD      then        
    2:  6  ID           x           
    2:  8  EQ           =           
    2: 10  NUM          17 

Error handling

When an error occurs one of the following exceptions is issued.

  • AlexDefinitionError,
  • AlexScanError,

AlexDefinitionError is raised when there is something wrong with your definitions and AlexScanError is raised when a problem is detected during the scanning of the input text.

AlexDefinitionError example - def_error.py

import alex


OPERATORS = (
    ("ADD", "+"),
    ("ADD", "-"),
)

REGEXPS = (
    ("NUM", '^["0123456789"]*'),
)


try:
    lexer = alex.Alex(
        operators=OPERATORS,
        regexps=REGEXPS)
    lexer.scan('1 + 2 - 123')
except alex.AlexDefinitionError as ex:
    print('----(Error report)----------')
    print(ex)

The output is

----(Error report)----------
Operator (ADD, '-'): Key 'ADD' already in use!

AlexScanError example - scan_error.py

import alex

OPERATORS = (
    ("EQ", "="),
)

REGEXPS = (
    ("ID", f"^[a-zA-Z_]*"),
    ("NUM", '^["0123456789"]*'),
)

lexer = alex.Alex(
    operators=OPERATORS,
    regexps=REGEXPS)

try:
    lexer.scan('x = 1 + 1 + 3')
except alex.AlexScanError as ex:
    print('----(Error report)----------')
    print(ex)
    print()

print('----(Tokens found)----------')
for token in lexer.tokens:
    print(token)

The output is:

----(Error report)----------
Unrecognized char '+' ordinal 43
Following 10 characters are:  1 + 3
Last scanned Token:     1:  5  NUM          1           

----(Tokens found)----------
    1:  1  ID           x           
    1:  3  EQ           =           
    1:  5  NUM          1           

If you don't want an exception you can define what shall happen when an unrecognized character is found.

  • skip_unrecognized_chars

  • treat_unrecognized_chars_as_an_operator

  • define a set of skip characters

    lexer = alex.Alex( operators=OPERATORS, regexps=REGEXPS, skip_unrecognized_chars=True)

Then the output will be:

----(Tokens found)----------
    1:  1  ID           x           
    1:  3  EQ           =           
    1:  5  NUM          1           
    1:  8  NUM          1           
    1: 11  NUM          3   

or you can do

lexer = alex.Alex(
    operators=OPERATORS,
    regexps=REGEXPS,
    treat_unrecognized_chars_as_an_operator=True)

and the output will be:

----(Tokens found)----------
    1:  1  ID           x           
    1:  3  EQ           =           
    1:  5  NUM          1           
    1:  7  UNRECOGNIZED-CHAR +           
    1:  9  NUM          1           
    1: 11  UNRECOGNIZED-CHAR +           
    1: 13  NUM          3    

or define a set of characters to skip. By default, space and tab are defined as characters to skip, but you can define your own set.

lexer = alex.Alex(
    operators=OPERATORS,
    regexps=REGEXPS,
    skip_chars=' \t+')

And the output will be:

----(Tokens found)----------
    1:  1  ID           x           
    1:  3  EQ           =           
    1:  5  NUM          1           
    1:  9  NUM          1           
    1: 13  NUM          3 

Regular expressions

In the definitions of regular expressions the ^ character must be the first character in the expression.

REGEXPS = (
    ("NUM", '^["0123456789"]*'),
)

Keywords lists

Duplicate definitions of the same keywords will be silently removed.

KEYWORDS = [
    'if',
    'if',
    'then',
]

by converting the keywords to a set.

{'if', 'then'}

The Token class

A Token class object has four attributes.

  • name
  • lexeme
  • line_nbr
  • col_nbr

and a repr() function that returns these attributes as one string.

Example - scanning python

This example shows how you can get tokens from a python file. It has been tested, but no garanti given that it works for every case.

It uses the flag

scan_python_indents=True

to save indent information as a token. The lexeme of such a token is the length of the indent.

This only works correct if the indents only consists of spaces and not tabs.

import alex


OPERATORS = (
    ("PLUS", "+"),
    ("MINUS", "-"),
    ("MUL", "*"),
    ("DIV", "/"),
    ("DOT", "."),
    ("COMMA", ","),
    ("COLON", ":"),
    ("SEMI", ";"),
    ("AT", "@"),
    ("MOD", "%"),
    ("EQ", "="),
    ("GT", ">"),
    ("LT", "<"),
    ("LP", "("),
    ("RP", ")"),
    ("LBR", "["),
    ("RBR", "]"),
    ("LCBR", "{"),
    ("RCBR", "}"),
    ("OR", "|"),
    ("XOR", "^"),
    ("NOT", "~"),
    ("AND", "&"),
    ("ADDEQ", "+="),
    ("SUBEQ", "-="),
    ("MULEQ", "*="),
    ("DIVEQ", "/="),
    ("IDIVEQ", "//="),
    ("MODEQ", "%="),
    ("LSHIFT", "<<"),
    ("RSHIFT", ">>"),
    ("IDIV", "//"),
    ("EXP", "**"),
    ("GE", ">="),
    ("LE", "<="),
    ("TYPE", "->"),
    ("NEQ", "!="),
    ("EQEQ", "=="),
    ("OREQ", "|="),
    ("XOREQ", "^="),
    ("ANDEQ", "&="),
    ("WALRUS", ":="),
    ("EXPEQ", "**="),
    ("LSHIFTEQ", "<<="),
    ("RSHIFTEQ", ">>="),
)
REGEXPS = (
    ("TSTR", r'^f?"""(?:\\.|(?!""").)*?"""|^f?\'\'\'(?:\\.|(?!\'\'\').)*?\'\'\''),
    ("STR", r'^f?"(?:\\.|[^"\\])*"|^f?\'(?:\\.|[^\'\\])*\''),
    ("NUM", '^["0123456789"]*'),
    ("REM", "^#[^\n]*"),
    ("ID", f"^[a-zA-Z_0-9]*"),
)
KEYWORDS = [
    "False",
    "None",
    "True",
    "and",
    "as",
    "assert",
    "async",
    "await",
    "break",
    "class",
    "continue",
    "def",
    "del",
    "elif",
    "else",
    "except",
    "finally",
    "for",
    "from",
    "global",
    "if",
    "import",
    "in",
    "is",
    "lambda",
    "nonlocal",
    "not",
    "or",
    "pass",
    "raise",
    "return",
    "try",
    "while",
    "with",
    "yield",
]

lexer = alex.Alex(
    operators=OPERATORS, regexps=REGEXPS, keywords=KEYWORDS, scan_python_indents=True
)

lexer.scan_file("test.py")

print("----(Tokens found)----------")
for token in lexer.tokens:
    print(token)

Building distribution

To build a distribution that can be uploaded to PyPi, run the following command:

tools/build_dist.cmd

In order for this to work the build package must be installed.

Upload distribution to PyPi

In order to upload a new distribution you first have to bump up the version number in pyproject.toml. You can't upload the same version twice.

Thereafter, run the command

tools/install_pypi.cmd

Run documentation server locally

To test the documentation web pages you can start a local server with the command:

tools/run_docs_local_server.cmd

Upload documentation to Github

The documentation is found at https://rogerlindberg.github.io/alex

To upload new or editied documentation, use the command:

tools/docs_to_github.cmd

The documentation is created with mkdocs, which must be installed.

Run tests

For testing the package pytest must be installed.

All tests are found. in the tests directory.

To run all tests:

python tools/run_tests.cmd

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alex_lexer-0.1.6.tar.gz (14.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

alex_lexer-0.1.6-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file alex_lexer-0.1.6.tar.gz.

File metadata

  • Download URL: alex_lexer-0.1.6.tar.gz
  • Upload date:
  • Size: 14.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for alex_lexer-0.1.6.tar.gz
Algorithm Hash digest
SHA256 4e25481f1d865ad9e389e4d3ba634e55a701ac564b2a631fe903a3de35a2e8f4
MD5 24ea669197c3a15b2adc002c8a2e3776
BLAKE2b-256 fd3e1006a9b0cbb1d7e22b2cf60f6405ff77d917164114bae893c8dd2a3688d0

See more details on using hashes here.

File details

Details for the file alex_lexer-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: alex_lexer-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 10.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for alex_lexer-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 3e75943da6c5bd4de4f1805305f4ecbbfcb305b709dcd851bdcf6efa1f08da67
MD5 616a0e16b638a3bcb300d65714a8b696
BLAKE2b-256 af9f996f0ec87c51d49529dc9e08a84e567143bf6f4acdb8be1b4e3539ad746b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page