marslang rs-0.19.1

Changelog#

What each release of the Rust interpreter changed, newest first. Each release also has its own page.

rs-0.19.1 — async and await, programs, and broadcast channels#

Programs can now do several things at once: async and await run tasks that wait without blocking each other, HTTP requests, sleeps, and the new std.process wait inside a task while the rest of the program goes on, and std.broadcast sends messages within a program, to the other Marslang programs on a computer, or across the network.

async and await#

mars
takepkg std.async;

async func slow(string name, float seconds){
    await async.sleep(seconds);
    ret name;
}

func m{
    out(await async.all(arr(slow("a", 0.2), slow("b", 0.2))));   // ["a", "b"]
}

Both tasks sleep at the same time, so this takes 0.2 seconds, not 0.4.

Waiting without blocking#

Inside a task, every std.http request, time.sleep, and std.process run waits on its own while other tasks go on. Five slow requests take as long as the slowest, not their sum. Outside tasks, nothing changes.

std.process#

mars
takepkg std.process;

func m{
    result = process.run(arr("git", "--version"));
    out(result.ok);   // true
}

run, run_in (another folder), run_with (text for standard input), check (raises unless the program succeeded), which, and set_timeout. There is no shell: every array element is one argument, so text from a user can never become a command.

std.broadcast#

mars
takepkg std.async;
takepkg std.broadcast;

func m{
    news = broadcast.channel("scores", "local");
    func listen(){ out(await news.receive()); }
    t = async.spawn(listen);
    news.send(map().set("player", "Ada"));
    await t;   // {"player": "Ada"}
}

Channels carry any JSON value, and there are three scopes:

Receiving is a task: receive, receive_for, on, and close.

Smaller changes#

Validation#

cargo test passes 501 tests on Windows, among them tasks under the collector, 1,000 concurrent tasks, and broadcasts between two running programs. Every push runs them on Windows, macOS, and Linux on x86_64 and ARM64, and runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2.

rs-0.19.0 — async and await, programs, and broadcast channels#

Not published: its Windows build crashed when a program ended with tasks still waiting. Everything below shipped in rs-0.19.1, with the fix.

Programs can now do several things at once: async and await run tasks that wait without blocking each other, HTTP requests, sleeps, and the new std.process wait inside a task while the rest of the program goes on, and std.broadcast sends messages within a program, to the other Marslang programs on a computer, or across the network.

async and await#

mars
takepkg std.async;

async func slow(string name, float seconds){
    await async.sleep(seconds);
    ret name;
}

func m{
    out(await async.all(arr(slow("a", 0.2), slow("b", 0.2))));   // ["a", "b"]
}

Both tasks sleep at the same time, so this takes 0.2 seconds, not 0.4.

Waiting without blocking#

Inside a task, every std.http request, time.sleep, and std.process run waits on its own while other tasks go on. Five slow requests take as long as the slowest, not their sum. Outside tasks, nothing changes.

std.process#

mars
takepkg std.process;

func m{
    result = process.run(arr("git", "--version"));
    out(result.ok);   // true
}

run, run_in (another folder), run_with (text for standard input), check (raises unless the program succeeded), which, and set_timeout. There is no shell: every array element is one argument, so text from a user can never become a command.

std.broadcast#

mars
takepkg std.async;
takepkg std.broadcast;

func m{
    news = broadcast.channel("scores", "local");
    func listen(){ out(await news.receive()); }
    t = async.spawn(listen);
    news.send(map().set("player", "Ada"));
    await t;   // {"player": "Ada"}
}

Channels carry any JSON value, and there are three scopes:

Receiving is a task: receive, receive_for, on, and close.

Smaller changes#

Validation#

cargo test passes 501 tests on Windows, among them tasks under the collector, 1,000 concurrent tasks, and broadcasts between two running programs. Every push runs them on Windows, macOS, and Linux on x86_64 and ARM64, and runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2.

rs-0.18.0 — match, SQLite, and secure tokens#

match replaces long if/elif chains, std.sqlite puts a real SQL database in one file, and std.secrets and std.uuid make tokens, passwords, and IDs that are safe to trust.

match#

mars
family Circle{
    func init(float r){ me.r = r; }
}

func describe(any value) => match (value){
    1, 2, 3 => "small";
    int n if (n > 100) => "big";
    string s => "text " + s;
    Circle c => "a circle";
    _ => "something else";
};

func m{
    out(describe(2), describe(500), describe("hi"), describe(Circle(1.0)));   // small big text hi a circle
}

Arms are tried top to bottom and the first that matches runs. Patterns are literals and lists of them, compared with ==; _; type patterns that bind a name, such as int n, string s, Circle c, or [int, float] n; and guards with if (...). As a statement, a value no arm matches does nothing; as an expression, it raises RangeError naming the value. Unreachable arms, repeated literals, and unknown types are compile errors with their line. Matching is faster than the if/elif chain it replaces. See match.

Compatibility: match is now a keyword, so a variable, function, or parameter named match must be renamed. obj.match as a member still works.

std.sqlite#

mars
takepkg std.sqlite;

func m{
    db = sqlite.open(":memory:");
    db.run("CREATE TABLE people (name TEXT, age INTEGER)", arr());
    db.run("INSERT INTO people VALUES (?, ?)", arr("Ada", 36));
    out(db.query("SELECT name, age FROM people", arr()));   // [{"name": "Ada", "age": 36}]
    db.close();
}

open a file or ":memory:"; run, query, query_one, value, last_id, script, and transaction, which rolls back when its function raises and uses savepoints when nested. Values go in only through ? parameters, so text can never be run as SQL. SQLite itself is built in.

std.secrets and std.uuid#

