package

github.com/konjac-lang/x

main / published Apr 27, 2026 / repository

A virtual machine for concurrent applications

X

A lightweight, Erlang-inspired virtual machine for concurrent, fault-tolerant applications in Crystal.

Crystal License: MIT Build Status

This project combines a clean stack-based bytecode interpreter with actor-model concurrency, lightweight processes, message passing, supervision trees, and built-in fault tolerance - all implemented in pure Crystal.

Inspired by Erlang's BEAM, but designed to be simple, hackable, and embeddable.

Features

  • Stack-based VM - 120+ instructions across stack, arithmetic, logic, control flow, process, and message operations
  • Actor-model concurrency - Lightweight processes with isolated stacks, mailboxes, and preemptive scheduling via reductions
  • Fault tolerance - Process linking, monitoring, exit trapping, and supervision trees (one-for-one, one-for-all, rest-for-one)
  • XASM assembler - Human-readable assembly language with modules, imports, exports, subroutines, and block-based control flow
  • Multi-module system - .require with automatic file resolution, .import/.export for cross-module calls
  • Hot code reloading - .dynamic modules can be reloaded at runtime without restarting processes
  • Elixir-style standard library - IO, String, Integer, Float, Array, Map, Type modules with 80+ built-in functions
  • Interactive debugger - Step, continue, breakpoints, stack/locals inspection, process filtering
  • Lambdas and closures - First-class functions with captured variables
  • Exception handling - try/catch/throw with stack unwinding
  • Pure Crystal - No external dependencies beyond standard library

Quick Start

Installation

git clone https://github.com/konjac-lang/x.git
cd x
shards install

Hello World

Create hello.xasm:

.module Hello

.import IO.printLine/1

.process main
  push "Hello, World!"
  call IO.printLine/1

  push :normal
  exit
.end

Run it:

shards run -- hello.xasm

Build the binary

shards build --release
./bin/x hello.xasm

The XASM Language

XASM is a human-readable assembly language for the X VM. Every line is a verb - there are no implicit operations.

Modules and Directives

Every .xasm file starts with a module declaration:

.module MyApp.Main

; Import functions from other modules or standard library
.import IO.printLine/1
.import String.concatenate/2

; Require other modules (auto-resolved from disk)
.require "MyApp.Utils"

; Export functions for other modules to use
.export myFunction/1

Processes

Processes are the unit of concurrency. Each process has its own stack, locals, and mailbox:

.process main
  push "I am a process"
  call IO.printLine/1

  push :normal
  exit
.end

Variables

Local variables are declared with .local and accessed with load/store:

.process main
  .local name
  .local counter

  push "alice"
  store name

  push 0
  store counter

  load name
  call IO.printLine/1

  push :normal
  exit
.end

Control Flow

Block-based control flow - no manual jump offsets:

; If/else
load x
push 10
gt
if
  push "x is greater than 10"
  call IO.printLine/1
else
  push "x is 10 or less"
  call IO.printLine/1
end

; Loops
push 0
store i
loop
  load i
  push 10
  gte
  break_if

  load i
  inc
  store i
end

; Try/catch
try
  push "something dangerous"
  throw
catch
  push "caught it"
  call IO.printLine/1
end

Subroutines

Reusable code blocks within a module:

.subroutine greet
  .local name
  store name
  push "Hello, "
  load name
  call String.concatenate/2
  return
.end

Message Passing

Processes communicate through messages:

; Send a message to a named process
push "worker"
push "do something"
send

; Receive a message (blocks until one arrives)
receive

; Wait for a process to register
await "worker"

Spawning Processes

; Spawn a new process
spawn
  self
  register "worker"
  loop
    receive
    call IO.printLine/1
  end
end

; Spawn with a link (crash propagation)
spawn_link
  push "linked child"
  call IO.printLine/1
  push :normal
  exit
end

Lambdas

; Create a lambda capturing a variable
lambda [multiplier]
  load multiplier
  mul
  return
end

