Skip to content
xzawedPublic

About

Statically-typed extension language: compile typed logic to a protected native module a host loads over a C ABI. No source shipped, no host rebuild.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Mathless

Compile typed logic into a native module. A host loads it over a plain C ABI — no source shipped, no host rebuild.

CI Rust Status Target License

You maintain a native application, and some of its rules keep moving. Pricing, discounts, eligibility, the things each customer wants slightly differently.

Recompiling the whole host for every change is not practical. Shipping those rules as readable source is not acceptable either.

Mathless is a small statically-typed language for that gap. You write the rules in its surface syntax, and the compiler turns them into a native module at compile time. Your application loads that module over a plain C ABI. What you ship is the binary.

It is built for native hosts: Delphi, C, C++, C#. Two of those are proven end to end on every push — a C host and an Object Pascal host both load a module and call it in CI — and real Delphi (dcc64) does the same in a local gate. C# is not verified yet. See Status below.

한국어 → README.ko.md

How it works

.mls  →  parse / typecheck  →  typed IR  →  native codegen  →  module (.dll)  →  [ C ABI ]  →  host

All of that happens at compile time. Today the module is a Windows .dll; a Linux .so is a target we have not taken on yet. Nothing interprets anything at runtime, and there is no bytecode VM. The host loads native code and calls it like any other library.

Why

Delphi and Object Pascal give you strong static types and native speed. They also sit in a closed ecosystem with a dated story for extensions and tooling.

Scripting languages solve the flexibility problem, but they are weak on typing. They are weaker still on protecting what you hand to a customer.

Mathless takes the typed, native side and makes it loadable. Four commitments hold throughout, and the rest of this file is measured against them.

  • Native only. No default VM, no bytecode runtime.
  • No source shipped. What you distribute is a native shared library.
  • The C ABI is the one first-class boundary. Per-language bindings are thin wrappers on top of it, which is why this is not a Delphi-only tool.
  • Protection is reported as cost, never as impossibility. We measure the exported-symbol count, the stripped binary size, and the absence of source in the artifact. We never translate those into "reversing difficulty".

Status

Phase 1's acceptance is complete on the C side. Everything below is measured on main, on Windows x64 with a pinned toolchain.

The compiler. mlc runs lex, parse, typecheck, a backend-independent IR, then codegen. The backend emits no_std, extern "C" Rust and builds it as a cargo cdylib.

The language today. Four types: f64, bool, i32 and string. The numeric ones come with arithmetic (+, -, *, /, and % on i32), comparisons, and an explicit as conversion between them; i32 division by zero gives 0 rather than a trap, and inside a fallible (-> T!) function an i32 result that overflows fails with a reserved status instead of wrapping. A string can be a parameter or a -> string! return; it compares with == and != on bytes, and concatenates with + — which only a return may hold, since the module has no allocator. i32 as string renders a number. Returning a string uses a caller-allocated buffer — the module never allocates. An array parameter is written xs: [i32] (elements may be i32, f64 or bool): it arrives as a borrowed pointer plus a length the compiler appends, so one surface parameter becomes two C parameters. It is read-only, len(xs) gives the element count, and xs[i] is bounds-checked — which is why a function that indexes must be declared -> T!, since an out-of-range read fails with a reserved negative status rather than through an invisible channel. An array can also be returned, using the caller-allocates protocol a returned string uses: the body declares its length with result <n>, which is where the host's capacity is checked, and then writes elements with result[i] = e. Because the check happens before the first write, a call that does not fit writes nothing at all. One difference worth knowing: for an array return ml_cap and *ml_needed count elements, where a string return counts bytes. Control flow is if, while and return; there is no else yet. Locals are let and let mut, with assignment. Operators include unary - and !, plus && and ||. The built-ins are floor, ceil, round and trunc — which match C's <math.h> exactly — plus len for arrays, byte_len and byte_slice for strings, and fixed for decimal formatting. A function can be fallible: -> T! with error NAME = N and fail NAME, which lowers to an integer status and an out-parameter, and it can declare extra out parameters to return several values. Internal fn declarations can call each other, but recursion is rejected at compile time. Constants are named literals: a const stays inside the module, while an export const is an i32 that the header names ML_<MODULE>_CONST_<NAME> and the interface fingerprint covers, so renumbering one changes the fingerprint. docs/LANGUAGE.md keeps the definitive list.

The CLI. mlc build <file.mls> -o <dir> writes four files side by side: the .dll module, a .h C header, a .pas Delphi import unit, and a .lib import library so a C host can link against the header instead of resolving every symbol at run time.

Four host paths run in CI, all measured. A Rust kernel32 oracle loads the module and calls it. So does a real C host built with MSVC, which compiles the generated header and resolves the exports through LoadLibrary and GetProcAddress (MATHLESS_GATE_D). A second C host does it the ordinary way — #include the header, link the packaged .lib, call the function, with no run-time lookup at all. Both C hosts check the module's interface fingerprint first and refuse one that drifted. And an Object Pascal host built with Free Pascal loads x64 modules and calls them, alongside a gate that compiles every generated .pas (MATHLESS_GATE_FPC_HOST and MATHLESS_GATE_FPC).

A fifth runs locally only. MATHLESS_GATE_DELPHI builds hosts/delphi-host with real dcc64 and calls the modules across the C ABI. The edition available refuses command-line builds, so the gate drives the IDE instead (bds.exe -b), which it does allow, and it passes on the development machine. It cannot run in CI: the runner has neither Delphi nor an interactive session.