secrets.token_hex(16), token_url, password(20), int_below, choice, and same use the operating system's secure random source: use them, never std.random, for passwords, keys, and tokens. uuid.v4() makes random IDs and uuid.v7() time-ordered ones, with valid and version to check them.

Validation#

cargo test passes 395 tests on Linux and 393 on Windows. Every push runs them on Windows, macOS, and Linux on x86_64 and ARM64, and runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2.

rs-0.17.0 — Errors that say where, f-strings, tests, and big numbers#

The biggest release yet: runtime errors now show the line and the calls that led there, strings can hold values with f"...", programs can be tested with marslang test, integers can be any size, and eight new standard packages reach the network, the terminal, and the rest of the world.

Errors say where#

text
error: RangeError: random.int_between needs low <= high, got 6 and 1
    at roll (dice.mars:4)
    at Game.play (game.mars:7)
    at m (game.mars:14)

Every uncaught error prints its line and the calls that led there, innermost first, as file:line, which editors open with a click. Calls inside the standard library are left out, and recursion collapses to one line with its count. Compile errors such as unknown name 'nope' now say their line too, and the VS Code extension underlines the exact name.

f-strings#

mars
func m{
    name = "Ada";
    age = 36;
    out(f"{name} is {age}, and {age + 1} next year");   // Ada is 36, and 37 next year
}

Any expression goes between the braces; {{ and }} are braces. A string inside the braces uses the other quote: f"{scores.get('ada')}". See f-strings.

Tests: std.testing and marslang test#

marslang test runs every function named test_... in every *_test.mars file, each on its own: a failure shows the line of the assertion and the next test runs, and the command ends with status 1 when anything failed. std.testing has equal (which compares containers by contents), not_equal, is_true, is_false, close, raises, and fail.

bigint: integers of any size#

bigint(2) ** 200, bigint("123456789012345678901234567890"), and literals beyond longint are a new kind, bigint, with every operator. Ints and longints mix with it; floats need an explicit conversion; equal numbers of every kind are the same map key. json.parse reads huge integers as bigints. std.bigint adds gcd, lcm, factorial, integer square root, floor division, and bases 2 to 36.

New packages#

Package For
std.http Fetching pages, JSON APIs, and downloads over HTTPS, through proxies from the environment
std.format fixed(3.14159, 2), number(1234.5, 2) is "1,234.50", percentages, and tables that line up even with Chinese and emoji
std.terminal Colours and styles, progress bars, and the terminal's size, respecting NO_COLOR
std.log log.info(...) with levels and timestamps, to standard error or a file
std.hash SHA-256, SHA-512, SHA-1, MD5, CRC-32 of text and files, and HMAC
std.encoding base64, hex, URL encoding, query strings, and bytes
std.testing Assertions for marslang test
std.bigint Integer arithmetic on bigints

Smaller changes#

Validation#

cargo test passes 300 tests on Linux and 298 on Windows, including the new packages' own test files; HTTP is tested against a local server. Every push runs them on Windows, macOS, and Linux on x86_64 and ARM64, and runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2.

rs-0.16.0 — Random numbers, regular expressions, and raw strings#

Two new standard packages: std.random for dice, cards, shuffles, weighted picks, and repeatable seeds, and std.regex for finding, checking, replacing, and splitting text by pattern. Raw strings, r"...", keep backslashes as they are written, so patterns read plainly.

std.random#

mars
takepkg std.random;

func m{
    out(random.int_between(1, 6));                                         // a dice roll
    deck = arr("A", "K", "Q", "J", "10");
    random.shuffle(deck);
    out(random.sample(deck, 2));                                           // two different cards
    out(random.weighted_choice(arr("common", "rare", "epic"), arr(80, 15, 5)));
}
Function Result
float(), float_between(low, high) A float, from 0.0 or low up to, but never, 1.0 or high
int_between(low, high), int_below(count) A whole number, every value equally likely
bool(), chance(probability) true half the time, or with that probability
choice, choices, sample One item; several, repeats allowed; several from different positions
shuffle, shuffled A random order, in place or as a copy
weighted_choice, weighted_choices Picks in proportion to weights
gauss(mean, deviation) The normal distribution
seed(seed), Generator(seed) Repeatable numbers; a generator of its own

After random.seed(42), or a string seed such as random.seed("level-1"), a program makes the same numbers on every run and on every platform: the tests check exact sequences on Windows, macOS, and Linux, on x86 and ARM. random.Generator(seed) gives one part of a program numbers of its own, so a procedural world stays the same however many dice the rest of the game rolls.

The generator is xoshiro256++, Julia's default and the one behind Rust's SmallRng, with Lemire's method for ranges, so no value is favoured. It is the only native part; shuffling, sampling, weights, and the normal distribution are Marslang, in std/random.mars. It is not for passwords or keys.

std.regex#

mars
takepkg std.regex;

func m{
    out(regex.matches(r"\d{4}-\d{2}-\d{2}", "2026-10-12"));             // true

    date = regex.find_whole(r"(?<year>\d{4})-(?<month>\d{2})-(\d{2})", "2026-10-12");
    out(date.named("year"), date.group(3));                            // 2026 12

    for (tag, regex.find_all(r"#(\w+)", "#rust and #mars")){ out(tag.group(1)); }
    out(regex.replace(r"(\w+)@(\w+)", "ada@home", "$2:$1"));             // home:ada
    out(regex.split(r"\s*,\s*", "a , b,c"));                            // ["a", "b", "c"]
}

matches, contains, find, find_all, find_whole, replace, replace_first, replace_with (a function makes each replacement), split, and escape, with a Match for each match (its text, character positions, numbered and named groups) and Regex(pattern) to check a pattern once and pass it around. Patterns are compiled on first use and kept.