; Invoke with arity
push 5
invoke 1

Hot Code Reloading

Mark a module as dynamic to enable live reloading:

.module MyApp.Config
.dynamic

.export version/1

.subroutine version
  push "1.0.0"
  return
.end

Reload at runtime from another process:

push "MyApp.Config"           ; module name to reload
push "MyApp.Config.v2"        ; file hint (resolved by module resolver)
reload

The new code is picked up by all processes on their next function call - no restart required.

Supervision Trees

Declarative supervisor configuration:

.supervisor pool :one_for_one max_restarts=3 window=5

  .child "logger" :permanent
    self
    register "logger"
    loop
      receive
      call IO.printLine/1
    end
  .end

  .child "worker" :transient
    push "working..."
    call IO.printLine/1
    push :normal
    exit
  .end

.end

CLI

Usage: x [options] <file.xasm> [file2.xasm ...]

Options:
    -v, --version                    Show version
    -h, --help                       Show this help
    -d, --debug                      Enable debug logging
    -q, --quiet                      Suppress info logging
    -I PATH, --include PATH          Add search path for module resolution
    --ast                            Print the AST and exit
    --instructions                   Print compiled instructions and exit
    --debugger                       Launch interactive debugger

Interactive Debugger

Launch with --debugger to step through execution:

-- Process <1> (room) @ instruction 0 --
  -> PROCESS_SELF
  Stack: (empty)
xdb>

Commands:

CommandShortDescription
stepsExecute one instruction
nextnStep over subroutines
continuecContinue until next breakpoint
runrRun without stopping
killkKill current process
stackstShow full stack
localslShow local variables
mailboxmbShow process mailbox
processespsList all processes
registryregShow named processes
instructionsisShow nearby instructions
callstackcsShow call frames
break <addr>bBreakpoint at instruction
break <name>bBreakpoint on named process
break <name>:<addr>bBreakpoint on process at instruction
filter <pid>fOnly break on one process
eval <expr>eInspect values
helphShow all commands
quitqExit

Press Enter to repeat the last command.

Standard Library

IO

FunctionDescription
IO.puts/1Print string with newline, returns :okay
IO.print/1Print string without newline, returns :okay
IO.printLine/1Alias for IO.puts/1
IO.inspect/1Print inspect representation, returns the value
IO.gets/0Read a line from stdin

String

FunctionDescription
String.concatenate/2Concatenate two strings
String.length/1String length
String.reverse/1Reverse a string
String.upcase/1Convert to uppercase
String.downcase/1Convert to lowercase
String.trim/1Strip whitespace from both ends
String.trimLeading/1Strip leading whitespace
String.trimTrailing/1Strip trailing whitespace
String.split/2Split string by delimiter
String.join/2Join array with separator
String.contains/2Check if string contains substring
String.startsWith/2Check prefix
String.endsWith/2Check suffix
String.replace/3Replace all occurrences
String.slice/3Extract substring (string, start, length)
String.at/2Character at index
String.toInteger/1Parse to integer
String.toFloat/1Parse to float
String.toAtom/1Convert to atom/symbol
String.duplicate/2Repeat string N times
String.padLeading/3Pad start to width
String.padTrailing/3Pad end to width
String.toString/1Convert any value to string

Integer

FunctionDescription
Integer.toString/1Convert to string
Integer.toFloat/1Convert to float
Integer.parse/1Parse string to integer
Integer.isEven/1Check if even
Integer.isOdd/1Check if odd
Integer.digits/1Get array of digits
Integer.gcd/2Greatest common divisor

Float

FunctionDescription
Float.toString/1Convert to string
Float.toInteger/1Truncate to integer
Float.parse/1Parse string to float
Float.round/2Round to N decimal places
Float.ceil/1Ceiling
Float.floor/1Floor
Float.isNan/1Check for NaN
Float.isInfinity/1Check for infinity

Array

