jdBasic Boots on Bare Metal
There is no operating system under this. No libc either. GRUB hands control over and what runs is jdBasic, compiled to a freestanding ELF object.
Someone told me you cannot write an operating system in BASIC. I think you can if you want to.
What is in there
A screen driver, a keyboard with German layout, a full-screen editor with syntax highlighting, a RAM disk with a command shell, an interpreter, and a JIT that emits x86-64. Every line of it is jdBasic source, compiled with --target=kernel.
How little runtime a compiled program needs
I measured it before I built anything. A program with integers, a loop and a PRINT calls 13 runtime symbols. Add strings and arrays and it goes to 18.
Arithmetic, loops and function calls emit as machine code with no runtime call at all. Of those 18 symbols only two touch memory, and jdBasic has no garbage collector, so a bump allocator over a fixed region is the entire memory manager.
That is why the freestanding runtime is 200 lines of C. It writes to the VGA text buffer and mirrors everything to COM1. jdb_runtime.cpp with its 4000 lines never comes near the image.
The compiler side
--target=kernel overrides the target triple to x86_64-unknown-none-elf, picks the small code model with static relocation, marks every function noredzone, and stops after the object file instead of linking an .exe. About 80 lines behind a build flag.
Then the hardware needs to be reachable from the language. SYS.INB, SYS.OUTB and PEEK and POKE in 8, 16 and 32 bit. They lower to ordinary runtime calls, because a port access waits on the bus anyway.
The editor
200 lines, the full arrow block with Home, End, PgUp and PgDn, a status bar, and syntax highlighting. Keywords come out magenta and numbers yellow.
Lines live one per array slot, so typing a character rewrites one line and nothing else.
The JIT
? at the prompt interprets. ?? compiles the line to x86-64 in RAM and calls it.
> ?? 6*7
42 [40 bytes of x86]
> x = 5
> ?? x+1
6 [39 bytes of x86]
Those 40 bytes are mov rax twice, a push, the operand shuffle, imul and ret. x was set by the interpreter and compiled code reads it, because both sides share the same variable table.
It goes the other way too. Run a compiled loop, then ask the interpreter what the variable holds:
> ?? n = 0; while n < 10 { n = n+1 }; n
10 [128 bytes of x86]
> ? n
10
Functions get a real stack frame. Calls read their target out of a table in memory at call time, so a function can call one that is defined later, and two can call each other.
> ?? func fib(n) { if n < 2 { return n } return fib(n-1) + fib(n-2) }
> ?? fib(20)
6765 [242 bytes of x86]
Then a game
The RAM disk has a program on it when the machine boots. LOAD pong, COMP, CALL, and 3006 bytes of x86 later you are playing.
a and d move the paddle, ESC leaves and answers your score. It writes every character cell with pokew and polls the keyboard with inb(96), so the compiled code drives the hardware itself.
LIST shows you the source. It is written in the same language you type at the prompt, so change it in the editor, COMP, CALL.
The frame delay counts to 35 million, which is roughly 35 ms on real hardware. Compiled code is fast.
The numbers
The whole OS is about 1000 lines of jdBasic. Arrays and strings carry most of it. The German keyboard layout is three arrays filled from short strings, the editor keeps one line per array slot, and the RAM disk is two string arrays with the program text in one of them.
Ten scenarios run end to end in the test suite, from the first PRINT on bare metal up to playing the game, driven through the QEMU monitor.
Where it lives
kernel/ in the jdBasic repo, with a README that covers the commands, the editor keys and the language. Build with the KERNEL flag and link it with ld. QEMU boots the image.
Does the world need a new operating system? Not today. But it turns out BASIC was never the reason it could not have one.
jdbasic.org, source at github.com/AtomiJD/jdBasic.