Matching is Rust's regex crate, which takes time in proportion to the text for every pattern: no pattern can hang a program, so patterns and text from users are safe. In return, backreferences (\1) and lookaround ((?=...)) are not supported, and say so. A pattern with a mistake raises a SyntaxError on one line that says where: regex "ab(c" at character 3: unclosed group.

Positions count characters, as len and slice do, so text.slice(m.start, m.end) is always the match, even after emoji.

Raw strings#

mars
pattern = r"\d+(\.\d+)?";      // the same as "\\d+(\\.\\d+)?"
folder = r"C:\Users\ada\new";  // \n is not a line break here

An r before the opening quote makes a raw string, in which a backslash is only a backslash: r"...", r'...', and r"""...""" over several lines. In an ordinary string a backslash before a character with no escape of its own stands for that character, so "\d" is just d, and a pattern written that way looks for the letter. See raw strings.

The VS Code extension 0.3.5 colours raw strings, and no longer marks the escapes \b, \f, \v, \xHH, and \u{...} as mistakes.

Validation#

cargo test passes 167 tests on Linux (20 unit, 146 execution, 1 memory) and 165 on Windows, including exact seeded sequences, fairness checks over thousands of draws, regex positions around emoji and combining accents, and every error message. Every push runs them on Windows, macOS, and Linux on x86_64 and ARM64, and runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2.

rs-0.15.1 — Security fixes#

Fixes found by a security review of the interpreter, its installer, and its release process. Everyone on rs-0.15.0 should update: writing a file could make a private one readable by every user of the machine.

file.write no longer exposes private files#

file.write replaces a file all at once, through a temporary file renamed over the old one. That temporary file took default permissions, so on Linux and macOS a private file such as a 600 .env full of tokens became 644, readable by everyone, after a single write. json.save, env.write, toml.write, ini.write, and csv.write all write through it.

The replacement now takes the old file's permissions, and its owner where the system allows. Writing to a link updates the file it points to and leaves the link in place, where before the link was replaced by a plain file.

Temporary files cannot be redirected#

The temporary file had a guessable name and was opened in a way that follows links, so another user of the same machine could plant a link where it would go and make your program overwrite a different file. It is now created exclusively, under an unguessable name, and never reuses anything already there. file.with_temp_dir folders are unguessable too, and on Linux and macOS only you can open them.

Deeply nested files no longer crash the interpreter#

A file with tens of thousands of nested parentheses overflowed the stack in marslang check and marslang symbols, which the VS Code extension runs on every file you open. One expression may now nest 1,000 levels deep and chain 10,000 operators in a row; beyond that, compiling stops with an error that says so, and compiling runs on the interpreter's large stack. No real program comes near either limit.

Safer installs#

Validation#

cargo test passes 154 tests on Linux (14 unit, 139 execution, 1 memory), including new ones for permissions, links, private temporary folders, and the nesting limits, and 152 on Windows, where the Unix permission tests do not apply. Every push runs them on Windows, macOS, and Linux on x86_64 and ARM64, and runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2.

rs-0.15.0 — Files, dates, and every Linux#

Programs can now work with files and folders through std.file, read and write configuration in CSV, .env, INI, and TOML, and handle the calendar and the clock with three new kinds: date, datetime, and duration. The Linux releases are now static binaries that run on every distribution.

std.file#

mars
takepkg std.file;
takepkg std.json;

func m{
    settings = json.load(file.here("settings.json"));
    settings.set("theme", "dark");
    json.save(file.here("settings.json"), settings);   // all or nothing
}

Reading, writing, checking, listing, copying, moving, and removing files and directories, in 28 functions. read_or covers a file that may not exist yet, each_line streams a file larger than memory, matching finds files by pattern ("**/*.mars"), and with_temp_dir gives a function a scratch directory that is removed afterwards, even when the function raises. here(name) finds a file beside the running program, however it was started.

Writing is all or nothing: the text goes to a temporary file that then replaces the old one, so a crash never leaves half a file. Every failure is a family you can catch on its own, such as file.NotFoundError, and every message names the path. remove_all refuses a drive root, the home directory, and the working directory.

Paths and file formats#

Package For
std.file.path Joining, splitting, normalize, and relative
std.file.csv CSV as rows or as records keyed by the header, with any separator
std.file.env .env files of KEY=value lines
std.file.ini INI files of [sections]
std.file.toml TOML 1.1, as Cargo and Python projects use it
std.json Gains load and save for JSON files

A mistake in a file names it and the line: people.csv: CSV line 4: 3 fields, but the header has 2.

date, datetime, and duration#

mars
takepkg std.time;

func m{
    left = date(2026, 12, 31) - time.today();
    out("days left:", left.days());

    meeting = datetime(2026, 9, 24, 14, 30, 0, "Asia/Shanghai");
    out(meeting.in_zone("America/New_York"));   // 2026-09-24T02:30:00-04:00[America/New_York]
    out(meeting + time.hours(1) + time.minutes(30));
}

They are values, like numbers: equal days are ==, < and sorting order them, and they work as map keys. A datetime is a moment seen in a time zone, with IANA names and daylight saving; two are equal when they are the same moment. A duration is an exact length, so ny + time.days(1) is exactly 24 hours later, while ny.add_days(1) keeps the time of day across a clock change.

std.time gains today(), now_datetime(), and days(), hours(), minutes(), seconds(), and milliseconds(), and sleep takes a duration. TOML reads and writes real dates; JSON writes them as ISO 8601 strings.

Linux, everywhere#

The Linux releases needed glibc 2.34 or newer, so they did not start on Debian 11, Ubuntu 20.04, RHEL 8, or Amazon Linux 2, and never on Alpine. They are now static binaries, for x86_64 and ARM64, and run on all of them. The ARM64 build is now tested on ARM hardware, where before it was cross-built and never run. A built-in copy of the time zone database is used where the system has none, as in minimal containers.

