DEV Community

Cover image for Compiling JavaScript Syntax to Bare-Metal x86 via LLVM IR
Jordan Kugler
Jordan Kugler

Posted on

Compiling JavaScript Syntax to Bare-Metal x86 via LLVM IR

Every day, modern servers burn through massive amounts of processing power just doing chores. In standard Node.js, whenever you handle large data streams, the system creates and destroys millions of tiny temporary files. That constant memory clutter leads to painful lag spikes while Node pauses to tidy up—or outright server crashes when it runs out of breathing room.

At the systems level, this raises an interesting question:

Hypothesis: Is JavaScript actually to slow to be a systems language, or are we just paying a massive tax for the engine, virtual machine, and memory cleanup running behind the scenes? Could you take JavaScript syntax, translate it directly into bare-metal machine code, and boot it straight onto raw hardware — with no runtime engine, no garbage collector, and no operating system standing in the way?

To answer this, my small team and I developed a brand new programming language Ohnrscript (.ohn) and associated kernel Ohn-Kernel - which we are very excited to make public tomorrow.

Ohnrscript Logo

Here is the engineering breakdown of how we built an AOT-compiled systems language and a 64KB HTTP-serving unikernel entirely out of JavaScript syntax.


1. Deconstructing the Myth: Is this "AOT Compiled TypeScript"?

Experiment Result: Busted.

Credit: Mythbusters

Ohnrscript is a lot of things, but it is certainly not an AOT wrapper around TypeScript (like AssemblyScript or Static Hermes). Systems compilers like AssemblyScript still bundle a runtime engine, Garbage Collector (GC), or Automatic Reference Counting (ARC) into the binary because standard JS/TS semantics demand dynamic heap allocation.

To be brief, we eliminated the concept of a runtime heap entirely. Ohnrscript operates strictly on 32-bit integer registers (i32) and physical memory word offsets.

In fact, the Ohnrscript compiler (ohnc) treats dynamic JavaScript features as hard compile-time errors:

  • Dynamic Objects ({ key: "val" }) $\rightarrow$ Rejected. Data must reside in flat, statically allocated TypedArray memory arenas.
  • 64-bit Floating-Point Numbers (1.5) $\rightarrow$ Rejected. Math utilizes bit-packed fixed-point arithmetic (Q26.6 format).
  • Dynamic String Concatenation ("a" + "b") $\rightarrow$ Rejected. Strings are byte arrays residing at fixed memory offsets.
  • Promises & Async/Await $\rightarrow$ Rejected. I/O is handled via direct hardware polling loops in Ring 0.
       [ Standard TypeScript Source ]
                      │
   ┌──────────────────┴──────────────────┐
   ▼                                     ▼
[ Dynamic Objects ]               [ Dynamic Strings ]
   │                                     │
   ✕ Rejected (No Heap)                  ✕ Rejected (No GC)
Enter fullscreen mode Exit fullscreen mode

Lexer & Parser Mechanics

Because standard TypeScript parsers (tsc or Babel) throw syntax errors on bare-metal constructs, we wrote a zero-allocation lexer and parser from scratch (compiler/src). Ohnrscript introduces explicit grammar for bare-metal memory and hardware manipulation:

  • slots[1024] buffer; — Custom token for static memory arena allocation.
  • 1.5fp — Q26.6 fixed-point numeric literal token (desugars to integer 96).
  • #'GET ' — 4-character string integer-cast token (desugars to little-endian 0x20544547).
  • ch'\n' — ASCII byte literal token (desugars to 10).
  • extern symbol; — Bare-metal FFI keyword for direct symbol binding.

Tooling Note: For IDE support, we built the official Ohnrscript VS Code Extension (ohnrscript-language-support), providing syntax highlighting, diagnostic linting, and Language Server Protocol (LSP) IntelliSense for .ohn files.


2. The Systems Synthesis: Why JS Syntax for Bare-Metal?

Why retain JS syntax for systems programming when C, Rust, or Zig exist?

