Features

How each language does it

A near-exhaustive matrix. Toggle languages to focus; each cell explains how the feature works there.

Columns: gujiGoOCamlPerlRakuRustPython

Concurrency & Parallelism

FeaturegujiGoOCamlPerlRakuRustPython
OS threadsSpawning kernel-scheduled threads that the OS maps 1:1 to hardware and can run in parallel across cores. Noguji deliberately exposes no 1:1 OS-thread API. Concurrency is expressed only with hatch tasks over an M:N scheduler that grows its thread pool internally (spec §17.6); raw OS threads are not a language primitive. PartialGo never exposes raw OS threads directly; goroutines are multiplexed M:N onto a pool of OS threads sized by GOMAXPROCS. You can pin work with runtime.LockOSThread, but there is no 1:1 thread-spawn API. YesSince OCaml 5.0 the Domain module spawns domains, each backed by its own OS thread and running truly in parallel. The legacy Thread module also gives concurrent (now parallel) systhreads. Partialuse threads provides interpreter (ithreads) backed by real OS threads, but each thread clones the entire interpreter, so they are heavy and have no shared memory by default. Their use is widely discouraged for new code. PartialRaku runs on a real OS-thread pool (the ThreadPoolScheduler) but discourages manual Thread.start; idiomatic code uses high-level Promise/Supply which the scheduler maps onto threads. Yesstd::thread::spawn creates a 1:1 native OS thread; the closure must be Send and 'static. Joining via the returned JoinHandle propagates the result or panic. Yesthreading.Thread creates real OS threads, but in the default build the GIL serializes bytecode so only one runs Python at a time (still useful for I/O). The free-threaded build (3.13 experimental, 3.14 supported) can run them in parallel.
Green threads / lightweight tasksUser-space tasks multiplexed M:N onto a few OS threads, cheap to create in large numbers. Yeshatch { ... } starts a lightweight task that runs the block concurrently and returns () immediately; an M:N scheduler multiplexes many tasks onto a few OS threads, so spawning is cheap (spec §17.1, §17.6). YesGoroutines are the canonical green threads: go f() starts one with a tiny growable stack, and the runtime schedules thousands of them M:N over OS threads. This is the headline feature of the language. Via libraryOCaml 5 has no built-in green threads, but effect handlers let libraries build them in direct style. Eio and Domainslib's task pool provide cooperative fibers scheduled cooperatively within and across domains. Via libraryCore Perl has no green threads. Coro provides real cooperative coroutines that share data in one interpreter, and event frameworks like IO::Async give cooperative concurrency on a single thread. PartialRaku does not expose explicit green threads; start/Promise schedule code onto a managed thread pool rather than spawning a thread per task, giving similar lightweight-task economics at a higher level. Via libraryThe standard library only has 1:1 OS threads. Async runtimes such as tokio and async-std provide green-thread-like tasks (tokio::spawn) multiplexed M:N over a worker-thread pool. Partialasyncio tasks are cooperative green-thread-like coroutines on one thread, but they are not preemptive and do not parallelize CPU work. True M:N preemptive lightweight threads are not part of CPython.
async / awaitLanguage-level syntax for writing non-blocking, suspendable code in a sequential style. Noguji has no async/await keywords and no future type; it follows Go's model where ordinary blocking code runs inside hatch tasks and the scheduler handles suspension at send/recv/select points (spec §17). 'One obvious way' favors tasks-and-channels over coloured functions. NoGo has no async/await by design; goroutines plus channels make every function effectively non-blocking from the caller's view, so functions are not 'coloured'. Blocking calls simply yield the goroutine to the scheduler. Via libraryOCaml 5 has no async/await keywords; instead effect handlers let Eio and Lwt/Async offer direct-style or monadic concurrency. Domainslib.Task exposes async/await as ordinary functions for parallel task pools. Via libraryCore Perl has no async syntax. The Future::AsyncAwait module adds real async/await keywords layered on IO::Async/Future, giving non-blocking code in a sequential style on a single thread. Yesawait is a built-in that blocks the current scheduler slot until a Promise (or list of them) completes, and start produces a Promise. Together they form Raku's first-class async model. Yesasync fn/async {} produce Futures and .await suspends them; futures are lazy and do nothing until polled by an executor. Rust ships the syntax and Future trait but no runtime, so you add tokio or async-std. Yesasync def defines coroutines and await suspends them, driven by an event loop (asyncio in the stdlib). It is cooperative and single-threaded, ideal for high-concurrency I/O.
Channels / CSPTyped conduits that pass values between concurrent tasks, the basis of Communicating Sequential Processes. Yeschannel() builds an unbuffered (rendezvous) Chan[T], channel(n) a buffered one. $ch.send($v) returns Result[Unit, Closed], $ch.recv() returns Option[T] (None once closed and drained), and for ranges a channel until it closes (spec §17.2-17.4). YesChannels are a core type: make(chan T) (unbuffered) or make(chan T, n) (buffered), with ch <- v to send and <-ch to receive. A closed channel yields the zero value with a false ok flag, and range drains it. Via libraryNo channels in the stdlib, but Domainslib.Chan gives multiple-producer/multiple-consumer bounded and unbounded channels across domains, and Eio.Stream offers fiber-level channels. Via libraryThread::Queue provides a thread-safe FIFO queue that acts as a channel between ithreads (enqueue/dequeue). Outside ithreads, Coro::Channel serves coroutines. YesChannel is a built-in thread-safe queue with .send and .receive; closing it lets a for loop or react/whenever drain it. Supply adds a pub/sub streaming complement. Yesstd::sync::mpsc gives multi-producer/single-consumer channels (Sender/Receiver); tokio::sync::mpsc and crates like crossbeam add async and MPMC variants. Send moves ownership, enforcing data-race freedom. Yesqueue.Queue is a thread-safe channel for threads, multiprocessing.Queue/Pipe for processes, and asyncio.Queue for coroutines. These are library types rather than a CSP language primitive.
Selecting over multiple channelsWaiting on several channel operations at once and proceeding with whichever becomes ready first. Yesselect { ... } waits on several channel ops and runs the first ready arm, choosing arbitrarily among several ready ones; an optional else makes it a non-blocking poll. A recv arm binds the Option[T] it yields (spec §17.5). Yesselect blocks until one of its channel cases can proceed, picking randomly among ready cases; a default clause makes it non-blocking. This is the primitive guji's select is modeled on. Via libraryNo built-in select, but Eio exposes Fiber.first/any to race fibers, and Reagents composes channel operations with choice. Domainslib channels lack a native multi-way select. Via libraryNo channel-select construct. Event loops (IO::Async, AnyEvent) let you await multiple async sources, and Future->wait_any/needs_any resolves on the first ready future. Yesreact { whenever $chan { ... } } reacts to whichever of many channels/supplies/promises emits first, the idiomatic multi-way wait. Promise.anyof also resolves on the first to complete. Via libraryThe standard library has no select. The tokio::select! macro awaits multiple async branches and runs the first ready one, and crossbeam's select! does the same for sync channels. Via libraryNo channel select for threads; asyncio.wait(..., return_when=FIRST_COMPLETED) or asyncio.as_completed selects the first ready awaitable, and selectors/select handle file descriptors.
Shared-memory synchronizationCoordinating concurrent access to mutable memory with locks, atomics, and other primitives. Noguji has no mutexes, atomics, or shared mutable state at all: every value crossing a channel is immutable and hatch may capture only immutable bindings, so there is nothing to lock (spec §17). Tasks share data solely by communicating. YesAlthough channels are preferred, sync.Mutex/RWMutex, sync.WaitGroup, sync.Once, and the sync/atomic package guard shared memory. Goroutines share one address space, so this is fully available. YesDomains share the heap, so OCaml 5 provides Mutex, Condition, Semaphore, and the Atomic module for lock-free counters. The memory model gives well-defined behavior for data-race-free programs. PartialWith ithreads, variables are thread-local unless marked :shared via threads::shared, and lock/cond_wait coordinate them. Without ithreads there is no shared mutable memory between Perl threads. YesRaku threads share memory and offer Lock, Lock::Async, Semaphore, and atomic ops (atomic-inc/ operators), though high-level Promise/Supply are preferred over manual locking. YesShared state uses Arc<Mutex<T>> or RwLock, plus std::sync::atomic types; the Send/Sync traits and borrow checker make misuse a compile error. This is the safe alternative to message passing. Yesthreading provides Lock, RLock, Semaphore, Event, and Condition. Under the GIL these mainly prevent logical races; the free-threaded build makes them load-bearing for memory safety too.
Data-race safetyWhether the language statically or structurally prevents two tasks writing/reading the same memory unsynchronized. YesData races are structurally impossible: values are immutable by default, every value sent over a channel is immutable, and hatch closures may capture only immutable bindings, so no task can ever observe another mutating shared data (spec §17). There is no shared mutable state to race over. NoGo does not prevent data races: goroutines can freely share mutable memory and a missing lock is a silent bug. The go run -race detector finds many races at runtime, but the compiler gives no guarantee. PartialOCaml 5's memory model guarantees that data-race-free programs are sequentially consistent and that even racy programs stay memory-safe (no crashes/tearing of immediates), but it does not statically forbid races; correctness still needs discipline. Partialithreads avoid races by making data thread-local by default (no sharing unless explicitly :shared), so accidental cross-thread races are rare, but :shared data still needs manual locking. There is no compile-time race check. NoRaku shares mutable memory across threads and does not prevent races at compile time; correctness relies on using high-level Promise/Supply/Channel or explicit locks. Low-level shared mutation can race. YesRust statically prevents data races in safe code: the borrow checker plus the Send/Sync marker traits make any unsynchronized shared mutation a compile error. This is the core of 'fearless concurrency'. PartialIn the default GIL build, only one thread runs bytecode at a time so low-level memory corruption is avoided, but high-level logical races (e.g. read-modify-write) still occur. The free-threaded build removes the GIL, making true data races possible.
Actors & message passingIsolated units of state that communicate only by asynchronous messages, never by shared memory. Partialguji has no actor type, but its tasks-plus-immutable-channels model is pure message passing: a hatch task owning a Chan[T] and looping for $msg in $ch is an actor in all but name, with state never shared (spec §17). The motto is communicate, don't share. PartialGo has no actor type but encourages the actor-like pattern: a goroutine owning some state and serving requests over a channel. 'Share memory by communicating' is the canonical idiom. Via libraryNo built-in actors; libraries provide them. You can build an actor from a Domainslib.Chan mailbox plus a domain, and effect-based libraries compose message-passing fibers. Via libraryNo core actor model. CPAN offers actor frameworks and message-passing layers, and Thread::Queue between ithreads gives mailbox-style communication. PartialRaku has no actor keyword, but OO::Actor-style patterns are easy, and Supply/Channel plus react/whenever express message-driven isolated workers idiomatically. Via libraryNo actors in std, but actix, ractor, and kameo are mature actor frameworks; the common channel-per-task pattern (a task owning state behind an mpsc receiver) is also widely used. Via libraryNo built-in actors. Frameworks like Pykka, Thespian, Ray, and Dramatiq provide actor or task-message models, and multiprocessing plus queues gives isolated processes communicating by messages.