install.sh picks the static archive, and still installs older releases, which only have glibc ones.

Fixes#

Validation#

cargo test passes 151 tests (13 unit, 137 execution, 1 memory) on Windows, macOS, and Linux on x86_64 and ARM64. Every push also runs the static Linux binary inside Alpine, Debian 11, Ubuntu 20.04, Rocky Linux 8, and Amazon Linux 2 with tests/platform/check.mars, and checks that it is as fast as a glibc build. Every example on the new documentation pages was run against this build.

rs-0.14.1 — rs-0.14.0, released#

rs-0.14.1 is the release of everything in rs-0.14.0: std.json, std.os, std.sys, and the built-ins ord and chr. rs-0.14.0 was tagged, but its Windows build failed a test, so no binaries were published for it and the installers stayed on rs-0.13.0. Install rs-0.14.1.

Environment names on Windows#

The failing test found a real difference between platforms. Windows ignores case in environment variable names, and some Windows systems spell PATH as Path. os.env("PATH") found it either way, but os.environment() listed it under Path, so os.environment().get("PATH") was null on those machines.

On Windows, environment() now spells every name in upper case, as Python's os.environ does, so the same program works on every platform:

mars
takepkg std.os;

func m{
    out(os.environment().get("PATH") != null);   // true on Windows, Linux, and macOS
}

On Linux and macOS names keep their spelling, because there Path and PATH are two different variables.

Validation#

cargo test passes 134 tests (13 unit, 120 execution, 1 memory) on Windows Rust, on WSL-native Rust, and on GitHub's Windows and Linux runners. The test now sets a mixed-case variable itself, so it no longer depends on how the machine running it spells PATH.

rs-0.14.0 — JSON, the environment, and code points#

No binaries were published for rs-0.14.0: its Windows build failed a test. Everything below shipped in rs-0.14.1.

Programs can now read and write JSON with std.json, look at the machine they run on with std.os and at the interpreter running them with std.sys, and convert between characters and their Unicode numbers with the new built-ins ord and chr.

std.json#

mars
takepkg std.json;

func m{
    data = json.parse("{\"name\": \"Ada\", \"tags\": [\"math\", \"code\"], \"age\": 36}");
    out(data.get("name"), data.get("tags")[1]);   // Ada code

    data.set("age", 37);
    out(json.stringify(data));   // {"name":"Ada","tags":["math","code"],"age":37}
}

parse, stringify, pretty, and indented. Objects become maps that keep their key order. Whole numbers follow the rule for literals, so 3000000000 is a longint, and floats are written so that 1.0 reads back as a float.

Reading is strict, as RFC 8259 says, and every mistake is a SyntaxError that says where:

text
SyntaxError: JSON line 3, column 3: expected a value, found 'o'
SyntaxError: JSON line 1, column 7: a comma before '}': JSON allows no trailing comma

Values JSON cannot hold raise and say what to write instead: sets and pairs, family instances, infinity and NaN, maps with keys that are not strings, and a container that holds itself.

Reading and writing are native Rust: a 1.9 MB document of 15,000 records reads in about 70 ms and writes back in under 20 ms. Reading JSON one Marslang character at a time would also have been wrong, because a quote written directly before a combining accent joins with it into one character.

std.os and std.sys#

mars
takepkg std.os;
takepkg std.sys;

func m{
    out(os.platform(), os.arch());          // windows x86_64
    out(os.env_or("EDITOR", "nano"));
    out(sys.VERSION, sys.INT_MAX);          // rs-0.14.0 2147483647
}

std.os is the machine: env, env_or, environment, cwd, home, platform, arch, pid, and the path separator SEP. std.sys is the interpreter: VERSION, the ranges of int and longint, MAX_SAFE, executable(), package_dir(), and standard_packages(). A program's arguments and exit status stay in std.cli.

ord and chr#

mars
out(ord("a"), ord("中"), ord("😀"));   // 97 20013 128512
out(chr(97), chr(ord("A") + 2));      // a C

A character that is several code points, such as e followed by a combining accent, has no single number, so ord refuses it and names the parts:

text
RangeError: ord needs one code point, but "é" is 2 (U+0065 U+0301); take ord of each part

chr refuses numbers outside Unicode and the surrogate halves 55296 to 57343. See characters and code points.

Validation#

cargo test passes 134 tests (13 unit, 120 execution, 1 memory) on Windows Rust and on WSL-native Rust. Every example on the new documentation pages was run against this build.

rs-0.13.0 — Algorithms, statistics, and memory#

Three new standard packages, all reachable now that functions can be passed around: sorting and searching in std.algorithms, descriptive statistics in std.stats, and object identity in std.memory. @Decorator.private also applies to a package's own functions and families.

std.algorithms#

mars
takepkg std.algorithms;

func m{
    people = arr(Person("Ada", 36), Person("Grace", 45), Person("Alan", 41));
    for (p, algorithms.sort_by(people, func(Person p) => p.age)){ slout(p.name + " "); }
    out("");                                                          // Ada Alan Grace
    out(algorithms.transform(arr(1, 2, 3), func(int x) => x * x));    // [1, 4, 9]
    out(algorithms.counts(arr("fig", "pear", "fig")));                // {"fig": 2, "pear": 1}
}

sorted, sort_by, and sort_with are stable and O(n log n) for every input. sort_by sorts the keys with the interpreter's own sort and regroups, so the comparisons stay native and it runs about five times faster than a comparator sort. sort_with, which must run Marslang code for each comparison, finds runs that are already ordered, the way Timsort does, and insertion-sorts below 16 items, the threshold pdqsort and introsort use: an array that is already in order costs one pass, and no input drops it to O(n²).

Also binary_search, lower_bound, upper_bound, transform, keep, fold, min_by, max_by, any_of, all_of, unique, counts, and group_by.

