DEV Community

Cover image for HEXAW programming language for WebAssembly instead WAT
Peter Vivo
Peter Vivo Subscriber

Posted on Edited on

HEXAW programming language for WebAssembly instead WAT

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

TLDR;

With Cat Coding - my old cat Pitypang press the final enter as the documentary video show below.

HEXAW: a tiny time machine for WebAssembly, because hexaw is a brand new programming language for WASM binary based on my previous Z80, 8086 assembler experience. Hexaw is human first programming language, clients is every programmer, who is try to solve any problem in browser by WASM directly, by hand as we programming long time before. The shell program main goal you can see the hexadecimal code next to the assembler tokens. This is a great starting point to deeply understand what going under the hood in WASM word.

What I Built

I once wrote a Z80 game on a Videoton TV Computer. I still miss typing an instruction and seeing the exact bytes it becomes. As I wrote in this old post

WebAssembly brought back the possibility. WAT brought me a parenthesis jungle. My brain wanted an assembler listing.

So I started building HEXAW: a new, experimental text format for WASM, with readable mnemonics and a retro assembler feeling.

It is still totally beta. The syntax and tooling are evolving. Expect rough edges.

The CLI can assemble HEXAW into .wasm, dump binaries back into readable instructions, and step through code forwards and backwards. There is also an AI mentor you can ask when the bytes stop making sense.

Demo

HEXAW is also on npm. You’ll need Node.js 22.18 or later. Save the example below as factorial.hexaw, then:

npm install -g hexaw
hexaw -i factorial.hexaw
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

The AI mentor needs a Groq API key. Put it in a .env file in your working directory:

GROQ_API_KEY=your_groq_api_key
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

Load that file explicitly when starting the shell. On Linux/macOS:

node --env-file=.env "$(command -v hexaw)" -i factorial.hexaw
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

Without a key, assembling, dumping, and debugging still work. Only the LLM help needs it.

or clone the repo

Clone the repository, run npm ci, and save the example below as factorial.hexaw in the repository folder. Then:

npm run shell -- factorial.hexaw
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

Inside the shell:

CALL factorial 5
G
BACK
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

That computes 5! = 120, then reverses the last execution step.

The shell also has an SST-inspired A mode: type an instruction, press Enter, see its hex bytes, and continue at the next address. An empty Enter applies the completed draft.

Code

Here is the factorial loop:

EXP "factorial" 0
FN 0
  PAR 0       ; n
  RES
  LOC 1       ; result

  CONST 1
  SET 1
  BLOCK
    LOOP
      GET 0
      EQZ
      BRIF 1  ; leave the outer block when n = 0

      GET 1
      GET 0
      MUL
      SET 1

      GET 0
      CONST 1
      SUB
      SET 0
      BR 0    ; repeat the loop
    END
  END
  GET 1
END
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

The default numeric type is i32. Prefixes L, F, and D select i64, f32, and f64. This is still WASM: a typed stack, locals, and structured control flow.

The dump keeps the bytes beside the instructions:

00000032 : 20 00 : GET 0
00000034 : 45    : EQZ
00000035 : 0D 01 : BRIF 1
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

Source: https://github.com/Pengeszikra/hexaw

How I Built It

I built it with Codex, using my TS+|> TypeScript fork for the CLI and a shared assembly/debugging engine.

The mentor uses the open-weight gpt-oss-120b model through Groq, carried over from my Terminal Is Your Friend project. Its teaching material now focuses on HEXAW and WASM, grounded in the actual opcode table and a limited snapshot of the current code, assembly draft, VM state, and recent errors.

With GROQ_API_KEY set before starting the shell, I can ask:

// What does BRIF 1 mean here?
AI ERROR
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

The reply arrives while I keep working. Suggested code stays an example until I enter it myself.

Why Does Open Innovation Matter?

I can change the language, the compiler fork, and the mentor’s teaching material. Anyone else can too: HEXAW is MIT-licensed.

An open-weight model leaves a future route to self-hosting. Today the mentor uses Groq’s hosted API; the assembler and debugger work locally.

The Lazy Payoff

Less manual byte decoding. Fewer trips between code, documentation, and a separate AI chat.

Apparently I am lazy enough to want all of that in one terminal. Just not lazy enough to leave assembly alone.

Prefix for simplicity

How I can simplify this language with prefixes?
My intentionally simple:

Default type is i32
Prefixes:
L = i64 (long)
F = f32 (float)
D = f64 (double)
So:

ADD → i32.add
LADD → i64.add
FADD → f32.add
DADD → f64.add
No numbers in mnemonics.
No brackets.
No special characters unless absolutely needed.

finnally the craziest language: WAT

Let's look this really basic example:

for brace lovers only!

(module
  (func (export "add") (param $first i32) (param $second i32) (result i32)
    ;; Push the $first and $second parameter value on to the stack
    local.get $first
    local.get $second

    ;; Pops the last two items off the stack, add them together, and
    ;; pushes the result onto the stack
    i32.add
  )
)
💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด 💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด

Top comments (11)

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
lakhyajit profile image
Lakhyajit Changmai •

The byte-alongside-instruction dump is the part I would actually use, and for a specific reason.