FunctionDescription
Array.new/0Create empty array
Array.length/1Array length
Array.size/1Alias for length
Array.first/1First element
Array.last/1Last element
Array.at/2Element at index
Array.get/2Alias for at
Array.set/3Set element at index
Array.append/2Add to end
Array.prepend/2Add to start
Array.push/2Alias for append
Array.pop/1Remove last, returns [array, element]
Array.concat/2Concatenate two arrays
Array.reverse/1Reverse array
Array.sort/1Sort array
Array.uniq/1Remove duplicates
Array.flatten/1Flatten one level
Array.contains/2Check membership
Array.indexOf/2Find index of element
Array.slice/3Extract sub-array
Array.take/2Take first N elements
Array.drop/2Drop first N elements
Array.zip/2Zip two arrays into pairs
Array.isEmpty/1Check if empty
Array.sum/1Sum of elements
Array.product/1Product of elements
Array.min/1Minimum element
Array.max/1Maximum element
Array.join/2Join elements with separator

Map

FunctionDescription
Map.new/0Create empty map
Map.put/3Set key-value pair
Map.get/2Get value by key
Map.getWithDefault/3Get with fallback
Map.delete/2Remove key
Map.hasKey/2Check if key exists
Map.keys/1Get all keys
Map.values/1Get all values
Map.size/1Number of entries
Map.merge/2Merge two maps
Map.toArray/1Convert to array of [key, value] pairs
Map.isEmpty/1Check if empty

Type

FunctionDescription
Type.of/1Get type name as string
Type.inspect/1Get inspect representation
Type.toString/1Convert any value to string
Type.isNull/1Check if null
Type.isInteger/1Check if integer
Type.isFloat/1Check if float
Type.isString/1Check if string
Type.isBoolean/1Check if boolean
Type.isArray/1Check if array
Type.isMap/1Check if map
Type.isSymbol/1Check if symbol
Type.isLambda/1Check if lambda
Type.isNumeric/1Check if integer or float

TCP

FunctionDescription
TCP.listen/1Create a TCP server on a port (binds to 0.0.0.0)
TCP.listenOn/2Create a TCP server on a specific address and port
TCP.listenWithBacklog/3Create a TCP server with custom backlog
TCP.accept/1Accept a client connection (async, blocks process)
TCP.acceptTimeout/2Accept with timeout in milliseconds
TCP.connect/2Connect to a host and port (async)
TCP.connectTimeout/3Connect with timeout in milliseconds
TCP.send/2Send string data over a socket (async)
TCP.sendBinary/2Send binary data over a socket (async)
TCP.receive/2Receive up to N bytes as string (async)
TCP.receiveBinary/2Receive up to N bytes as binary (async)
TCP.receiveTimeout/3Receive with timeout in milliseconds
TCP.receiveLine/2Read a line up to max bytes (async)
TCP.receiveExact/2Read exactly N bytes (async)
TCP.close/1Close a socket or server
TCP.isClosed/1Check if a socket is closed
TCP.setNodelay/2Enable/disable TCP_NODELAY
TCP.setKeepalive/2Enable/disable keepalive
TCP.setReceiveBufferSize/2Set receive buffer size
TCP.setSendBufferSize/2Set send buffer size
TCP.setReadTimeout/2Set read timeout in milliseconds
TCP.setWriteTimeout/2Set write timeout in milliseconds
TCP.clearReadTimeout/1Remove read timeout
TCP.clearWriteTimeout/1Remove write timeout
TCP.setReuseAddress/2Enable/disable SO_REUSEADDR
TCP.setReusePort/2Enable/disable SO_REUSEPORT
TCP.setLingerOption/3Set linger option (socket, enabled, timeout)
TCP.localAddress/1Get local [address, port]
TCP.remoteAddress/1Get remote [address, port]
TCP.shutdown/2Shutdown socket direction ("read", "write", or "both")

UDP