std.stats#

mars
takepkg std.stats;

func m{
    marks = arr(2, 4, 4, 4, 5, 5, 7, 9);
    out(stats.mean(marks), stats.median(marks), stats.mode(marks));   // 5 4.5 4
    out(stats.stdev(marks));                                          // 2.138089935299395
    out(stats.quantile(marks, 0.25));                                 // 4
}

mean, median, mode, quantile, sum, variance, pvariance, stdev, pstdev, covariance, and correlation. Sums are compensated, so stats.sum(arr(0.1, 0.2, 0.3)) is 0.6 where adding left to right gives 0.6000000000000001. Variance is computed in two passes, which keeps its digits where the one-pass form loses them, and quantiles interpolate as NumPy and R type 7 do. Every result in the tests is checked against Python's statistics module and NumPy.

std.memory#

mars
takepkg std.memory;

func m{
    a = arr(1, 2);
    b = a;
    out(memory.same(a, b), memory.same(a, arr(1, 2)));   // true false
    out(memory.pointer(a));                              // array@0x1e248d94d30

    cycle = arr(); cycle.add(cycle); cycle = null;
    out(memory.collect());                               // 1
}

address and pointer say where a value lives, same whether two names hold one object, refs how many references keep it alive, and collect runs the cycle collector and reports what it freed. Numbers, booleans, and null live inside the value itself and have no address, which the error says.

There is no alloc or free: Marslang frees values by itself, so manual allocation would teach C's habits without C's reason for them. An address is for telling objects apart and for debugging, never for reaching back into memory, and it means nothing between two runs.

Private package declarations#

@Decorator.private used to be a compile error above a function or family outside a family. It now means the one thing it can mean there: the package does not export it.

mars
takepkg std.Decorator;

@Decorator.private
func check(array values){ ... }

func mean(array values){ check(values); ... }   // fine, same package

An importer asking for stats.check gets stats has no member 'check'. A leading underscore still does the same, and marslang symbols now reports each function's real access. @Decorator.subclass stays methods-only.

Validation#

cargo test passes 131 tests (13 unit, 117 execution, 1 memory) on Windows Rust and on WSL-native Rust. Every example on the new documentation pages was run against this build.

rs-0.12.0 — Functions as values, string methods, and std.cli#

Functions can now be written inline and passed around, strings gain the methods text processing needs, and programs can read their command line. The syntax is written down in full in the new grammar page.

Anonymous functions and closures#

func(type name) => expression makes a function without a name, and a func declared inside a block is a local function. Both close over the variables around them by reference, so a counter keeps its count:

mars
func apply(Function f, int x) => f(x);

func counter(){
    count = 0;
    func next(){
        count = count + 1;
        ret count;
    }
    ret next;
}

func m{
    double = func(int x) => x * 2;
    out(apply(double, 21));   // 42
    tick = counter();
    tick(); tick();
    out(tick());              // 3
}

Function and Family are parameter types for code that takes a function or a family as an argument; they are not allowed on variables. Inside a method, a closure keeps me and the family's private access. Closures made in a loop share the loop variable, as in Python; see functions as values.

Strings#

s[i] and a[i] read one character or element, counting whole characters, and strings gain iget(i) to match arrays. Indexing is read-only: change an array element with modify(i, value).

Method Result
split(), split(sep) Pieces around whitespace, or around sep
lines() The lines, without their line endings
strip(), lstrip(), rstrip() Without surrounding whitespace, or the given characters
upper(), lower() Changed case
replace(old, new) Every old replaced by new
find(x), rfind(x) The first or last position of x, or -1
contains(x), starts_with(x), ends_with(x) Booleans
mars
out("  name, age ,city  ".strip().split(","));   // ["name", " age ", "city"]
out("héllo👋"[5]);                               // 👋

std.cli#

Words after the program's file are now the program's arguments: marslang greet.mars --name Ada --loud. std.cli reads them with args(), parses --flags, --options, and positionals with cli.parser, prints a generated --help, and stops with an exit status through cli.exit(code). An exit passes every handle, but then blocks still run. See std.cli.

Integer literals beyond 32 bits#

A whole-number literal that does not fit in 32 bits is now a longint, so 3000000000 + 1 is 3000000001. It used to be an int that printed but raised int overflow in any arithmetic. Passing one to an int parameter still raises int overflow.

Fixes#

Validation#

cargo test passes 126 tests (13 unit, 112 execution, 1 memory) on Windows Rust and on WSL-native Rust. The memory test now covers cycles through closures.

rs-0.11.1 — A documented standard library#

Every public function, family, and method of the standard library now has a docstring, and marslang symbols describes the packages a program imports, so an editor can explain math.sqrt as well as your own names.

Docstrings in the standard library#

std.math, std.strings, std.containers, std.types, and std.time carry 111 docstrings between them, written from their reference pages:

mars
takepkg std.math;

func m{
    out(math.sqrt(2.0));   // hover: func math.sqrt([int,longint,float] x)
}                          //        Square root, as a float. A negative x raises RangeError.

A test now fails if a public standard-library declaration has no docstring, so new ones cannot be added without one.

marslang symbols describes imported packages#

The JSON gains packages: for each takepkg, the alias the program uses, the package's name, and its public functions, families with their public methods, and fixed/hot values with their types. Parameter names are shown as written. A variable made by a package's family, such as s = containers.stack();, is known to be a containers.stack.

Validation#

cargo test passes 118 tests (13 unit, 104 execution, 1 memory) on Windows Rust and on WSL-native Rust.

rs-0.11.0 — Docstrings, triple-quoted strings, and marslang symbols#

Functions, families, and methods can carry documentation, strings can span lines, and the interpreter can describe a program to an editor.

Triple-quoted strings#

mars
banner = """Marslang
  says "hello"
  to\tyou""";