Functional programming

FeaturegujiGoOCamlPerlRakuRustPython
First-class functionsFunctions are ordinary values: they can be bound to names, passed as arguments, and returned from other functions. YesA sub is a value (§7.1). It can be passed, returned, and stored in a binding; the uniform call rule means any function is callable directly or method-style. YesFunctions are first-class values with a func type; they can be assigned, passed, and returned. Closures are supported via function literals. YesFunctions are first-class and curried by default; passing and returning them is the everyday idiom in OCaml. YesSubroutines are first-class via code references (\&name or anonymous sub { ... }), called with $ref->(...). YesRoutine/Block objects are first-class values; subs and blocks are passed and returned freely, and &name references a named sub. YesFunctions can be passed as fn pointers or via the Fn/FnMut/FnOnce closure traits, typically behind generics or Box<dyn Fn>. YesFunctions are objects: they can be assigned, passed, returned, and stored in data structures with no special syntax.
ClosuresAnonymous functions that capture and retain bindings from their enclosing lexical scope. YesLambdas are anonymous subs (or { ... } topic blocks, §7.4) that capture surrounding bindings. Because captures are immutable by default, a closure observes a stable snapshot. YesFunction literals close over surrounding variables by reference, so captured variables stay live and can be mutated. YesEvery anonymous function (fun) closes over its lexical environment; captured immutable bindings are the norm. YesAnonymous sub { ... } blocks capture lexical my variables, forming proper closures that persist after the enclosing scope returns. YesBlocks and pointy blocks (-> $x { ... }) close over lexical variables, the basis of much of Raku's functional style. YesClosures capture environment by reference or by value (move); the compiler infers the least-restrictive Fn trait that fits. YesNested functions and lambda close over enclosing locals; rebinding a captured name requires nonlocal.
Immutability by defaultBindings (and ideally values) are immutable unless mutation is explicitly opted into. YesBindings are immutable by default (§4); reassignment is an error and mut is required to opt in. "Methods" that change an object return a new instance, and channel-crossed values are immutable (§17). Partialconst exists only for compile-time scalar/string constants; variables, structs, slices, and maps are mutable, so immutability is not the default. Yeslet bindings and most data are immutable; mutation is explicit via ref, mutable record fields, or arrays. PartialVariables are mutable by default; immutability is opt-in via core use constant (scalars) or the Readonly/Const::Fast modules. PartialVariables are mutable, but value types (Int, Str, etc.) are immutable, and constant/sigilless bindings give immutable names; Map/List are immutable by default while Hash/Array are mutable. Yeslet bindings are immutable by default; mutation requires let mut, and the borrow checker enforces aliasing-vs-mutation at compile time. PartialNames are rebindable and most containers are mutable; immutability is available per-type (tuple, frozenset, str) or via @dataclass(frozen=True), not as a default.
Pattern matchingDestructuring and dispatching on the shape of values, binding sub-parts in the process. Yesmatch (§6.2, §10) is an expression with literal, binding, wildcard, variant, and nested patterns plus if guards. The compiler checks exhaustiveness and reports missing cases. PartialThere is no structural pattern matching; switch (including type switches) dispatches on values or dynamic types but cannot destructure or bind nested fields. Yesmatch ... with is a core construct over variants, tuples, records, and lists, with nested patterns, guards (when), and compiler-checked exhaustiveness. PartialNo native structural matching; the experimental given/when is discouraged. Idiomatic dispatch uses if/regex or modules; Perl 5.38+ adds experimental class/field but not match. Yesgiven/when smart-matches against types, ranges, regexes, and values, and signature destructuring (-> (:$x, :$y)) plus where clauses cover structural cases. Yesmatch over enums, tuples, structs, and slices with nested patterns, bindings, ranges, and guards; the compiler enforces exhaustiveness. YesStructural pattern matching (match/case) landed in Python 3.10 (PEP 634) with sequence, mapping, class, and capture patterns plus guards; it is not exhaustiveness-checked.
Recursion and tail-call optimizationSupport for recursive functions and whether tail calls run in constant stack space (TCO). NoRecursion works, but the v0 tree-walking evaluator provides no TCO: each call consumes native stack, so deep tail recursion (~1M frames) overflows. Idiomatic iteration uses collection methods (§6.3); a guaranteed-tail-call form is not in v0. NoRecursion is supported but the Go compiler does not perform tail-call optimization; deep recursion grows the (growable) goroutine stack. Loops are idiomatic. PartialTail calls in tail position run in constant stack as an implementation property; it is not a formal language guarantee, and non-tail recursion (e.g. naive list build) needs the opt-in [@tail_mod_cons] to stay tail-recursive. PartialOrdinary recursion grows the call stack with no automatic TCO; an explicit tail call can be written with goto &sub, and Sub::Recursive style helpers exist. PartialRecursion is supported; Rakudo does not guarantee general TCO, so deep recursion can overflow. Lazy sequences and iteration are preferred for unbounded work. NoRecursion is supported but tail-call elimination is not guaranteed by the language; LLVM may optimize some cases, so deep recursion can overflow. Loops/iterators are idiomatic. NoRecursion works but there is no TCO, and a recursion-depth limit (~1000) raises RecursionError; iteration is the idiomatic alternative.
Currying and partial applicationBuilding a new function by fixing some arguments of an existing one (partial application) or treating multi-arg functions as nested single-arg ones (currying). PartialNo dedicated currying or partial-application syntax ("one obvious way"). Partial application is expressed by returning a closure that captures the fixed arguments. PartialNo built-in currying or partial application; you write a closure that captures the bound arguments explicitly. YesAll functions are curried by default, so partial application is just supplying fewer arguments — no helper needed. PartialNo native currying; partial application is done with a closure, or via CPAN helpers such as List::Util/Sub::Curry. YesThe built-in .assuming method primes a routine with fixed arguments, and the * whatever-star (* + 2) also produces partially applied callables. PartialNo currying syntax; partial application is a closure capturing the fixed arguments (often move). Yesfunctools.partial (standard library) fixes leading/keyword arguments; closures or lambda also work for ad-hoc cases.
Lazy evaluationDeferring computation until a value is needed, enabling infinite sequences and avoiding unused work. NoEvaluation is strict in v0: collection methods like map/filter materialize eagerly, and lazy/infinite sequences are explicitly deferred (§21) as a possible future opt-in construct, not lazy-by-default. PartialNo language-level laziness, but you can defer work with goroutines plus channels, or with closures (thunks) computed on demand. YesThe Lazy module with the lazy/Lazy.force keywords gives explicit memoized thunks, and Seq provides lazy, potentially infinite sequences. Via libraryNo core laziness; CPAN modules such as List::Gen, Scalar::Defer, or tied/iterator patterns provide lazy lists and on-demand evaluation. YesLists are lazy by default where possible; the lazy/gather/take machinery and infinite ranges (1..∞) support lazy and infinite sequences natively. YesIterators are lazy adapters that do no work until consumed, and std's LazyCell/LazyLock (and OnceCell) provide lazy, memoized initialization. YesGenerators and generator expressions evaluate lazily, and itertools builds lazy (even infinite) pipelines pulled element by element.
Pipelines and compositionComposing data transformations so values flow left-to-right through a series of operations. YesThere is no separate pipeline operator; the data-first uniform call convention (§7.2) makes . the single composition mechanism, so transformations chain left-to-right (§7.3). PartialNo pipeline operator or method-chaining stdlib for collections; you compose with nested calls, explicit loops, or channel-based pipeline stages between goroutines. YesThe pipe operator |> threads a value through functions left-to-right, and @@ / Fun.compose express ordinary composition. PartialNo pipeline operator; transformations chain by nesting map/grep (right-to-left) or via method chains on objects; CPAN modules add pipe-like syntax. YesThe feed operators ==> (and <==) pass a list through stages left-to-right, and method chaining plus o/ composition are first-class. PartialNo pipe operator, but iterator adapters chain fluently left-to-right via .; general function composition has no dedicated syntax. PartialNo pipe operator; composition is nested calls or comprehensions, and method chaining is common in fluent libraries (e.g. pandas) rather than a language feature.

