SubsetJuliaVM: A Julia Subset Execution Environment for iOS and Web

terasakisatoshi
Wed, 31 Dec 2025 06:18:49 +0000
Tooling

Hello everyone. I want to introduce SubsetJuliaVM, a subset of Julia that works on iOS devices and web browsers.

The following text was generated using https://translate.preferredai.jp/ to rewrite the following Japanese manuscript into English.

Julia のコードを解釈し実行する仮想マシンを作成し iOS アプリ化，WASM化した話

In short

Give it a try:

iOS: ‎SubsetJuliaVM App - App Store

Android: https://play.google.com/apps/testing/jp.atelierarith.subsetjuliavm.subset_julia_vm_flutter

Contact me to send your Google account. I’ll add it to the internal development group.

Web: SubsetJuliaVM Playground

Landing page: SubsetJuliaVMPage.jl · SubsetJuliaVMPage

Introduction

Julia is excellent. We want to increase the number of people who can use Julia. The realization of this wish becomes easier when Julia is introduced into educational settings.

Some high schools now offer classes using online content on iPads. In mathematics classes, when students become frustrated with complex problems before completing them by hand, teachers use Google Colab to have students perform numerical computations using Julia or Python, then visualize function graphs to enhance understanding.

There’s growing demand in Japan’s school environment for running Julia on iPads.

There is a growing demand for running Julia on iPads in Japanese schools. While it’s certainly possible to run Julia on platforms like JuliaHub or Google Colab online, stable internet connectivity isn’t always guaranteed in Japanese educational settings. Various constraints may prevent access to environments like JuliaHub or Google Colab. Even if a connection is possible, launching a Julia execution environment instance can take minutes—even for simple calculations like 1 + 1.

I thought it was great that Julia can run on iOS devices without internet connectivity.

Running Julia code within an iOS application

Running Julia code within an iOS application is challenging. Since Julia generates native code at runtime, it doesn’t comply with the App Store’s security policies. Additionally, I’m not sure how to even begin building Julia for iOS devices…

Several applications, such as Pythonista and Carnets, allow running Python on iOS devices without an internet connection. I realized that creating a parser and virtual machine that accepts Julia syntax would be the way to go.

While ideas come to me easily, I lack the technical skills to implement them. I know terms like compilers and LLVM, but I’ve never actually applied this knowledge to create functional applications.

Meanwhile, recent years have seen significant advancements in coding assistance agents like Codex, Cursor, and Claude Code. I realized that by leveraging these tools, I could potentially bring my ideas to life without getting bogged down in implementation details.

Your turn! Rust

I chose Rust as the foundation for building an environment to run Julia. Rust is a statically typed, pre-compiled, modern language with package management capabilities. While Rust is considered challenging, my previous development experience has shown that compared to C/C++, it’s overwhelmingly more productive when working with LLMs. See the following blogs to learn more.

Farewell libsparseir, Hello sparse-ir-rs by Satoshi Terasaki

Why I Migrated from C++ to Rust (Development Experience in the Age of Generative AI) by Hiroshi Shinaoka

Within just two weeks, we pushed Claude Code Opus 4.5 to its performance limits and successfully developed SubsetJuliaVM, a subset execution environment for JuliaLang.

How it works: 4-layer pipeline

The SubsetJuliaVM consists of four components: the parser, lowering, compiler, and VM.

The parser is a pure Rust parser that parses all Julia syntax. It produces CST (Concrete Syntax Tree). It is inspired by tree-sitter-julia. We could use the tree-sitter-julia itself. However, we encountered technical barriers when attempting to WebAssembly-ize SubsetJuliaVM, so we rewrote the parser from tree-sitter-julia to Pure Rust from scratch (using LLM, of course).

The component lowering takes an abstract syntax tree (CST) parsed by parser and determines whether its components are supported by the VM. For example, if the function does not support certain macros like eval, @eval, @simd, @inbounds, or Base.Threads.@threads, the lowering will raise an UnsupportedFeature error. If the CST consists of a node that is supported by a VM, it is transformed to Core IR.