A string in triple double quotes keeps its line breaks, indentation, and inner " characters; escapes still work. Lines after it keep their numbers, so an error further down still names the right line.

@Decorator.docstring#

mars
takepkg std.Decorator;

@Decorator.docstring("""
    A circle, measured from its radius.

    area() uses pi to five places.
""")
family Circle{
    @Decorator.docstring("The area inside the circle.")
    func area() => 3.14159 * me.r * me.r;
}

Decorators now work above top-level functions and families as well as methods. @Decorator.docstring takes one string and removes the indentation its lines share, so it can be indented with the code. It changes nothing at runtime. @Decorator.private and @Decorator.subclass still apply to methods only, and say so when written elsewhere.

marslang symbols#

marslang symbols file.mars prints, as JSON, every function, family, and method with its parameters, source lines, and docstring, and every variable with its scope and type: the annotation when there is one, otherwise the type its first value shows, marked as inferred. --stdin reads the source from standard input, so an editor can describe unsaved text. The VS Code extension uses it to show types and docstrings on hover.

Changed#

Validation#

cargo test passes 116 tests (13 unit, 102 execution, 1 memory) on Windows Rust and on WSL-native Rust.

rs-0.10.0 — std.Error#

The built-in error families now have a namespace of their own, and families are raised by value everywhere, never by name as a string.

std.Error#

mars
takepkg std.Error;

family ParseError(Error.Base){}

func m{
    run{ err(Error.RangeError, "too big"); } handle(Error.RangeError e){ out(e); }
    run{ x = 1 / 0; } handle(Error.Base e){ out(e); }       // RangeError: division by zero
}

std.Error exports Base, TypeError, RangeError, OutOfBoundsError, and SyntaxError. They are the interpreter's own families, not copies, so a handler for Error.RangeError catches a division by zero, and Error.TypeError is the same family as the bare TypeError, which keeps working without the import.

A family can extend one from a package#

family ParseError(Error.Base){} and family Timeout(net.NetworkError){} now work: a parent written as alias.Family is looked up in the packages the file imports. Before, only families declared in the same file and the bare built-in names could be parents.

Families are raised, not called#

err(TypeError, "message") raises; calling TypeError("message") now fails with a TypeError that says to use err. Previously it failed with "TypeError has no init and takes no arguments", and TypeError() with no arguments quietly built an error without a message. A family you declare with an init still constructs, and err(instance) raises it.

Changed#

Validation#

cargo test passes 114 tests (13 unit, 100 execution, 1 memory), with the new std.Error cases among them, on Windows Rust and on WSL-native Rust.

rs-0.9.1 — Line numbers in compile errors#

A patch release: syntax and import errors now say which line they are on, so an editor can put the error where it belongs.

Where an error is#

text
error: line 9: expected expression, got End
error: line 2: package 'shapes.circle' not found: looked for ...

Statements are parsed from normalized text, where one source line may hold several statements and one statement may span several lines, so positions were lost before parsing began. Each normalized piece now keeps the source line it started on, and a statement that fails reports that line, however deeply it is nested in functions, methods, loops, or run blocks.

A takepkg keeps its line as well, so a misspelled package, a missing one, a circular import, or an error inside an imported package is reported against the import that caused it.

Name resolution does not have positions yet: error: unknown name 'nope' says what is missing but not where. Runtime errors are unchanged.

Editor support#

vscode-marslang gives Visual Studio Code highlighting for .mars files and shows what marslang check reports as a diagnostic on the reported line. With this release, those diagnostics land on the right line; an error without a position is placed on the name it quotes.

Validation#

cargo test passes 112 tests (13 unit, 98 execution, 1 memory) on Windows Rust and on WSL-native Rust. The extension's tests run this interpreter over broken programs and check the line each error is reported on.

rs-0.9.0 — Installation and the user package directory#

Marslang installs without a Rust toolchain, and packages installed for a user are importable from every program that user writes.

Install#

install.ps1 and install.sh download the archive for the platform they run on, verify it against the release's SHA256SUMS, put marslang in a per-user directory, and add that directory to PATH. Nothing is compiled and nothing outside the home directory is written, so neither script needs administrator or root rights.

powershell
irm https://marslang.kevin-z.com/install.ps1 | iex
sh
curl -fsSL https://marslang.kevin-z.com/install.sh | sh

Both take the release to install (-Version / --version), the package sets to install (-Use / --use, from std and ext), the install directory, and a switch to leave PATH alone. std is built into the interpreter; ext is accepted, reports that extension packages are not published yet, and installs nothing.

.github/workflows/release.yml builds the archives when an rs-* tag is pushed: Windows x86-64, Linux x86-64 and AArch64, and macOS on both architectures. It runs the tests for every target it can execute, writes SHA256SUMS across the archives, and uploads them to that tag's release.

The user package directory#

A package that the program's own directory does not provide is now looked for in the user's package directory: MARSLANG_PKGS, or marslang_pkgs in the home directory. marslang pkgs prints the directory in use.

mars
takepkg greet;            // a program file, or a package installed for you

The program's directory is searched first, so a file beside the program always wins over an installed package of the same name, and installing something later cannot change a program that already works. The two never mix inside one package: a package found in the user directory runs its parents' init.mars files from there as well. A failed import lists every file that was looked for, in both directories.

Validation#

cargo test passes 112 tests (13 unit, 98 execution, 1 memory) on Windows Rust and on WSL-native Rust.

rs-0.8.1 — Access, type-check, and padding fixes#

A patch release fixing the four findings of the rs-0.8.0 review. std.Decorator now also lists the markers it provides.

Fixes#

