A hands-on introduction. Each section builds on the last.
package main
func main() {
println("hello, moxie")
}
Save as hello.mx. Build and run:
export MOXIEROOT=/path/to/moxie
moxie build -o hello .
./hello
.mx files are Moxie source. The compiler produces a static binary with no dependencies.
package main
func main() {
x := 42 // int (always 32-bit in Moxie)
var y float64 = 3.14
z := x + 1
println(x, y, z)
}
Moxie has the same declaration syntax you'd expect: var, :=, const, type.
Numeric types: int8, int16, int32, int64, uint8-uint64, float32, float64, byte, bool.
int and uint are always 32-bit. There is no complex64/128. uintptr is only available in packages that import "unsafe".
string and []byte are the same type in Moxie. They have identical layout (pointer, length, capacity) and are interchangeable. Use string in signatures for readability, []byte when the byte-level nature matters.
package main
import "bytes"
func main() {
name := "moxie"
greeting := "hello " | name | "!" // | concatenates text
println(greeting)
// bytes package works on strings (they're []byte)
upper := bytes.ToUpper(name)
println(upper)
// direct byte access
println(name[0]) // 109 (ASCII 'm')
}
Key rules:
+ on text is a compile error. Use |.import "strings" is a compile error. Use "bytes".range over text yields bytes, not runes. Use an encoding library for Unicode iteration.package main
func add(a, b int32) int32 {
return a + b // + on numbers is fine
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func main() {
println(add(3, 4))
}
Multiple return values, variadic parameters, closures, and method receivers all work as expected.
package main
type Point struct {
X, Y float64
}
func (p Point) Dist() float64 {
return p.X*p.X + p.Y*p.Y
}
func main() {
p := Point{X: 3, Y: 4}
println(p.Dist()) // 25
}
Allocate on the heap with &:
p := &Point{X: 1, Y: 2} // *Point
There is no new(). Use &T{} or var x T; p := &x.
package main
type Shape interface {
Area() float64
}
type Rect struct {
W, H float64
}
func (r Rect) Area() float64 {
return r.W * r.H
}
func printArea(s Shape) {
println(s.Area())
}
func main() {
printArea(Rect{W: 3, H: 4}) // 12
}
Type assertions and type switches work as expected:
switch v := s.(type) {
case Rect:
println("rect:", v.W, v.H)
}
Interfaces work freely within a domain. They cannot cross a spawn boundary — see section 11.
package main
func main() {
s := []int32{1, 2, 3}
s = append(s, 4, 5)
// size literals (alternative to make)
buf := []byte{:1024} // 1024 zero bytes
table := []int32{:0:100} // len=0, cap=100
// equality works on slices
a := []int32{1, 2}
b := []int32{1, 2}
println(a == b) // true
// concatenation
c := a | b // [1, 2, 1, 2]
println(len(c)) // 4
}
package main
func main() {
m := map[string]int32{
"alpha": 1,
"beta": 2,
}
m["gamma"] = 3
v, ok := m["alpha"]
println(v, ok) // 1 true
delete(m, "beta")
}
Slices can be map keys (if the element type is comparable).
package main
func main() {
// if
x := 10
if x > 5 {
println("big")
} else {
println("small")
}
// for (all three forms)
for i := 0; i < 5; i++ {
println(i)
}
for x > 0 {
x--
}
items := []string{"a", "b", "c"}
for i, v := range items {
println(i, v)
}
// switch (no fallthrough — use comma-separated cases)
switch x {
case 0:
println("zero")
case 1, 2, 3:
println("small")
default:
println("other")
}
}
break, continue, goto, and labeled loops all work.
This is the heart of Moxie. There are no goroutines. All concurrency within a domain is expressed through channels and select.
An unbuffered channel send transfers execution to the waiting select case. Think of it as a function call via the channel:
package main
func main() {
tick := chan struct{}{} // unbuffered dispatcher
done := chan struct{}{}
// In a real program, these sends would come from
// I/O callbacks, timers, or child domain messages.
// Event loop
select {
case <-tick:
println("tick received")
case <-done:
println("shutting down")
}
}
Buffered channels enqueue messages for the next select iteration:
package main
func main() {
msgs := chan string{10} // buffer of 10
msgs <- "first"
msgs <- "second"
// Process buffered messages
for {
select {
case m := <-msgs:
println(m)
default:
println("queue empty")
return
}
}
}
The pattern: create channels for each event type, run a select loop.
package main
import (
"fmt"
"os"
)
type Server struct {
requests chan string
quit chan struct{}
}
func newServer() *Server {
return &Server{
requests: chan string{100},
quit: chan struct{}{},
}
}
func (s *Server) run() {
for {
select {
case req := <-s.requests:
fmt.Println("handling:", req)
case <-s.quit:
fmt.Println("server stopped")
return
}
}
}
func main() {
s := newServer()
s.requests <- "GET /index"
s.requests <- "GET /about"
close(s.quit)
s.run()
}
This is the fundamental execution pattern. Every Moxie program is a tree of event loops connected by channels.
The runtime integrates with epoll (Linux) / kqueue (macOS). I/O operations yield to the event loop and resume when data is ready.
package main
import (
"fmt"
"net"
)
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
fmt.Println("listening on :8080")
for {
conn, err := ln.Accept()
if err != nil {
continue
}
// Handle synchronously — one connection at a time per domain.
// Use spawn for concurrent connection handling.
buf := []byte{:4096}
n, _ := conn.Read(buf)
conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"))
conn.Close()
_ = buf[:n]
}
}
Within a single domain, I/O is sequential. For concurrent I/O handling, spawn child domains.
spawn creates a child domain: a separate OS process with its own heap and event loop. The parent and child communicate through IPC channels.
package main
func worker() {
println("hello from child domain")
}
func main() {
done := spawn(worker)
_ = done
println("parent continues immediately")
}
Data crossing the spawn boundary must implement moxie.Codec. The moxie package provides codec wrappers for all primitive types:
package main
import "moxie"
func compute(id moxie.Int32, scale moxie.Float64) {
result := float64(id) * float64(scale)
println(result)
}
func main() {
spawn(compute, moxie.Int32(1), moxie.Float64(2.5))
spawn(compute, moxie.Int32(2), moxie.Float64(3.0))
// Both children run concurrently as separate processes
}
Pass channels to spawn for bidirectional communication. Channel element types must implement moxie.Codec:
package main
import "moxie"
func producer(out chan moxie.Int32) {
for i := int32(0); i < 10; i++ {
out <- moxie.Int32(i)
}
close(out)
}
func main() {
ch := chan moxie.Int32{}
spawn(producer, ch)
// Receive results from child domain
for {
v, ok := <-ch
if !ok {
break
}
println(v)
}
}
Define EncodeTo and DecodeFrom for your own types:
package main
import (
"io"
"moxie"
)
type Point struct {
X moxie.Float64
Y moxie.Float64
}
func (p Point) EncodeTo(w io.Writer) error {
if err := p.X.EncodeTo(w); err != nil {
return err
}
return p.Y.EncodeTo(w)
}
func (p *Point) DecodeFrom(r io.Reader) error {
if err := p.X.DecodeFrom(r); err != nil {
return err
}
return p.Y.DecodeFrom(r)
}
func plotWorker(pt Point) {
println(float64(pt.X), float64(pt.Y))
}
func main() {
spawn(plotWorker, Point{X: 1.5, Y: 2.7})
}
Values passed to spawn are moved — they cannot be used afterward:
data := computeData()
spawn(worker, data)
println(data) // compile error: ownership moved to child
Constants and channels are exempt.
| Rejected | Reason |
|---|---|
Raw int32, bool, etc. | Use moxie.Int32, moxie.Bool, etc. |
| Pointers | Cannot share memory across processes. |
| Functions | Cannot serialize code. |
| Interfaces | Type erasure prevents serialization. |
Spawn multiple workers, collect results:
package main
import "moxie"
func worker(id moxie.Int32, results chan moxie.Int32) {
// Simulate work
results <- moxie.Int32(int32(id) * int32(id))
}
func main() {
n := 4
results := chan moxie.Int32{}
for i := int32(0); i < int32(n); i++ {
spawn(worker, moxie.Int32(i), results)
}
for i := 0; i < n; i++ {
v := <-results
println(v)
}
}
Chain domains together:
package main
import "moxie"
func stage1(out chan moxie.Int32) {
for i := int32(0); i < 5; i++ {
out <- moxie.Int32(i)
}
close(out)
}
func stage2(in chan moxie.Int32, out chan moxie.Int32) {
for {
v, ok := <-in
if !ok {
close(out)
return
}
out <- moxie.Int32(int32(v) * 2)
}
}
func main() {
ch1 := chan moxie.Int32{}
ch2 := chan moxie.Int32{}
spawn(stage1, ch1)
spawn(stage2, ch1, ch2)
for {
v, ok := <-ch2
if !ok {
break
}
println(v) // 0, 2, 4, 6, 8
}
}
spawn returns chan struct{} that closes when the child exits. Use it to wait for completion or detect failure:
done := spawn(worker, args...)
select {
case <-done:
println("worker finished")
case <-timeout:
println("worker took too long")
}
Moxie compiles to JavaScript for browser deployment. Each domain maps to a BroadcastChannel-isolated context.
moxie build -target js/wasm -o output/ .
This produces ES modules in the output directory, including a $runtime/ folder and $entry.mjs.
The JS runtime provides bridge packages for browser APIs:
package main
// JS bridge packages provide typed access to browser APIs.
// Import paths under jsbridge/ map directly to runtime modules.
func main() {
// DOM operations via bridge
// WebSocket connections via bridge
// Service Worker lifecycle via bridge
// IndexedDB transactions via bridge
}
Bridge packages available: dom (elements, events), ws (WebSocket), sw (Service Worker lifecycle, caching, SSE), localstorage, idb (IndexedDB), crypto (secp256k1), subtle (SubtleCrypto).
Within the JS target:
async/await.The compiler automatically marks functions as async when they contain channel operations, propagating transitively up the call chain.
| Moxie | Equivalent | Notes |
|---|---|---|
"hello" | []byte("hello") | String literals produce []byte |
a \| b | concat(a, b) | Slice concatenation (any matching slice type) |
[]T{:n} | Slice with length n | make() is not available |
[]T{:n:c} | Slice with length n, capacity c | |
chan T{} | Unbuffered channel | |
chan T{n} | Buffered channel with capacity n | |
&T{...} | (heap allocation) | Replaces new(T) |
| Removed | Alternative | Why |
|---|---|---|
go f() | spawn + channels | Hidden execution paths. |
new(T) | &T{} | Redundant. |
+ on text | \| | Unifies slice concatenation. |
fallthrough | case A, B: | Implicit control flow. |
complex64/128 | Separate float fields | Rarely needed. |
uintptr | Explicit pointers | Raw pointer arithmetic is unsafe only. |
import "strings" | import "bytes" | string = []byte makes strings redundant. |
interface{} at spawn | moxie.Codec types | Serialization requires concrete types. |