Acceptance A, B, C and D all pass. It compiles, the oracle calls it, the export and size proxies hold, and a real C host loads the same module. The stripped no_std build is about 9.0-9.5 KB — the exact byte count depends on the machine's linker, so docs/SECURITY.md keeps the measurements — and it exports only the functions you declare plus the reserved symbols the ABI defines, cross-checked against dumpbin /exports so it does not rest on our own PE reader alone.

What CI does check on the Pascal side is Object Pascal, not Delphi: MATHLESS_GATE_FPC compiles every generated unit and MATHLESS_GATE_FPC_HOST builds a Pascal host that loads x64 modules and calls them, both with Free Pascal. That is a real host path, and it is the reason the count above is four. It is not a stand-in for the Delphi gate — -Mdelphi is dialect emulation, and the two disagree on something as basic as what string means. So the precise gap is Delphi itself, not the language.

For current numbers, open decisions and the next piece of work, see docs/STATUS.md.

Example

export fn discount(price: f64, vip: bool) -> f64 {
  let rate = 0.9
  if vip { return price * rate }
  return price
}
mlc build discount.mls -o out/
#  out/discount.dll   native module — exports mlx_discount + ml_module_abi_version + ml_iface_hash_discount
#  out/discount.h     C header
#  out/discount.pas   Delphi import unit (Delphi: checked by MATHLESS_GATE_DELPHI, which does not run in CI)
#  out/discount.lib   MSVC import library (link-time binding)

A C host loads discount.dll, calls mlx_discount(100.0, true), and gets 90.0 back. The host itself is never rebuilt.

Honest limits

  • Windows x64 for now. A Linux .so target is a decision we have not taken yet, rather than an oversight.
  • The surface is small on purpose. docs/LANGUAGE.md lists exactly what compiles today, and what does not.
  • A call into a module may not return. A while loop can run forever, and nothing across the C ABI can time it out or cancel it. Treat modules as trusted code, and call them on a worker thread if your host has to stay responsive.
  • Names are provisional. The .mls and .mll extensions, and the C API names, may still change.
  • A killed build leaves a staging directory behind. mlc build stages its four files in a .mlc-stage-<pid>-<n> directory inside the output directory, so that either all four land or none do. It removes that directory on success and on failure — but a process that is killed outright never gets to. Measured: that leftover is inert, and the next build into the same directory succeeds normally, so it is litter you can delete. One exception, and mlc says so when it happens: if a failed build could not put your previous artifacts back, it keeps the staging directory on purpose and prints … could NOT be put back — N file(s) are left in <dir> and must be moved back by hand. That directory holds the only copy of those files. Delete a .mlc-stage-* only when no such message was printed.

Documentation

Document What it covers
docs/STATUS.md Measured state of main, open decisions, and the next slice
docs/HISTORY.md The index of the dated session log (§9-N), newest first — one stub per entry, whose text is in docs/history/9-N.md. An archive: look up the entry a document cites, not the whole log. docs/history/ also holds the ▶ block records and the STATUS sections moved out on 2026-09-25
docs/VISION.md Why Mathless exists, and what success looks like
docs/LANGUAGE.md Surface syntax vs the internal model
docs/HOST_ABI.md The C ABI, host integration, and the liveness contract
docs/SECURITY.md Protection goals, stages, and the measured proxies
docs/ROADMAP.md MVP → expansion
docs/DECISIONS.md Confirmed decisions (D01–D23) and rejected alternatives
docs/OPEN_QUESTIONS.md What is still undecided, and what each answer would cost
docs/ARCHITECTURE.md The intended pipeline and module boundaries
docs/COMPETITIVE.md Where this sits next to the alternatives
docs/GLOSSARY.md The terms this repository uses precisely
docs/slices/ Per-feature SPECs — the unit of work — and their status
runtime/ · hosts/c-host/ · hosts/c-host-link/ · hosts/delphi-host/ The C ABI header, and the three host directories that consume it

Contributing

Every change arrives as a PR, and nothing is committed to main directly. Work is test-driven, and CI runs fmt, clippy and the test suite on two jobs.

windows-latest is the one that matters, because the acceptance tests actually execute there. ubuntu-latest compiles the platform-independent frontend as insurance.

Setting up a machine — what to install, and which measurements legitimately differ on another one — is in CONTRIBUTING.md. The suite needs Rust and the MSVC Build Tools and nothing else; third-party Rust dependencies are zero. Where to start work is the "▶ 여기서 시작한다" block in docs/STATUS.md §9.

License

Licensed under either of

at your option. This is the Rust ecosystem's usual pair: MIT keeps things simple and GPLv2-compatible, and Apache-2.0 adds an explicit patent grant.

What mlc produces from your source is yours — see LICENSE-OUTPUT-EXCEPTION. The generated .dll, .h, .pas and .lib carry no obligation from the licences above, even though a generated header is mostly our template text — a generated header for a one-function module carries a single declaration line from your source and everything else from ours, which is exactly why the exception is written down rather than assumed. (A line count used to stand here; it was measured once in 2026-09-02 and was wrong by the time anyone re-ran it. Build examples/discount.mls and count.) It covers output only; the compiler itself stays Apache-2.0 OR MIT. It is not legal advice.

Unless you say otherwise, any contribution you intentionally submit for inclusion in this work, as defined in the Apache-2.0 license, is dual licensed the same way, with no extra terms.

About

Statically-typed extension language: compile typed logic to a protected native module a host loads over a C ABI. No source shipped, no host rebuild.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages