Releases: Louiml/Rak
Release list
Rak v0.8.4
0.8.4 — 2026-10-03
Security and performance release. This one should be read before upgrading, not
after.
Security
FFI pointer provenance — an arbitrary write anywhere in the process is closed.
ffi_write and ffi_read did this:
unsafe { *((ptr as usize + off) as *mut u8) = byte }with no check that ptr was ever allocated, and none that off was inside it.
Because ffi_ptr(n) builds a pointer from any integer,
ffi_write(ffi_ptr(ADDRESS), 0, 0x41) could write a byte anywhere the process
could reach. ffi_read was the matching read. This is the most severe item in the
security review, and unlike most of that list it was completely real.
A pointer is now provenanced if Rak allocated it (ffi_alloc,
ffi_string_to_cstr) or was told it owns a region (ffi_trust). Provenanced
pointers are range-checked on every access against the size recorded at allocation.
A pointer that is neither is refused by name, and the error says what to do about
it.
ffi_trust is the deliberate escape hatch. A language with FFI that refused every
foreign address would be useless, and resolving a symbol's address and then calling
it is legitimate work. What is not acceptable is an address that is silently
accepted, because that is indistinguishable from a safety check that always passes.
ffi_trust turns unchecked pointer arithmetic into an assertion written down in the
source — the same bargain unsafe { reason } makes everywhere else. It also
narrows: trusting 4 bytes of a 16-byte allocation makes an access at offset 4 fail
again, which is tested.
ffi_cstr_to_string was a denial of service by another route — it scanned for a NUL
byte with no bound, so a pointer to a NUL-free buffer walked off the end into
unmapped memory, reachable from a pointer that came out of a network response. It
is now bounded at 1 MiB.
ffi_read_i32 checks all four bytes, not merely that off is inside: off being
valid while the read runs three bytes past the end is still a read past the end.
The bounds arithmetic is done in u128, not u64, on purpose. ptr + off in u64
wraps, and a bounds check a large offset can defeat is not a check. There is a test
that specifically tries to defeat it.
Both backends share rak_stdlib::ffi::Allocations so the logic lives in one tested
place. ffi_trust is covered by the existing ffi_ capability prefix, so it needs
no new sandbox entry.
Existing ffi_alloc/ffi_free and legitimate access are unchanged and tested.
Performance
Constant folding. compile_expr had none. Every 2 * 3 became two LoadConsts,
an AddI and a push, on every evaluation — inside the innermost loop of every
numeric program. fold_int_binary now folds the literal-on-literal case for the
arithmetic and bitwise operators, and is deliberately narrow: both operands must be
literals; division or remainder by zero, out-of-range shift counts, and overflow all
fold to nothing so the runtime behaviour is unchanged.
rakc bench now measures something. The old version timed
rakc::eval(&source) against vm.run() and nothing else. That is not a backend
comparison: the interpreter figure included lexing, parsing and setup, the VM figure
was bytecode execution on an already-compiled chunk, and compile time was attributed
to nobody. It also used as_millis(), so anything under a millisecond printed
0 ms.
It now reports five phases — lex, parse, compile, interp, vm — each over --repeat
samples (default 5) after an unmeasured warm-up, as a median. The two execution
numbers are finally like for like, and the output labels them "execution only" so
they cannot be misread. On examples/bench.rak in a debug build it reports 6.61x,
which is a measured number where the README previously had a hand-written estimate.
A benchmark corpus. examples/bench/ has six workloads covering recursion,
arithmetic, arrays, map fields, strings and bytes.
Fixes
Three backend message divergences, all the same defect — the VM and the interpreter
described the same failure differently, so an error message told you which backend
you were on:
| expression | interpreter | VM (before) |
|---|---|---|
5 % 0 |
Division by zero |
rem by zero |
5 / 0 |
Division by zero |
div by zero |
n * x |
Undefined variable: x |
Undefined: x |
The first two were Rust's internal panic wording copied into hand-written Err
strings. The interpreter was itself inconsistent on the third (Undefined variable:
in two places, Undefined: in a third); both backends are now normalised to the
clearer form.
Upgrade notes
If you use FFI, you may need to add ffi_trust calls. Any ffi_write/ffi_read
through a pointer that did not come from ffi_alloc or ffi_string_to_cstr will now
fail at the point of use rather than corrupting memory silently — which is the
intended behaviour, but it is a behavioural change.
Release notes are generated from .github/release_body.md at tag time.
Rak v0.8.3
Rak v0.8.3
Cross-module variables that behave like Python's, byte-exact binary file I/O, a
for loop that survives a NUL byte, and fmt that understands a format spec.
About the version number
This release is tagged v0.8.3. The tags before it read v0.4.0 … v0.7.2 and
then switched to v8.0.0, v8.1.0, v8.1.1, so v0.8.3 sits numerically below the
8.x line it follows.
That is deliberate, and it has one consequence worth stating plainly: cargo
will read this as a downgrade from 8.1.1. A cargo update will move the dependency
backwards rather than forwards, and a lockfile pinning 8.1.1 will not move to
0.8.3 on its own. If you depend on Rak, take the commit rather than the tag, or
specify =0.8.3 deliberately.
The two commits this release contains are titled Rak v8.2.0, which was the number
in use when they were written. Rewriting them would have meant force-pushing already
public history to make the subjects agree with a number decided afterwards, which
seemed the worse trade. The titles describe the work; the tag names the release.
A for loop stopped at the first 0x00
The headline fix, and it is worse than anything else in this release.
let buf = bytes([0, 15, 16, 255])
let mut n = 0
for b in buf { n = n + 1 }
dump n // interpreter: 4 VM, before: 0
The VM's loop decided whether to continue by testing the element's truthiness
rather than the loop bound — IndexGet, then JumpIfFalse. Since 0, "" and
false are all falsy, iteration ended at the first of them. No diagnostic, and
silent, because the interpreter was fine and the divergence gate did not cover it.
0x00 is the most common byte in a binary file, so for b in buffer walked a buffer
and then stopped dead at the first NUL. It was found by writing a hex editor on top
of Rak: the engine's first buffer walk died partway through a test file, and the
cause was a language bug rather than anything in the editor.
Op::Len now makes the bound idx < len. A loop bound has to be a comparison
against a length, not a test of the value being carried.
It survived review because the one test covering byte iteration used bytes([1, 2]),
which has no falsy byte in it.
fmt takes real format specs
dump fmt("{:02X}", 5) // 5, before. 05, now.
dump fmt("{:#x}", 255) // 0xff
dump fmt("[{:>4}]", n) // right-aligned, width 4
dump fmt("{:.2f}", ratio) // two decimals, on both backends
The spec was matched by asking whether the text contained 04X, 08X, x or X.
Exactly two widths worked and no other type did, so {:02X} — the width a hex dump
wants — fell through to the default rendering.
The VM's copy had drifted further and had no float branch at all, so
fmt("{:.2f}", x) printed a rounded value on the interpreter and the raw float on
the VM. Two hand-written copies of the same chain is the underlying mistake; they are
now one shared parser (rakc/src/fmt_spec.rs), and rakc/tests/fmt_specs.rs runs a
table of specs through both backends and fails if they disagree.
The width counts the sign, matching Rust, Python and Go: {:05} of -42 is -0042.
Cross-module variables
import m used to bind a snapshot of the module's exports, so a pub let mut the
module reassigned never reached the importer. A module's own top-level let mut
reset on every call, because a module body ran in a scope on the importer's
environment and Env::clone deep-copies scopes — bump(); bump() gave 1, 1.
import m
m.X = v // writes the module's state, and only if it is `pub let mut`
from m import x as y // a copy, on both backends
The interpreter already did this. Five of the six documented VM differences are now
closed: a private top-level is no longer visible to the importer, two modules may
export the same name, from m import x copies with or without an alias, mod { }
blocks no longer collide, and reading a private or misspelled name through a handle
reports it by name instead of returning nil.
The last one is not fixed, and docs/V8-KNOWN-ISSUES.md says what it needs: a
module body still sees a name it never declared. Closing it requires a module
scope in the compiler, and the compiler has no list of builtin globals — the VM
registers those at startup — so it cannot tell len from a leaked name. Guessing
would break every module that calls a builtin. The test that covers it fails loudly
with "convert this to agree_on" if it ever closes.
Byte-exact binary file I/O
file_read is fs::read_to_string, so it fails on any file containing a byte
sequence that is not valid UTF-8, and write coerces through UTF-8, so a 0xFF
came back as U+FFFD. There was no way to open a binary file at all.
let buf = file_read_bytes("firmware.bin")
buf[0] = 0xFF
file_write_bytes("patched.bin", buf)
let m = mmap_open("firmware.bin", "rw")
mmap_write(m, 0x100, 0x90) // the mapping *is* the file
Plus bytes([...]), byte indexing, byte assignment, byte iteration, and buffer
concatenation. mmap_write refuses a read-only mapping and an out-of-range offset
by name, and a multi-byte write that would run past the end applies none of its
bytes.
Smaller things
Hexno longer renders padded.dump 0x00printed
0x0000000000000000on the VM. The value carried a digit width andDisplay
used it, but nothing ever set that width from the source — the compiler
hardcoded 64 — so all sixteen slots were used. The field had exactly one reader,
so it is gone.- A corrected claim. The known-issues entry said the two backends compared
Hexdifferently. They never did:PartialEqhas a cross-representation numeric
fallback andOp::Eqgoes straight through it. Retracted, with the evidence,
rather than left as a warning that would send someone hunting a bug that is not
there. :disdesynced by one byte after everyforloop, becauseIterItemshas
a 1-byte operand and was missing from the width table.- The VM's
lendid not accept a struct, which the interpreter's did. - The IDE version is pinned from the tag by
scripts/pin-ide-version.sh, which
has been run againstv0.8.3to confirm all three files take. v8.1.0 shipped
four Tauri bundles named8.0.0because that check did not exist.
Tests
505 workspace tests pass. module_state.rs is 33, fmt_specs.rs is 9,
bytes_io.rs is 11 — each running its cases on both backends and failing on any
disagreement. The tests that used to pin a divergence now assert agreement; the one
that is still real says so in its name.
The hex editor these primitives were built for is at
https://github.com/Louiml/HexEditor.
Rak v8.1.0
Rak v8.1.0
The release where the two backends stopped being two languages.
Why this release exists
Rak has shipped a tree-walking interpreter and a bytecode VM for several
versions, behind one frontend. Every builtin and every language feature has to
be implemented twice, and until now nothing checked that it was.
v8.0.0 shipped nineteen builtins that existed only in the VM. rakc run — the
default backend — failed with Unknown function for every one of them. The test
suite passed, because it exercised each backend separately and nothing covered
the new builtins on the interpreter side at all. A per-backend test suite cannot
catch a per-backend omission.
So this release starts by measuring. rakc::run_on_both runs a program on both
backends and classifies how they agree, and tests/backend_parity.rs gates on
that two ways: a structural check that compares the registration tables
directly, and behavioural tests that require identical output.
The measurement found the gap was 140 builtins, not the three
docs/rak-features-spec.md described. abs, sort, split, sum,
to_string and print were all missing from rakc vm. It could not run an
ordinary program.
After this release: 34. All 34 are blocked on the same thing, and that is a
real limitation rather than a list of forgotten registrations — see Known
limitations.
Backend parity
The gate
Nothing compared the backends before, so a builtin could be added to one and
forgotten on the other and the suite stayed green. Now it cannot:
registrations_matchreads the registration sites in both backends and
requires the name sets to match. Four shapes of registration are recognised,
each of which was a false positive that would have made the gate untrustworthy
if left: alternation arms ("regex_match" | "regex_is_match"),for name in [..]loops,vm_natives()tables, and the scope ofeval_builtinitself so
thatregexmethod arms are not mistaken for builtins.- Twenty behavioural tests run programs on both and require identical
output, which catches not just missing builtins but any disagreement. parity_backlog_reportprints the current gap, so the backlog is
measurable rather than remembered.
Closed
- Sets (spec 7A.11), on both backends:
set_of/add/has/discard/
len/has_all/union/intersect/diff/to_array, plusfor x in setand
x in set. - ~100 further builtins the spec never mentioned: math, strings, codecs,
arrays, JSON, HTML, files, zip, process/environment, and assertions. - Spec 7A.4 — VM streams: array, file-line, TCP-line,
map,filter,
take, CSV and JSONL, with a lazyfor-over-stream lowering. - Spec 7A.5 — VM
tunnelandudp_*. - Spec 7A.6 —
import pkg.subon the VM. - GUI — the VM had no GUI natives at all.
Sets are ordered, and the spec said they would not be
The spec proposed backing sets with Map, on the premise that Map already
iterates in insertion order. It does not: both backends store maps in
std::HashMap, whose order is arbitrary and differs between runs. A set built
that way would enumerate differently every time, which defeats the point for
deduplication and diffing.
So a set is an order-preserving Vec alongside a HashSet of element keys.
Insertion order for iteration, O(1) average membership, deterministic output.
1, 0x1 and 1.0 are one element; 1 and "1" are two.
Two bugs the harness found
- The backends derived different tunnel keys from the same passphrase. The
salt, iteration count and key length were inlined separately in the
interpreter and the compiler, and the two had drifted. Both looked correct.
They now come from one definition, and a test runs onetunnelthrough both
backends and compares the key. udp_recvreported a read timeout as an error on Windows andnilon
Linux, for identical code. A socket read timeout isEAGAINon Unix but
WSAETIMEDOUTon Windows, andstdsurfaces those as different
ErrorKindvariants. The implementation matched only the first. Both
variants are handled now, with a regression test.
GUI
The GUI was, in v8.0.0, a HashMap<i64, ()> that discarded the Window and
WebView it created. gui_update, gui_title and gui_close did nothing.
JavaScript could not call into Rak. Closing a window ended the process. Linux
did not work at all.
Now: one event loop on the main thread (tao binds to the display connection
there and rejects any other thread, which is why Linux failed), real handles
kept alive, a command channel from the interpreter's worker thread to the loop,
JS→Rak IPC through rak_call with results returned via rak_result, and
gui_quit(code) for the exit status. Closing a window no longer ends the
process; the loop stops when the program is finished with the GUI.
fn on_click(n) { return n + 1 }
gui_callback("clicked", on_click)
let w = gui_open("Demo", html, 600, 400)
gui_wait()
One real limitation: a callback gets a copy of the environment as it stood
at gui_callback, so it cannot write to a variable the main script later reads.
That follows from Rak having no reference types.
Inline assembly
asm reaches the CPU through a short list of read-only queries: CPUID feature
bits, rdtsc, rdtscp, the invariant-TSC frequency. Every one is a stable
core::arch intrinsic, so there is no hand-written machine code in Rak.
Three independent gates, all required: the asm capability (its own, not
folded into ffi or raw_sockets, because a capability granted alongside raw
would be granted by habit), an unsafe block with a written justification, and
a new inline-asm lint rule.
The operand is restricted to alphanumerics, so it cannot encode an arbitrary
byte string, and an unknown instruction is an error naming what is available.
Tests and CI
421 unit and integration tests, up from 399, plus 20 parity tests and an example
suite that runs every example on both backends.
CI now exists. Through v8.0.0 the release workflow built and published but
never ran a test — which is how the nineteen missing builtins shipped. ci.yml
runs on every push and pull request, on Linux and Windows, because the UDP
timeout bug existed precisely because the two platforms disagree and a
Linux-only test cannot see it. A separate job runs the hardening verifier's
self-test, so a bug in the verifier cannot silently make every release report
"all hardening checks passed".
Known limitations
The full list is in docs/V8-KNOWN-ISSUES.md. The two that matter most:
A function body without return evaluates to nil.
fn dbl(x) { x * 2 }
dump dbl(3) // [DUMP] nil
This is the most likely thing to bite you, because it looks like it works. It
reproduces identically on both backends and on the v8.0.0 tag, so it is not a
regression — and it is not fixed here, because changing it is a semantics
change rather than a bug fix, and it belongs with the v9 work. Use return in
every function body.
34 builtins exist only on the interpreter, and all of them are blocked on
one thing. A VM native has signature fn(&[Value]): no &mut Vm, no frame. So
a native cannot call a Rak function, suspend, or resume. That rules out
channel, select, timeout, await_all, task_group, the socket family,
spawn, and FFI trampolines. The fix is coroutines in the VM, which is a
project rather than a list of registrations. The parity gate fails on these 34
deliberately, so the gap cannot be forgotten.
Instrumented CFI is not available. The binaries are CFG-compatible with a
guarded dispatch table and CET shadow stacks, but call sites are not
instrumented, and the hardening verifier says so on every build. The spike is in
docs/CFI-SPIKE.md: it needs nightly, full LTO, a single codegen unit, a
rebuilt std, and a CFI-clean dependency graph — and it is not supported for
the Windows target at all.
Not planned: enclaves (use a container or a VM), and a prover (contracts are
checked at runtime, with an honest inconclusive result when the budget runs out
before the program does).
Upgrading
No breaking changes for rakc run programs. Two things to know:
import pkg.subnow works on the VM as well, so a program that only ran under
rakc runwill run underrakc vm.asmis new, and it is behind a capability, anunsafeblock and a lint
rule. It is interpreter-only; a bytecode VM has no instructions to escape
into.
Documentation
docs/V8-BACKEND-PARITY.md— the gate, what is closed, and the 34 with
reasons.docs/V8-KNOWN-ISSUES.md— behaviour you would not expect, led by the
implicit-return issue.docs/CFI-SPIKE.md— why instrumented CFI is not shippable here.docs/V8-ROADMAP.md— what v8.1.0 closed, and ownership and borrowing on
v9.0.0.
Rak v8.0.0
Rak v8.0.0
What's new in 8.0.0
Security primitives
unsafe "reason" { ... }blocks. Anything touching raw memory, FFI, or a
wire format can be wrapped in a block carrying a written justification. The
reason is enforced by the parser, so an unjustified exemption is a parse
error rather than a warning, andgrep unsafereturns every exemption with the
author's own words attached.rakc lint --auditprints the same list as a
review artifact, and the interpreter records which blocks were actually
entered during a run. This is a review boundary, not a permission: the block
executes exactly as written.requires/ensurescontracts onfn. Preconditions are checked with
the parameters bound and before a single statement of the body, so a violated
precondition blames the caller and a function that would corrupt state before
validating never gets the chance. Postconditions run after deferred cleanup,
withresultand the parameters in scope. Contracts survive intoasync fn,
on both the sequential and the concurrent drive path.rakc verify. Runs a script under finite step, loop-iteration and
recursion-depth budgets. It reports three outcomes and they are deliberately
distinct, because conflating them would be misleading:PASS(completed, all
contracts held),FAIL(a contract broke, an assert tripped, or a runtime
error), andSKIP(a budget was hit, so nothing was proved). Exit code 2 means
inconclusive, never "looks fine".- Constant-time comparison.
ct_eq,ct_eq_hex, andct_select. Rak's==
is a data-dependent branch that leaks the length of a shared prefix through its
exit timing, which is enough to recover a MAC or a session token one byte at a
time. - Zeroization.
zeroize(bytes)overwrites a buffer through a volatile path
the optimizer cannot elide.secret_deleteand the newsecret_delete_all
wipe values from memory and overwrite the store file with zeroes before
rewriting it. The serialized buffer in the secrets store is wiped too, and
file permissions are now set before the data lands rather than after.
Crypto
- RSA —
rsa_keypair,rsa_sign,rsa_verify,rsa_encrypt,rsa_decrypt
(PKCS#1 v1.5 over SHA-256, OAEP with SHA-256). Keys are DER. - ECDSA over NIST P-256 —
ecdsa_keypair,ecdsa_sign,ecdsa_verify, with
64-byter||ssignatures. DER keys.
The suite previously had only Ed25519 and X25519, both non-standard curves, so
nothing interoperated with ordinary OpenSSL tooling. Everything remains pure Rust
with no OpenSSL linkage.
Static analysis
- Six new security lint rules:
hardcoded-secret,plaintext-url,
weak-crypto,secret-compare,ffi-raw-pointer,insecure-transport, plus
unsafe-thin-reason. - This required fixing a real bug: the linter had no
Expr::Stringarm, so
string literals fell through to_ => {}and no rule could ever inspect a
byte of user text. A hardcodedsk-live-...was completely invisible. There is
a regression test for exactly that. rakc fmtandrakc lintare now listed inrakc --help.
Packet crafting
- ICMP —
net_raw_icmp,net_raw_icmp_ping,net_raw_icmp_echo_reply, with
matchingid/seqso a reply can be correlated to its request. - ARP —
net_raw_arp_request,net_raw_arp_reply, andnet_raw_arp_parse,
which accepts a frame with or without the Ethernet header and returnsnilon
anything that is not ARP, so raw capture can be fed straight in.
caps.rs previously gated a net_raw_icmp builtin that did not exist; the gate
now matches real builtins, and packet builders are documented as reachable
inside a sandbox because they only construct a bytes value.
Fuzzing on the stable toolchain
rakc fuzz <target>— a deterministic mutation loop in the compiler
itself, covering the lexer, parser, interpreter and the stdlib parsers. 12
targets, reproducible via--seed, with a crashing input written to disk and
the seed printed for replay.- The 13
cargo-fuzztargets infuzz/are still there for long
coverage-guided campaigns, but they need nightly and-fsanitize=fuzzer,
whichstable-x86_64-pc-windows-msvccannot provide. Nothing was running them
automatically before this.
Build hardening
- Release profile gains
lto = "fat",codegen-units = 1,panic = "abort"
andstrip = "symbols". The one that matters most isoverflow-checks = true:
Cargo's default in release isfalse, which silently wraps and turns a length
calculation into a heap overflow. .cargo/config.tomladds/guard:cf,/CETCOMPAT,/DYNAMICBASE,
/HIGHENTROPYVA,/NXCOMPATon MSVC, and full RELRO, a non-executable stack,
a stack canary and forced frame pointers on Linux.- Stated precisely, because these are easy to oversell.
/guard:cfmarks the
image CFG-compatible and guards the dispatch table, but rustc does not
instrument Rust's own indirect calls, so there is no__guard_check_icallin
the binary. This is not equivalent to Clang's-fsanitize=cfi. It closes a
real class of bugs; it does not close ROP or JOP. dist/verify_hardening.pyreads the produced binary back and asserts the bits
are really set, and both release jobs run it. It has a--self-testcovering
both the PE and ELF parsers on synthetic headers, positive and negative. This
already caught a real mistake: an early flag set passed both/NXCOMPATand
/NXCOMPAT:NO, silently disabling DEP.
Validating the hardening against real binaries
Both platforms were checked before tagging, by building the actual rakc and
reading the result back rather than trusting the flag list.
- Linux:
rakc(7.8 MB) built in WSL with the repo's exact
.cargo/config.toml. PIE, full RELRO, non-executable stack andBIND_NOW
all land correctly. The stack canary does not, and cannot be made to:
rustc'sx86_64-unknown-linux-gnutarget does not enable-fstack-protector,
and-C target-feature=+stack-protectoris rejected outright with "not a
recognized feature for this target". Getting a canary into Rust code needs
nightly-Z stack-protector, or a C object built with GCC's
-fstack-protectorand linked in. - So the verifier reports the canary as an advisory finding rather than a
required one. A check the stable toolchain cannot satisfy is either a
permanently red build or something everyone learns to ignore, and neither is
useful. The four checks that are satisfiable remain hard failures. - The same exercise turned up that
rustflagsis silently ignored when placed in
Cargo.toml("unused manifest key"). It only takes effect in
.cargo/config.toml, which is where it lives. A virtual manifest also rejects
a[target]section outright.
Deep recursion no longer kills the process
The tree walker burned several native frames per Rak call, and a Rak call costs
several KB of native stack in a debug build. fib(15) needed roughly a megabyte,
which is more than the default thread stack allows, so a moderately recursive
program died with a bare "has overflowed its stack" and no Rak-level line number.
The interpreter now runs on a thread with an explicit 64 MB stack. rakc verify
additionally bounds recursion depth, loop iterations and total steps, so runaway
recursion is a reportable outcome rather than a crash.
Tooling
- The VS Code TextMate grammar's builtin list was a single 2,900-character line,
close to TextMate's practical limits and unmaintainable. It is now 17 grouped
per-family patterns, the longest 371 characters. - The builtin lists in the LSP, the IDE editor, and the grammar were all stale
and disagreed with each other. All three are updated, and the LSP list had been
missing the entire batteries, OSINT, iterator, FFI, mmap andnet_rawfamilies. - New
docs/content/safety.md, covering what is enforced and — just as
importantly — what is not.
Correctness
docs/content/vm.mdclaimed binary pattern matching, method-call dispatch and
user enum patterns were unsupported on the VM. All three shipped; the table was
wrong.Pattern::Binddoes not exist: the@binding-pattern syntax the spec
advertised does not parse. Marked[SPEC]rather than[SHIPPED], with the
implementation steps recorded. Spec markers in both directions were wrong and
are now corrected against the code.expr_strfell back to the AST debug form for calls, indexing, field access
and ranges. Contract clauses and assertion failures are mostly calls, so the
most important diagnostics were the least readable. Now rendered as source.
Housekeeping
- All version strings moved to 8.0.0, including the REPL banner (still said
v0.3.0) andrakpkg(still said 0.7.0). raklib/was an empty untracked directory. Removed.adblocker/added to.gitignore: it is a nested git repository kept
deliberately local, and without thisgit add -Afails outright.scripts/run-tests-safe.ps1runs the test binary under a memory and time
watchdog. A stack overflow becomes a Windows Error Reporting event, and with
the system default of automatic memory dumps that writes a multi-gigabyte file.
Known limitations
Stated plainly rather than discovered later.
- The GUI is incomplete and Windows-only.
gui_update,gui_titleand
gui_closeare declared but do nothing:gui.rsstoresHashMap<i64, ()>and
discards theWindowandWebViewhandles. JavaScript cannot call into Rak —
the IPC handler receives the message and drops it. Closing a window ends the
process, because tao'sruncallsprocess::exit. Linux does not work at all,
becausegui_openbuilds the event loop off the main thread, which tao
rejects. The VM has no GUI natives, sorakc vmreports
Undefined: gui_open. The README and docs now say this instead of implying
otherwise. - **No ownership or...
Rak v0.7.2
Rak v0.7.2
What's new in 0.7.2
Debugger (DAP server)
- New
rakc dap— a Debug Adapter Protocol server so any DAP-capable editor/IDE can debug.rakscripts: set/remove breakpoints, step, step-over, finish, continue, inspect stack frames, scopes, and variables, and evaluate expressions live. - The VS Code extension ships a debug configuration (adapter type
rak, launchingrakc dap) — open a script, set a breakpoint, and press F5. Includes a matchingexamples/dap_demo.rak. - Fixed the DAP line-number mapping so client-side (1-based) breakpoints hit the correct VM (0-based) line.
stdlib batteries
- New built-in modules covering common tasks:
- Time —
time_now,time_parse,time_format,time_utc,time_elapsed, sleep helpers. - Random —
rand_int,rand_float,rand_uuid,rand_seed,rand_bytes. - Data formats —
csv_*read/write (RFC-4180),yaml_parse, JSON round-tripping helpers (newstdlib/src/datafmt.rs). - Archives —
gzip/gunzip,zip_archive/zip_list/zip_extract(newstdlib/src/archive.rs).
- Time —
- New gallery examples:
batteries_demo.rak.
OSINT pack
- Practical open-source-intelligence toolkit (new
rakc/src/ext_osint.rs,stdlib/src/{whois,ctlogs,yara,report}.rs):- WHOIS —
whois_lookup+whois_parsefor domain records. - CT logs —
ct_subdomainscertificate-transparency subdomain enumeration. - YARA-lite —
yara_scanover raw bytes (hex/string patterns,at/and/all of them/none of them). - Reports —
report_markdownstructured Markdown evidence output.
- WHOIS —
- New gallery examples:
osint_demo.rak,osint_pack_demo.rak, plus docs pageosint.md.
Language core
inoperator —x in collectionmembership tests for arrays, maps, strings, and streams.- Destructuring
let— bind multiple variables from an array/map in one statement. - Slice & negative indexing —
a[1..3],a[-1], array/byte/string slicing with safe bounds. - New docs page
batteries.md; README "What's new in 0.7.2".
IDE upgrades
- Autocomplete + syntax highlighting for every new batteries & OSINT builtin.
- Three new example scripts in the gallery (
batteries,osint,core_v072), each verified to run byte-identically on both the interpreter and the bytecode VM. - IDE + VS Code extension bumped to 0.7.2.
Installers & bundles
- Linux: portable
rak-idetar.gz, offlinerak-bundletar.gz,.deb, and.AppImage. - Windows: portable
rak-idezip, offlinerak-bundlezip, NSIS-setup.exe, and WiX.msi. - Standalone
rakc,rakpkg, andrak-setupbinaries for both platforms.
Bug fixes
- DAP: 1-based ↔ 0-based line translation on
setBreakpoints/stackTrace. examples/osint_pack_demo.rak: fixture keys now match the normalized WHOIS map; YARA demo uses a verified rule set; live network lookups commented so the demo stays hermetic.- Removed the redundant
core_v08.rakexample (duplicate ofcore_demo.rak).
Rak v0.7.1
Rak v0.7.1
This release pulls the language together. Rak now type-checks before it runs, mutability actually means something, and a few long-missing primitives (char, base literals) are first-class. Same installer, IDE, vscode-rak extension, and docs as 0.7.0, refreshed to match.
What changed
Static type checking
rakc check <file> now runs a real type-checking pass, not just lex+parse. It infers literal types, checks let x: T = value annotations and function-call arguments, and reports expected-vs-found types with the source line (error[E0308]). Numeric literals are mutually compatible; char and string are not, so let x: char = "hi" is caught before anything runs.
Mutability is now enforced
let x = 10; x = 20 is an error (cannot assign to immutable variable 'x'). Use let mut x to rebind. Enforced on both the interpreter and the bytecode VM, so the two backends agree.
char type
A first-class Unicode scalar, distinct from u8 and bytes. let c: char = 'א', plus \u{...} and \xNN escapes. Works across the interpreter, VM, and type checker.
Base literals
0b1010, 0o755, and underscore separators (1_000_000). All integer forms compare equal by value, so 0xA == 10 is true on both backends.
Generic functions
fn identity<T>(v: T) -> T, callable as identity<int>(42) or inferred identity(42). Type arguments are validated and then ignored; the runtime stays dynamic.
Enum variant patterns
match e { Event::Connect(host) => ... } with tuple and unit variants, on newline- or comma-separated arms. The checker flags non-exhaustive matches and repeated variant patterns.
defer
defer cleanup() runs in LIFO order when the function exits, on a normal return or a raised error. New Op::DeferCall makes the bytecode VM behave identically to the interpreter.
Test framework
rakc test [file] [--filter NAME] [--verbose] runs test "name" { ... } blocks. assert, assert_eq/ne/true/false, expect_error(fn() {...}), and panic(msg) all work. A failing test exits non-zero.
Synced tooling
The vscode-rak TextMate grammar, the IDE tokenizer/autocomplete, and the docs-site syntax highlighter all recognize the new keywords (defer, test, assert), the char type, \u{...} escapes, and 0b/0o literals. Full docs updated to match.
Full changelog
rakc check static type checker
char type Unicode scalar with \u{...} / \xNN escapes
let mut immutable bindings reject reassignment
0b / 0o base literals + underscores; 0xA == 10
fn f<T>() generic functions (explicit or inferred)
Event::Variant() enum variant patterns; exhaustive-match warnings
defer expr() LIFO cleanup in interpreter and VM (Op::DeferCall)
rakc test test blocks + assert / assert_eq / expect_error / panic
Install
See README for curl | bash (Linux) and iwr | iex (Windows), or grab rak-setup from this release. The Tauri installers (NSIS/MSI/.deb/AppImage) are also attached.
Rak v0.7.0
Rak v0.7.0
Upgraded compiler, new key features, IDE + installer fixes.
What's new in 0.7.0
Compiler & language upgrades
- Structured errors —
catch ebinds a first-classErrorvalue with akind, source span (file:line:col),cause, context map, and backtrace. New builtins:error,err_message,err_kind,err_line,err_col,err_file,err_cause,err_context,err_with_context(interpreter + VM). - True async concurrency —
await_all,select(race),timeout,task_group,async_sleep,async_yield, and deferredasync fnbodies running on a bounded worker pool (thousands of lightweight ops). Added the shared Tokio runtime (async_rt.rs) with a non-Tokio counting semaphore. - Streaming — pull-based
Value::Streamwithstream_from_array,stream_map,filter,take,stream_next,collect,read_lines, andtcp_stream; lazily consumed byfor. - CLI — optional
fn main(argv) -> intentry with real process exit codes, plusargv(),stdin_read_line,stdin_read_all,eprint, and theparse_args(spec, argv)flag parser. - Data processing — lazy
stream_csv(RFC-4180) /stream_jsonlparsers andparse_csv_line, plusgzip/gunzip/deflate/inflateandzip_archive/zip_list/zip_extract(newstdlib/src/stream_io.rs). - VM line mapping — the compiler emits a source line-marker per top-level statement into
Chunk.linesfor source-to-bytecode mapping.
rakc debug
- New bytecode-VM source debugger:
break,continue/c,step/s,next/n,finish,locals,stack/backtrace,frame,print <name>, and fulldisassemblewith line markers + operands.
rakpkg
- Now a lib + bin so
parse_manifest_stris reusable/fuzzable. - Version/rev constraints (
user/repo@^1.2,#rev),rakpkg.lock(resolved rev + manifest SHA-256 checksum), and new commands:update,lock,tree(cycle-safe dependency graph),audit,publish.
Fuzzing
- 8 proptest harnesses (stable CI) for the lexer, parser, DNS, packet builders, WebSocket, TLS, JSON, and tunnel framing (
rakc/tests/proptest_harness.rs). - A standalone libFuzzer / cargo-fuzz pack (
fuzz/, 13 targets: lexer, parser, eval, manifest, dns, tls, json, websocket, tunnel, netraw, csv, gzip, zip).
IDE + VS Code
- Fixed bugs in the IDE.
- Added the full v0.7.0 syntax vocabulary to the IDE editor (CodeEditor) and the VS Code extension (vscode-rak grammar, version 0.5.1 → 0.7.0): structured errors, async orchestration, streaming, CLI, and compression builtins, plus 9 new editor snippets (
fn main,await_all,task_group,timeout,stream,read_lines,stream_csv,parse_args, structured errors).
Documentation
- Restructured the docs: content is now split into markdown page files under
docs/content/*.md(based onREADME.mdandrak-features-spec.md), anddocs.htmlis now a lightweight markdown-driven viewer (dependency-free renderer + hash router) served by GitHub Pages.
Bug fixes
Rak installer (rak-setup)
- Fix PATH clobbering on Windows — the installer no longer overwrites a user's entire PATH. Previously a broken
reg queryparse could capture the registry type token andsetx /Mcould write the system PATH even for user installs, breaking unrelated CLIs (e.g. rustc/cargo). Now PATH edits are append-only on the correct registry hive (HKCU for user, HKLM for system),setxis removed, and aWM_SETTINGCHANGEbroadcast refreshes running programs. - Uninstall now removes only the rak PATH entry (instead of deleting the whole PATH value) and only recurses into rak-owned directories.
- Component selection is now an explicit multi-select on the first screen (space to toggle, enter to confirm), with
rakc+rakpkgpreselected. --installvalidates component names and rejects unknown ones.