┌────────────────────────────────────────────────────────────────────────┐
│                        THE TRADE-OFF MATRIX                            │
├─────────────┬──────────────────┬─────────────────┬─────────────────────┤
│ Language    │ Memory Control   │ Build/Header    │ Mental Overhead     │
│             │                  │ Overhead        │                     │
├─────────────┼──────────────────┼─────────────────┼─────────────────────┤
│ C           │ Manual (Pointers)│ High (Make/Hdr) │ Low Syntax, High Err│
│ Rust        │ Borrow Checker   │ Cargo Stack     │ High (Lifetimes)    │
│ Ohnrscript  │ Arena Offsets    │ Single Binary   │ Zero New Grammar    │
└─────────────┴──────────────────┴─────────────────┴─────────────────────┤
Enter fullscreen mode Exit fullscreen mode
  1. Bare-Metal Control with Zero C Boilerplate: You get C-like physical memory layouts without header file cascades, macro gymnastics, or complex build toolchains.
  2. First-Class Low-Level Sugars: Writing if (magic === #'GET ') replaces opaque byte masking like if (magic === 0x20544547). Expressing fixed-point scales as 1.5fp eliminates manual bit-shifting errors.
  3. No Borrow Checker Friction: Safety is enforced statically by banning dynamic heap pointers altogether rather than tracking complex reference lifetimes.

Is .ohn a valid systems-language: Confirmed

Credit: Mythbusters


3. Comparative Analysis: Processing Packets Across Runtimes

To test the paradigm shift, here is an identical workload across all three tiers: validating a packet header, scaling 4 audio samples by 1.5x, calculating a checksum, and emitting an ASCII status code.

1. Ohnrscript (.ohn) — Bare-Metal Execution

Static allocations, Q26.6 fixed-point math, register-mapped bitwise operators.

'use strict';

// Bare-metal FFI binding
extern vgaWriteChar;

// Static arena allocation (1024 i32 slots)
slots[1024] buffer;

// Q26.6 Fixed-Point Multiplication (Software FPU Bypass)
function multiplyFixedPoint(a, b) {
    let v1 = a >> 6;
    let s1 = a & 0x3F;
    let v2 = b >> 6;
    let s2 = b & 0x3F;

    let finalValue = (v1 * v2) | 0;
    let finalScale = (s1 + s2) | 0;

    while (finalValue > 33554431 || finalValue < -33554431) {
        finalValue = finalValue >> 1;
        finalScale = (finalScale - 1) | 0;
    }

    return (((finalValue & 0x3FFFFFF) << 6) | (finalScale & 0x3F)) >>> 0;
}

export function processPacketAndAudio(baseOffset) {
    // Read header word using 4-character integer cast
    let headerMagic = buffer[baseOffset >>> 2] | 0;
    if (headerMagic !== #'GET ') {
        vgaWriteChar(ch'E', 0x0F);
        return -1;
    }

    let volumeScale = 1.5fp; // Desugars to Q26.6 integer 96
    let checksum = 0;
    let sampleOffset = (baseOffset + 4) | 0;
    let i = 0;

    while (i < 4) {
        let wordIndex = (sampleOffset + (i << 2)) >>> 2;
        let rawSample = buffer[wordIndex] | 0;

        let scaledSample = multiplyFixedPoint(rawSample, volumeScale) | 0;
        buffer[wordIndex] = scaledSample;
        checksum = (checksum + scaledSample) | 0;

        i = (i + 1) | 0;
    }

    vgaWriteChar(ch'\n', 0x0F);
    return checksum & 0xFFFF;
}
Enter fullscreen mode Exit fullscreen mode

2. TypeScript (.ts) — Managed Execution

Dynamic interfaces, 64-bit floating-point math, heap-allocated array returns.

export interface PacketResult {
    success: boolean;
    checksum: number;
    samples: number[];
}

export function processPacketAndAudioTS(
    header: string, 
    rawSamples: number[]
): PacketResult {
    if (header !== "GET ") {
        console.log("E");
        return { success: false, checksum: -1, samples: [] };
    }

    const volumeScale: number = 1.5;
    let checksum: number = 0;
    const processedSamples: number[] = [];

    for (let i = 0; i < rawSamples.length; i++) {
        const scaledSample: number = Math.floor(rawSamples[i] * volumeScale);
        processedSamples.push(scaledSample);
        checksum += scaledSample;
    }

    console.log("\n");
    return {
        success: true,
        checksum: checksum & 0xFFFF,
        samples: processedSamples
    };
}
Enter fullscreen mode Exit fullscreen mode

3. JavaScript (.js) — Dynamic V8 Execution

Higher-order function iterations (.map), dynamic objects, runtime JIT coercions.

export function processPacketAndAudioJS(header, rawSamples) {
    if (header !== "GET ") {
        console.log("E");
        return { success: false, checksum: -1, samples: [] };
    }

    const volumeScale = 1.5;
    let checksum = 0;

    const processedSamples = rawSamples.map(sample => {
        const scaled = Math.floor(sample * volumeScale);
        checksum += scaled;
        return scaled;
    });

    console.log("\n");
    return {
        success: true,
        checksum: checksum & 0xFFFF,
        samples: processedSamples
    };
}
Enter fullscreen mode Exit fullscreen mode

4. Systems Design: Zero-Reflection AOT Serialization

Traditional web services spend significant CPU power inspecting object metadata at runtime. In Ohnrscript (examples/RegistrationPayload.ohn), decorators act as ahead-of-time byte layout generators rather than runtime metadata inspectors:

// RegistrationPayload.ohn (40-field music metadata schema)
@cbor
export class RegistrationPayload {
  _v: number;
  _t: string;
  title: string;
  artist: string;
  duration_ms: number;
  isrc: string;
  upc: string;
  p_line: string;
  c_line: string;
  primary_genre: string;
  // ... 30 additional fields

  constructor(_v, _t, title, artist, duration_ms, isrc, upc, p_line, c_line, primary_genre) {
    this._v = _v;
    this._t = _t;
    this.title = title;
    this.artist = artist;
    this.duration_ms = duration_ms;
    this.isrc = isrc;
    this.upc = upc;
    this.p_line = p_line;
    this.c_line = c_line;
    this.primary_genre = primary_genre;
  }
}
Enter fullscreen mode Exit fullscreen mode

The @cbor decorator compiles into an explicit static offset writer. When emitting binary serialization code, the compiler calculates exact byte positions during build time, outputting zero-copy LLVM IR instructions.


5. Self-Hosting: The Zero-Copy LLVM IR Generator

To prove language stability, the Ohnrscript compiler (compiler/src) is self-hosted—written completely in .ohn.

To prevent the compiler itself from allocating memory during AST traversal, generator-llvm.ohn explicitly bans string concatenation. All LLVM IR primitives are emitted as pre-allocated byte slices written directly to file descriptors:

// compiler/src/codegen/generator-llvm.ohn
// Zero-copy LLVM IR emission buffers

const LLVM_HEADER = Buffer.from('; Ohnrscript LLVM IR Backend\n\n');
const LLVM_DEFINE_I32 = Buffer.from('define i32 @');
const LLVM_FUNC_OPEN = Buffer.from(') {\nentry:\n');

// Alloca/mem2reg pattern: LLVM's mem2reg pass promotes these allocas 
// to true SSA registers — bypassing manual SSA phi node construction.
const LLVM_ALLOCA_I32 = Buffer.from('  %');
const LLVM_ALLOCA_SFX = Buffer.from(' = alloca i32\n');
Enter fullscreen mode Exit fullscreen mode

6. Empirical Stress Test: Benchmarks

We ran identical algorithmic workloads through standard Node.js V8 runtimes versus native Ohnrscript binaries compiled via LLVM -O3.

Benchmark Name Iterations Standard Stack (Node.js/V8) Ohnrscript (LLVM -O3) Performance Delta
AI Vector Processing (1536 floats) 100,000 194.86 ms (DataView) 3.50 ms 55.70x Faster
Cryptographic UUID (ohn-uuid) 5,000,000 5,438.36 ms 119.98 ms 45.33x Faster
614 MB Payload Parse 100,000 Fatal Crash (ERR_STRING_TOO_LONG) Success (0.55 MB Heap) Infinity (Bypassed V8 Limit)
AST Parser Allocation 100,000 22.7M Short-lived Objects 0 Objects Zero GC Sweeps

Test Environment: Benchmarks executed on Apple Silicon M2 Max and x86_64 Linux host (Ubuntu 22.04), single-core isolation via QEMU 8.0 & native clang -O3 -march=native. Full benchmark results available tomorrow at: benchmarking/BENCHMARK_RESULTS.md.


7. The Ultimate Test: ohn-kernel (A 64KB HTTP Unikernel)

For the ultimate test, we threw out the operating system entirely. We built ohn-kernel—a lightweight 64KB file that boots straight onto raw hardware and processes web traffic with zero OS beneath it, powered entirely by .ohn code.

┌─────────────────────────────────────────────────────────────┐
│                        POLL LOOP (Ring 0)                   │
│                                                             │
│  ┌─────────────────┐           ┌──────────────────────────┐ │
│  │  VirtIO-Net RX  │ ────────▶ │    TCP State Machine     │ │
│  │ Ring Descriptors│           │ HTTP Parser & Router     │ │
│  └─────────────────┘           └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

It implements:

  • PCI Bus Discovery & Bare-Metal Device Initialization
  • VirtIO-Net Ring Buffer Polling (No Interrupt Overhead)
  • Zero-Copy ARP & TCP State Machine
  • Embedded HTTP Router

It boots via Multiboot on QEMU/SeaBIOS with a baseline latency of sub-3 milliseconds (without any additional packages linked), answers incoming HTTP requests, and operates with zero OS kernel beneath it.

Is This Really Cool: Plausible

Credit : Mythbusters


Launch

We are opening the repository to the public tomorrow!

Whether you are interested in unikernels, LLVM backend architecture, zero-allocation network processors, or low-level systems programming, we'd love for you to explore the source code.

Top comments (1)

Collapse
 
lauren_puricelli profile image
Lauren Puricelli

For anyone wondering what this changes day to day: it's the difference between tuning a garbage collector and not having one to tune. Most of what we took out, we took out because we were tired of working around it.