What a browser cannot give you — and why that is an error, not a crash
Ring in a page is the same Ring: the same compiler, the same virtual machine, byte-identical output on roughly 850 verified programs. What changes is the world around it. This is the practical map of what your program loses, exactly how each loss behaves, and the one design rule that makes the difference between a caught error and a dead page.
The rule, first
Exclusion is an error, never a crash.
A Ring program that asks for something a browser does not have gets a catchable Ring error with a line number, and the VM — with every global, every class, every loaded library still in it — keeps running. Nothing in the verification corpus has ever brought the runtime down. That is the promise you can build on, and it is why the rest of this page is a table rather than a warning.
The map
| What your code asks for | What happens in a page |
|---|---|
write, remove, rename, tempfile |
fails the way an unwritable file fails — a trappable Ring error (the R35 family) |
read, load |
resolve against the embedded library map baked into the wasm; an unknown path is a trappable error |
fexists, getfilesize, getpathtype |
answer from that same map, so if fexists(f) … read(f) behaves as written |
direxists, directory listing |
always false, always empty — truthfully, because there are no directories to have |
system(), chdir, currentdir |
compiled out entirely |
| threads, sockets, RingQt and other bindings | not present — they are separate C extensions, never part of the core VM |
syssleep |
returns immediately; a page does not get to block |
clock(), time(), date() |
fully working, on the host's local clock |
random() |
works — and is unseeded, exactly as in native Ring |
iswindows(), filename(), pointer addresses |
answer truthfully for the wasm environment: 0, Ring_EmbeddedCode, and so on |
Three of those deserve a paragraph
fexists was a real gap, and a good lesson
Redirecting fopen to the embedded map is the obvious half of
having no filesystem. The half that is easy to miss: several Ring functions
ask about a file instead of opening one. fexists,
getfilesize and getpathtype go through
stat, not fopen — so for a while they reported
that embedded files did not exist, and the most idiomatic guard in Ring
skipped files that were sitting right there:
if fexists("ringlib/stzZql.ring") # said 0 — but the file is embedded
load "ringlib/stzZql.ring"
ok
The fix was to redirect stat to the same resolver, so the
question and the answer come from one place. The lesson generalises: when
you replace a capability, find every door into it, not just the one your
first test used.
random() repeats — for every visitor
Ring does not seed its generator, natively or here. On a desktop program run once a day that is a curiosity. On a page, it means every visitor gets the same sequence of numbers, in the same order, forever. If the randomness matters — sample data, a shuffled quiz, an id — seed it yourself:
nSeed = (clock() % 100000) + 1
for i = 1 to nSeed x = random(100) next # burn a variable prefix
Or, better in a page: ask JavaScript, which has a real source of
entropy, and pass the number in through ring.call.
Time works, and the timezone is done by hand
wasi-libc carries no timezone database, so a naive build reports UTC and
your clock is politely wrong for most of the planet. RingScript applies the
browser's own offset before handing the time to Ring, which is why
date() and time() agree with the clock in the
corner of the screen. It is a small thing that would have been a
long-running bug report.
Shipping your own library into the page
Because load resolves against an embedded map, a pure-Ring
library can travel into the browser unchanged. That is three steps:
# 1. drop mylib.ring into src/ringlib/
# 2. add one line to embedded_files in src/bridge.zig:
.{ .name = "ringlib/mylib.ring", .data = @embedFile("ringlib/mylib.ring") },
# 3. zig build serve
load "ringlib/mylib.ring" # now works in any page
One contract keeps this honest: embedded libraries stay pure Ring — no file or OS calls. That is exactly what makes them portable into a browser without edits, and it is why the ZQL engine, written years earlier for a filesystem, needed none. If you are writing a Ring library today and would like it to run in a page tomorrow, that is the whole of the discipline.
Writing code that runs in both places
The practical shape is a seam, not a fork. Keep the rules, the calculations and the data structures in plain Ring that touches nothing but values — that part is identical on the server and in the page. Put the things that differ, storage above all, behind two or three functions, and give each environment its own implementation.
A page's version of "save" is a call out to JavaScript; a server's is a file. The 300 lines in between never learn which one they are talking to, and that is the entire portability story.
The full compatibility statement, with the corpus behind it: docs/compatibility.md. What the API looks like from both sides: docs/api.md.