Issue Now
A private method taken as a value could be called from outside its family Every call of a bound method is checked against the method running at that moment, including methods stored in fields or containers
A rejected insertion still converted and restricted the value being inserted An insertion checks all of a container's restrictions as one plan and applies it only if every part passes; a map set checks key and value together
Nested unions rejected shared containers that a different choice would have accepted Union choices are recorded while planning and retried depth-first when a later constraint conflicts (up to 64 attempts)
pad_start/pad_end could return fewer characters than requested The padded result is measured; a fill that merges with its neighbours (a lone combining accent, one flag letter) raises RangeError

Changed: a container's restrictions must all hold#

Every element must satisfy each of its container's restrictions as stored, so conflicting restrictions now raise TypeError and change nothing:

mars
x (array[int]) = arr(1);
y (array[float]) = x;      // TypeError: cannot satisfy both

Previously the alias converted x's elements to floats even though x promised int, leaving the array inconsistent with its own annotation.

std.Decorator lists its markers#

std/Decorator.mars now exports one value per marker, so @Decorator.NAME is accepted only for a name the package provides, an unknown marker reports the available list, and out(Decorator.private) prints private. Markers that the interpreter does not apply yet still report "not implemented yet".

Validation#

cargo test passes 111 tests (13 unit, 97 execution, 1 memory) on Windows Rust and on WSL-native Rust.

rs-0.8.0 — Private methods and reliability fixes#

This release adds private methods through std.Decorator and fixes the six findings of the rs-0.7.0 review, including two ways to crash the interpreter.

Private methods#

mars
takepkg std.Decorator;

family Account{
    @Decorator.private
    func _audit(string action){ out("audit " + action); }

    @Decorator.subclass
    func _limit() => 100;

    func deposit(int amount){ me._audit("deposit"); }
}

family Savings(Account){
    func limit() => me._limit();     // allowed
}

func m{
    Account()._audit("x");           // TypeError: _audit is private to Account
}

Fixes#

Issue Now
time.sleep(1e30) crashed the interpreter Raises a catchable RangeError; then blocks still run
An error whose message contained itself overflowed the stack when printed Prints <cycle>
A panic inside any native Rust function crashed the interpreter Becomes a catchable Error (defense in depth)
Shared containers could be partly changed by a failed type check Every change is computed first and written together; failed alternatives and annotations change nothing
strings.find/rfind could return a position past the end Searches match whole characters only
math.gcd(INT_MIN, 1) raised overflow Returns 1; gcd(INT_MIN, 0) still raises overflow
priority_queue.items() was missing Returns items in pop order without changing the queue

Changed: string searches match whole characters#

find, rfind, contains, starts_with, ends_with, split, and replace in std.strings only match at character boundaries, so a position from find always works with lenslice. A combining accent inside a character is not found on its own, and "\r\n" is one character, so split(text, "\n") does not split Windows line endings; use lines(text).

Validation#

cargo test passes 106 tests (13 unit, 92 execution, 1 memory) on Windows Rust and on WSL-native Rust.

Remaining work#

The remaining decorators, opaque native handles, a bytes type, program arguments, closures and function-typed parameters, match, file/JSON/random/regex packages, the per-user package directory and installer, and async remain pending.

rs-0.7.0 — Error handling, cycle collection, and new standard packages#

This release adds error handling, fixes the five findings of the rs-0.6.0 interpreter review (including a memory leak), and adds four standard packages.

Error handling#

mars
family ParseError(Error){}

func m{
    run{
        err(ParseError, "empty input");
    } handle(ParseError e){
        out(e);                     // ParseError: empty input
    } handle([TypeError, RangeError] e){
        out(e.message);
    } then{
        out("always runs");
    }
}

Memory and interpreter fixes#

Standard packages#

Package Contents
std.containers stack, queue, deque, priority_queue (lowest priority first, ties in push order); written in Marslang
std.math Now 51 functions: adds gcd, lcm, is_even, is_odd, div_floor, div_ceil, factorial, perm, comb, keeping the arguments' integer kind
std.strings split, join, lines, trim, find, replace, upper/lower, repeated, padding; positions count characters
std.types kind, family_name, is_instance, is_number
std.time now, monotonic, sleep

std.strings is named so that takepkg does not hide the built-in string() conversion. Native helpers live in std/rs/string.rs and std/rs/time.rs; rs.core gains family_name and is_instance.

Validation#

cargo test passes 98 tests (12 unit, 85 execution, 1 memory) on Windows Rust and on WSL-native Rust. The memory test measures allocations: no bytes remain after a run, and peak memory stays flat as a program creates more garbage cycles.

Remaining work#

Opaque native handles, a bytes type, program arguments, closures and function-typed parameters, decorators, match, file/JSON/random/regex packages, the per-user package directory and installer, and async remain pending.

rs-0.6.0 — Rust interpreter and packages#

Marslang is now an interpreted language. The JavaScript backend is removed: marslang file.mars parses, checks, and runs a program directly in Rust. Node is no longer needed to run programs or tests.

Interpreter#

Behavior changes from rs-0.5.0:

Packages#

takepkg is a package system, similar to Python's:

See Packages.

Standard library in Marslang#