FunctionDescription
UDP.open/1Open a UDP socket bound to a port
UDP.openOn/2Open a UDP socket bound to address and port
UDP.openUnbound/0Open an unbound UDP socket
UDP.connect/3Connect a UDP socket to a remote host and port
UDP.send/2Send string data on a connected socket (async)
UDP.sendTo/4Send string data to a specific host and port (async)
UDP.sendToBinary/4Send binary data to a specific host and port (async)
UDP.receive/2Receive up to N bytes as string (async)
UDP.receiveFrom/2Receive data with sender [data, address, port] (async)
UDP.receiveFromBinary/2Receive binary data with sender info (async)
UDP.receiveFromTimeout/3Receive with sender info and timeout (async)
UDP.close/1Close a UDP socket
UDP.isClosed/1Check if socket is closed
UDP.setBroadcast/2Enable/disable broadcast
UDP.setReceiveBufferSize/2Set receive buffer size
UDP.setSendBufferSize/2Set send buffer size
UDP.joinMulticastGroup/2Join a multicast group
UDP.leaveMulticastGroup/2Leave a multicast group
UDP.setMulticastLoopback/2Enable/disable multicast loopback
UDP.setMulticastHops/2Set multicast TTL/hops
UDP.localAddress/1Get local [address, port]

Unix

FunctionDescription
Unix.listen/1Create a Unix domain socket server at a path
Unix.listenWithBacklog/2Create server with custom backlog
Unix.accept/1Accept a client connection (async)
Unix.acceptTimeout/2Accept with timeout in milliseconds
Unix.connect/1Connect to a Unix socket path (async)
Unix.send/2Send string data (async)
Unix.sendBinary/2Send binary data (async)
Unix.receive/2Receive up to N bytes as string (async)
Unix.receiveBinary/2Receive up to N bytes as binary (async)
Unix.receiveTimeout/3Receive with timeout in milliseconds
Unix.receiveLine/2Read a line up to max bytes (async)
Unix.close/1Close a socket or server
Unix.isClosed/1Check if closed
Unix.unlink/1Delete the socket file from disk
Unix.setReadTimeout/2Set read timeout in milliseconds
Unix.setWriteTimeout/2Set write timeout in milliseconds
Unix.clearReadTimeout/1Remove read timeout
Unix.clearWriteTimeout/1Remove write timeout
Unix.path/1Get the socket file path

Socket

FunctionDescription
Socket.resolve/1Resolve hostname to list of IP addresses (async)
Socket.resolveAll/3Resolve with service and family filter ("ipv4", "ipv6", "any") (async)
Socket.parseIpAddress/2Parse an IP address string and port into [address, port, family]
Socket.isValidIp/1Check if a string is a valid IP address

Instruction Set Reference

Stack Operations

XASMDescription
push <value>Push literal (string, integer, float, symbol, true, false, null)
popDiscard top of stack
dupDuplicate top
overCopy second element to top
swapSwap top two
rotRotate top three up
-rotRotate top three down
nipRemove second element
tuckCopy top below second
depthPush stack depth
pickCopy Nth element to top
rollMove Nth element to top

Arithmetic

XASMDescription
adda + b
suba - b
mula * b
diva / b
moda % b
negNegate
absAbsolute value
incIncrement by 1
decDecrement by 1
powPower
floorFloor
ceilCeiling
roundRound
minMinimum of two
maxMaximum of two

Bitwise

XASMDescription
bandBitwise AND
borBitwise OR
bxorBitwise XOR
bnotBitwise NOT
shlShift left
shrShift right
ushrUnsigned shift right

Comparison

XASMDescription
eqEqual
neqNot equal
ideqIdentical (strict)
nideqNot identical
ltLess than
lteLess than or equal
gtGreater than
gteGreater than or equal
is_nullCheck null
is_not_nullCheck not null

Logic

XASMDescription
andLogical AND
orLogical OR
notLogical NOT
xorLogical XOR

Variables

XASMDescription
load <name>Push local variable onto stack
store <name>Pop stack into local variable
gload <name>Load global variable
gstore <name>Store global variable

Control Flow

