Skip to main content

MaoLang

MaoLang is a small interpreted language whose extension commands are ordinary Python programs. The project provides a Python API, a command-line interface, and a bundled standard library. Extra library directories can be supplied by an application or on the command line without changing the installed package.

MaoLang is intentionally minimal. Every value is a string, code can be stored in variables, and control flow is built by generating and executing more MaoLang source. This makes the language easy to extend, but it is not designed for high-performance numerical work.

Requirements and installation

MaoLang requires Python 3.9 or newer and has no runtime dependencies.

Install the project from this repository:

python -m pip install .

Use an editable install while developing the package:

python -m pip install -e .

Quick start

Create hello.maolang:

name := {Mao}
! print {Hello, ${name}!}

Run it with either command:

maolang hello.maolang
python -m maolang hello.maolang

The output is:

Hello, Mao!

Command-line interface

The general form is:

maolang [OPTIONS] [FILE]

With no FILE, MaoLang starts the interactive REPL when standard input is a terminal. Redirected or piped standard input remains a batch source. Passing - as FILE also explicitly selects batch input. Use -c to execute inline source:

maolang -c "! print {Hello from MaoLang}"
echo "! print {Hello from standard input}" | maolang

Available options:

Option Meaning
-c CODE, --code CODE Execute CODE instead of reading a file
-i, --interactive, --repl Enter the REPL, optionally after executing a file or -c code
-L DIR, --lib-dir DIR Search an extra library directory before the bundled library; repeatable
-D NAME=VALUE, --define NAME=VALUE Add a string to the initial environment; repeatable
--encoding NAME Read source files with this encoding; defaults to UTF-8
-v, --verbose Write statement and interpolation traces to standard error
--dump-env Write the final environment as JSON to standard error
--version Print the installed MaoLang version

For example, this injects name before execution:

maolang -D name=Mao -c '! print {Hello, ${name}!}'

Interactive REPL

Run maolang without arguments in a terminal to start an interactive session:

$ maolang
MaoLang 0.1.0. Type :help for help.
>>> name := Mao
>>> ! print {Hello, ${name}!}
Hello, Mao!
>>> action := {
...     ! print {Running a stored block}
... }
>>> & {${action}}
Running a stored block
>>> :quit

The environment persists between statements. A statement with unmatched opening braces continues at the ... prompt until its braces balance. A failed statement prints a diagnostic and returns to the prompt without discarding variables from earlier successful statements. Ctrl+C cancels the current input or execution, and end-of-file exits the session.

REPL commands are recognized when no multiline statement is being collected:

Command Meaning
:help Show the REPL command list
:env Print the current environment as JSON
:clear Remove every value from the current environment
:quit, :exit Leave the REPL

Use -i to enter the REPL after executing a file or inline source. The REPL inherits the resulting environment:

maolang -i program.maolang
maolang -i -c 'answer := 42'
maolang --repl -D name=Mao

When --repl receives redirected input, it processes that input as an interactive session without displaying prompts. This is useful for automation and tests. In contrast, redirected input without --repl is parsed as one batch program and does not recognize colon commands.

Python API

One-shot helpers

Use run for a source string and run_file for a UTF-8 source file:

from maolang import run, run_file

environment = run(
    """
name := {Mao}
! eval {6 * 7} answer
! print {Hello, ${name}. The answer is ${answer}.}
""".strip(),
    env={"origin": "Python"},
)

file_environment = run_file(
    "program.maolang",
    lib_dirs=["./project_lib"],
    env={"name": "Mao"},
)

Both functions return the final dict[str, str] environment. The input env is copied and is never mutated. lib_dirs is optional; the bundled standard library is appended automatically. run_file injects the source file's absolute path as _FILEPATH; run does not because a source string has no file path.

Optional stdin, stdout, and stderr text streams make input and output easy to control. The libraries mapping registers fast in-process commands:

from io import StringIO
from maolang import run

output = StringIO()
final_env = run(
    "! print {captured}",
    stdin=StringIO(""),
    stdout=output,
)
assert output.getvalue() == "captured\n"

Reusable interpreter

Create an Interpreter when multiple runs share configuration:

from maolang import Interpreter

interpreter = Interpreter(
    lib_dirs=["./project_lib"],
    verbose=False,
)

first = interpreter.execute("answer := {42}")
second = interpreter.execute_file("program.maolang", env=first)

Interpreter.execute accepts an optional source_name for diagnostics. Interpreter.execute_file accepts an optional encoding; source files default to UTF-8. Library modules are normal Python source files and follow Python's encoding rules.

REPL API

run_repl exposes the same persistent interactive loop to Python applications:

from maolang import Interpreter, run_repl

interpreter = Interpreter(lib_dirs=["./project_lib"])
final_env = run_repl(
    interpreter,
    env={"name": "Mao"},
    banner="Embedded MaoLang console",
)

The function accepts optional stdin, stdout, and stderr text streams, making an embedded console testable without a real terminal. show_prompts defaults to the input stream's isatty() result. The returned dictionary is the environment at the time the user exits.

Errors

Syntax and execution failures raise MaoLangError:

from maolang import MaoLangError, run

try:
    run("! missing-command")
except MaoLangError as error:
    print(error)
    first_location = error.diagnostics[0]

str(error) is formatted for a terminal. error.diagnostics contains structured Diagnostic objects with the source name, source text, line range, and message.

Language reference

Execution model

A program is processed as a sequence of statements. A statement normally occupies one line. Balanced braces can make a statement span multiple lines. Leading and trailing statement whitespace is ignored.

The interpreter stores all variables in one environment mapping. Every key and value is a string. Nested blocks and generated library code execute against the same logical environment, so successful assignments remain visible afterward.

The core statement forms are:

Form Purpose
name := VALUE Assign a string
${name} Interpolate an environment value
& {SOURCE} Execute grouped MaoLang source
! command ARGUMENTS Run a Python library command
// comment Ignore the remainder of the line

Predefined environment values

File execution injects one predefined value before the first statement:

Name Value
_FILEPATH Absolute path of the MaoLang source file currently being run

The value is available through normal interpolation:

! print {Running ${_FILEPATH}}

Interpreter.execute_file, run_file, and CLI file execution all inject the value. It overrides an _FILEPATH supplied in the initial environment, while the caller's original mapping remains unchanged. Inline run calls, -c, batch standard input, and a newly started REPL do not inject _FILEPATH because they do not represent a source file.

When maolang -i program.maolang enters the REPL after running a file, the file's final environment is preserved, so _FILEPATH remains available in that session. The bundled run command also preserves _FILEPATH while executing child files.

Values and grouping

Whitespace separates ungrouped values. Braces group spaces or newlines into one value and may be nested:

single := one-word
sentence := {one value with spaces}
block := {
    first line
    second line
}

For assignments, multiple parsed values are joined with newline characters. Use braces whenever spaces must be preserved as spaces. Braces are structural; there is no separate quoted-string or brace-escape syntax, so all braces must balance.

Comments

// discards the rest of its physical line:

answer := 42 // this text is ignored

Comment removal happens before brace parsing, including inside grouped text. A literal // cannot currently be escaped.

Assignment

The only core assignment operator is :=:

name := {Mao Lang}
empty := {}
multiline := {first
second}

Assignment does not infer numbers or booleans. The strings 42, True, and False only gain those meanings when a library command interprets them.

Interpolation

${name} is replaced with the current string value of name before a statement is split into its command and arguments:

name := Mao
greeting := {Hello, ${name}!}
! print {${greeting}}

Interpolation is recursive. An undefined identifier or a cyclic expansion raises MaoLangError. ${} expands to an empty string.

Because interpolation happens before execution, code stored for a later stage often uses $_{name}. That spelling has no special meaning in the core parser; the bundled late command converts $_ to $ one execution stage later.

Executing a block

& executes one grouped value as MaoLang source:

action := {
    result := {done}
    ! print {Action ${result}}
}

& {${action}}

The & command requires a following brace. Assignments made by the block are merged into the current environment.

Calling a library command

! dispatches an in-process Python library command. A command can be a callable registered by the Python API or a module found in a library directory:

! eval {20 + 22} answer
! print {The answer is ${answer}.}

The first value after ! is the command name. Remaining values become LibraryCall.arguments. The .py suffix is optional for module commands. Resolution uses this order:

  1. In-process handlers registered through libraries, register_library, or the library decorator.
  2. Directories supplied through lib_dirs or -L, in the supplied order.
  3. The bundled maolang/lib directory.

The first match wins. A project directory can therefore override a bundled name, while an explicitly registered callable overrides both directory and bundled commands. Every directory module must export maolang_library(call). Modules are loaded in the current process and cached by file path and modification time.

Bundled standard library

