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#
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.
- Calling and awaiting
- Calling an
async funcstarts a task and returns it at once. awaitwaits for a task, giving its result or raising its error. It works anywhere, inmand in ordinary functions too, so async does not spread through a program the way it does in JavaScript.
- Calling an
- The task kind:
done(),result(), andcancel(). Cancelling raisesCancelledErrorat the task's nextawait, so itsthenblocks run. - std.async:
sleep,all,race,timeout,settled,spawn, andyield_now. - End of the program and errors
- Tasks still running when
mends finish first. - An error that nothing awaited fails the program, rather than vanishing.
- Traces say which task was waiting:
at m (app.mars:11), awaiting load.
- Tasks still running when
- Cost: each task runs on its own stack, about 12–25 KB of memory. Synchronous programs are no slower.
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#
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#
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:
"local"reaches the same program."machine"reaches every Marslang program you run on this computer. It uses a folder only your user can open, and no background service, so other users cannot send or listen."network"uses UDP broadcast on the local network. Only use it when you mean it, since anyone there can read and send. Messages are limited in size, and malformed ones are dropped.
Receiving is a task: receive, receive_for, on, and close.
Smaller changes#
rs-0.19.0 was tagged but never published: its Windows build crashed when a program ended with tasks still waiting (a deadlock, or
cli.exitwhile tasks wait). rs-0.19.1 fixes that and is the first release with everything above.A block may open with its
{on the next line, as well as on the header's line, for every block.asyncandawaitare now keywords.takepkg std.async;still givesasync.sleepand the other functions.
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#
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.
- Calling and awaiting
- Calling an
async funcstarts a task and returns it at once. awaitwaits for a task, giving its result or raising its error. It works anywhere, inmand in ordinary functions too, so async does not spread through a program the way it does in JavaScript.
- Calling an
- The task kind:
done(),result(), andcancel(). Cancelling raisesCancelledErrorat the task's nextawait, so itsthenblocks run. - std.async:
sleep,all,race,timeout,settled,spawn, andyield_now. - End of the program and errors
- Tasks still running when
mends finish first. - An error that nothing awaited fails the program, rather than vanishing.
- Traces say which task was waiting:
at m (app.mars:11), awaiting load.
- Tasks still running when
- Cost: each task runs on its own stack, about 12–25 KB of memory. Synchronous programs are no slower.
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#
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#
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:
"local"reaches the same program."machine"reaches every Marslang program you run on this computer. It uses a folder only your user can open, and no background service, so other users cannot send or listen."network"uses UDP broadcast on the local network. Only use it when you mean it, since anyone there can read and send. Messages are limited in size, and malformed ones are dropped.
Receiving is a task: receive, receive_for, on, and close.
Smaller changes#
- A block may open with its
{on the next line, as well as on the header's line, for every block. asyncandawaitare now keywords.takepkg std.async;still givesasync.sleepand the other functions.
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#
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#
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#
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#
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#
- A statement can go on over several lines inside
( )or[ ], so long calls and arrays can be laid out; a bracket left open says where. - The installers find the latest release without GitHub's API, whose limit of 60 lookups an hour per address made installs fail on shared networks.
- Release binaries are stripped: about 30% smaller, even with HTTPS added.
types.family_name(RangeError)names a family itself.
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#
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#
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#
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#
install.shruns only once it has downloaded completely, so a connection dropped in the middle ofcurl ... | shruns nothing instead of half of the steps. It also refuses any download that is not HTTPS.Every archive now carries a signed build attestation, proving it was built by the release workflow from this repository and not changed since:
bashgh attestation verify marslang-rs-0.15.1-x86_64-unknown-linux-musl.tar.gz --repo marslang-project/marslangThe release workflow's build jobs, which run third-party build scripts, can no longer change the repository or its releases; only the final publishing job can.
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#
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#
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#
boolis accepted as a type annotation:func check(bool ok)used to raiseunknown type bool.
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:
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#
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:
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#
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#
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:
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#
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#
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#
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.
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:
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 |
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#
marslang lexunderstands triple-quoted strings and escaped quotes.- The README no longer describes the package directory as planned.
- Every push is now tested on Linux and Windows.
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:
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#
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#
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#
- Errors from reading the source now start with the line, like other compile
errors:
line 2: unterminated string starting at column 9instead ofunterminated string at 2:9, and an unclosed string is reported where it opens rather than where the file ends. - The decorator marker list is
docstring, private, subclass, static, class, overload.
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#
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#
rs.core.raisetakes a family,core.raise(RangeError, "message"), instead of its name as a string. It raises built-in families only; standard packages use it, and everything else useserr.- A family prints as
<family TypeError>rather than<func TypeError>.
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#
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.
irm https://marslang.kevin-z.com/install.ps1 | iex
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.
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:
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#
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
}
std.Decoratoris a namespace of markers written above family methods; importing it makes@Decorator.NAMEavailable (or@alias.NAMEwith an alias).@Decorator.private: only methods of the declaring family may call the method.@Decorator.subclass: methods of families inheriting from it may call it too.- Calls and bound-method values from anywhere else raise
TypeError. Mistakes such as a missing import, unknown markers, orprivatetogether withsubclassare compile errors.static,class, andoverloadare planned. std.containersnow marks its internal helper methods private.
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#
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");
}
}
- Errors are families.
Erroris the base;TypeError,RangeError,OutOfBoundsError, andSyntaxErrorinherit from it. Declare your own withfamily Name(Error){}. Errors have amessagefield and print asName: message. err(Family, message)raises;err(e)raises a caught error again.handle(Type e),handle([T1, T2] e), andhandle(T1, T2)(no name). The first matching handler runs; unmatched errors continue outward. Handlers also catch the interpreter's own errors, such as division by zero, and package errors byalias.Family.thenalways runs: after the body or a handler, while an error propagates, and on earlyret,break, orcontinue.lasterr()returns the most recently handled error, ornull.
Memory and interpreter fixes#
- Cycles are reclaimed. A cycle collector frees unreachable self-referencing data (an array containing itself, instances pointing at each other) while the program runs and when it ends. Previously every such cycle leaked.
- Family type annotations accept inherited families and compare declarations, not
names: a parameter typed
Parentaccepts aChild, and same-named families from different packages are distinct.alias.Familynames a package's family. - Union annotations no longer change a value while trying an alternative that fails.
obj.f(args)selectsfbefore evaluating the arguments.- Output is flushed before
in()/inln()read, so prompts appear; on a terminal eachout/sloutappears immediately. Output errors are reported. - The
anytype annotation accepts every value:func push(any item).
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#
src/interp.rsis a tree-walking interpreter over the resolved syntax tree;src/value.rsholds runtime values. The previousruntime.js.incsemantics are preserved: numeric kinds, checked integers, fixed containers, deep copies, grapheme-based strings, slicing, and the error kinds.- CLI:
marslang file.marsormarslang run file.marsruns a program;marslang check file.marsreports compile errors without running;marslang --version. Thecompile ... -o out.jscommand is removed. - Runtime errors print
error: <Kind>: <message>and exit with status 1. - Calls nested deeper than 10,000 levels raise
RangeError: maximum call depth exceeded. - The REPL shows only the output produced by each new line and discards lines that fail to compile or run.
- String segmentation uses the
unicode-segmentationcrate.
Behavior changes from rs-0.5.0:
- Numbers compare by exact value across every kind:
1 == longint(1)andlongint(5) < 10are true, and equal numbers share set elements and map keys. - Calling a function, method, or built-in with the wrong number of arguments raises
TypeError; reading a missing family field raisesTypeError. - A block function without
retreturnsnull. - Containers print readably, such as
[1, 2],{"k": 1}, andBox { v: 3 }. The display format is still not a stable serialization format. - Integer overflow of 32-bit operands reports
int overflow.
Packages#
takepkg is a package system, similar to Python's:
takepkg std.math;loads a standard package built intomarslang.takepkg util;loadsutil/init.mars(a package directory) orutil.marsfrom the main program's directory;takepkg shapes.circle;maps to subdirectories.takepkg .sibling;andtakepkg ..parent;are relative imports inside packages.- Importing
a.brunsa/init.marsfirst. Each package loads once. Packages export functions, families, andfixed/hotbindings; names starting with_are private. Circular imports are rejected.
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.
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#
.marsand.js.incfiles are excluded from GitHub Linguist language detection.- The public API reference documents the package and dynamic typing rules.
cargo testpasses 63 tests: 7 unit and 56 integration, none ignored, using WSL-native Rust with Windows Node. The toolchains use separate target directories.
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.
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#
- Structured expressions and lexical scope resolution: bare assignment updates
the nearest existing binding; annotations and
coldexplicitly declare locals. - Runtime type checks, checked signed 32/64-bit integers, noncoercing equality, identity-based container equality, and boolean short-circuiting.
- Fixed bindings, recursive container immutability through aliases, mutable deep
.copy()with cycle/shared-reference preservation, and hot constant substitution. while, iterable and three-partfor,break, andcontinue; snapshot iteration.- Working array/set/pair APIs and insertion-ordered
map()/dict()with null for missing keys, inspection methods, and typed collection mutation checks.
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:
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:
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#
- Updated parser, reference grammar, bundled examples, and syntax fixtures.
- Node execution tests plus malformed-parameter and nested-type parser checks.
- Language contract derived from the original prompts and later design decisions.
- Feature inventory, project review, and staged self-hosting roadmap.
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.