std.math (42 functions, six constants) is written entirely in Marslang in std/math.mars, including argument checks, rounding, signs, integer pow, hypot, and interpolation. Only irreducible primitives are native Rust packages under std/rs/: rs.core (raise a named error, read a value's kind) and rs.math (platform float functions). Only standard packages may import rs.*.

build.rs registers every std/*.mars file as std.NAME and every std/rs/*.rs file as rs.NAME, so adding a standard package needs no list edits.

Validation#

cargo test passes 78 tests (9 unit, 69 execution, none ignored) on Windows Rust and on WSL-native Rust. Execution tests run programs in the interpreter and assert their output or runtime errors.

Remaining work#

A per-user package directory (such as C:\Users\<user>\marslang_pkgs) and an installer are planned. Wildcard imports, decorators, deque, further standard packages, match, run/handle/then, async, and self-hosting remain pending.

rs-0.5.0 — Initial math package design#

This release introduces the first Marslang-written standard-library package and runtime numeric type tracking. The math package is an early, provisional design: more functionality will be added, and its API may evolve.

std.math#

takepkg std.math; provides math.min, math.max, math.abs, and math.clamp. Explicit aliases and storing functions in variables are supported. The algorithms are written in std/math.mars, compiled by Rust, and bundled into the generated JavaScript with the shared runtime.

mars
takepkg std.math;
func m{
    out(math.clamp(12,0,10)); // 10
}

Arguments must have matching numeric types (int, longint, or finite float). Clamp includes both bounds and rejects reversed bounds. Integer overflow, incorrect argument counts, and invalid numeric arguments raise runtime errors. See the math API for details and current limits.

Dynamic typing and numeric behavior#

Unannotated variables can change type on reassignment. Annotated bindings retain runtime checks, and fixed still prevents reassignment. Numeric kinds survive function calls, returns, containers, family fields, and copies: 1.0 remains a float even when its value is integral. Numeric unions prefer the existing kind.

Arithmetic now uses runtime operand kinds, including through union-typed calls. Integer arithmetic checks overflow; float arithmetic retains float results. Mixed longint/float arithmetic requires an explicit conversion. Scalar equality and collection lookups retain their value-based numeric behavior. Fixed containers cannot have their numeric kinds changed through a differently annotated alias.

Migration: integer calculations that previously bypassed overflow checks through unannotated or union-typed values can now raise an error. Use longint or float explicitly when those numeric semantics are required. := remains excluded.

Repository and validation#

Remaining work#

More math APIs, decimal/fraction support, full filesystem modules/exports, other standard packages, decorators, and self-hosting remain pending. Infinity/NaN math semantics are deferred; this initial package requires finite float inputs.

rs-0.4.0#

The canonical Marslang source extension is now .mars.

Migration#

Rename your .mrs source files to .mars and update references in commands and scripts. The bundled files are now hello.mars, stdlib.mars, and docs/api/examples/strings.mars. Generated output still uses .js.

sh
cargo run -- compile hello.mars -o hello.js
node hello.js

The CLI accepts explicitly supplied source paths without enforcing an extension, so older filenames can still be compiled. CLI help, tests, and current API documentation use .mars. Historical release notes retain their original names.

Validation#

cargo test: 55 passing tests (7 unit and 48 integration), none ignored. The renamed bundled example compiles and runs under Node, printing 3 and 78.53975. The renamed Unicode API example is covered by an execution test.

Language behavior and the limitations documented in rs-0.3.0 are otherwise unchanged.

rs-0.3.0#

Rust bootstrap compiler release, 2026-09-18. Backend: JavaScript running on Node.

Core language and runtime#

Unicode strings and slicing#

String length, iteration, reversal, and slicing count extended grapheme clusters. Combining marks and joined emoji stay together. Node must provide Intl.Segmenter. String .reverse() returns a new string without changing the original.

String and array .slice(start,end) and .lenslice(start,length) accept an optional third boolean argument, including the slicing-only reverse=true form. Reverse slicing counts from the end and preserves the original order:

mars
func m{
    out("ABCDE".lenslice(0,3,reverse=true)); // CDE
}

Invalid bounds raise OutOfBoundsError. Zero-length slices are valid at the end. Incorrect argument types or counts raise TypeError.

Fixes and migration notes#

The bundled example now executes successfully. Regressions addressed include repeated assignment, comparisons mistaken for declarations, comment markers in strings, compact blocks, compound me expressions, and shadowed built-ins.

Explicit redeclarations in the same scope and undefined names are rejected. Programs relying on accidental JavaScript coercion or out-of-range array slicing must be updated. Array slices now use strict bounds rather than silently clipping or accepting negative indices. Array reversal still mutates the array.

Comma-separated function parameters from rs-0.2.0 remain required.

Documentation and validation#

The API reference covers syntax, strings, collections, built-ins, and errors, with a runnable example. Discussion documents are removed from the tracked tree and remain local; docs/api/ and docs/releases/ are exempt from the documentation ignore rule.

Validation: cargo test passes 55 tests (7 unit and 48 integration), none ignored. Execution tests run generated JavaScript under Node and cover the API example.

Remaining limitations#

Self-hosting, a unified lexer/parser with source locations, complete modules and exports, standard-library packages, decorators, deque, async, match, and run/handle/then remain pending. General named arguments are deferred. Unicode segmentation follows the host's Unicode/ICU version. The compiler still targets Node rather than a native runtime.

rs-0.2.0#

Rust bootstrap compiler release, 2026-09-16. Backend: JavaScript running on Node.

Breaking syntax change#

Function and method declarations now use comma-separated parameters with no trailing semicolon:

mars
func add(int a, int b) => a + b;

Migrate func add(int a; int b;) to the form above. Old semicolon signatures are rejected with a migration diagnostic. Commas inside bracketed parameter types remain part of the type. Trailing commas are not accepted. Zero-parameter forms and statement-ending semicolons are unchanged.

Included#

Validation and limitations#

cargo test: 15 passing tests, with 8 known regression cases explicitly ignored. Node must be on PATH, or selected through MARSLANG_NODE.

The broader language additions are documented targets, not implemented features: decorators (std.Decorator), maps/deque, additional loops, library packages, wildcard/relative module handling, and copy/immutability semantics remain pending.

Existing core gaps remain, including the bundled example's collection runtime failure, hot substitution, compound me expressions, and line-oriented parsing. The language contract and feature inventory are local discussion documents; they are not distributed in the current checkout. These release notes describe the rs-0.2.0 snapshot, not subsequent bug fixes.