DEV Community

Gargee Bhattacharjee
Gargee Bhattacharjee

Posted on

Building jugaad-mustache: Porting mustache.js to Safe, 30x Faster Rust 🦀

I Ported mustache.js to Safe Rust in 72 Hours — Here's What Actually Broke

By Gargee Bhattacharjee | Port Mortem 2026 | Track F


When competing in Port Mortem 2026 (Track F), I set out to tackle a problem almost every developer runs into: why do we need a 45 MB Node.js runtime and 47 MB of node_modules just to render a basic template from a shell script?

mustache.js is an absolute classic in web development. But using it as a command-line tool or inside CI pipelines feels unnecessarily heavy. Cold startup takes ~150ms before your template even starts rendering.

I decided to build jugaad-mustache (mustache-rs) — a zero-dependency, static Rust binary that compiles to ~2MB, executes in 5ms (30× faster), and preserves 100% behavioral equivalence with mustache.js.

Here is the complete 4-minute video walkthrough of the finished project:


What Actually Broke (The Unexpected Edge Cases)

Porting a dynamic JavaScript engine to a strongly-typed language like Rust sounds straightforward until you start running original spec test suites. Here are the unexpected technical hurdles I ran into:

1. JavaScript's "Truthiness" vs. Rust's Type System

In mustache.js, section blocks evaluate using JavaScript's implicit truthiness rules. Values like 0, 0.0, NaN, null, undefined, and "" are all falsy, while empty objects {} evaluate to truthy.

Rust doesn't coerce numbers into booleans. If you pass a JSON number 0 to a naive Rust implementation, it evaluates to true and renders the section!

To fix this while keeping behavioral equivalence, I built a custom evaluator:

pub fn is_truthy(val: &Value) -> bool {
    match val {
        Value::Null => false,
        Value::Bool(b) => *b,
        Value::Number(n) => n.as_f64().map_or(false, |f| f != 0.0 && !f.is_nan()),
        Value::String(s) => !s.is_empty(),
        Value::Array(a) => !a.is_empty(),
        Value::Object(_) => true,
    }
}

2. Enforcing Zero Unsafe Code

Many high-performance parsers use unsafe raw pointers or unchecked indexing. I wanted this port to be 100% memory safe, so I put #![forbid(unsafe_code)] at the very first line of src/main.rs.

Turning unsafe into a hard compiler error meant ensuring that all string building and token array traversals used standard safe primitives without sacrificing execution speed.

3. Dynamic Scope Walking

In mustache.js, looking up a key in a nested section walks up the parent context stack dynamically. Translating this stack-walking logic into Rust without running into ownership and borrow-checker issues required carefully modeling scope lifetimes.


The Secret Sauce: Jugaad Mode (--jugaad)

Standard template engines panic or crash when fed malformed input — like an unclosed tag or a missing section boundary.

In production, a small syntax typo shouldn't take down an entire deployment pipeline. Inspired by the Indian philosophy of jugaad (frugal, clever problem-solving), I added Jugaad Mode:

  • An opt-in pre-processor that auto-repairs unclosed tags and mismatched section boundaries on the fly.
  • Outputs friendly diagnostic logs (theek kar diya (fixed it), band kar diya (closed it)) while continuing execution cleanly.

Proving Equivalence: 30,000+ Fuzzing Runs

To prove that mustache-rs behaves 100% identically to mustache.js, I built an automated differential fuzzer in Node.js (fuzz/run_fuzz.js).

The fuzzer generated random templates and JSON context data, feeding them to both engines simultaneously and byte-comparing the outputs. Over a continuous test run of 30,567 iterations, the engines produced 0 output divergences!


Reflections & Thank You!

Building this over 72 hours was an incredible experience. Huge shoutout to the Port Mortem 2026 organizers and the Hackathon Raptors community for putting together such an inspiring, high-quality hackathon track. It forced me to think like a compiler engineer and build a tool I'll actually use in my daily workflow.

Top comments (0)