In this phase, println("Hello World) in Julia is converted to the following Core IR:

Stmt::Expr {
    expr: Expr::Call {
        function: "println",
        args: [
            Expr::Literal(Literal::Str("Hello World"), span)
        ],
        kwargs: [],
        span,
    },
    span,
}

The compiler generates stack-based bytecode from Core IR. In the case of println("Hello World"), it is converted to:

[
    Instr::PushStr("Hello World".to_string()),  // Generated by compile_expr for string literal
    Instr::PrintStrNoNewline,                   // Generated by compile_builtin_call based on type
    Instr::PrintNewline,                        // Generated by compile_builtin_call
    Instr::PushNothing,                         // Generated by compile_builtin_call (return value)
    Instr::ReturnNothing,                       // Generated by emit_return_for_type for top-level
]

Finally the VM which is a stack-based virtual machine executes bytecode.

The VM fetches the instruction at ip, increments ip += 1, then executes.

// VM state
struct Vm {
    code: Vec<Instr>,      // Bytecode
    ip: usize,             // Instruction pointer
    stack: Vec<Value>,     // Execution stack
    output: String,        // Output buffer
    rng: R,                // RNG
}

// Execution flow (conceptual)
loop {
    match instr {
        Instr::PushStr(s) => {
            stack.push(Value::Str(s.clone()));
            // Stack: [Str("Hello World")]
        }
        Instr::PrintStrNoNewline => {
            let s = pop_str(&mut stack);  // Pop "Hello World"
            output.push_str(&s);          // Append to output buffer
            // Stack: []
            // output: "Hello World"
        }
        Instr::PrintNewline => {
            output.push('\n');            // Append newline
            // output: "Hello World\n"
        }
        Instr::PushNothing => {
            stack.push(Value::Nothing);   // Return value (println returns nothing)
            // Stack: [Nothing]
        }
        Instr::ReturnNothing => {
            // Top-level return, end execution
            return Value::Nothing;
        }
    }
}

Supported syntax

The SubsetJuliaVM should work the following samples:

# Mandelbrot escape time algorithm
function mandelbrot_escape(c, maxiter)
    z = 0.0 + 0.0im
    for k in 1:maxiter
        if abs2(z) > 4.0        # |z|^2 > 4
            return k
        end
        z = z^2 + c
    end
    return maxiter
end

# Compute grid using broadcast (vectorized)
# xs' creates a row vector, ys is a column vector
# Broadcasting creates a 2D complex matrix C
function mandelbrot_grid(width, height, maxiter)
    xmin = -2.0; xmax = 1.0
    ymin = -1.2; ymax = 1.2

    xs = range(xmin, xmax; length=width)
    ys = range(ymax, ymin; length=height)

    # Create 2D complex grid via broadcasting
    C = xs' .+ im .* ys

    # Apply escape function to all points at once
    # Ref(maxiter) prevents maxiter from being broadcast
    mandelbrot_escape.(C, Ref(maxiter))
end

# ASCII visualization
@time grid = mandelbrot_grid(50, 25, 50)
println("Mandelbrot Set (50x25):")
for row in 1:25
    for col in 1:50
        n = grid[row, col]
        if n == 50
            print("#")
        elseif n > 25
            print("+")
        elseif n > 10
            print(".")
        else
            print(" ")
        end
    end
    println("")
end

module MyGeometry

using Statistics: mean

export distance
export Point

struct Point{T<:Real}
    x::T
    y::T
end

Base.:+(p::Point{T}, q::Point{T}) where T <: Real = Point{T}(p.x + q.x, p.y + q.y)
Base.:-(p::Point{T}, q::Point{T}) where T <: Real = Point{T}(p.x - q.x, p.y - q.y)

function distance(p::Point{T}, q::Point{T}) where T <: Real
    return sqrt((q.x - p.x)^2 + (q.y - p.y)^2)
end

function centroid(points::Vector{Point{T}}) where T <: Real
    x = mean([point.x for point in points])
    y = mean([point.y for point in points])
    Tnew = promote_type(typeof(x), typeof(y))
    return Point{Tnew}(x, y)
end

end #module

using .MyGeometry

p = Point(3, 4)
q = Point(0, 0)

@assert distance(p, q) == 5.0
@assert p isa Point{Int}
@assert typeof(p) === Point{Int}
@assert p + q == Point(3, 4)
@assert p - q == Point(3, 4)
@assert MyGeometry.centroid([Point(1, 2), Point(3, 4), Point(5, 6)]) == Point(3.0, 4.0)

println("All assertions passed!")
println(distance(p, q))

Note that macros like @assert, @show, and @time are parsed internally as Rust built-in functions. User-defined macros are not currently supported.

iOS application

Although still in the development phase, the SubsetJuliaVM was successfully tested on my iPad mini.

Web Assembly

Give it a try:

terasakisatoshi.github.io

SubsetJuliaVM Playground

sjulia with REPL

sjulia is a CLI tool to launch a SubsetJuliaVM process.

Compile Julia code

The SubsetJuliaVM is lightweight, so Julia code containing syntax supported by SubsetJuliaVM can run self-contained by packaging the VM. This serves as the SubsetJuliaVM equivalent of JuliaC.

Is it performant?

Unfortunately, NO. Our SubsetJuliaVM looks like a Julia runtime but walks like slow Python. It is slower than Python.
High-performance operation lies beyond the scope of our current development focus. However, we are greedy. Perhaps our dreams might someday become reality (Really???).

Conclusion

We implemented a Julia interpreter called SubsetJuliaVM that supports a subset of Julia, written in Rust.
Our virtual machine is too slow to replace the official Julia, which uses JIT compilation.
As long as Rust-supported environments remain available, our virtual machine can operate anywhere—particularly in environments where the original Julia has difficulty functioning.
The current code remains unreleased. With sufficient financial support, we would be more than willing to make it publicly available. Are you interested​ ?

24 posts - 9 participants