The standard library is stored inside the installed package and needs no manual path configuration. Its seven maolang/lib/*.py modules implement the same maolang_library(call) protocol available to user libraries. They are imported on first use and then reused by the interpreter.

print

! print [VALUE ...]

Joins its arguments with spaces and prints one line. Internally it assigns the special _PRINT environment key; the interpreter writes and removes that key at the next output boundary.

! print {Hello world}
! print one two three

Output:

Hello world
one two three

input

! input DESTINATION

Reads one line from standard input and assigns it to DESTINATION:

! print {What is your name?}
! input name
! print {Hello, ${name}!}

eval

! eval EXPRESSION [DESTINATION]

Evaluates a Python expression, converts the result with str, and assigns it to DESTINATION. The default destination is _ANS when only an expression is provided. Embedded newlines in the expression are changed to spaces.

! eval {6 * 7} answer
! eval {sum(range(1, 6))} total
! print {answer=${answer}, total=${total}}

eval is useful for arithmetic because MaoLang itself only stores strings. It is not sandboxed and can execute arbitrary Python code. Only evaluate trusted source.

if

! if FLAG TRUE_VALUE FALSE_VALUE DESTINATION

Assigns one of two values to DESTINATION. Case-insensitive false, none, and the string 0 are false; every other string is true.

if selects a value but does not directly execute it. Store code in the selected value and use & when conditional execution is required:

enabled := true
! if {${enabled}} {! print {enabled}} {! print {disabled}} selected
& {${selected}}

late

! late SOURCE

Delays interpolation by one library execution stage. It replaces every $_ with $ and every literal \n sequence with a real newline, then returns the result as generated MaoLang source.

name := Mao
! late {! print {Hello, $_{name}!}}

The original statement does not recognize $_{name}. late emits a new statement containing ${name}, and that generated statement resolves the variable. This is the key mechanism used by stored loop bodies and recursive examples.

wrap

! wrap PREFIX CONTENT...

Prefixes each nonempty top-level content line and wraps the line in braces. It is useful for generating repeated commands:

! wrap {! print} {alpha
beta
gamma}

Output:

alpha
beta
gamma

Newlines inside nested brace blocks are emitted as literal \n sequences so they can survive another generation stage and later be restored by late.

run

! run RELATIVE_PATH

Reads another UTF-8 MaoLang script and executes it directly in the current environment. Assignments and other side effects made by the child script remain available after run returns.

The command requires exactly one relative path. Use braces when the path contains spaces. Absolute paths are rejected. In a file, the path is resolved relative to the file containing the run statement. In the REPL or string-based run() API, the first path is resolved relative to the current working directory.

For example:

project/
|-- main.maolang
`-- setup/
    `-- values.maolang

main.maolang:

name := Mao
! run {setup/values.maolang}
! print {${name}: ${answer}}

setup/values.maolang:

! eval {6 * 7} answer

The output is Mao: 42. A child script may call run again; that nested path is relative to the child script containing the call.

run never changes _FILEPATH. When execution started from main.maolang, the main file's absolute path remains visible in the parent, child, and nested child environments. A REPL session remains without _FILEPATH unless user code assigns it explicitly.

Complete examples

The examples below are deliberately small. Loops generate MaoLang code recursively, so large numerical ranges are not a goal of the current interpreter even though all library calls run in-process.

Fibonacci sequence

examples/fibonacci.maolang prints the first eight Fibonacci numbers. step updates the state, while loop uses eval and if to select either another step or an empty block.

// Print the first eight Fibonacci numbers.
count := 8
index := 0
current := 0
next := 1

step := {
    ! late {! print {$_{current}}}
    ! late {! eval {$_{current} + $_{next}} sum}
    ! late {current := {$_{next}}}
    ! late {next := {$_{sum}}}
    ! late {! eval {$_{index} + 1} index}
    ! late {& {$_{loop}}}
}

loop := {
    ! late {! eval {$_{index} < $_{count}} keepGoing}
    ! late {! if {$_{keepGoing}} {$_{step}} {} selectedStep}
    ! late {& {$_{selectedStep}}}
}

! print {First ${count} Fibonacci numbers:}
& {${loop}}

Run it with:

maolang examples/fibonacci.maolang

Expected output:

First 8 Fibonacci numbers:
0
1
1
2
3
5
8
13

Prime-number table

examples/primes.maolang tests candidates from 2 through 10. The Python expression checks divisors only through the square root of the candidate, while MaoLang controls iteration and output.

// Print every prime number up to ten.
limit := 10
candidate := 2

testAndAdvance := {
    ! late {! eval {all($_{candidate} % divisor != 0 for divisor in range(2, int($_{candidate} ** 0.5) + 1))} isPrime}
    ! late {! if {$_{isPrime}} {! print {$_{candidate}}} {} printStep}
    ! late {& {$_{printStep}}}
    ! late {! eval {$_{candidate} + 1} candidate}
    ! late {& {$_{loop}}}
}

loop := {
    ! late {! eval {$_{candidate} <= $_{limit}} keepGoing}
    ! late {! if {$_{keepGoing}} {$_{testAndAdvance}} {} selectedStep}
    ! late {& {$_{selectedStep}}}
}

! print {Prime numbers up to ${limit}:}
& {${loop}}

Run it with:

maolang examples/primes.maolang

Expected output:

Prime numbers up to 10:
2
3
5
7

Writing a custom library command

All custom libraries use one in-process protocol. A handler receives a LibraryCall and may return:

Return value Effect
Mapping[str, str] Merge values directly into the environment
str Execute the string as generated MaoLang source
LibrarySource Execute generated source with optional diagnostic and relative-path context
None Complete without generated source or environment changes

All mapping keys and values must be strings. Handler exceptions and invalid results become MaoLangError diagnostics at the calling source line.

LibraryCall exposes:

Attribute Meaning
name Command name used after !
arguments Parsed and interpolated list[str] arguments
stdin Interpreter input text stream
stdout Interpreter output text stream
stderr Interpreter diagnostic text stream
path Module path, or None for a directly registered callable
source_path MaoLang source file containing the call, or None for a REPL/string source

source_path tracks the currently interpreted source for relative child loading; it may change during nested run calls. _FILEPATH remains the original entry script's environment value. Custom loaders can return LibrarySource when they need to provide a new source name or path context without changing _FILEPATH.

Registering callables from Python

Register several handlers when constructing an interpreter:

from maolang import Interpreter, LibraryCall


def multiply(call: LibraryCall) -> dict[str, str]:
    left, right, destination = call.arguments
    return {destination: str(int(left) * int(right))}


interpreter = Interpreter(libraries={"multiply": multiply})
environment = interpreter.execute("! multiply 6 7 answer")
assert environment["answer"] == "42"

The one-shot helpers also accept libraries:

from maolang import run

environment = run(
    "! multiply 6 7 answer",
    libraries={"multiply": multiply},
)

Use the decorator API when building an interpreter incrementally. Returning source allows a handler to use normal MaoLang execution and output behavior:

from maolang import Interpreter, LibraryCall

interpreter = Interpreter()


@interpreter.library("greet")
def greet(call: LibraryCall) -> str:
    return f"_PRINT := {{Hello, {call.arguments[0]}!}}"


interpreter.execute("! greet Mao")

The equivalent non-decorator form is interpreter.register_library("greet", greet).

Writing a directory module

The CLI cannot receive a Python callable directly, so -L loads command modules from a directory. Each module must define exactly the same handler protocol under the name maolang_library.

For example, create project_lib/greet.py:

from maolang import LibraryCall, LibraryResult


def maolang_library(call: LibraryCall) -> LibraryResult:
    return {"_PRINT": f"Hello, {call.arguments[0]}!"}

Call it from MaoLang:

! greet {Mao}

Then make the directory available through either interface:

maolang -L project_lib program.maolang
from maolang import run_file

run_file("program.maolang", lib_dirs=["project_lib"])

Modules are loaded lazily and cached inside each Interpreter. Module globals can therefore hold reusable state or expensive initialized objects. When a module's modification timestamp changes, the next call reloads it. Imports placed at module scope may load helper modules from the same library directory.

Direct registration and directory modules use identical call and return values. Direct registration only avoids the initial file lookup and module import. Library modules run inside the host process and are not sandboxed, so only load trusted code.

Performance model

All library commands execute in-process. Modules are cached, and mapping results update the environment without generating or reparsing assignment source. Generated source from late, wrap, and custom handlers still passes through the normal interpreter, preserving MaoLang's staged execution model.

The mathematical examples intentionally keep small ranges because recursive generated code is still not intended to compete with native Python loops. For heavy computation, place the algorithm inside a Python library handler and return only its final strings to MaoLang.

Development and verification

Run the complete test suite with:

python -m unittest discover -s tests -v

The example tests execute both mathematical programs through the real interpreter and compare their complete output. Their ranges intentionally remain small to exercise recursion without turning the test suite into a stress test.

Build a wheel with:

python -m pip wheel . --no-deps

The wheel includes maolang/lib/*.py, so the standard library remains available after installation without an external data directory.

Release files for maolang 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for maolang 0.1.0
File Size Uploaded
maolang-0.1.0.tar.gz 33.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for maolang 0.1.0
File Interpreter ABI Platform
maolang-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.2 kB

Release files / maolang-0.1.0.tar.gz

Download URL maolang-0.1.0.tar.gz
Size 33.2 kB
Tags Source
SHA-256 checksum
How to use checksums
abeca9517e955fb760c76f78de248b097b8c37e4fd4fcc3be5baa5702f95523f
BLAKE2b-256 checksum
How to use checksums
c47774ead35241cab4d771f152a307a4ab4df86446bbc099d809a4b259e31ff3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.1 CPython/3.14.6 Windows/11

Release files / maolang-0.1.0-py3-none-any.whl

Download URL maolang-0.1.0-py3-none-any.whl
Size 23.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fca9a4bf35cb393b715ebdff6d286fb43e00753f83b77b3c9677453880edc186
BLAKE2b-256 checksum
How to use checksums
720f6095e162df1d72460f5bc15f7977dd1ed5563195878e9e01af4659f3ed86
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.1 CPython/3.14.6 Windows/11

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page