MiniLang (.ml) is a small, dynamically typed language that compiles to a native Windows x64 console executable (PE32+) via the Win64 compiler tool (mlc_win64.py).
It is completely developed with the help of generative AI (ChatGPT version >= 5.2)
- 1. Quickstart
- 2. Files & Running
- 3. Comments
- 4. Types & Literals
- 5. Variables & Assignments
- 6. Operators & Expressions
- 7. Arrays
- 8. Control Flow
- 9. Functions
- 10. struct
- 11. enum
- 12. Modules, namespace & import
- 13. Standard Library & Builtins
- 14. extern
- 15. Error handling:
error&try - 16. Syntax Reference (short)
- 17. Examples
- Native compiler status
Hello World
print "Hello MiniLang!"Variables and math
x = 10
y = 5
print x + yIf/then
age = 18
if age >= 18 then
print "ok"
else
print "nope"
end ifInline form also works:
if age >= 18 then print "ok" else print "nope" end ifProgram entry via main(args):
function main(args)
print "argc=" + len(args)
if len(args) > 0 then
print "first=" + args[0]
end if
return 0
end function- Source files use the extension:
.ml
python mlc_win64.py input.ml output.exe [options]
Notes:
- Flags can appear before or after the positional arguments.
- On non-Windows hosts you can still compile, but running the resulting `.exe` requires Wine.Common options:
Import / modules
-I <dir>/--import-path <dir>add an import search path (repeatable). The directory ofinput.mlis always an implicit import root.
Listings / diagnostics
--asmwrite a combined.asmlisting (default: off)--asm-out <path>override listing path (default: output basename +.asm)--asm-cols addr,opcodes,codechoose columns (default: all)- or
--asm-no-addr,--asm-no-opcodes,--asm-no-code
- or
--asm-datainclude.rdata/.data/.idatadumps (constants and imports)--asm-peinclude a PE32+ header + section table dump in the listing
Diagnostics
--keep-goingcontinue after the first error and report multiple diagnostics--max-errors <n>cap the number of diagnostics when using--keep-going(default: 20)
Heap / GC tuning (native runtime)
--heap-reserve <size>reserve heap address space (e.g.256m)--heap-commit <size>initial committed heap bytes (e.g.16m)--heap-grow <size>minimum commit growth step when the heap needs to grow (e.g.1m)--heap-shrinkenable decommit after GC (trim-from-top). Default: off--heap-shrink-min <size>minimum committed heap when shrinking (default: initial commit)--gc-limit <size>bytes allocated between periodic GC runs (default: backend constant)--no-gc-periodicdisable periodic GC trigger (collect only on OOM)
Profiling / tracing
--profile-callsinstrument user functions with call counters; enablescallStats()--trace-callsprint each entered function name to stderr (runtime trace)
Tip: python mlc_win64.py --help prints the full option list.
Notes (current implementation):
- Targets Windows x64 console (PE32+).
- Heap parameters can be configured via
--heap-*flags (reserve/commit/grow/shrink). - If a top-level
function main(args)exists, the native entrypoint will call it after module initialization has completed. Imported module initializers and the entry file's top-level initialization run automatically beforemain.argsisargv[1..]as an array of strings. The returned int becomes the process exit code (void -> 0). - The native runtime uses a VirtualAlloc heap with separate reserve/commit: it reserves a large address range and commits pages on demand.
- Listing order is stable: optional PE dump ->
.textlisting -> optional section dumps. - The compiler uses the shared MiniLang frontend for parsing (tokenizer/parser).
There is a small auto-formatter written in MiniLang: tools/mlfmt.ml.
Compile it once:
python mlc_win64.py tools/mlfmt.ml mlfmt.exe -I .Format a single file:
mlfmt.exe src.ml --inplace
mlfmt.exe src.ml out.ml --indent 2 --max-blank 2Format a whole tree (recursive, in-place):
mlfmt.exe .Insert an Apache 2.0 header (only if missing):
mlfmt.exe . --apache "Authorname"
# or:
mlfmt.exe . --author "Authorname"Notes:
--max-blank -1allows unlimited blank lines.- Directory formatting uses Win32 directory enumeration (so it is meant to run on Windows / Wine).
- When
<path>is a directory,mlfmtformats all*.mlfiles recursively in-place (the optionaloutput.mlargument is only valid for single-file formatting). --apache/--authoruses the local year (viastd.time.win32.GetLocalTime()in the compiled binary).- The formatter is intentionally conservative (it does not change program semantics).
./output.exe [args...]Running tests:
python tests/run_tests.py
python tests/run_tests.py --verbose
python tests/run_tests.py --only import
python tests/run_tests.py --allow-skipNotes:
- The test runner compiles a set of
.mlprograms to Windows.exefiles and executes them. - On Windows,
.exeruns natively; on non-Windows you needwineto execute the produced binaries. --only PATfilters by substring,--verboseprints full stdout/stderr, and--allow-skipexits with code 0 even if some tests were skipped (e.g. no Wine).
// this is a comment
print "hi" // comment at end of line/*
Multi-line comment
is ignored
*/
print "ok"MiniLang is newline-oriented, but supports a few "robust syntax" rules to make formatting easier:
- Newlines separate statements.
;can also separate statements (useful for single-line / inline code).
a = 1; b = 2; print a + bNewlines are allowed (and ignored) in common "continuation" positions:
-
After operators (and after unary operators):
x = 1 + 2 + 3 y = - 5 z = not false
-
Inside bracketed lists and calls (after
[/(, after commas, and before the closing]/)):a = [ 1, 2, 3, 4, 5, 6, ] print add( 1, 2, 3, )
-
Inside indexing (after
[and before]):v = a[ 0 ]
Trailing commas are allowed in array literals and call argument lists:
a = [1, 2, 3,]
print add(1, 2, 3,)MiniLang values:
- Int:
1,-42 - Hex int:
0xabc,-0x10 - Binary int:
0b10101,-0b10 - Float:
3.14,-0.5
a = 10
b = -3.5
h = 0xFF
m = 0b1010Note: the tokenizer currently treats a leading - as part of a numeric literal. In expressions like a-1, write spaces (a - 1) to ensure - is parsed as an operator.
- Strings use double quotes:
"Text" - Common escapes are supported, e.g.
\n,\t,\",\\
s = "Hello\nWorld"
print struefalse
flag = true- Literals:
[1, 2, 3],["a", "b"] - Trailing commas are allowed:
[1, 2, 3,] - Multiline literals are allowed (see section 7).
arr = [1, 2, 3]bytes is a mutable raw byte buffer (values 0..255). You create it with bytes(...) (or legacy byteBuffer(...)).
- Indexing returns an
intbyte value. - Assignment
buf[i] = nexpectsnin0..255.
See 13.3 for details and file / encoding examples.
void is the “no value” literal.
You get void when a function ends without return, or explicitly via return void. It is a real runtime value, so it can be assigned:
function maybeGetName()
if input() == "" then
return void
end if
return "Nina"
end function
x = maybeGetName()
if x is void then
print "no name"
else
print x
end ifStrict void handling (runtime): using void in most operations produces a runtime error(...):
- calling it:
void()orx()whenxisvoid - member access:
void.field - indexing:
void[i]ora[void] - arithmetic / bitwise ops:
+ - * / % & | ^ ~ << >> - ordered comparisons:
< <= > >= - boolean ops:
and/or/not(if an operand isvoid) - as a condition in
if/while/loop ... while len(void)
For type checks, prefer:
x is void/x is not voidx is int,x is string, etc. (primitive type checks; sugar fortypeof(x) == "...")x is Thing,x is Color, etc. (concrete struct/enum type checks; compares the internal type id)
Equality/inequality (==, !=) still works with void (e.g. void == void).
Note:
print void(and printing unsupported heap objects) raises a runtimeerror(...).
Older MiniLang versions treated void as an internal-only value that was not directly writable (e.g. assignment and printing were rejected). With strict void handling, void is writable, but using it as a real value in operations now fails loudly as described above.
name = "Max"
score = 100Variables do not need to be declared.
Native compiler: supported (top-level and inside functions).
const PI = 3.14159
const NAME = "MiniLang"Rules:
- A
constbinding can only be assigned once. - At top-level / in namespaces, the initializer must be
constexpr(compile-time evaluable). Typicalconstexprexpressions include literals, arithmetic/bitwise operations on constexpr values, references to otherconsts, and enum values. - Inside functions, the initializer may be any expression, but the name is still write-once.
Note: const makes the binding immutable (you can’t reassign the name). It does not deep-freeze objects like arrays/bytes.
Allowed standalone statements are:
- assignments (e.g.
x = 1) - function calls (e.g.
foo(1,2)) print <expr>
Not allowed, for example:
1 + 2 // invalid: expressions alone are not statementsStatements can be separated by newlines or by ;.
| Operator | Meaning |
|---|---|
+ |
add / string concat / array concat / bytes concat |
- |
subtraction |
* |
multiplication |
/ |
division |
% |
modulo |
Important:
-,*,/,%work only with numbers (notbool).+is special:- number + number -> number
- array + array -> array concatenation
- bytes + bytes -> bytes concatenation
- otherwise -> string concatenation (both sides are converted to strings automatically; there is currently no
str()builtin)
| Operator |
|---|
== |
!= |
> |
< |
>= |
<= |
is <type> |
is not <type> |
and,or(short-circuit)not(unary)
if not (x == 10) and true then
print "ok"
end if- shifts:
<<,>> - bitwise AND:
& - bitwise OR:
| - bitwise XOR:
^ - bitwise NOT:
~x
orand|^&==,!=,is>,<,>=,<=<<,>>+,-*,/,%- unary:
not,-x,~x
Parentheses override precedence.
Newlines may appear after operators (see 3.1).
a = [1, 2, 3]
b = ["x", "y"]
c = array(4) // [void, void, void, void]
d = array(3, "hi") // ["hi", "hi", "hi"]array(size[, fill]) initializes a new array with size elements.
If fill is omitted, elements are initialized with void.
Invalid size (non-int, negative, or too large) returns a runtime error
(catchable via try(...)).
a = [
1, 2, 3,
4, 5, 6,
]arr = [10, 20, 30]
print arr[0] // 10Multiline indexing is allowed:
print arr[
2
] // 30Index must be an int (not bool).
Out of bounds indexing (or indexing a non-indexable value) raises a runtime error
that you can catch with try(...).
arr = [1, 2, 3]
arr[1] = 99
print arr // [1, 99, 3]Invalid index assignment (wrong target type, non-int index, out of bounds, invalid byte value)
raises a runtime error (catchable via try(...)).
x = [1,2]
y = [3,4]
z = x + y
print z // [1,2,3,4]Block form:
if <cond> then
...
else if <cond> then
...
else
...
end ifInline form (single-line / compact):
if <cond> then <stmt> end if
if <cond> then <stmt> else <stmt> end ifUse ; to put multiple statements on one line:
if x > 0 then a = 1; b = 2; print a + b end ifwhile <cond>
...
end whileBody executes at least once.
loop
...
while <cond>
end loopfor <var> = <start> to <end>
...
end forstartandendmust be int- runs automatically up or down (step +1 or -1)
Iterates over arrays, strings, or bytes.
for each <var> in <iterable>
...
end forJumps to the next loop iteration.
i = 0
while i < 5
i = i + 1
if i == 3 then
continue
end if
print i
end whileExits the current loop or a switch.
while true
print "once"
break
end whilebreak 2 breaks two nested levels (e.g. inner + outer loop).
while true
while true
print "stop"
break 2
end while
print "never reached"
end whileNote: break/continue should only be used inside matching constructs (loops, and switch for break).
switch <expr>
case <value>
...
end case
case <value1>, <value2>, <value3>
...
end case
case <start> to <end>
...
end case
case default
...
end case
end switchcase X, Y, Z= multiple valuescase A to B= range (mainly useful for ints)case default= fallback- When a case matches, its body runs and the switch is exited afterwards.
breakinside a case also exits the switch.
Robust syntax for value lists:
- Trailing commas are allowed before the case body:
case 1, 2, 3, - Value lists can span multiple lines:
switch x
case 1, 2, 3,
4, 5, 6
print "hit"
end case
end switchfunction <name>(a, b, c)
...
return <expr>
end function- parameters are names (identifiers)
returnis optional- without
return, the function returnsvoid return;is allowed and is equivalent toreturn- Robust syntax: a bare
returncan appear directly before a block terminator in inline forms, e.g.if cond then return end if
Example:
function add(a, b)
return a + b
end function
print add(2, 3)Multiline parameters are allowed (trailing comma optional):
function add3(
a,
b,
c,
)
return a + b + c
end functionYou can mark top-level functions and struct methods as inline:
function inline clamp01(x)
if x < 0 then return 0 end if
if x > 1 then return 1 end if
return x
end functionWhen you write a direct call like clamp01(v), the compiler expands the callee body at the call site (no call/ret overhead).
Current behavior / limits:
- Only supported for top-level functions and struct methods (
function inline ...). - Only direct calls are inlined. Calls through a variable (e.g.
f = clamp01; f(v)) are not inlined. - Inline bodies must not capture variables (no closures / env hops / boxed captures).
- Inline bodies must not contain nested
functiondefinitions. - Inline recursion / mutual recursion is rejected.
return <expr>returns from the inline call (the call yields the return value).- The inline expansion uses an isolated scope so it won't clobber caller locals.
print add(2, 3)Multiline call arguments are allowed (trailing comma optional):
print add3(
1,
2,
3,
)Functions are first-class values. A function name evaluates to a pointer to that function and can be:
- assigned to a variable
- stored in arrays / structs
- passed to other functions
- called indirectly via
fn(...)
function add(a, b)
return a + b
end function
fn = add
print fn(2, 3) // 5Passing a function:
function apply(fn, a, b)
return fn(a, b)
end function
print apply(add, 2, 3) // 5Storing in an array (dispatch table):
function sub(a, b)
return a - b
end function
ops = [add, sub]
print ops[0](10, 4) // 14
print ops[1](10, 4) // 6Notes:
typeof(add)is"function".- Inline expansion applies only to direct calls (e.g.
add(1,2)), not to indirect calls likefn(1,2).
Native compiler:
- Direct and indirect calls are supported (functions are values; you can store/pass/call them).
If a top-level function named main exists with exactly one parameter, it is treated as the program entrypoint:
function main(args)
// args is an array of strings (argv[1..], without the program path)
if len(args) > 0 then
print args[0]
end if
return 0
end functionRules:
mainmust be declared at top-level (not inside anamespace).- Signature must be
main(args)(exactly 1 parameter). argscontainsargv[1..](arguments after the executable name), parsed with Windows quoting rules.- If
mainreturns anint, it becomes the process exit code. If it returnsvoid(no return), the exit code is0. - The entrypoint call happens after module initialization has executed. Imported modules are initialized automatically before the entry file continues, and all module-init blocks run at most once.
function fact(n)
if n <= 1 then
return 1
else
return n * fact(n - 1)
end if
end function
print fact(5)Native compiler:
- Lexical block scopes inside functions (variables are introduced on first assignment in the current block; shadowing is allowed).
- Functions are first-class values (you can store them in variables, pass them around, and call indirectly).
- Nested functions + closures are supported (captured vars are boxed and stored in an environment frame).
- Current limitation: shadowing of a captured name is rejected by the compiler.
- Reading a name that has never been assigned in any visible scope is a compile error (“undefined variable”).
- Writing to a global from inside a function requires an explicit
globaldeclaration.- Unqualified names resolve to the active
package/namespacecontext of the file. - If the global does not exist yet (no prior top-level initialization), the compiler creates it automatically and initializes it to
void. - Globals are keyed by fully-qualified name, so
package Bar+Fuis different frompackage Bar2+Fu.
- Unqualified names resolve to the active
global inside functions:
package demo
function inc()
global counter
if typeof(counter) == "void" then counter = 0 end if
counter = counter + 1
end function
inc()
inc()
print counter // 2You can also declare a qualified global explicitly:
function setOther()
global other.pkg.counter
other.pkg.counter = 123
end functionRobust syntax: trailing commas are allowed in global declarations:
function f()
global counter, total,
counter = 1
end functionNative compiler backend: supported.
struct Person
name
age
end struct
p = Person("Alice", 30)
print p.name
p.age = p.age + 1
print p.ageInline methods: You can also write function inline name(...) inside a struct to force full body inlining for direct calls (see 9. Functions).
You can define instance methods and static methods inside a struct.
- Instance methods get an implicit first parameter
this(the instance). - Access fields via
this.field. - Call instance methods via
obj.method(...). - Call static methods via
StructName.method(...).
struct Box
value
function show()
print this.value
end function
static function make(v)
return Box(v)
end function
end struct
b = Box.make(123)
b.show()Native notes:
- Struct constructors are calls:
Person(arg0, arg1, ...)(argument count must match the field count). - Field reads/writes are supported:
p.name,p.age = .... - The native backend currently has no exceptions: type errors typically evaluate to
void(reads) or become no-ops (writes).
Native compiler backend: supported (ordinal enums + optional explicit values).
Ordinal enums currently support up to 65536 variants per enum and up to 65535 ordinal-enum types in one program.
Basic form:
enum Color
Red
Green
Blue
end enum
c = Color.Red
print cEnum variants can optionally have = <constexpr> values (ints, strings, etc.). If a variant has no explicit value, the native compiler will:
- auto-increment by
+1if the previous value is anint, otherwise - require an explicit value (compile error).
enum Http
Ok = 200
Created // 201
Accepted // 202
NotFound = 404
end enumThe native compiler supports compile-time composition:
namespacegroups declarations under a qualified name.importmerges other.mlfiles into the program before code generation.
namespace geom
function add(a, b)
return a + b
end function
struct Point
x
y
end struct
end namespaceHow to use it:
- Calls / constructors can be qualified:
geom.add(1,2),geom.Point(1,2). - In the native compiler, namespaces are not runtime objects; they are only used to qualify symbol names.
import "path/to/other.ml"Module-style form (syntactic sugar):
import foo.bar // resolves to "foo/bar.ml"Example with an include root:
python mlc_win64.py main.ml out.exe -I srcYou can add multiple search roots by repeating the flag. The compiler also always treats the directory of the entry file as an implicit import root.
# repeat -I / --import-path (recommended)
python mlc_win64.py main.ml out.exe -I src -I std -I vendorNotes:
-Iis repeatable. The current CLI does not split platform path lists likesrc;std;vendorautomatically.
Rules:
- Paths are resolved relative to the importing file’s directory (absolute paths are also allowed).
- If the file is not found there, the compiler also searches the include roots in order: entry file directory (implicit) first, then the
-I/--import-pathdirectories (in the order provided). - If an import matches multiple files across the search paths, compilation fails with an ambiguous import error listing the matches.
- Diagnostics prefer short, stable paths (relative to the entry file directory) when possible.
- Imported modules remain declaration-oriented. At top-level (and inside
namespaceblocks) the supported forms are:package,import,namespacefunction,struct,enumextern function/extern struct- global
const(initializer must beconstexpr) - global assignments (runtime initializers are allowed)
- enum variants with explicit
= <value>must also beconstexpr
- Imported top-level global assignments are compiled as internal module initialization code. They run automatically before
main(args)and each module-init block runs at most once. - Side-effectful top-level statements other than global assignments are still rejected in imported modules (for example
print, top-levelif/while/for, or arbitrary expression statements). - Harmless import cycles are supported, and self-imports are ignored. Cycles that create unsafe cross-module initialization reads are diagnosed at runtime during module initialization.
import ... as <alias>is supported: it creates a compile-time alias for the imported module’spackagename, so you can write e.g.g.add()instead ofgeom.vec.add(). The imported file must declarepackage ....- Alias names must be valid identifiers and must not be reserved (
try,error). - If an imported file declares
package foo.bar, its location must match that package when resolved via a stable root (importing directory or-Iroot): the file should be found asfoo/bar.mlunder that root. (Absolute-path imports skip this check.)
A file can declare its package name once at the very top:
package foo.barThis is used by the native compiler’s import system (for import ... as <alias> and for verifying that a module’s file path matches its declared package when resolved via an import root).
Notes:
packagemust be the first statement in the file (beforeimport,namespace,function, etc.).- It is compile-time only (no runtime effect).
Imported modules may contain top-level global assignments such as:
package demo
players = [void, void, void, void]
count = len(players)These assignments are compiled into internal module-init code. The compiler/runtime ensures that:
- imported modules initialize automatically before
main(args) - each module is initialized at most once
- self-imports are ignored
- simple cyclic imports are allowed
- unsafe cross-module reads during initialization are reported instead of silently using half-initialized state
Top-level const still stays compile-time only:
const Answer = 42MiniLang ships with a source-based standard library in std/. You import it the same way you import your own modules:
import std.string as s
import std.time as t
import std.fs as fsThe stdlib is compiled together with your program (there is no separate link step). Most “systems” features are Windows-oriented because the native backend targets Windows x64.
Common modules (subset; evolves over time):
- std.core: small helpers (e.g.
min/max/clamp, …) - std.assert: assertions for tests and small programs
- std.string: string utilities (
trim,split,join,replaceAll, …) - std.bytes: bytes helpers (
concat,equals,ctEquals(constant-time-ish), …) - std.encoding.hex, std.encoding.base64: encoding helpers
- std.array, std.sort, std.random, std.math, std.fmt
- std.time: monotonic
ticks()/sleep(ms), Win32 wall-clock wrappersstd.time.win32.GetLocalTime()/GetSystemTime()(returnsSystemTime), plusDate/Time/DateTimehelpers - std.fs: file system & file I/O (see 13.3); plus basic directory helpers (
isDir/isFile/listDir/joinPath) - std.net: TCP/UDP networking
There is no separate std.result module anymore. MiniLang stdlib code uses the native error(...) propagation model plus try(...) where explicit handling is needed.
- std.ds.*: stack/queue/hashmap/set
Stdlib APIs that can fail (I/O, networking, parsing, …) use MiniLang's native error(...) system. In practice this means a function either returns its normal value or an error value that automatically propagates unless you intercept it with try(...).
import std.fs as fs
w = try(fs.writeAllText("demo.txt", "hello\n"))
if typeof(w) == "error" then
print "write failed: " + w.message
end ifLength of arrays, strings, or bytes.
print len([1,2,3]) // 3
print len("abc") // 3
print len(bytes(4)) // 4Native compiler behavior (current): unsupported types return 0 (no exceptions yet).
Creates an array with a fixed size and optional fill value.
a = array(5) // 5x void
b = array(5, 42) // 5x 42Invalid size (non-int, negative, or > 2147483647) returns a runtime error
(catchable via try(...)).
Reads one line from stdin.
name = input("Name: ")
print "Hello " + nameConverts string -> int/float (or returns numbers unchanged).
a = toNumber("123") // 123 (int)
b = toNumber("3.14") // 3.14 (float)
c = toNumber(10) // 10Native compiler behavior (current): invalid inputs return void (no exceptions yet).
Not allowed:
toNumber(true/false)toNumber(void)- non-parsable strings
Returns a string describing the type of x.
Type strings: int, float, bool, string, array, bytes, void, function, enum, struct, error, unknown.
print typeof(123) // "int"
print typeof("hi") // "string"
print typeof([1,2,3]) // "array"
// error values
err = error(2, "bad input")
print typeof(err) // "error"Returns a concrete type name for structs/enums.
- For struct instances (and struct constructor values), returns the struct name.
- For enum values, returns the enum name.
- For all other values, behaves like
typeof(x).
Note: typeof(x) intentionally stays coarse ("struct" / "enum") for backward compatibility.
struct Animal
name
end struct
enum Color
Red
end enum
a = Animal("Fay")
print typeof(a) // "struct"
print typeName(a) // "Animal"
print typeof(Color.Red) // "enum"
print typeName(Color.Red) // "Color"Constructs an error value (fields: .code and .message).
See Chapter 15 for full semantics (automatic propagation and try(...)).
Stops automatic error propagation for the given expression and returns either the normal value or the error value.
See Chapter 15 for full details.
Native compiler backend: bytes() / byteBuffer() supported. File I/O is provided via the stdlib module std.fs (see “File I/O” below).
Creates a mutable bytes buffer.
Native compiler backend (current):
-
bytes(size[, fill])andbyteBuffer(size[, fill])allocatesizebytes, filled withfill(default 0). -
bytes(...)supports additional forms:bytes()(empty),bytes(string)(UTF-8),bytes(list[int]), andbytes(bytes)(copy). -
byteBuffer(size)is a legacy alias (1 argument only). Usebytes(size[, fill])if you need a fill value.
buf = bytes(8)
print typeof(buf) // "bytes"
print len(buf) // 8
buf[0] = 255
print buf[0] // 255Decodes a byte buffer to a string.
- Accepts
bytes()(and legacylist[int]). - Honors
encodingusing Python's codec names (e.g."utf-8","latin-1", ...).
Native compiler backend (current):
- Expects a
bytesobject. - Treats the payload as UTF-8.
- If
encodingis provided it must be a string, but the value is currently ignored (UTF-8 only).
b = bytes(3)
b[0] = 65
b[1] = 66
b[2] = 67
print decode(b) // "ABC"
print decode(b, "utf-8") // "ABC"Decodes a bytes object as UTF-8, but stops at the first NUL byte (0x00).
Returns void on type errors.
Interprets a bytes object as UTF-16LE and stops at the first UTF-16 NUL (0x0000).
Returns void on type errors.
Typical use: converting wstr data coming from extern calls into a MiniLang string.
Encodes a bytes object as a lowercase hexadecimal string.
b = bytes(4)
b[0] = 0
b[1] = 17
b[2] = 170
b[3] = 255
print hex(b) // "0011aaff"Parses a hexadecimal string into a bytes object. Accepts an optional leading 0x / 0X prefix,
case-insensitive hex digits, and ignores common separators: spaces, tabs, newlines, _, -, :.
Native compiler behavior (current): invalid input returns void (no exceptions yet).
b = fromHex("00 11 aa ff")
print len(b) // 4
print hex(b) // "0011aaff"The stdlib module std.encoding.base64 provides Base64 encode/decode:
import std.encoding.base64 as b64
b = b64.fromBase64("SGVsbG8=") // bytes("Hello")
if typeof(b) == "bytes" then
print decode(b) // "Hello"
print b64.toBase64(b) // "SGVsbG8="
end ifNotes:
fromBase64(text)ignores whitespace and returnsbyteson success,voidon invalid input.toBase64(bytes)returns a string on success,voidon invalid args.
Returns a new bytes object containing a copy of length bytes starting at offset.
Rules (native compiler backend, current):
offsetandlengthmust be integers.offsetmay be negative (like indexing):offset < 0meansoffset += len(bytes).- Bounds are strict (no clamping): requires
0 <= offset <= len(bytes)and0 <= lengthandoffset + length <= len(bytes). - On any type/bounds error, returns
void.
b = fromHex("00 11 22 33 44 55")
print hex(slice(b, 2, 3)) // "223344"
print hex(slice(b, -2, 2)) // "4455"The native runtime currently does not expose low-level file-handle builtins.
File I/O is provided by the standard library module std.fs, with convenience helpers like:
writeAllText,readAllText,readAllLines,appendAllTextwriteAllBytes,readAllBytesexists,delete,fileSize,copyFile,moveFile
Most functions that can fail return either their normal value or error(...).
A few APIs return plain bool (e.g. exists, delete).
Example:
import std.fs as fs
import std.string as s
p = "hello.txt"
chk = try(fs.writeAllText(p, "hello\nworld\n"))
if typeof(chk) != "error" then
r = try(fs.readAllText(p))
if typeof(r) != "error" and s.startsWith(r, "hello") then
print "ok"
end if
end ifNative compiler only (for debugging / validating the runtime).
Returns the number of currently live heap blocks (objects that are not marked as free).
Returns the current bump pointer offset: heap_ptr - heap_base.
Note: after GC + optional shrink, heap_ptr may move backwards (trim-from-top).
Returns the currently committed heap bytes: heap_end - heap_base.
Returns the reserved heap address space: heap_reserve_end - heap_base.
Returns the total number of bytes in the free-list (sum of free blocks).
Returns the number of blocks currently in the free-list.
Runs the mark/sweep collector and returns void.
Notes (when does GC run?):
- The GC runs automatically when an allocation cannot be satisfied and the heap can’t grow further; the runtime triggers a
fn_gc_collectonce and retries the allocation. - You can also trigger it manually via
gc_collect().
Notes:
- The allocator reuses freed blocks via a free-list and falls back to bump allocation.
- If the bump pointer would exceed the committed end, the runtime commits more pages (up to the reserved limit).
- If
--heap-shrinkis enabled, the runtime may decommit unused pages at the top of the heap after GC (trim-from-top).
When compiling with --profile-calls, the compiler instruments user functions with call counters.
At runtime you can query them via callStats().
stats = callStats()
if typeof(stats) == "array" then
for each s in stats
// each entry is a small struct-like record; print it to inspect fields
print s
end for
end ifNotes:
- Without
--profile-calls,callStats()is not meaningful (and may returnvoid). - Instrumentation adds overhead; use it for profiling/debugging, not for release benchmarking.
The native compiler can generate PE imports from extern declarations.
Syntax:
extern function <Name>(<params...>) from "<dll>" [symbol "<exportedName>"] [returns <type>]Parameter forms:
<type>(type-only)<name> as <type>(named, type-checked)out <type>/out <name> as <type>(experimental, see below)
Supported ABI types (inputs):
int/i64/u64/i32/u32bool(acceptsboolorintat the call site)ptr(acceptsptr,int, orvoid;voidbecomesNULL)cstr(MiniLangstring→char*UTF-8;voidbecomesNULL)wstr(MiniLangstring→wchar_t*UTF-16LE;voidbecomesNULL)bytes(MiniLangbytes→ pointer to the payload;voidbecomesNULL)
Supported return types:
voidint/i64/u64/i32/u32/ptrboolcstr(reads a NUL-terminatedchar*and converts to a MiniLangstring;NULL→void)wstr(reads a NUL-terminatedwchar_t*and converts to a MiniLangstring;NULL→void)
Notes:
- Arity mismatches are a compile error.
- Type mismatches at runtime currently return
void(no exceptions yet). wstrarguments use a fixed temporary UTF-16 buffer. Very long strings may fail and returnvoid.- If the DLL or symbol can’t be resolved, Windows will usually refuse to start the program (loader error) because imports are resolved by the OS loader.
Example: MessageBox
extern function MessageBoxW(hwnd as ptr, text as wstr, caption as wstr, style as int)
from "user32.dll" symbol "MessageBoxW" returns int
MessageBoxW(void, "Hello from MiniLang!", "MiniLang", 0)Example: GetTickCount
extern function GetTickCount() from "kernel32.dll" returns u32
print GetTickCount()The frontend also accepts extern struct declarations to describe an ABI layout:
extern struct POINT
x as i32
y as i32
end structThis is intended for future interop features (e.g. passing/receiving structured data via pointers / out-params).
Current status: declarations are parsed and validated, but full marshaling support is still WIP.
You can mark trailing parameters as out:
extern function GetCursorPos(out p as POINT) from "user32.dll" returns boolRules:
outparameters must appear at the end of the parameter list (so they can be implicitly handled at call sites).- Current status: the compiler validates
outdeclarations, but code generation is still WIP.
MiniLang uses error values for lightweight error handling (no exception mechanism).
An error is a normal value with:
.code(int).message(string)
Use the builtin error(code, message):
return error(2, "bad input")You can also construct and return errors from within helper functions and stdlib code.
If a function call evaluates to an error value, the caller will automatically return that error immediately (as if an implicit return <that error> happened).
This continues up the call stack until the error is handled or it reaches top-level.
function parseInt(s)
// ... on failure:
return error(100, "not a number")
end function
function loadConfig(path)
// If parseInt(...) returns an error, loadConfig(...) returns it automatically.
port = parseInt("oops")
return port
end function
// If unhandled, an error that reaches top-level terminates the program.
loadConfig("cfg.txt")Some builtins intentionally return void to indicate failure/absence, e.g.:
fromHex(str)slice(bytes, off, len)decode(bytes, encoding)
If you prefer strict behavior, use the stdlib wrappers that return error(...) instead:
std.encoding.hex.decodeOrError(s)std.bytes.fromHexOrError(s)std.bytes.subOrError(b, off, len)std.bytes.decodeUtf8OrError(b)
Use try(expr) to stop the automatic propagation and get back either the normal value or the error value.
try(...) is a special form (its argument is evaluated lazily so it can intercept the propagation).
e = try(loadConfig("cfg.txt"))
if typeof(e) == "error" then
print "config error: " + e.message
else
print "config ok, port=" + e
end ifTypical pattern:
- call with
try(...) - check
typeof(x) == "error" - handle / recover, or re-
return xto propagate manually
The toolchain reports errors with:
- filename
- line/column (when available)
- the relevant source line
- a
^marker (when available)
ParseError(syntax / parsing)
CompileError(code generation / backend validation)
Example (schematic):
ParseError: unexpected token
at main.ml:3:10
x = 5 / ?
^
Statements are separated by newlines or ;.
print <expr>const <ident> = <expr>(native compiler; top-level/namespace requiresconstexpr)<lvalue> = <expr><ident> = ...<expr>.<field> = ...<expr>[<index>] = ...(multiline indexing allowed)
function name(a,b) ... end function(multiline params allowed, trailing comma optional)- (native) optional entrypoint:
function main(args) ... end function return/return <expr>/return;(and barereturndirectly beforeend/else/casein inline blocks)global x, y, z(inside functions; native compiler only; trailing comma optional; names may be qualified likefoo.bar.x)if <expr> then ... end if(block or inline)while <expr> ... end whileloop ... while <expr> end loop(legacy:loop ... end loop while <expr>)for i = <expr> to <expr> ... end forfor each x in <expr> ... end forbreak/break <int>continueswitch <expr> ... end switchstruct Name ... end struct(optional legacyareafter the name)enum Name ... end enum(optional legacyareafter the name; native supports optional= <constexpr>values)namespace Name ... end namespace(top-level or nested in namespaces; imported modules remain declaration-oriented, but top-level global assignments are allowed; native compiler)package foo.bar(top-level only; must be the first statement; native compiler)import "relative/or/absolute/path.ml" [as <alias>](top-level only; native compiler)import foo.bar [as <alias>](module-style import; resolves tofoo/bar.ml; native compiler)extern struct Name ... end struct(native compiler; experimental)extern function Name(...) from "dll" ...
- literals: number, string,
true/false,[ ... ](multiline + trailing comma allowed) - variable:
name - call:
f(a,b)(multiline args + trailing comma allowed) - index:
arr[i] - member:
obj.field - unary:
-x,not x,~x - binary:
+ - * / % == != > < >= <= and or - bitwise:
<< >> & | ^
Newlines are allowed after operators/unary operators and in common "list" positions (see 3.1).
for i = 1 to 30
if i % 15 == 0 then
print "FizzBuzz"
else if i % 3 == 0 then
print "Fizz"
else if i % 5 == 0 then
print "Buzz"
else
print i
end if
end forfunction sum(arr)
total = 0
for each x in arr
total = total + x
end for
return total
end function
nums = [1,2,3,4]
print sum(nums)struct User
name
role
end struct
u = User("Nina", "Admin")
switch u.role
case "Admin"
print u.name + " is admin"
end case
case default
print u.name + " is user"
end case
end switchenum Role
Admin
Guest
end enum
r = Role.Admin
print rThe Windows x64 native backend generates a PE32+ console executable.
What works:
- core types: int, float, bool, string, array, bytes, void
- control flow:
if/else,while,loop ... while ... end loop,for ... to,for each ... in,switch/case,break/break n,continue - first-class functions: user functions and many builtins are values; direct and indirect calls are supported
- nested functions + closures (captured vars are boxed and stored in an environment frame)
main(args)entrypoint (argv[1..] asarray<string>,return int-> process exit code)globaldeclarations inside functions (required for accessing globals from a function; resolves to package/namespace-qualified globals; missing globals are auto-created asvoid)struct(constructors + field read/write)enum(values likeColor.Red, comparisons, printing,switch)namespaceblocks (compile-time name qualification)package+import(compile-time multi-file merge; imported modules support runtime-initialized globals, self-import ignore, and harmless import cycles)const(write-once bindings; top-level/namespace consts are evaluated at compile time)enumexplicit values (constexpr) + auto-increment for missing int valuesextern functionvia the PE import table (IAT)- builtins / special forms:
len,input,toNumber,typeof,typeName,error,try,array,bytes/byteBuffer,decode,decodeZ,decode16Z,hex,fromHex,slice, plus debug helpers:heap_count,heap_bytes_used,heap_bytes_committed,heap_bytes_reserved,heap_free_bytes,heap_free_blocks,gc_collect
Debugging / listings:
--asmwrites a combined.asmlisting--asm-peprepends a PE header + section table dump--asm-dataappends.rdata/.data/.idatadumps (useful to inspect constants and imports)
Heap sizing flags:
--heap-reserve <size>: reserved address space--heap-commit <size>: initial committed bytes--heap-grow <size>: minimum commit growth step--heap-shrink: enable decommit after GC (trim-from-top)--heap-shrink-min <size>: minimum committed heap when shrinking
Optimizations (always-on, conservative):
- Constant pooling: identical
.rdataconstants are stored once and referenced by multiple sites. - Peephole optimization in the asm emitter (local rewrites only; no control-flow changes).
- Helper pruning: only referenced
fn_*runtime helpers are emitted.
GC flags:
--gc-limit <size>overrides the periodic GC threshold (default:1min the current backend).--no-gc-periodicdisables periodic GC triggering (GC runs only on allocation failure / OOM path).