Memory & Runtime

FeaturegujiGoOCamlPerlRakuRustPython
Automatic garbage collectionUnreachable memory is reclaimed automatically by a runtime collector rather than freed by hand. Yesguji is memory-managed: bindings and values are immutable by default and the runtime reclaims unreachable data automatically, so a program never frees memory by hand. The v0 reference is a tree-walking evaluator running on the Go runtime, and the planned native compiler ships its own managed runtime in the single binary (§17.6, §18). YesGo has a non-generational, concurrent tri-color mark-and-sweep collector with write barriers; it runs mostly alongside your goroutines with only two brief stop-the-world pauses. It is tuned via GOGC/GOMEMLIMIT and runtime.GC() forces a cycle, but you essentially never manage memory manually. YesOCaml uses a generational collector: a small minor heap collected by copying, and a major heap collected by an incremental mark-and-sweep. Since OCaml 5 the runtime is parallel (a per-domain minor heap over a shared, mostly-concurrent major heap); the Gc module exposes tuning and stats. PartialPerl reclaims memory by reference counting: a value is freed the instant its refcount hits zero, giving prompt destruction. There is no cycle collector, so a circular reference leaks until interpreter shutdown unless you break it (see manual memory below). YesRaku (on MoarVM) uses a real tracing, generational, parallel garbage collector rather than reference counting — a deliberate choice for a multi-threaded core. Collection is not prompt, so there is no timely destruction; cycles are reclaimed correctly without weakening. NoRust has no garbage collector. Memory is freed deterministically when a value's owner goes out of scope (RAII / Drop), decided entirely at compile time by ownership. Shared-ownership opt-ins (Rc/Arc) use reference counting, but there is no background collector. YesCPython combines reference counting (prompt) with a cyclic generational collector (the gc module) that reclaims reference cycles refcounting alone cannot. Since 3.13 a second, free-threaded GC implementation pauses other threads instead of relying on the GIL.
Ownership & borrowingA compile-time discipline that tracks who owns each value and lends temporary references, preventing use-after-free and data races without a GC. Noguji has no ownership or borrow checker. It achieves memory and thread safety through a different route — a managed runtime plus immutability by default, where every value crossing a channel is immutable so data races are not even expressible (§17). There are no lifetimes, references, or &/&mut distinctions to reason about. NoGo has pointers but no ownership system or borrow checker; the GC makes lifetime tracking unnecessary. Aliasing and concurrent mutation are possible, so data races are caught only at run time by the -race detector, not prevented by the type system. NoOCaml relies on its GC for memory safety and has no ownership/borrowing. Immutability is the default and mutation is explicit (ref, mutable fields, arrays), but lifetimes are not tracked; aliasing is unrestricted and safe because nothing is freed manually. NoPerl has no ownership model; lifetime is governed entirely by reference counting at run time. Any number of references may alias the same value, and the last one to be dropped frees it — there is no compile-time borrow analysis. NoRaku has no ownership or borrowing; the tracing GC owns all lifetimes. Containers and bindings (my $x) alias freely, and is rw/is copy control mutability of parameters, but none of this models ownership transfer or borrow lifetimes. YesOwnership and borrowing are Rust's defining feature: every value has exactly one owner, the borrow checker enforces either one mutable (&mut) or any number of shared (&) references at a time, and lifetimes ensure no reference outlives its referent. This guarantees memory and thread safety at compile time with no GC. NoPython has no ownership or borrowing; names are bindings to shared, refcounted objects and any number of names may alias the same object. Lifetime is a run-time property of the reference count plus cyclic GC, not a compile-time discipline.
Manual / explicit memory managementThe programmer can or must explicitly allocate, free, or otherwise direct the lifetime of memory. Noguji exposes no allocation or free primitives — there is no malloc/free, no pointers, and no unsafe escape hatch in v0. Memory is wholly the runtime's concern; the programmer only creates immutable values and lets them go out of scope. PartialDay-to-day Go is fully managed, but the unsafe package and unsafe.Pointer allow raw pointer arithmetic and manual layout control, and arena-style or off-heap allocation is possible via cgo/syscalls. These are escape hatches, not the normal path. PartialAllocation is automatic, but OCaml offers Bigarray and Stdlib.Bytes for off-heap/contiguous buffers, and the C FFI plus the Gc and Obj modules let you manage external memory and tune or trigger collection. Manual free only applies to memory the GC does not own (e.g. C-allocated). PartialThere is no free, but because reclamation is reference-counted you manage lifetime by managing references — and crucially you must break reference cycles manually, typically with Scalar::Util::weaken, or they leak. undef-ing the last reference frees promptly. NoRaku does not expose manual memory management; the tracing GC handles everything, including cycles, so there is no weaken-for-correctness need as in Perl. The trade-off is no guaranteed, timely destruction — you cannot force when an object is reclaimed. YesFreeing is automatic and deterministic via Drop, but Rust gives full manual control when needed: Box/Vec own heap allocations, std::mem::drop frees early, custom allocators implement the GlobalAlloc trait, and unsafe with raw pointers (*mut T) plus alloc/dealloc allow C-style management. PartialMemory is automatic, but you can influence it: del drops a reference, gc.disable()/gc.collect() control the cyclic collector, and weakref avoids keeping objects alive. True manual allocation lives in C extensions and the ctypes/buffer-protocol layer.
Compiled vs. interpreted executionWhether programs are compiled ahead-of-time to native code or executed by an interpreter / virtual machine. Yesguji is specified as ahead-of-time compiled to native code (source → tokens → typed tree → IR → native executable, §18). In v0 only the tree-walking evaluator stages are implemented — it runs programs interpreted today — with native code generation as a planned roadmap stage (§19). YesGo is compiled ahead-of-time to native machine code by the gc toolchain (go build). There is no interpreter or bytecode VM in the standard distribution; the compiled binary embeds the runtime and scheduler. YesOCaml offers both: ocamlopt compiles to optimized native code, while ocamlc produces portable bytecode run by a VM, and a top-level REPL interprets interactively. Native is the default for production performance. NoPerl is interpreted: perl compiles source to an internal opcode tree at startup and then walks it; there is no standard ahead-of-time native compiler. The compile-then-run happens every invocation. PartialRaku source is compiled to bytecode for a virtual machine (primarily MoarVM), which then executes it with a profiling JIT for hot paths. It is not interpreted line-by-line like Perl, but neither does the standard toolchain emit a standalone native executable. YesRust is compiled ahead-of-time to native code through LLVM (rustc/cargo build), with no interpreter or runtime VM. Monomorphization and optimization happen entirely at build time. PartialCPython compiles source to bytecode (.pyc) and then interprets it on a stack VM — so it is compiled-to-bytecode but not to native code. 3.13 added an experimental JIT; alternative implementations (PyPy) JIT to native.
Single self-contained binaryWhether the toolchain can produce one standalone executable with no external runtime or interpreter to install on the target. YesProducing a single self-contained native executable with no external runtime is an explicit design principle (§1.1, §18): the compiler emits one binary and the task scheduler is part of it (§17.6). This is a v0 goal delivered by the planned code-generation stage; the current evaluator runs source directly. YesGo is famous for this: go build produces a single statically-linked binary that embeds the runtime and GC, and with CGO_ENABLED=0 it has no libc dependency, so the binary runs on a bare image. This drives Go's popularity for containers and CLIs. Yesocamlopt links the runtime into the executable, producing a native binary that needs no OCaml installation to run. It dynamically links libc by default, but is otherwise self-contained (fully-static links are achievable with extra flags/musl). Via libraryStock Perl needs the perl interpreter present, but packagers like PAR::Packer (pp) or staticperl bundle the interpreter and modules into one self-contained executable. It is a third-party tool, not the core workflow. NoRaku programs require the Rakudo runtime and MoarVM to be installed; the standard toolchain does not emit a standalone native binary. Distribution means shipping the compiled bytecode plus a Raku runtime (or a container image). Yescargo build yields a single native binary with the small Rust runtime statically linked. Targeting *-musl produces a fully-static, dependency-free executable; by default it dynamically links the system libc only. Via libraryCPython itself needs an interpreter on the target, but PyInstaller, Nuitka, or cx_Freeze package the interpreter, your code, and dependencies into one self-contained executable. These are third-party, and the bundle is large.
Startup & runtime modelWhat must exist and execute when a program starts: a managed runtime/VM, a scheduler, an entry point, and the cost of getting going. YesExecution begins at sub main(), which may return Unit or an Int exit code (§18). A managed runtime (memory management plus, when concurrency lands, the M:N task scheduler, §17.6) is embedded in the binary — there is no separate runtime to install, and native startup is intended to be near-instant. YesA compiled Go binary boots its embedded runtime (GC, goroutine scheduler) and then calls func main() in package main; startup is fast and self-contained. The M:N scheduler multiplexes goroutines onto OS threads from the first instruction. YesA native OCaml executable links in a small runtime (GC, exception machinery) and starts at the top-level module bindings, by convention let () = .... Startup is fast; OCaml 5 adds a domain scheduler for parallelism but a single-domain program pays little for it. PartialEach run starts the perl interpreter, which parses and compiles the whole program to opcodes before executing top-to-bottom (no main is required). Startup cost is the interpreter launch plus compiling the script and its used modules. PartialStarting Raku spins up MoarVM and the Rakudo runtime, then runs the program (top-level statements, or a sub MAIN for CLI argument handling). Historically startup was heavy; precompilation of modules has reduced but not eliminated the runtime-launch cost. YesRust has a minimal runtime — essentially just std setup before calling fn main() — with no GC and no scheduler, so startup is very fast and predictable. async work needs a runtime library (e.g. Tokio) you opt into, not a built-in one. PartialLaunching python initializes the interpreter, then imports and runs the module top-to-bottom; the if __name__ == "__main__": guard is the conventional entry point. Interpreter and import startup is the dominant cost for short scripts.
Value vs. reference semanticsWhether assigning or passing a value copies it or shares (aliases) the same underlying object. YesBecause bindings and values are immutable by default, the value-vs-reference distinction is mostly invisible: nothing can observe sharing when nothing mutates, and methods that 'modify' return a new instance (§8.3). The runtime is free to share structurally without changing observable behaviour. YesGo assigns and passes by value — structs and arrays are copied — but slices, maps, and channels are small headers that reference shared backing storage, and you take pointers explicitly with &. So copy-by-default with explicit sharing. YesOCaml binds names to immutable values by default; assignment binds, it does not mutate. Mutable cells are explicit (ref, mutable record fields, arrays) and are shared by reference, while ordinary data is conceptually copied because it never changes. PartialPlain scalars/arrays/hashes are copied on assignment (value semantics), while references (\$x, [...], {...}) are explicit aliases to shared storage. @_ aliases the caller's arguments, a notable reference-like exception. PartialRaku distinguishes containers from values: $x is a mutable container, but binding (:=) shares while assignment (=) copies into the container. Sigil-bearing structures and objects are reference-like, and is copy/is rw control argument sharing. YesRust assigns by move by default (ownership transfers, the source becomes invalid), copies only for Copy types, and shares only through explicit borrows (&) or smart pointers (Rc/Arc). The semantics are made explicit by the type and the borrow checker. NoPython uses call-by-object-reference: names always bind to shared objects, so assignment never copies. Mutating a shared mutable object (a list, dict) is visible through every name bound to it; you copy explicitly with copy/deepcopy.
Deterministic destruction & finalizersWhether resources are released at a predictable, well-defined moment (scope exit / refcount zero) versus whenever a collector eventually runs. Noguji v0 has no destructors, finalizers, or RAII; cleanup is the managed runtime's concern and timing is not specified. With immutability and no exposed resource-handle types in v0 (the only IO primitive is print, §15.4), there is no user-visible deterministic-cleanup mechanism yet. PartialGo has no destructors; the idiom is defer to release resources at function-scope exit, which is deterministic and explicit. runtime.SetFinalizer exists but runs at an unpredictable GC time and is discouraged for resource cleanup. PartialOCaml has no RAII; deterministic cleanup is done explicitly with Fun.protect / try ... finally-style wrappers (e.g. In_channel.with_open_text). Gc.finalise registers a finalizer, but it fires at collection time, not deterministically. YesReference counting gives Perl prompt, deterministic destruction: an object's DESTROY method runs the instant its last reference goes away (typically scope exit). This is why RAII-style guard objects work cleanly — the cycle-leak caveat aside. NoBecause Raku uses tracing GC, there is no timely destructionDESTROY (submethod) may run much later or not at all. For deterministic cleanup you use a LEAVE phaser or explicit .close, not object destruction. YesRust's Drop trait gives fully deterministic destruction: a value's destructor runs exactly when it goes out of scope (or on explicit drop), in reverse declaration order. This is the foundation of RAII for files, locks, and memory. PartialRefcounting makes __del__ usually prompt in CPython, but it is not guaranteed (cycles, other implementations) so it is not relied on. The deterministic idiom is the context manager — with/__enter__/__exit__ — which releases at block exit.