I ship WebAssembly in production - an ONNX model running in the browser for background removal - and the debugging story for browser wasm is not really a syntax problem. When it goes wrong it is almost never "I cannot read the disassembly", it is "linear memory grew, a threaded init broke because the page is not cross-origin isolated, or an allocation path in generated code did something I did not predict". At that point a WAT pane in devtools is close to useless, because the address disappears the moment you scroll away from the line.

That is why 00000032 : 20 00 : GET 0 is a better artefact than a prettier disassembler: the address lives in the line, so a bug report can quote one line and know it points at exactly one instruction. A debugger affordance that survives copy-paste is worth more to me than nicer mnemonics, and the BACK step is the other half of that - it is the only part of wasm inspection I have ever wanted and never had.

To the "why not target a wasm-native language" question in the comments: in the browser the parts that eat the time sit below the language layer. Memory can only grow, never shrink, so one large file leaves that RAM unavailable for the rest of the session. Threads need SharedArrayBuffer, which needs cross-origin isolation, which breaks ordinary third-party script loading. I ended up with the boring shape - plain JS orchestration, wasm only where the math is heavy - because the readability of the wasm source was never the bottleneck.

So I would put HEXAW exactly where you have it: small hand-written modules and teaching, where a single-page module can genuinely be held in your head. That is a real niche, not a compromise.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
pengeszikra profile image
Peter Vivo •

In the next version, I'm planning to make dynamic memory addresses easier to track.

For example, function definitions and code bodies live in separate sections, and WASM makes extensive use of LEB128 encoding:

  • CONST -1 → 41 7F
  • CONST 300 → 41 AC 02

So the length of an instruction can change depending on its value, which makes tracking byte offsets a bit tricky.

And yes, that's exactly the idea behind HEXAW: a simple way to write small programs and see their WASM binary representation in almost real time.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
lakhyajit profile image
Lakhyajit Changmai •

The part that will bite you when you add the address tracking: the length prefix is itself LEB128. Each function body in the code section is size:u32 followed by the body, so the moment a body crosses 127 -> 128 bytes the prefix widens from 1 byte to 2, and every function after it shifts by one - the offset next to function 3 changes even though the edit was in function 1. The local-declaration counts inside the body do the same thing.

So an offset column can't be a running sum over the visible lines; it has to be recomputed from the encoded bytes of the whole module on each keystroke, or it quietly lies after the first carry. Colouring the size fields and the immediates separately is worth doing for exactly that reason - they are the two places where source position and byte position stop being one-to-one.

Data addresses are the forgiving case: those come from data segment offsets, so growing the code section leaves them alone. Only code offsets churn.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
alexshev profile image
Alex Shev •

The forward/backward stepper is a useful teaching affordance because it lets a learner test a single control-flow assumption instead of treating the compiled module as opaque. For the BRIF 1 example, a compact trace that shows stack values, the chosen branch, and the instruction address before and after the step would make the debugger’s contract easier to evaluate without requiring the reader to infer it from the bytes.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
vijay_g profile image
vijay •

This is such a brilliant blend of retro nostalgia and modern engineering! Escaping the WAT parenthesis jungle with a clean, Z80-inspired assembler listing is a dream for anyone who loves low-level programming.

Seeing the exact hex bytes right next to the mnemonics (00000032 : 20 00 : GET 0) is an incredible educational tool for truly understanding what's happening under the hood in WebAssembly. Massive props for shipping HEXAW and integrating time-travel debugging! 👏

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
unitbuilds profile image
UnitBuilds •

Ngl, this is pretty cool! I've been diving deep into WASM for a while now and the idea is awesome, makes me think of the old Commodore 64 era cartridges (before my time, but my mom had an old 1 that was in a box at my grandma's house), simply slot it into a new pc and it just works. But everyone looks at it as a 'final destination' for code, never as a destination that needs a new road? I mean why write it in python if it ends up in WASM, as opposed to a language written specifically for WASM (yes, ok Rust exists, but it's not the same). I'm currently (or well my AI agent I set to the task is) working on a standalone OS, built specifically for 1 single purpose... To run WASM and to scale across multiple nodes (so it can run on a datacenter as a unified OS). It's gunna take a LOOOONG time, but I've got time... Not enough patience to wait for it, but that's fine, it's why I have a bunch of other projects to keep me busy 😂

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
pengeszikra profile image
Peter Vivo •

Feel free to test this language maybe worth to use it. Even contribute it.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
unitbuilds profile image
UnitBuilds •

Will do, though atm a bit busy, I'll bookmark for now and get back to it! For now, I'm fixated on my latest experiment, I wanted to make a DB-driven LLM, it cant think, but it does show incredible promise for codebase optimizations... I ingested 5 codebases and it analyzed the functions in it and suggested parity functions between the codebases that are more effective (better error handling, more performant, simplicity, etc.), so gunna dive into that first a bit more, so I can see if I can get codebase translation working (eg. python to rust), I'll have a look whether I can ingest HEXAW and see how it compares, would be cool if it's more optimized than the rest!

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
robertpaschal profile image
Odinaka Robert Nnamani •

rewind for wasm. tiny format, fun idea.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
pengeszikra profile image
Peter Vivo •

I would like to proposed as human first programming language, where any one can feel the assembly programming simplicity. When we go down to the roots.

💬 ชาวเน็ตแห่แชร์: pengeszikra/hexaw a lazy wasm text format gn - สรุปดราม่าล่าสุด
 
robertpaschal profile image
Odinaka Robert Nnamani •

what does human-first look like past the first assembly lesson?