You are browsing as a guest. Sign up (or log in) to start making projects!

2h 45m 26s logged

One interpreter, three hosts

Last devlog my language could define a weapon in my shipped game. Since then it
left the browser too, and the way out taught me more than the way in.

STRINGS WITHOUT LYING ABOUT NUMBERS

The C boundary only carried doubles. That was a deliberate choice: a number is
copied, so nobody has to ask who owns it. Then I tried to write a bank UI and
stopped in the first ten minutes. An IBAN is text. An error message is text.

The tempting fix is to pass a handle: “this double is really an index into a
string table”. I did not do it. A contract like that is invisible, and the first
time somebody misreads it the program moves the wrong money without a warning.

So strings travel BESIDE the numbers instead of inside them. args[] did not
change by a single byte, rs_host_fn kept its signature, and my already-shipped
game’s bridge kept working untouched. Text is pulled by the script, not pushed
by the host:

script:  iban = ui.input("iban")
host  :  reads the field name, returns the value

THE 64 KILOBYTE FLOOR

Then I asked whether the language could drive an animated web page, and measured
instead of guessing. 490,000 host calls per second. 200 elements updated twice
each = 1.45 ms, nine percent of a frame. Fine.

What was not fine: recursion died at depth 130. Not with an error - with memory
corruption. After it, the whole WebAssembly module was dead; I could not even
open a fresh VM on it. Emscripten’s default stack is 64 KB, and a tree-walking
interpreter spends several C++ frames per script frame. Adding -sSTACK_SIZE=8MB
moved the safe depth past 1000 and turned overflow into a catchable error. Cost:
the .wasm grew by three bytes.

Then a worse one. If a host function throws, the exception unwinds through the
interpreter’s frames without restoring the stack pointer, and that space never
comes back. I measured the drift: 5,000 escaping exceptions took the safe depth
from 1000 to 937. At 50,000 the module died. Fix: never let an exception cross
the boundary - catch it in the bridge.

THE THIRD HOST

Then JNI, and the language went into a Minecraft server. Same C header, a
different bridge, and the design copied deliberately: numbers in the array,
strings in the channel beside it.

The plugin is a custom enchanting table. Sixteen enchants, seven rarity tiers,
slot limits per rarity, conflicts, an XP cost curve, and the block of text drawn
on the item - all of it in one .rai file. The Java side is two classes that do
not contain a single enchant name. Stats are written into the schema my existing
combat plugin already reads, so enchants change real damage without one line
changing in the plugins that were already there.

Because there is no compile step, /rai reload changes the rules while players
are online.

A live server found a bug I had not: NamespacedKey only accepts [a-z0-9/._-] and
my enchant ids are camelCase. The bridge’s catch turned what would have been a
crash into a log line, and the log line named the key.

THE SAME BUG THREE TIMES

Native: depth 1000, then a silent death, exit code 127. WebAssembly: 130, then
corruption. JVM: 500, then the whole server dies with no Java exception and no
crash log. One cause - I recurse on the native stack and never count the depth.
Three hosts made it obvious in a way one host never would have. That counter is
next.

ON THE AI SPLIT

The core is mine: lexer, parser, resolver, interpreter, REPL, C API, the string
channel. AI has not touched src/ and I checked the history before writing this.
The host bindings and the demos are AI-written from my designs - the browser
binding, the JNI bridge, and three demo apps. Logged hours are my editor time
only.

0
12

Comments 0

No comments yet. Be the first!