Modules, Tooling & Errors

FeaturegujiGoOCamlPerlRakuRustPython
Module / package systemHow code is split into namespaced units and brought into scope. YesOne file is one module; its path relative to the project root is the module path, with / written as ::. import brings a module into scope under its final segment and members are reached with ::. Top-level declarations are module-private unless marked pub; selective/aliased imports are deferred past v0. YesA package is a directory of files sharing a package clause; capitalized identifiers are exported. import paths are resolved through the module system (go.mod), and a module declaration sets the import-path root. YesEach .ml file is a module (named by the capitalized filename) with an optional .mli interface controlling what is exported. First-class modules, functors (modules parameterized by modules), and explicit signatures make the module system unusually expressive. YesA package is a namespace (package Foo::Bar;), conventionally one per .pm file under a matching path. use Module; loads the file and runs its import, typically exporting symbols via Exporter. YesCode is organized into module, class, role, and package blocks; unit module Foo; makes a whole file one module. use/need/require load distributions and you control exports with the is export trait. Lexical, fine-grained importing is the default. YesA crate is the compilation unit; inside it mod defines a tree of modules and pub controls visibility. use brings paths into scope and items are private to their module by default. YesEach .py file is a module and a directory with __init__.py (or a namespace package) is a package. import/from ... import load modules and bind names; everything is public by convention, with a leading _ signalling private.
Package manager & registryThe standard way to declare, fetch, and version third-party dependencies. Noguji v0 has no package manager or registry. The spec covers modules and import within a project root only; dependency resolution and a published ecosystem are out of scope for the current evaluator/compiler milestones. YesGo modules are built into the go tool: go.mod pins versions and go get/go mod tidy resolve them with minimal version selection. There is no central registry — modules are fetched directly from their VCS URLs, fronted by a public module proxy and checksum database. Yesopam is the standard package manager and opam.ocaml.org the main repository; it manages packages and switches (isolated compiler+package environments). dune is gaining integrated lockfile-based package management (dune lock) layered on opam's repositories. YesCPAN is the long-standing central archive; cpanm (App::cpanminus) is the popular zero-config installer. cpanfile plus Carton (or Carmel) gives reproducible, project-local dependency installs. Yeszef is the official installer that resolves and installs distributions; the modern fez ecosystem is the primary upload/registry path (browsable at raku.land). A META6.json describes a distribution's dependencies. YesCargo is the official package manager and build tool; Cargo.toml declares dependencies and Cargo.lock pins them. Packages ("crates") are published to and fetched from the central crates.io registry. Partialpip ships with CPython and installs from PyPI, but it alone doesn't lock or manage virtual environments well, so projects layer on tools. The fast Rust-based uv has become the popular modern all-in-one (resolver, lockfile, venv, Python versions); Poetry/PDM are older alternatives.
Build toolThe standard command that compiles/assembles a project into a runnable artifact. PartialThe language is specified to compile ahead-of-time to a single self-contained native executable, but v0 ships only a tree-walking evaluator (and a REPL) — native code generation is a later roadmap stage. There is no separate build-tool/project format yet. YesThe go tool is the build system: go build compiles a static native binary and go run builds-and-runs. Builds are fast and dependency-free thanks to the integrated module system. Yesdune is the de-facto build system, driven by small declarative dune files; dune build compiles to native or bytecode. It composes across packages and handles tests, docs, and install rules. PartialPerl is interpreted, so scripts run with no build step. For distributions, ExtUtils::MakeMaker (Makefile.PL) or Module::Build produce the standard perl Makefile.PL && make && make test && make install flow, often driven by Dist::Zilla. PartialPrograms run directly via the raku runtime (compiling to MoarVM bytecode on the fly), so there is no separate build command for scripts. Distribution build/test/install is handled by zef build/zef test against META6.json. YesCargo is the build tool: cargo build (debug) / cargo build --release (optimized) compile via rustc, caching artifacts and wiring up all crate dependencies. cargo run builds and runs. Via libraryPython runs source directly, so most projects need no build. Packaging into wheels uses a PEP 517 build backend (setuptools, Hatchling, Flit) invoked through the build frontend or uv build.
Code formatterA canonical auto-formatter that normalizes source layout. NoNo formatter ships with v0. The language leans on its "one obvious way" design — minimal redundant syntax — but a gofmt-style canonical formatter is not part of the current toolchain. Yesgofmt (and go fmt) is the canonical, non-configurable formatter shipped with the toolchain; goimports adds import management. Formatting is culturally mandatory, ending all style debates. Yesocamlformat is the standard auto-formatter, configured per-project via an .ocamlformat file and integrated with editors and dune build @fmt. It is distributed through opam. Via libraryPerl::Tidy (perltidy) is the widely used formatter, configurable via .perltidyrc. It is a CPAN module rather than part of core Perl. Via libraryThere is no official formatter in core; community tooling (e.g. the Rakudo::Formatter / editor-based efforts) exists but is not yet a single canonical standard, so layout is largely manual. Yesrustfmt (cargo fmt) is the official formatter, installed with the standard toolchain and tunable via rustfmt.toml. Running it is standard practice across the ecosystem. Via libraryNo formatter ships with CPython, but Black ("the uncompromising formatter") and the much faster Rust-based Ruff formatter (Black-compatible) are the de-facto standards installed from PyPI.
Error-handling modelHow a program signals, propagates, and recovers from failure. Yesguji has no exceptions: fallible operations return Result[T, E] or Option[T], and the postfix ? operator propagates an Err/None early. Truly unrecoverable bugs use panic, which aborts the process and cannot be caught (§11). YesErrors are ordinary error values returned as the last result and checked explicitly with if err != nil; errors wrap/unwrap via %w, errors.Is/errors.As. panic/recover exists but is reserved for exceptional, unrecoverable situations. YesOCaml offers both styles: built-in exceptions (raise/try ... with) and a typed result/option discipline encouraged by the standard library and let* binding operators. Idiom increasingly favours result for recoverable errors. YesClassic Perl signals errors by die-ing (a string or object) and catching with eval { }; many functions also return a false value plus $!. Perl 5.34+ provides native try/catch (stable in 5.40), and Try::Tiny remains popular. YesRaku uses exceptions thrown with die/fail and handled by try/CATCH blocks; fail returns a lazy Failure that throws only when used. Typed exception classes and resumable control flow round it out. YesRecoverable errors use Result<T, E> (and Option<T>) with the ? operator for early propagation; panic! handles unrecoverable bugs. Crates like thiserror/anyhow ease error types — Rust's ? model directly inspired guji's. YesPython is exception-based: raise signals errors and try/except/else/finally handles them, with a rich exception class hierarchy. "Easier to ask forgiveness than permission" (EAFP) is the idiom.
Testing storyThe standard way to write and run automated tests. Nov0 has no in-language test framework; the implementation's own strategy is a directory of .guji programs each paired with expected output, run end-to-end. A user-facing testing API is not yet specified. YesTesting is built in: *_test.go files with func TestXxx(t *testing.T) are run by go test, which also handles benchmarks, fuzzing, and coverage. No third-party framework is needed. Via libraryNo test runner ships with the compiler, but dune orchestrates tests (dune test) and the ecosystem standardizes on libraries like Alcotest, OUnit, and inline ppx_expect tests. YesTesting is a core strength: Test::More/Test2 emit TAP (Test Anything Protocol) and prove runs t/*.t suites. The toolchain has assumed TAP-based tests since the early days. YesThe core Test module ships with Rakudo and emits TAP; zef test / prove6 run t/ suites. Functions like ok, is, and is-deeply are available out of the box. YesTesting is built in: #[test] functions (unit, integration in tests/, and doc-tests) are run by cargo test. Assertions like assert_eq! and #[should_panic] come with the language. YesThe standard library bundles unittest and doctest, but the third-party pytest is the de-facto standard for its plain-assert style and fixtures. Tests run via pytest or python -m unittest.
OOP / classesSupport for user-defined types with fields, methods, and (optionally) inheritance. Partialguji has class (product types with has fields, public $./private $! twigils, and $self methods) and enum sum types with methods — but it is functional-first: values are immutable, "mutating" methods return new instances, and there is no inheritance (polymorphism is via match, with traits deferred). PartialGo has struct types with methods and interfaces for polymorphism, plus composition via struct embedding — but deliberately no classes or implementation inheritance. Interface satisfaction is structural (implicit). YesOCaml has a full object system (class, objects, inheritance, structurally-typed row-polymorphic methods), though idiomatic OCaml favours modules, records, and variants instead. Objects are present but used sparingly. YesClassic Perl OOP is built from packages, blessed references, and @ISA inheritance, usually smoothed by Moose/Moo. Perl 5.38+ adds a native class/field/method system (Corinna), stabilizing but still marked experimental as of 5.42. YesRaku has a rich native object model: class with has attributes and methods, roles for composable behavior, multiple inheritance, and multi-dispatch. The metaobject protocol makes it deeply introspectable. PartialRust has struct/enum types with impl methods and traits for polymorphism (static or dyn dynamic dispatch), but no classes and no inheritance — composition and traits replace it. YesPython is thoroughly object-oriented: class with methods, __init__, multiple inheritance (C3 MRO), and dynamic duck typing. @dataclass reduces boilerplate for data-holding classes.
Foreign function interface (FFI)Calling into C or other native libraries from the language. Nov0 has no FFI. The only IO primitive specified is print; file, stream, and any C-interop facilities are deferred until after the evaluator and compiler pipeline are in place. Yescgo lets Go call C by importing the pseudo-package "C" with C in a preamble comment; the toolchain compiles and links it. There is overhead per call, so pure-Go is preferred when practical. YesOCaml has a mature C FFI via external declarations and stub C files (the manual's interop chapter), and the ctypes library binds C libraries without writing C stubs by describing types at the OCaml level. YesThe classic route is XS (C glue compiled into a .so); the modern, no-compiler-needed route is FFI::Platypus, which binds shared libraries at runtime. Both are widely used CPAN-backed mechanisms. YesRaku ships NativeCall in core: declare a sub is native('lib') and Raku marshals arguments to the C library at runtime, with CStruct/CArray for native data. No separate compile step is needed. YesRust calls C through extern "C" blocks inside unsafe, with the libc crate and tools like bindgen (C→Rust) and cbindgen (Rust→C) generating bindings. It is a first-class, zero-overhead-friendly path. YesThe standard library bundles ctypes to call shared libraries with no compilation, and cffi is the popular alternative. For performance-critical glue, C-extension modules and Cython/PyO3 are used.

Text & Metaprogramming

FeaturegujiGoOCamlPerlRakuRustPython
First-class regular expressionsRegex patterns as a built-in literal/type with a dedicated match operator, rather than strings passed to a library. YesRegex is a built-in type with a slash-delimited literal (/\w+/) and the ~~ match operator yielding Option[Match]. Unicode-aware by default; recursion/conditionals are deliberately pushed to grammars. Via libraryNo regex literals or operators; the stdlib regexp package (RE2 engine) compiles patterns from strings via regexp.MustCompile. No backreferences or lookaround by design. Via libraryNo regex in the language or stdlib Str aside; idiomatic code uses the re library (or pcre). Patterns are values built from string syntax, with no literal form. YesRegex is core to the language: =~ and !~ operators, /.../ literals, m//, s///, named captures via %+. Perl's regex engine is the de-facto reference for many others. YesRegexes are first-class objects with rx/.../ literals and the smartmatch ~~ operator; they double as the building blocks of grammars. A full redesign of Perl 5 regex with declarative, composable syntax. Via libraryNo regex in std; the official regex crate (linear-time, no backtracking) is near-universal. Patterns compile from strings at runtime, or via macros like regex! for compile-time checking. Via libraryNo regex literals/operators, but the re module ships in the stdlib and is idiomatic everywhere. Patterns are strings (commonly raw r"...") compiled with re.compile.
String interpolationEmbedding variables and expressions directly inside a string literal that is evaluated to a formatted result. YesDouble-quoted strings interpolate sigil bindings ($x, @xs, %m) directly and arbitrary expressions inside { ... }. Single-quoted strings are literal with no interpolation. NoNo interpolation; strings are concatenated or formatted with fmt.Sprintf using verbs like %d/%s/%v. The verbs and arguments are positional and separate from the literal. NoNo interpolation in the language; OCaml has no universal value-to-string, so Printf.sprintf with typed format specifiers is idiomatic. PPX extensions (e.g. ppx_string) add interpolation as a library. YesDouble-quoted strings interpolate scalars ($x) and array/hash elements directly; arbitrary expressions interpolate via @{[ ... ]}. Single quotes are literal. YesDouble-quoted strings interpolate scalars and, with { ... }, any expression; method calls on a sigil'd variable also interpolate. Fine-grained control via quoting adverbs (q/qq). Partialformat!/println! macros support inline captured identifiers in {name} (since Rust 1.58), but arbitrary expressions inside braces are not allowed — only named arguments or captured variables. Yesf-strings (f"...", since 3.6) embed arbitrary expressions in { }, with format specs and (3.8+) = self-documenting form. Evaluated at runtime in the local scope.
Grammars / PEG parsingA built-in, reusable, structured parser construct (named rules, ordered choice) for parsing beyond what flat regex can express. Yesgrammar is the language's signature feature: PEG ordered-choice with token/rule/regex productions, a TOP entry, and <name> subrule references. Grammar.parse returns Option[Bush], a navigable parse tree. Via libraryNothing built in; parser generators like goyacc (LALR), or PEG libraries such as pigeon and participle, are used. Hand-written recursive-descent parsers are also common. Via libraryNo grammar construct in the language, but OCaml has strong parsing tooling: ocamllex/menhir (LR), and PEG/combinator libraries like angstrom. Menhir is the de-facto standard. Via libraryNo grammar keyword, but recursive regexes ((?R), (?1)) and modules like Regexp::Grammars and Marpa provide full grammar parsing on top of the regex engine. YesFirst-class grammar declarations group named token/rule/regex productions with a TOP; grammars are inheritable, overridable classes and can carry semantic actions. Raku's own parser is a Raku grammar. Via libraryNo grammar construct in std; ecosystem crates cover it: pest (PEG from a .pest grammar), nom (parser combinators), and lalrpop (LR). pest derives a parser from a grammar file at compile time. Via libraryNo grammar in the language for user code, though CPython 3.9+ uses a PEG parser internally. Libraries like pyparsing, parsimonious (PEG), and lark provide grammar-based parsing.
Macros / syntactic metaprogrammingCompile-time transformation of source/AST into code, letting users extend the language's syntax or generate code. NoNo macro system. Following the "one obvious way" principle, guji has no user-facing syntactic metaprogramming; code generation lives only in the compiler. Grammars handle structured text, not language extension. NoNo macros by design. The closest mechanism is go generate, which runs external code-generators (e.g. stringer) as a build step, but this is text tooling, not in-language macros. Via libraryNo macros in the core language, but the PPX ecosystem rewrites the typed AST at compile time ([@@deriving ...], [%expr ...]) via ppxlib. This is the standard metaprogramming path. PartialNo true macros, but source filters and especially Keyword::Declarator/PL_keyword_plugin (used by modules like Function::Parameters) let modules add new keywords at parse time. Powerful but niche. PartialRaku is designed for self-extension via slangs and a compile-time macro/AST API, but macros remain experimental and unstable pending the RakuAST rewrite. BEGIN-time code and custom operators cover most needs today. YesTwo kinds: declarative macro_rules! (pattern-based, hygienic) and procedural macros (function-like, derive, and attribute) that transform a TokenStream. Macros are central to the ecosystem (vec!, derive, serde). NoNo macro system. Metaprogramming is done at runtime via decorators, metaclasses, and ast/exec, but there is no compile-time syntactic macro facility that extends the grammar.
Runtime reflection / introspectionInspecting and manipulating types, fields, and values dynamically at runtime. NoFully static with no runtime type information required for dispatch (§18); there is no reflection API. Type identity is erased at compile time and match over sum types replaces runtime type inspection. YesThe stdlib reflect package inspects and manipulates arbitrary values, types, struct fields, and tags at runtime. Widely used by encoders (encoding/json) and ORMs. NoStatically typed with full type erasure and no runtime type information, so there is no reflection. Introspection is approximated at compile time with PPX deriving rather than at runtime. YesHighly introspective: symbol-table access (%main::), ref/blessed, can/isa, and modules like Class::MOP/Moose provide a full meta-object protocol at runtime. YesBuilt on a Meta-Object Protocol: .^name, .^methods, .^attributes, and the full .HOW interface expose and modify classes/roles at runtime. Reflection is a first-class language facility. PartialNo general reflection; types are erased. std::any::Any allows limited runtime downcasting of 'static types, and trait objects give dynamic dispatch, but field/type enumeration is not available. YesDeeply reflective at runtime: type, dir, getattr/setattr, inspect, __dict__, and metaclasses let code examine and modify objects, classes, and call frames dynamically.
Dynamic code evaluation (eval)Compiling or interpreting source code constructed at runtime, from within the running program. NoAhead-of-time compiled to a single native binary with no embedded compiler or interpreter, so there is no eval. Runtime dynamism is limited to Regex.compile, which builds patterns (not arbitrary code) from strings. NoCompiled with no runtime eval; the standard go/parser and go/types packages can parse and type-check source, but executing it requires invoking the toolchain or a third-party interpreter (yaegi). PartialNo eval in compiled programs, but the bytecode compiler-libs/toplevel can be embedded to evaluate source at runtime, and the MetaOCaml dialect offers staged code generation. Not idiomatic for ordinary code. YesString eval EXPR compiles and runs Perl source at runtime (block eval { } is exception handling, not codegen). Powerful and a classic security caveat. YesThe EVAL routine compiles and runs a string of Raku source at runtime; it must be explicitly enabled with use MONKEY-SEE-NO-EVAL as a safety gate. NoAhead-of-time compiled with no runtime eval. Dynamic code requires shelling out to rustc, embedding an interpreter, or scripting crates like rhai/mlua for a different language. YesBuilt-in eval() evaluates an expression and exec() runs statements from a runtime string, with optional namespace dicts. compile() produces reusable code objects.
Operator overloadingGiving built-in operators (e.g. +, ==, []) custom meaning for user-defined types. NoOperators have fixed meaning on built-in types only; users cannot overload them. This is deliberate under "one obvious way" — behavior is expressed through named, data-first methods instead. NoNo operator overloading at all; operators work only on built-in types. User types compose behavior through ordinary methods (e.g. a.Add(b)). PartialOperators cannot be overloaded (no ad-hoc polymorphism), but you can define brand-new infix operators or locally rebind existing symbols (( +. ), custom ( ^^ )). Each numeric type already has its own operators (+ vs +.). YesThe overload pragma maps operators (+, "", ==, ...) to methods on a blessed class. Stringification and numification overloads are common. YesOperators are just multi-dispatch subs; define multi sub infix:<+> (or prefix/postfix/circumfix) to overload existing operators or create new ones with arbitrary Unicode names. YesOperators are sugar for std::ops traits; implement Add, Mul, Index, PartialEq, etc. for a type to overload +, *, [], ==. Cannot invent new operator symbols. YesDunder methods (__add__, __eq__, __getitem__, ...) define operator behavior on classes. Pervasive in numeric/array libraries like NumPy.
Compile-time code generation / derivationAutomatically generating boilerplate (printers, equality, serializers) for a type at build time instead of writing it by hand. NoNo user-facing derivation mechanism; there are no traits/interfaces in v0 and no derive. Common operations are provided as built-in prelude functions rather than generated per type. PartialNo language-level derivation, but go generate plus tools (stringer, mockgen) generate .go source as a build step. Reflection covers many cases at runtime instead. Via library[@@deriving ...] (via ppx_deriving/ppxlib) generates show, eq, compare, and (with ppx_yojson) serializers from a type definition at compile time. The standard boilerplate solution. Via libraryNo compile-time derivation, but object frameworks like Moose/Moo generate accessors and methods from attribute declarations at class-construction time (effectively at BEGIN). PartialNo derive attribute, but the Meta-Object Protocol lets traits and roles synthesize methods/accessors at compile time; is traits and custom trait_mods generate behavior. Accessors are auto-generated from public attributes. Yes#[derive(...)] invokes procedural macros to generate trait impls (Debug, Clone, PartialEq, and serde's Serialize/Deserialize) at compile time. Central to idiomatic Rust. Partial@dataclass and attrs/pydantic generate __init__/__repr__/__eq__ at class-definition time from field annotations. Done at runtime (import time) rather than via a compiler.

Type System

FeaturegujiGoOCamlPerlRakuRustPython
Static vs dynamic typingWhether types are checked at compile time or carried by values at run time. YesStatically typed: every binding and expression has a type known at compile time, with no runtime type information needed for dispatch. The two operands of an arithmetic operator must be the same numeric type — there is no implicit Int/Float coercion. YesStatically typed and compiled; every variable has a fixed type the compiler enforces. Conversions between numeric types must be explicit (float64(i)), and there is no implicit coercion. YesStatically typed with a sound Hindley-Milner system; ill-typed programs are rejected before they run. It never coerces between numeric types, which is why it has separate operators for Int (+) and Float (+.). NoDynamically typed: a scalar holds any value and its meaning comes from context"10" + 5 is 15 (numeric), "10" . 5 is "105" (string). Errors like calling a missing method surface only at run time. PartialGradually typed: untyped by default like a dynamic language, but optional type constraints on variables, parameters, and attributes are enforced — some at compile time, the rest at run time. You opt into as much static checking as you want. YesStatically and strongly typed with no implicit numeric coercion; mismatches are compile errors, and even integer widths must be converted explicitly with as or .into(). NoDynamically typed: names are rebindable to any type and checks happen at run time. Optional type hints (PEP 484) are accepted but not enforced by CPython — they document intent and feed external checkers like mypy.
Type inferenceHow much type information the compiler works out for you versus what you must annotate. YesInference is local and complete within a function body — bindings and sub-expressions rarely need annotations. Only top-level pub declarations must annotate their parameter and return types, to pin a stable module interface. PartialThe short form := infers a local's type from its initializer, and Go 1.21+ infers many generic type arguments. But function parameters, return types, and struct fields must always be annotated — inference is deliberately local. YesFull Hindley-Milner inference: most code needs no annotations at all, and the compiler infers the most general (polymorphic) type. Annotations are an occasional aid for clarity or to resolve ambiguity (e.g. record-field overloading). NoNo type inference because there are no static types to infer. The sigil ($, @, %) marks the broad shape of a variable, but a scalar's value type is decided at run time by context. PartialVariables default to the untyped Any; there is no Hindley-Milner-style inference, but literals carry concrete types and the compiler can constant-fold and check some things early. You annotate where you want static guarantees. YesLocal type inference is pervasive: let bindings and closure parameters are usually inferred from context, including backward from later use. Function signatures, however, must be fully annotated. NoThe runtime needs no inference — names just bind to objects. Static checkers (mypy, Pyright) do infer types of locals from assignments, but that is tooling layered on top, not part of the language runtime.
Sum / algebraic data typesA type that is exactly one of several labeled variants, each carrying its own typed fields. Yesenum is a first-class sum type: a value is one of its variants, each with typed fields, and the only way to read those fields is to match. Enums may be generic, and Option/Result are just standard-library enums. NoGo has no sum type. The usual workaround is a sealed interface implemented by a fixed set of structs, recovered with a type switch — but the compiler does not check that all variants are handled. YesVariant types (type t = A | B of int) are OCaml's original algebraic data type and the construct that popularized pattern matching. They compose with records, tuples, and generics to form arbitrary algebraic shapes. NoNo sum type exists. The closest idiom is a tagged array (or hash) whose first element names the variant, dispatched with a hand-written if/elsif ladder on the tag — with no exhaustiveness check. PartialPlain enum gives only flat constant values, not payload-carrying variants. To model a true sum you build a role that a closed set of classes does, then dispatch with given/when or multi dispatch — without compile-time exhaustiveness. Yesenum is a true sum type whose variants carry named or positional fields, and it is the backbone of the standard library (Option, Result). match over it is checked for exhaustiveness at compile time. PartialThere is no native sum type, but a Union[A, B] / A | B annotation plus structural match/case (3.10+) approximates one for checkers. Exhaustiveness is verified only by external tools (e.g. mypy via assert_never), not by the runtime.
Exhaustiveness checkingWhether the compiler proves that a match handles every possible case. Yesmatch must be exhaustive: the compiler rejects a match that does not cover every possible value and reports which cases are missing. Adding a new enum variant without a matching arm becomes a compile-time error. NoA switch on a type or value is never checked for completeness; a missing case simply falls through (or hits default). Catching unhandled cases relies on a runtime panic or third-party linters like exhaustive. YesThe compiler emits a non-exhaustive match warning (promotable to an error) when a constructor is unhandled, and also flags redundant arms. This is a core part of why ADTs are safe to refactor. NoNo structural matching and therefore no exhaustiveness. An if/elsif chain on a tag leaves a forgotten variant to fall through to else/die at run time, if it is caught at all. Nogiven/when is an ordered first-match construct with no completeness analysis; a value matching none of the when arms just runs the default (or nothing). You guarantee coverage yourself. Yesmatch must cover every variant or the program does not compile, making an added enum variant a guided refactor across the codebase. A _ arm explicitly opts out of that check for the remaining cases. NoStructural match/case performs no exhaustiveness check at run time — an unmatched value falls through and the function returns None. Static checkers can flag it via a case _ as x: assert_never(x) guard.
Generics / parametric polymorphismWriting functions and types parameterized over the types they operate on, with compile-time type safety. YesType parameters are written in square brackets after the name (sub first[T](@xs: List[T]): Option[T]), and enum/class declarations may be generic too. Inference usually lets you call generic code without spelling out the type arguments. YesGenerics arrived in Go 1.18: type parameters in square brackets with constraint interfaces (now defined as type sets) bound the permitted arguments. any is the unconstrained constraint; there is no specialization or variance. YesParametric polymorphism is built in and inferred: a function over 'a list is generic with no annotation, and types like 'a option are everywhere. The compiler infers the most general type automatically. YesBecause everything is dynamically typed, subroutines are uniformly polymorphic — a sub that indexes an array works for any element type with no type machinery at all. There is no static parametricity to enforce, though. YesGenerics come via parameterized roles (role Container[::T] {...}) and type-captures (::T) in signatures, letting you constrain and reuse a type variable across a routine. It is more dynamic than HM but fully typed when you ask. YesGenerics with trait bounds (fn first<T>(xs: &[T]) -> Option<&T>) are monomorphized for zero-cost specialization. Bounds like T: Ord let the compiler verify the operations a generic uses are available. PartialFunctions are duck-typed and thus generic at run time. For static generics, type hints use TypeVar/Generic (and the PEP 695 def f[T](...) syntax in 3.12+), checked only by external tools — CPython erases them.
Nullability / optionalsHow a possibly-absent value is represented and whether the absence is visible in the type. YesThere is no null. Absence is the standard sum type Option[T] (Some/None), so a possibly-missing value is visible in the type and the compiler forces you to handle None — via match, unwrap_or, or the ? operator. PartialPointers, interfaces, maps, slices, and channels can be nil, and the type does not record whether a value may be absent — a nil dereference panics at run time. The common 'comma-ok' idiom (v, ok := m[k]) signals absence with a separate bool. YesOCaml has no null. The 'a option type (Some/None) makes absence explicit and the pattern match forces you to handle the None case, eliminating null-dereference bugs. PartialThe single absent value is undef; any scalar may be undef and you test it with defined. The defined-or operator // supplies a fallback. Absence is not tracked in any type — using undef numerically just warns. PartialEvery type has an undefined (type-object) state and a defined state, expressed with definedness smileys: Int:D requires a value, Int:U an undefined one. The defined-or // returns the first defined operand as a fallback. YesRust has no null; Option<T> (Some/None) encodes absence in the type and match/?/unwrap_or force you to deal with it. A niche optimization makes Option<&T> and Option<Box<T>> cost no extra space. PartialNone is the universal absent value and any reference may be None, so attribute access on it raises AttributeError at run time. The Optional[T] / T | None hint records the possibility for static checkers only.
Structural vs nominal typingWhether type compatibility is decided by shape (members/methods) or by the declared type name. NoTyping is nominal: a class or enum is compatible only with itself, constructed and matched by name, and two classes with identical fields are distinct types. v0 has no structural/interface-style typing (traits are deferred). PartialStructs are nominal, but interfaces are structural: a type satisfies an interface just by having the right method set, with no implements declaration. So Go mixes both — named structs, duck-typed interfaces. PartialRecords and variants are nominal (compatibility is by type name), but objects and polymorphic variants are structural via row polymorphism — an object type is inferred from the methods invoked, with no class needed. PartialObject dispatch is essentially duck typing: a method call works on any blessed reference that can respond, regardless of class — so compatibility is by behavior. The blessed package name gives a loose nominal ref/isa check when you want it. PartialClasses and roles are nominal by default (matched by name / does), but subset types add structural-style where constraints, and smartmatch can test shape. The community also explores explicit structural protocols on top. NoRust is nominal: structs and enums are matched by name, and a type implements a trait only via an explicit impl block. Two structurally identical structs are unrelated types — there is no structural subtyping. PartialAt run time Python is duck typed — behavior is all that matters. Statically, Protocol classes give structural typing (a type matches by having the right methods), alongside ordinary nominal class/isinstance checks.