XASMDescription
if...else...endConditional block
loop...endLoop block
breakExit loop
break_ifExit loop if top is true
break_unlessExit loop if top is false
continueJump to loop start
call Module.func/arityCall built-in or module function
returnReturn from subroutine
nopNo operation
haltHalt execution

Process Operations

XASMDescription
selfPush current process ID
register <name>Register process with a name
unregister <name>Remove name registration
whereis <name>Look up process by name
spawn...endSpawn a new process
spawn_link...endSpawn with bidirectional link
spawn_monitor...endSpawn with monitor
exitExit current process (reason on stack)
killKill a process
sleepSleep for duration
yieldYield to scheduler
linkLink to another process
unlinkUnlink from a process
monitorMonitor a process
demonitorStop monitoring
trap_onEnable exit signal trapping
trap_offDisable exit signal trapping
alive?Check if process is alive
await <name>Wait for a process to register

Messages

XASMDescription
sendSend message (name and value on stack)
send_afterSend message with delay
receiveReceive next message (blocks)
receive_timeoutReceive with timeout
peekPeek at next message without consuming
mailbox_sizePush mailbox size

Exceptions

XASMDescription
try...catch...endException handling block
throwThrow an exception
rethrowRe-throw current exception

Hot Reload

XASMDescription
reloadReload a dynamic module (file hint and module name on stack)

Module Resolution

When you .require "MyApp.Utils", the resolver searches for matching files in this order:

  1. MyApp/Utils.xasm
  2. my_app/utils.xasm
  3. my_app.utils.xasm
  4. MyApp/utils.xasm
  5. Utils.xasm
  6. utils.xasm
  7. MyApp.Utils.xasm

Search roots include the directory of the entry file and any paths added with -I.

If no file matches by name, the resolver scans .xasm files for a matching .module declaration.

Examples

The examples/ directory contains working demos:

Run any example:

shards run -- examples/single_module/Stick.Messaging.xasm
shards run -- examples/chat/Chat.Main.xasm
shards run -- examples/dynamic/Dynamic.Main.xasm

Architecture

                    XASM Source
                   (.xasm files)
                        |
                   +----v----+
                   |  Lexer  |  Tokenization
                   +----+----+
                   +----v----+
                   |  Parser |  AST generation
                   +----+----+
                   +----v---------+
                   | Code Generator|  Bytecode compilation
                   +----+---------+
                   +----v----+
                   |  Loader |  Module resolution, wiring
                   +----+----+
                        |
    +-------------------v---------------------+
    |            X VM Engine                   |
    |                                          |
    | +--------+  +--------+  +--------+       |
    | |Proc <1>|  |Proc <2>|  |Proc <N>|       |
    | | Stack  |  | Stack  |  | Stack  |       |
    | | Locals |  | Locals |  | Locals |       |
    | |Mailbox |  |Mailbox |  |Mailbox |       |
    | +--------+  +--------+  +--------+       |
    |                                          |
    | +--------------------------------------+ |
    | |           Scheduler                  | |
    | | Reduction-based preemptive scheduling| |
    | +--------------------------------------+ |
    |                                          |
    | +-------------+  +-------------------+   |
    | |Fault Handler|  |Supervisor Registry|   |
    | |Links/Monitors| |Restart strategies |   |
    | +-------------+  +-------------------+   |
    |                                          |
    | +--------------------------------------+ |
    | |    Built-in Function Registry        | |
    | | IO String Array Map Type Integer ... | |
    | +--------------------------------------+ |
    +------------------------------------------+

Why X?

  • Learning - Understand how actor-model VMs work from the inside
  • Embedding - Drop a concurrent runtime into your Crystal application
  • Experimentation - Build DSLs, game scripting engines, or workflow systems
  • Compiler target - XASM is a clean compilation target for higher-level languages

Contributing

  1. Fork it (https://github.com/konjac-lang/x/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

API

  • String

    A String represents an immutable sequence of UTF-8 characters.

  • Symbol

    A symbol is a constant that is identified by a name without you having to give it a numeric value.

  • X