Dryad Conventions

Language Specification and Theoretical Foundation

Version 1.0 · May 2026 · Draft Specification

This document defines the formal conventions, syntax, semantics, and execution model of the Dryad programming language. It serves as the authoritative reference for language implementors and advanced users, analogous to the ECMAScript specification for JavaScript.

§1 Introduction and Language Philosophy

Dryad is a multi-paradigm programming language designed with the following fundamental objectives: syntactic familiarity (JavaScript/TypeScript-inspired syntax to reduce the learning curve), paradigm flexibility (simultaneous support for procedural, object-oriented, and functional programming), optional dynamic typing, a hybrid execution model (AST interpreter, Bytecode VM, and AOT native compilation), native concurrency primitives, a robust module system with FFI, and automatic memory management.

Definition 1.1

Dryad distinguishes itself through: (i) a triple execution architecture — tree-walking interpreter, stack-based bytecode VM, and AOT compiler; (ii) a structured error system where all errors carry unique codes, source locations, and correction suggestions; (iii) the namespace operator (::) for qualified access (C++/Rust style); (iv) native directives via #<module> for loading native modules; and (v) native template string interpolation with ${expr}.

Axiom 1.1

The Dryad language is governed by the following axioms: (i) Clarity over Performance — readability and code clarity are unconditionally prioritized over premature optimization; (ii) Syntactic Consistency — syntactic structures follow consistent, predictable patterns; (iii) Informative Errors — every error must provide location, context, and correction suggestions; (iv) Composability — language primitives must be combinable; (v) Zero Surprises — language behavior must be predictable and thoroughly documented.

Design GoalDescription
Syntactic FamiliarityJS/TS-inspired syntax to minimize learning curve.
Paradigm FlexibilityProcedural, OOP, and functional in one language.
Optional TypingDynamic by default; opt-in strict static mode for AOT.
Triple ExecutionAST interpreter, Bytecode VM, and AOT native compiler.
Native ConcurrencyOS threads, async/await, and mutex primitives.
Self-HostingStandard library written in pure Dryad atop ~50 intrinsics.
↑ Back to top

§2 Lexical Structure

The lexical structure of Dryad is defined over a Unicode UTF-8 alphabet. The lexer performs deterministic, unambiguous tokenization according to the principle of non-ambiguity: for every valid input sequence there exists exactly one lexical interpretation.

Definition 2.1

The Dryad alphabet consists of: (i) Unicode UTF-8 encoded characters; (ii) letters [a-zA-Z] and underscore _; (iii) digits [0-9]; (iv) special symbols { } ( ) [ ] ; , . : = + - * / % & | ^ ~ ! < > ? @ # $ \ ' " `; and (v) whitespace (spaces, tabs, line breaks).

Definition 2.2

The Dryad lexer produces the following token types: Identifier (matching [a-zA-Z_][a-zA-Z0-9_]*), Number (f64, supporting decimal, hex 0x, binary 0b, octal 0o), String (delimited by " or '), Boolean (true, false), Null (null), TemplateString (comprising TemplateStart, TemplateContent, Interpolation markers, TemplateEnd), Keyword, Operator, Symbol, NativeDirective (#<name>), and Eof.

Property 2.1

Whitespace (spaces, tabs, newlines) is generally insignificant in Dryad, serving only as token separators and within string/template literals. Unlike Python, indentation carries no syntactic meaning.

Axiom 2.1

The Dryad lexer guarantees that for every valid input sequence s, there exists exactly one lexical interpretation t such that lex(s) = t. The # character is disambiguated as follows: if followed immediately by < (no whitespace), it is tokenized as a NativeDirective; otherwise it produces a lexer error.

CategoryReserved Words
Variable Declarationlet, const
Flow Controlif, else, for, while, do, break, continue, match, in
Functionsfunction, fn, async, await, return, thread, mutex
Object Orientationclass, new, extends, this, super, static, public, private, protected
Modulesimport, export, use, as, from
Exception Handlingtry, catch, finally, throw
Literalstrue, false, null
Definition 2.6

A native directive has the form #<module_name> where module_name is a valid identifier delimited by < and >. Example: #<io> loads the I/O native module. If whitespace appears between # and <, a lexer error is produced.

↑ Back to top

§3 Type System

Definition 3.1

Dryad implements a dynamic type system where types are associated with values, not variables. Variables may change type during execution. Type checking occurs at runtime. Type annotations are optional and serve primarily for documentation and tooling support.

Definition 3.2

The fundamental primitive types of Dryad are: number (IEEE 754 f64 floating point), string (UTF-8 encoded character sequence), bool (boolean value), null (null/undefined value), and any (universal type, implicit when omitted).

Definition 3.3

Dryad supports the following composite types: Type[] (arrays — ordered, dynamically-sized, heterogeneous collections with zero-based indexing); (T1, T2, ...) (tuples — fixed-size, potentially heterogeneous, accessed via numeric index like tuple.0); fn(P1, P2, ...) -> R (function types — first-class values supporting closures); objects (key-value collections with dot and bracket access); and classes (user-defined types with state, behavior, and inheritance).

Property 3.1

When the opt-in "use strict types" directive is enabled, the AOT compiler eliminates dynamic type checks at runtime, enables stack allocation via escape analysis, performs static method devirtualization, removes dead code branches, and applies SIMD vectorization to typed array loops. This yields an estimated 30-50% reduction in type-checking overhead on hot paths.

Axiom 3.1

Strict type mode must be opt-in and fully interoperable with dynamic code. Modules may specify "use strict types" at the top of the file. Strict code may interoperate with dynamic code through explicit boundary checks. Individual functions may be annotated @strict or @dynamic. Implicit type conversion occurs at the boundary: dynamic values are verified upon entering strict code.

Property 3.3

Dryad performs the following automatic conversions: number + string yields string concatenation; bool in arithmetic context becomes 1 (true) or 0 (false); null receives special treatment in comparisons; arrays and objects are compared by reference.

Definition 3.4

The AOT compiler employs a three-level gradient approach to resolve types for code compiled under "use strict types" or when the compiler can statically prove type safety: (i) Static CFG Inference — the compiler traverses the Control Flow Graph and propagates type constraints forward, promoting variables to their most specific native type (e.g., f64, i32) whenever all paths produce the same type; (ii) Annotation-Guided Resolution — user-provided type annotations (e.g., let x: i32) are treated as authoritative binding directives, bypassing inference for the annotated variable; (iii) Dynamic Fallback — when the inference engine cannot determine a single type (genuine polymorphism, e.g., a variable receiving both number and string across paths), the Variant Enum is preserved for that variable alone. This fallback is scoped to the individual binding, not the entire function.

Property 3.4

Unlike JIT-compiled languages (V8, SpiderMonkey) that may deoptimize and re-optimize as type profiles change, Dryad's AOT compiler performs a single, irreversible type binding at compile time. Once a variable is promoted to a native type via gradient inference, that binding is final. This eliminates the complexity and runtime stutter of deoptimization/reoptimization cycles, at the cost of requiring the compiler to be conservative when types are genuinely ambiguous. The trade-off is deliberate: deterministic performance with no warm-up phase, aligning with Dryad's axiom of Zero Surprises.

↑ Back to top

§4 Expressions

Definition 4.1

Expressions in Dryad follow a strict precedence hierarchy (highest to lowest): (1) primary: ., [], (), new (left-associative); (2) postfix ++, -- (left); (3) prefix ++, --, !, -, +, ~ (right); (4) exponentiation ** (right); (5) multiplicative *, /, % (left); (6) additive +, - (left); (7) bitwise shift (left); (8) relational (left); (9) equality (left); (10-14) bitwise AND/XOR/OR and logical AND/OR (left); (15) assignment (right).

Definition 4.6

a + b: if both are numbers, performs addition; if either is a string, performs concatenation. a - b: subtraction (requires numbers). a * b: multiplication (requires numbers). a / b: division (requires numbers; division by zero raises Runtime Error 3000-3999). a % b: modulo (requires numbers). a ** b: exponentiation (requires numbers).

Definition 4.7

a && b: returns a if falsy, otherwise b. a || b: returns a if truthy, otherwise b. !a: boolean negation. Falsy values: false, null, 0, "", NaN. Truthy values: all others.

Definition 4.11

The match expression provides functional branching via pattern matching. Syntax: match value { pattern1 => result1, pattern2 => result2, _ => default }. Supported patterns: literals (1, "text", true), wildcard (_), and optional guards (boolean conditions following the pattern).

Match Expression Example
let status = 404;
let message = match status {
  200 => "Success",
  404 => "Not Found",
  _   => "Unknown error"
};

// With guard condition
let value = 15;
match value {
  v => v > 10 && v < 20 => "Middle range",
  _ => "Out of scope"
};

Match expressions return the selected branch value directly. The wildcard pattern _ is required to cover all remaining cases.

↑ Back to top

§5 Statements

Definition 5.1

let declares a mutable, block-scoped variable. Syntax: let identifier [: type] [= value];. If not initialized, the value defaults to null. Multiple declarations are supported: let a = 1, b = 2;. const declares an immutable, block-scoped variable. Syntax: const identifier [: type] = value;. Must be initialized at declaration. For objects and arrays, the binding is immutable but the content may be modified.

Definition 5.3

The if statement executes conditionally. Syntax: if (condition) { ... } else if (condition2) { ... } else { ... }. The condition is evaluated as truthy/falsy. Only the first true block is executed. else and else if are optional.

Definition 5.4

Dryad supports four loop constructs: (i) while (condition) { ... } — executes while condition is truthy; (ii) do { ... } while (condition) — executes at least once; (iii) for (init; condition; update) { ... } — C-style iteration; (iv) for (element in iterable) { ... } — for-each iteration over collections. break exits the innermost loop; continue skips to the next iteration.

Definition 5.6

Exception handling uses the try { ... } catch (error) { ... } finally { ... } construct. The try block is monitored for exceptions. catch binds the caught exception to a variable. finally executes unconditionally (on both success and failure). At least one of catch or finally must be present.

Definition 5.7

throw expression; raises an exception that propagates up the call stack until caught by a catch block or reaches the top-level, terminating the program.

Definition 5.8

return [expression]; exits the current function immediately and returns the specified value (or null if omitted). Valid only within function and thread bodies.

↑ Back to top

§6 Declarations — Functions and Classes

Definition 6.1

Functions are declared with the function keyword. They are hoisted (can be called before their declaration in the same scope). Functions create closures over their surrounding scope. Parameters support default values (param = defaultValue) and rest parameters (...rest). Arrow functions ((params) => expression or (params) => { statements }) are not hoisted and provide implicit return for single-expression bodies.

Definition 6.3

Functions declared with async function may use await to suspend execution until a Promise resolves. Async functions implicitly return a Promise. Execution may be paused at each await expression.

Definition 6.4

Functions declared with thread function execute in a separate OS thread. Invocation: let handle = thread name(args); returns a handle to retrieve the result. Threads provide context isolation. Communication between threads is done via mutexes.

Definition 6.5

Classes are declared with the class keyword. They support instance properties, a constructor method, instance methods, and static members. Inheritance is supported via extends. Access modifiers: public (default, accessible from anywhere), private (class-internal only), protected (class and subclasses). The super keyword references the parent class; this references the current instance.

Class with Inheritance and Access Modifiers
class Shape {
  public x: number;
  protected name: string;
  constructor(x: number, name: string) {
    this.x = x;
    this.name = name;
  }
  area(): number { return 0; }
}

class Circle extends Shape {
  private radius: number;
  constructor(r: number) {
    super(0, "circle");
    this.radius = r;
  }
  area(): number {
    return 3.14159 * this.radius ** 2;
  }
}

let c = new Circle(5);
console.log(c.area()); // 78.53975

Demonstrates single inheritance, access modifier enforcement, and method overriding via the extends mechanism.

↑ Back to top

§7 Module System

Definition 7.1

Dryad supports four import forms: (i) Named imports — import { name1, name2 } from "module";; (ii) Namespace import — import * as ns from "module";, accessed as ns.function(); (iii) Side-effect import — import "module"; executes the module for side effects; (iv) Identifier import — import moduleName; imports without quotes, accessed via the namespace operator: moduleName::function.

Definition 7.5

Direct export: export function name() { }, export const VALUE = 42;, export class ClassName { }. Re-export: export { name1, name2 } from "other_module";, export * from "other_module";.

Definition 7.6

Native modules (implemented in C or Rust) are loaded via #<module_name> directives. Available native modules include: #io (file I/O operations), #crypto (cryptography), #http (HTTP client/server), #math (mathematical functions), #tcp, #udp (networking), #time (date and time), and others. Native functions are loaded into the global scope following the C calling convention.

↑ Back to top

§8 Execution Model

Definition 8.1

Dryad supports three execution strategies: (i) Tree-Walking Interpreter — executes the AST directly; the default mode, optimized for development and debugging; (ii) Bytecode VM — compiles the AST to stack-based bytecode; enabled with set_compile_mode(true); offers intermediate performance; (iii) AOT Compiler — compiles bytecode/IR to native code for ARM64 and x86_64, producing ELF or PE executables; maximum performance.

Definition 8.3

The scope hierarchy in Dryad follows: Global Scope → Module Scope → Function Scope → Block Scope. The variable lookup algorithm checks the current scope first, then walks up the parent scope chain to the global scope. If the variable is not found in any scope, a ReferenceError is raised. let and const are block-scoped; function declarations are hoisted to the enclosing function or module scope.

Definition 8.5

Functions capture variables from their definition scope. Captured variables remain alive as long as the function exists. Modifications to captured variables are visible across all closures sharing the same captured scope. Multiple closures may share the same captured variables.

Property 8.2

Memory management employs reference counting (via the Rust runtime). Allocation is automatic — no programmer intervention is required. Primitive values remain on the stack; complex types (arrays, objects, closures) are heap-allocated. Values are freed when their reference count reaches zero. The system provides transparent garbage collection with no tracing GC pauses.

Definition 8.8

Runtime values are represented as a discriminated variant enum: Number(f64), String(String), Bool(bool), Null, Array(Vec<Value>), Object(HashMap<String, Value>), Function(...), Class(...), and Thread(...).

Definition 8.9

The Bytecode VM does not emit generic opcodes — it emits type-specialized instructions based on type observations collected during the interpretation phase. When the interpreter detects that variable soma is always f64, the VM emits ADD_F64 instead of ADD_GENERIC. Each compiled function carries a Metadata Header containing: (i) the observed type signature for every slot and parameter; (ii) the total number of opcodes; (iii) the stack depth required; and (iv) a per-slot type confidence level (STATIC, OBSERVED, UNKNOWN). The AOT compiler reads this header to pre-populate its symbol table, eliminating the need to re-infer types from scratch. If a slot's type is STATIC (inferred with certainty via CFG analysis) or OBSERVED (seen consistently during interpretation), the emitted bytecode uses the fast native opcode directly. If UNKNOWN, the generic Variant opcode is emitted. This design distributes type intelligence across all three layers: the interpreter observes, the VM annotates, and the AOT consumes.

Definition 8.10

The @compile(mode) decorator replaces the global set_compile_mode() function for fine-grained compilation control. It may be applied to function declarations or block statements. Accepted modes: @compile("interpreter") — forces AST interpretation for this scope (useful for debugging); @compile("bytecode") — compiles the scope to bytecode VM instructions; @compile("AOT") — marks the scope for AOT compilation when producing a native binary. When applied to a block: @compile("bytecode") { ... }, the statements inside the block are compiled in the specified mode while the surrounding code retains its default mode. When applied to a function declaration, all invocations of that function use the specified mode. The decorator is transitive: a function called from within an @compile(AOT) scope is also compiled to native, unless it has its own @compile override.

Property 8.4

The three execution layers are not independent implementations — they form a pipeline with a strict contract. Layer 1 (Interpreter): Walks the AST directly. Collects runtime type observations for each variable slot. Records call-site frequencies and branch taken/not-taken profiles. Layer 2 (Bytecode VM): Reads the AST and the interpreter's type observations. Emits type-specialized opcodes using the Metadata Header. Functions compiled with high-confidence type observations use fast native opcodes; functions with low confidence fall back to generic dispatch. The VM may also apply inline caching for property access based on observed Shapes. Layer 3 (AOT Compiler): Consumes the type-annotated bytecode and Metadata Header. Performs the final Bind Once decision: if the metadata indicates STATIC or OBSERVED with high confidence, the compiler emits native LLVM IR with zero type checks. If confidence is low, it emits a guarded branch (fast path with a runtime type check fallback). The contract guarantees that a program behaves identically across all three layers — the only difference is performance, not semantics.

↑ Back to top

§9 Error System

Definition 9.1

All errors in Dryad are instances of DryadError carrying: a unique numeric code, a category label, a human-readable message, source location (file, line, column), a stack trace, and debug context (suggestions, local variables, documentation references).

RangeCategoryExamples
1000-1999Lexer ErrorsUnexpected character, unterminated string, invalid number, invalid escape sequence.
2000-2999Parser ErrorsUnexpected token, unbalanced parentheses, invalid syntactic structure.
3000-3999Runtime ErrorsUndefined variable, type incompatibility, division by zero, stack overflow.
4000-4999Type ErrorsType incompatibility, incorrect number of arguments.
5000-5999I/O ErrorsFile not found, permission denied, I/O operation failure.
6000-6999Module ErrorsModule not found, unknown native module, resolution error.
7000-7999Syntax ErrorsGeneral syntax violations.
8000-8999WarningsNon-fatal warnings with Low/Medium/High severity.
9000+System ErrorsOS-level errors and runtime internal failures.
Property 9.1

Uncaught exceptions propagate up the call stack: if no local try-catch handler exists, the exception propagates to the caller, continuing until a matching catch is found or the top of the call stack is reached. If uncaught at the top level, the program terminates with an error report. Each exception carries a stack trace ordered from deepest frame to shallowest, including function name and source location.

↑ Back to top

§10 Concurrency Model

Definition 10.1

Functions declared with async may use the await keyword to suspend execution until a Promise resolves. Async functions implicitly return a Promise. The await keyword permits non-blocking asynchronous code with synchronous-style syntax.

Definition 10.2

Functions declared with thread function execute in a separate OS thread. Threads provide full context isolation. Communication between threads is performed via mutexes or return value handles. Invocation returns a thread handle that can be awaited to retrieve the result.

Definition 10.3

The mutex() primitive creates a new mutex. The API provides mu.lock() (acquires the lock, blocking if occupied) and mu.unlock() (releases the lock). Mutexes protect critical sections in multi-threaded code.

Concurrent Execution with Mutex
let mu = mutex();
let sharedData = 0;

thread function increment(id: number) {
  mu.lock();
  sharedData++;  // critical section
  mu.unlock();
  return id * 10;
}

let h1 = thread increment(1);
let h2 = thread increment(2);
let r1 = await h1;  // 10
let r2 = await h2;  // 20

Multiple OS threads execute in parallel, synchronized via the built-in mutex primitive. Return values are retrieved via await on thread handles.

Definition 10.4

The mu.lock(timeout_ms: number): bool variant attempts to acquire the lock within the specified timeout in milliseconds. Returns true if the lock was acquired, false if the timeout expired. This enables non-blocking synchronization patterns and guards against deadlock scenarios where a thread fails to release its lock. The blocking variant mu.lock() is equivalent to mu.lock(Infinity).

Mutex with Timeout — Deadlock-Safe Pattern
let mu = mutex();
let shared: any[] = [];

thread function producer() {
  for (let i = 0; i < 100; i++) {
    // Attempt to acquire within 50 ms
    if (mu.lock(50)) {
      shared.push(i);
      mu.unlock();
    } else {
      // Timeout — resource contested; retry or log
      io_write("warn", "producer: lock timeout on iteration " ++ i);
    }
  }
}

thread function consumer() {
  for (let i = 0; i < 100; i++) {
    if (mu.lock(50)) {
      if (shared.len() > 0) {
        let val = shared.pop();
        io_write("data", "consumed: " ++ val);
      }
      mu.unlock();
    }
  }
}

Timeout-based locking prevents indefinite blocking. The pattern is particularly important for producer-consumer workloads and for guarding against deadlock from uncoordinated thread termination.

↑ Back to top

§11 Foreign Function Interface

Definition 11.1

Native modules are implemented in C or Rust and exposed to Dryad via native directives (#<module>). Functions from native modules become available in the global scope. The interface uses dynamic typing at the boundary. The calling convention follows the C ABI (cdecl on x86).

Dryad TypeC TypeNotes
numberdouble (f64)IEEE 754 double precision.
stringconst char*UTF-8 null-terminated string.
boolbool / uint8_t0 for false, 1 for true.
nullNULL / nullptrNull pointer address.
arrayvoid*Opaque pointer; reference-counted.
objectvoid*Opaque pointer to persisted object.
Property 11.1

FFI calls follow the C calling convention: parameters are passed left-to-right; on x86-64, return values are placed in the rax register; the caller cleans the stack (cdecl convention). Complex types are passed as opaque pointers — only primitive types have direct conversion. Conversion responsibility lies with the native implementation.

Definition 11.2

When a Dryad object reference crosses the FFI boundary into native code, the garbage collector may move or reclaim the object during the call, producing a dangling pointer. The gc.pin(obj) intrinsic prevents the GC from moving or collecting the object for the duration of the FFI call. It returns a raw pointer (ptr<u8>) valid for the pinned scope. gc.unpin(obj) releases the pin, allowing the GC to resume normal management. Pins are reference-counted: nested pin calls require an equal number of unpin calls.

Pinning Objects for FFI Calls
@intrinsic("gc.pin")
extern function __gc_pin(obj: any): ptr;

@intrinsic("gc.unpin")
extern function __gc_unpin(obj: any): void;

// Safe FFI wrapper: pin → call → unpin
function writeBuffer(fd: i32, buf: Buffer) {
  let raw = __gc_pin(buf);       // prevent GC from moving buf
  let written = __write(fd, raw, buf.len);
  __gc_unpin(buf);               // release pin
  return written;
}

Every FFI call that passes a heap-allocated object must pin the object first. The runtime asserts that all pins are released before a thread terminates; leaked pins produce a runtime diagnostic (9000+ System Error).

↑ Back to top

§12 Minimal Runtime Architecture

Definition 12.1

Dryad adopts a self-hosting architecture where the C++ runtime provides only approximately 50 primitive intrinsics (syscalls). The entire standard library is implemented in 100% pure Dryad. There is zero need to write FFI bindings manually. High-level libraries (HTTP, JSON, cryptography) are written in pure Dryad atop the intrinsic layer.

Property 12.1

This approach, also employed by Go, Zig, and Rust, offers: single maintenance (no code duplication across C++ and Dryad), extensibility (users implement libraries in Dryad), testability (mock implementations without touching native code), debuggability (all code visible and inspectable), and portability (only 50 syscalls need OS-specific adaptation).

Definition 12.2

An intrinsic is a special function recognized by the compiler that does not generate a normal CALL instruction. Instead, it compiles directly to a specialized opcode or inline assembly sequence with near-zero execution overhead. Intrinsics are declared with the @intrinsic("syscall.name") decorator and map directly to C++ runtime code.

Intrinsic Declaration and Usage
// Declared in runtime (C++)
enum class SyscallID : uint16_t {
  OPEN = 1, READ = 2, WRITE = 3, CLOSE = 4,
  SOCKET = 5, CONNECT = 6,
  // ... ~50 syscalls total
};

// Used in Dryad standard library
@intrinsic("syscall.open")
extern function __open(path: string, flags: i32): i32;

@intrinsic("syscall.read")
extern function __read(fd: i32, buf: ptr, len: usize): isize;

@intrinsic("syscall.write")
extern function __write(fd: i32, buf: ptr, len: usize): isize;

// Bytecode output for: __open("file.txt", 0)
//   LOAD_CONST "file.txt"
//   LOAD_CONST 0
//   INTRINSIC_SYSCALL 1   // ID 1 = syscall.open
//   STORE_LOCAL fd

Intrinsics are dispatched via a switch table in the VM. The opcode INTRINSIC_SYSCALL carries the syscall ID, enabling the VM to jump directly to the corresponding C++ handler without function call overhead.

LayerComponentsLanguage
High-Level APIsHTTP, JSON, crypto, file system abstractions100% Dryad
Core I/OFileSystem, Buffer, Socket, TCP/UDPDryad + Intrinsics
Runtime Layer~50 syscalls: open, read, write, socket, epoll, malloc, ...C++
Property 12.2

All intrinsics with side effects or raw pointer arguments MUST be wrapped in a safe Dryad function that validates inputs and manages cleanup. Raw intrinsic calls in application code are prohibited outside of the standard library wrapper layer. This boundary guarantees that type invariants (non-null, valid range, correct lifetime) are enforced before control reaches native code. Violations produce Runtime Error 4000-4999 at the wrapper boundary.

Definition 12.3

The VM dispatches intrinsics via a flat lookup table of function pointers indexed by syscall ID, replacing the traditional switch-based dispatch. The lookup table is a const fn_ptr[N] array populated at compile time. Dispatch cost: two loads (base + offset) plus an indirect call — constant time regardless of N. For the bytecode VM, the INTRINSIC_SYSCALL opcode carries the syscall ID directly; for AOT-compiled code, the syscall ID is resolved to an absolute address at link time. Future work includes direct-threaded dispatch, where each syscall handler appends its own successor address, eliminating the central dispatch loop entirely.

Definition 12.4

When the runtime is compiled with DRYAD_DEBUG_INTRINSICS=1, every intrinsic call emits a structured trace record containing: timestamp (monotonic ns), thread ID, syscall ID, argument values (truncated to 64 bytes), return value, and elapsed time (ns). Traces are written to a ring buffer (configurable size, default 4096 entries) accessible at runtime via #<debug>.dump_trace(). The ring buffer is lock-free (single producer per thread). Production builds compile out all tracing code with zero overhead.

Definition 12.5

The native Buffer type represents a contiguous byte region with an embedded length and capacity. Every read and write operation validates the offset and extent against the buffer's length: out-of-bounds access raises Runtime Error 3000-3999 with the attempted range and buffer capacity. The bounds check compiles to a single cmp + jae (ARM64: cmp + b.cs) and is elided only when the AOT optimizer can statically prove the access is within bounds via range analysis.

Property 12.3

The Buffer type supports zero-copy I/O operations: read_into(buf) fills a pre-allocated buffer directly from a file descriptor or socket without intermediate copying. write_from(buf) transmits the buffer contents directly. The slice(offset, len) operation produces a view into an existing buffer without copying — the view shares the same backing memory and is reference-counted via the GC. This eliminates all unnecessary data movement between Dryad and the kernel for I/O-bound workloads.

Note

Intrinsic declarations carry a JSON metadata descriptor (intrinsics.json) that specifies each parameter's expected type, direction (in/out/inout), nullable flag, and safety constraint. The runtime validates calls against this schema in debug mode. Example: { "syscall.read": { "params": [{"name":"fd","type":"i32","dir":"in"}, {"name":"buf","type":"ptr","dir":"out","nullable":false}, {"name":"len","type":"usize","dir":"in"}], "returns": "isize" } }. This schema is used by the AOT compiler to generate type-safe call sites and by IDE tooling for inline documentation.

Definition 12.6

The intrinsic set is partitioned into a portable HAL that abstracts OS-specific syscall differences. The HAL exposes uniform operations: hal_open(path, flags, mode) — maps to open(2) (POSIX) or CreateFileW (Windows); hal_mmap(addr, len, prot, flags, fd, offset) — maps to mmap(2) (POSIX) or MapViewOfFile (Windows); hal_spawn(path, args) — maps to fork+exec (POSIX) or CreateProcessW (Windows). Only the HAL layer contains platform-specific code; all intrinsics above the HAL are pure abstract syscalls. This limits porting effort to approximately 10-15 HAL primitives.

Property 12.4

Intrinsics that translate directly to a kernel syscall (e.g., __read, __write, __mmap) use a pure syscall calling convention that eliminates the C runtime wrapper. Arguments are passed in the architecture's syscall registers (x0-x5 on ARM64, rdi-rsi-rdx-r10-r8-r9 on x86_64) and the syscall instruction (svc #0 / syscall) is emitted inline with no libc trampoline. This convention applies only to intrinsics annotated @intrinsic("syscall.*"); complex operations requiring libc still use the standard C ABI wrapper.

Definition 12.7

The @intrinsic("syscall.mmap") and @intrinsic("syscall.munmap") intrinsics expose virtual memory management directly. __mmap(addr, len, prot, flags, fd, offset): ptr<u8> maps a file or anonymous region into the process address space. __munmap(addr, len): void unmaps the region. These enable efficient memory-mapped file I/O and custom allocator construction in pure Dryad. The Buffer type may optionally back its storage with mmap for large regions (>64 KB), falling back to malloc for smaller allocations.

Memory-Mapped File I/O in Pure Dryad
// Memory-map a file for zero-copy read
function readFileMmap(path: string): Buffer {
  let fd = __open(path, 0);            // O_RDONLY
  if (fd < 0) throw "Failed to open";
  let len = __lseek(fd, 0, 2);         // SEEK_END
  __lseek(fd, 0, 0);                    // SEEK_SET
  let prot = 3;                         // PROT_READ | PROT_WRITE
  let flags = 2;                        // MAP_PRIVATE
  let mapped = __mmap(null, len, prot, flags, fd, 0);
  __close(fd);
  return Buffer.fromPtr(mapped, len);   // Wrap in Buffer (no copy)
}

Memory-mapped I/O eliminates double-copy between kernel and userspace. The Buffer wraps the mmap'd region directly; the region is unmapped when the Buffer is GC'd via a registered finalizer.

↑ Back to top

§12.5 Standard Library

The Standard Library (STDLIB) is the collection of modules, types, and functions provided with every Dryad installation. It is implemented entirely in pure Dryad atop the ~50 intrinsic syscalls exposed by the runtime layer. The STDLIB provides high-level abstractions for file I/O, networking, JSON processing, cryptography, mathematics, date/time, operating system interaction, collections, and debugging.

Axiom 15.1

All STDLIB modules MUST be implemented in 100% pure Dryad code with zero FFI dependencies. The intrinsic syscall layer (~50 primitives) is the sole bridge to native code. This guarantees: (i) the STDLIB is fully portable across all platforms supported by the runtime; (ii) every STDLIB function is debuggable and inspectable in Dryad; (iii) the STDLIB serves as the canonical example of idiomatic Dryad code; and (iv) users can read, modify, and extend any STDLIB module without touching C++.

Axiom 15.2

STDLIB identifiers follow these conventions: (i) functions use snake_case (e.g., `read_file`, `parse_json`); (ii) types and classes use PascalCase (e.g., `TcpSocket`, `FileStat`); (iii) constants use SCREAMING_SNAKE (e.g., `O_RDONLY`, `PROT_READ`); (iv) modules are single lowercase words with no separators (e.g., `io`, `fs`, `http`); (v) internal (non-exported) functions are prefixed with a single underscore (e.g., `_validate_path`). These conventions are enforced by the STDLIB build pipeline and should be followed by all user libraries.

Definition 15.3

STDLIB functions follow a consistent error handling protocol: (i) functions that can fail due to external conditions (I/O, network, OS) return a Result type: `Result` where `E` is an error type carrying a numeric code and message. The caller MUST handle both variants. (ii) Functions that can fail due to programmer error (invalid arguments, precondition violations) throw a typed exception that propagates up the call stack. (iii) Every exported function documents its error conditions and the error codes it may produce. (iv) Error codes follow the module prefix convention: I/O errors use 5000-5999, networking uses 6000-6099, JSON uses 6100-6199, crypto uses 6200-6299, OS uses 6300-6399.

Definition 15.4

Each STDLIB module is a single `.dryad` file placed in the `lib/` directory. Every module follows this structure: (i) a module-level comment block describing the module's purpose and error codes; (ii) intrinsic and FFI declarations (wrapped in safe functions); (iii) type and class definitions; (iv) public function declarations, each with a documentation comment; (v) a consistent `export` block at the end. Public API functions are explicitly listed in the export statement; all other identifiers are module-private. Breaking changes to exported APIs require a major version bump.

Definition 15.5

The Buffer type (`Buffer`) represents a contiguous byte region with embedded length and capacity. API contract: (i) `Buffer.new(capacity: number): Buffer` — allocates a new buffer with the given initial capacity (zero-filled). (ii) `buf.len(): number` — returns the current length of valid data. (iii) `buf.capacity(): number` — returns the total allocated capacity. (iv) `buf.read(offset: number, len: number): Buffer` — returns a copy of the byte range [offset, offset+len). Raises Runtime Error 3000-3999 if the range exceeds length. (v) `buf.write(offset: number, data: Buffer): void` — writes data at the given offset, growing the buffer if necessary. (vi) `buf.slice(offset: number, len: number): BufferView` — returns a zero-copy view into the buffer's memory (no allocation). The view shares the backing store and is reference-counted. Modifications to the view are visible in the parent buffer. (vii) `buf.resize(new_len: number): void` — changes the length. If new_len exceeds capacity, the buffer grows using a doubling strategy. (viii) `buf.fromPtr(ptr: ptr, len: usize): Buffer` — wraps a raw pointer in a Buffer without copying (intended for use with mmap'd regions). The caller is responsible for ensuring the pointer remains valid for the Buffer's lifetime. (ix) Bounds checking: every read and write validates the offset and extent against `len`. Out-of-bounds access raises Runtime Error 3000-3999 with the attempted range and buffer capacity. The bounds check compiles to a single `cmp + jae` on both ARM64 and x86_64 and is elided only when the AOT optimizer can statically prove safety.

Definition 15.6

The STDLIB provides three collection types with defined contracts: (i) **List** — ordered, dynamically-sized sequence. Contract: `add(item)`, `get(index): any`, `set(index, value)`, `remove(index): void`, `len(): number`, `isEmpty(): bool`, `contains(item): bool`, `indexOf(item): number`, `sort(comparator?)`, `map(fn): List`, `filter(fn): List`, `reduce(fn, initial): any`, `forEach(fn)`, `slice(start, end): List`, `concat(other): List`, `toArray(): any[]`. (ii) **Map** — key-value dictionary (hash-based). Contract: `set(key, value)`, `get(key): any`, `has(key): bool`, `delete(key): void`, `len(): number`, `keys(): any[]`, `values(): any[]`, `entries(): [any, any][]`, `forEach(fn)`. (iii) **Set** — unique value collection (hash-based). Contract: `add(item)`, `has(item): bool`, `delete(item): void`, `len(): number`, `values(): any[]`. All three collections support the Iterator protocol via `[Symbol.iterator]()` returning an object with `next(): { value, done }`.

ModuleNamespaceDescription
io#ioFile I/O operations: read, write, list, delete, temp files, stdin/stdout
fs#fsHigh-level file system: copy, move, watch, stat
net#netTCP networking: connect, listen, sockets, DNS resolution
http#httpHTTP client: GET, POST, request, response handling
json#jsonJSON parsing and serialization
crypto#cryptoCryptographic hashing, random generation, encryption
math#mathMathematical functions, constants, random number generation
time#timeDate/time operations: now, sleep, format, parse
os#osOperating system: platform, environment, arguments, exit
debug#debugDebug utilities: assertions, logging, tracing, benchmarking
collections(built-in)List, Map, Set types with full collection interfaces
STDLIB Module Usage Pattern
// Import a native module
#io

// STDLIB functions are used directly
function backup(file: string) {
  let content = io_read_file(file);
  let backup_name = file ++ ".bak";
  io_write_file(backup_name, content);
  io_println("Backed up: " ++ backup_name);
}

// Import multiple modules
#net
#json

function fetchConfig(host: string, port: number) {
  let socket = net_connect(host, port);
  socket.write("GET /config HTTP/1.0\r\n\r\n");
  let response = socket.read();
  socket.close();
  return json_parse(response);
}

Native modules are loaded via #module directives. Functions become available in the global scope. The STDLIB follows consistent naming and error handling conventions across all modules.

↑ Back to top

§13 Bytecode and Intermediate Representation

Definition 13.1

The Dryad bytecode consists of stack-based OpCodes organized into the following categories: Stack management (Push, Pop, Dup); Arithmetic (Add, Sub, Mul, Div, Mod, Pow); Logical (And, Or, Not); Bitwise (BitwiseAnd, BitwiseOr, BitwiseXor, Shift); Comparison (Equal, NotEqual, Less, Greater, LessEqual, GreaterEqual); Control Flow (Jump, JumpIfFalse, JumpIfTrue, Call, Return); Variables (GetLocal, SetLocal, GetGlobal, SetGlobal); Functions (DefineFunction, CallFunction); Objects (GetProperty, SetProperty, NewObject, NewArray); Classes (DefineClass, NewInstance, CallMethod).

Definition 13.2

The VM maintains four core structures: the Value Stack (operands and intermediate results), the Instruction Pointer (IP — current position in the bytecode stream), the Call Stack (function frame chain), and the Global Environment. The execution cycle is: Fetch (read OpCode at IP), Decode (identify operation and operands), Execute (perform the operation, manipulating the stack), Advance (increment IP), Repeat until a Return opcode or program end.

Definition 13.3

The IR is a register-based representation (unlike the stack-based bytecode) used by the AOT compiler. It is more abstract than assembly but more concrete than the AST. The IR is architecture-independent and enables classical compiler optimizations before code generation. Core structures: IrModule (compilation unit), IrFunction (function definition with parameters and body), IrInstruction (Load, Store, Add, Call, etc.), and IrValue (constants, variables, virtual registers).

OpCode CategoryInstructionsDescription
StackPush, Pop, DupOperand stack management.
ArithmeticAdd, Sub, Mul, Div, Mod, PowNumeric operations.
LogicalAnd, Or, NotBoolean operations.
BitwiseAnd, Or, Xor, ShiftBit-level manipulation.
ComparisonEq, Neq, Lt, Gt, Le, GeRelational comparisons.
ControlJump, JmpIfFalse, JmpIfTrue, Call, RetBranches and calls.
VariablesGetLocal, SetLocal, GetGlobal, SetGlobalScope access.
ObjectsGetProp, SetProp, NewObj, NewArrObject/array ops.
ClassesDefClass, NewInst, CallMethodClass operations.
↑ Back to top

§14 AOT Compilation

Definition 14.1

The AOT compiler follows four phases: (1) Bytecode-to-IR Conversion — translates stack-based OpCodes to register-based IR, performs control flow analysis, and constructs a Control Flow Graph (CFG); (2) IR Optimization — applies constant folding, dead code elimination, and register allocation; (3) Backend Code Generation — performs instruction selection for the target architecture, physical register allocation, and assembly generation; (4) Object File Generation — produces the executable format (ELF or PE) and links with runtime libraries to produce a native binary.

Definition 14.2

ARM64: RISC 64-bit architecture with 32-bit fixed instructions, registers x0-x30, sp, pc. x86_64: CISC 64-bit architecture with variable-length instructions, registers rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, r8-r15. Object formats: ELF (Linux/Unix, sections .text, .data, .bss, .rodata) and PE (Windows, sections .text, .data, .rdata).

Note

The AOT compiler is currently experimental. Not all language features are supported in AOT mode. The interpreter and bytecode VM are the recommended execution modes for development. AOT should be used for stable hot paths and production builds where maximum performance is required.

Definition 14.3

The AOT backend resolves function calls through statically-defined linker symbols rather than dynamic dispatch tables. Each function in the compiled IR is assigned a unique, deterministic symbol name following the scheme _D<module_hash>_<function_name>. The linker resolves cross-module references at link time, eliminating all runtime dispatch overhead. Virtual method calls on classes with a single known implementation (monomorphic sites) are devirtualized during IR optimization and replaced with direct static calls. Polymorphic sites fall back to a call *rax indirect dispatch through a vtable, but the vtable is populated at link time and resides in .rodata.

AOT Symbol Resolution and Devirtualization
// Dryad source
class Logger {
  function write(msg: string) { ... }
}
let log = new Logger();
log.write("hello");

// AOT-compiled symbol: _Da1b2_write
// Callsite after devirtualization:
//   lea rdi, [rip + .Lstr_hello]   ; string argument
//   call _Da1b2_write              ; direct call — zero dispatch overhead

// With dynamic dispatch (polymorphic site):
//   mov rax, [rdi]                 ; load vtable pointer
//   call [rax + 16]               ; indirect call via vtable slot

Static dispatch produces a call instruction with a fixed target address resolved by the linker. This is the fastest possible calling mechanism — the CPU's branch predictor learns the target permanently and the instruction pipeline is not disrupted by an indirect branch.

Definition 14.4

For each function with dynamic parameters, the AOT compiler generates monomorphized variants based on the types inferred at compile time. A function function add(a, b) { return a + b; } produces add_f64(f64 a, f64 b) -> f64 and add_str(string a, string b) -> string stubs. Each variant eliminates the Variant Enum discriminant check for all operations within the function body. The binding is performed once at compile time — there is no deoptimization or reoptimization (no JIT feedback loop). This avoids the runtime stutter and complexity of adaptive optimization systems (V8's TurboFan, SpiderMonkey's Warp), replacing it with a deterministic compilation that always produces the same binary for the same source.

Definition 14.5

When the AOT compiler encounters object literal construction or class instantiation, it infers a Shape (also called Hidden Class or Map) that describes the layout of properties in memory. Each Shape encodes property names and their byte offsets relative to the object base pointer. Objects sharing the same Shape access properties via load [obj + offset] instead of a HashMap lookup. Shape transitions occur when properties are added or removed; the compiler generates fast-path inline cache checks for the common Shape and a slow-path fallback. For monomorphic sites (a single Shape throughout the program), the compiler eliminates all Shape checks, producing a direct memory load identical to C++ struct member access.

Property 14.2

The AOT optimizer traverses the Control Flow Graph (CFG) to propagate type constraints across basic blocks. If a variable x is used exclusively as number on all reachable paths within a function, the compiler promotes x to a native f64 (or i32 when range analysis permits). The promotion is scoped: a variable may be promoted to native types inside hot loops while retaining the Variant Enum representation outside the loop. This per-block promotion is the key difference from whole-function type inference — it permits fine-grained optimization without requiring the entire function to be monomorphic.

Property 14.3

The AOT compiler performs escape analysis on all allocated objects. If an object is determined to not escape its allocating function (no return, no store to global, no capture by closure), the object is allocated on the stack rather than the heap. Stack allocation eliminates: (i) the malloc/free call overhead; (ii) reference counting operations (the object's lifetime is tied to the stack frame); and (iii) GC pressure. Combined with Shape-based property access, a non-escaping object becomes indistinguishable from a C++ stack-allocated struct in the generated assembly — zero runtime overhead for allocation, access, and deallocation.

Note

The gradient inference strategy resolves types at three levels: Level 1 (Static CFG Inference) promotes variables when all paths agree; Level 2 (Annotation-Guided) respects user type annotations as binding; Level 3 (Dynamic Fallback) preserves the Variant Enum only for genuinely polymorphic bindings. This design ensures that correctly typed programs compile to 100% native code with zero runtime type checks, while genuinely dynamic programs remain correct — they simply run in the slower Variant Enum path where needed.

AOT Monomorphization Example
// Dryad source (dynamic types)
function scale(arr, factor) {
  let result = [];
  for (let i = 0; i < arr.len(); i++) {
    result.push(arr[i] * factor);
  }
  return result;
}
let doubled = scale([1, 2, 3], 2);

// AOT-compiled result (after type inference):
// CFG inference determines: arr -> f64[], factor -> f64, result -> f64[]
// Generated monomorphized version:
//   function scale_f64_arr(arr: f64[], factor: f64) -> f64[] {
//     let result: f64[] = allocate_array(0);
//     for (let i: i32 = 0; i < arr.len; i32++) {
//       // No type check: arr[i] and factor are both f64
//       result.push(arr[i] * factor);  // f64 * f64 = native FMUL
//     }
//     return result;
//   }
//
// Generated assembly (ARM64, simplified):
//   scale_f64_arr:
//     stp x29, x30, [sp, #-16]!
//     mov x29, sp
//     ...loop:
//     ldr d0, [x0, x1, lsl #3]  // arr[i] as f64
//     fmul d0, d0, d1            // d1 = factor
//     str d0, [x2, x3, lsl #3]  // result.push
//     add x3, x3, #1
//     cmp x3, x4                // x4 = arr.len
//     b.lt ...loop
//     ldp x29, x30, [sp], #16
//     ret

After gradient inference, the generated code contains zero type checks. The loop body compiles to a single FMUL instruction and a store — identical to what a C++ compiler would produce for std::vector<double>. The Variant Enum is entirely eliminated for this function.

Definition 14.6

The AOT compiler does not generate machine code or object files directly. Instead, it emits LLVM Intermediate Representation (LLVM IR), a low-level, platform-independent language that LLVM's optimization passes and code generation backend consume. The pipeline is: (1) Bytecode → Micro-IR (3-Address, SSA) — the stack-based bytecode is converted to a register-based intermediate representation in Static Single Assignment form, where each instruction has the form R1 = R2 op R3. This conversion uses a simple stack-to-register algorithm that tracks the expected operand types from the Metadata Header. (2) Micro-IR → LLVM IR — each Micro-IR instruction maps to one or more LLVM IR instructions. Types are mapped: f64double, i32i32, stringi8* (pointer), objecti8* (opaque pointer). Structures (tuples, objects with known Shapes) are emitted as LLVM struct types with named fields. (3) LLVM Optimization — LLVM's built-in pass manager applies constant folding, dead code elimination, loop unrolling, vectorization (SIMD), and inlining. (4) Code Generation — LLVM's backend selects target-specific instructions for ARM64 or x86_64, performs physical register allocation, and emits an object file (.o). (5) Linking — the object file is linked against the Dryad runtime library (containing the ~50 intrinsic implementations and the reference counting GC) to produce the final ELF or PE executable.

Bytecode → Micro-IR → LLVM IR Translation
// Dryad source: let z = x + y;    (x, y observed as f64)

// Stack bytecode (from VM):
//   LOAD_FAST x        // push x onto stack
//   LOAD_FAST y        // push y onto stack
//   ADD_F64             // pop two f64 values, add, push result
//   STORE_FAST z        // pop and store to local

// Micro-IR (3-address, SSA):
//   %1 = load_f64 @x          ; R1 = x
//   %2 = load_f64 @y          ; R2 = y
//   %3 = fadd_f64 %1, %2      ; R3 = R1 + R2
//   store_f64 @z, %3          ; @z = R3

// LLVM IR emitted:
//   define void @_D42_example(double* %x_ptr, double* %y_ptr, double* %z_ptr) {
//     %x = load double, double* %x_ptr
//     %y = load double, double* %y_ptr
//     %z = fadd double %x, %y
//     store double %z, double* %z_ptr
//     ret void
//   }

// Resulting ARM64 assembly (via LLVM):
//   ldr d0, [x0]    ; load x
//   ldr d1, [x1]    ; load y
//   fadd d0, d0, d1 ; native f64 add
//   str d0, [x2]    ; store z
//   ret

The entire translation is type-driven. Because the Metadata Header recorded x and y as f64, every layer emits native floating-point operations. No Variant Enum logic appears anywhere in the pipeline — the Bind Once decision at the bytecode level propagates all the way to the silicon.

Definition 14.7

Every intrinsic syscall (open, read, write, socket, etc.) has two entry points in the C++ runtime. Fast Path (syscall_read_fast(i32 fd, ptr<u8> buf, usize len) → isize) — expects unboxed native types matching the C ABI. Arguments are passed in CPU registers following the platform calling convention. No Variant Enum construction or destruction occurs. The AOT compiler emits a direct call instruction to this entry point when the Metadata Header confirms all argument types statically. Slow Path (syscall_read_variant(Variant* args) → Variant) — accepts a Variant array, extracts and validates each argument at runtime, calls the same internal implementation, and wraps the result in a Variant. Both paths share the same internal logic (internal_read). The VM and interpreter always use the Slow Path. The AOT compiler selects between Fast and Slow Path per-call-site: if all arguments have STATIC or OBSERVED types, Fast Path is used; if any argument is UNKNOWN (genuinely dynamic), Slow Path is used. This dual-port design ensures zero overhead for statically-typed code paths while preserving correctness for dynamic code — without duplicating the intrinsic implementation.

Property 14.4

The dryad --analyze <file.dryad> command performs a static analysis pass over the code and reports every call site that would fall into the Slow Path during AOT compilation. Output includes: (i) the exact line and column of each Slow Path call site; (ii) the reason for the fallback (e.g., 'argument type UNKNOWN — add type annotation to force Fast Path'); (iii) the estimated performance impact (number of Variant operations per call); and (iv) a suggested fix (e.g., 'add type hint let data: Buffer'). The analysis is non-destructive — it does not modify the source file or emit any binary. It is implemented as a separate pass over the AOT compiler's frontend that stops after type inference and emits the diagnostic report. This tool serves both as a debugging aid and as an educational resource, teaching developers how the compiler interprets their type declarations.

Using dryad --analyze
$ dryad --analyze server.dryad

Dryad AOT Analysis Report
━━━━━━━━━━━━━━━━━━━━━━━
File: server.dryad

[SLOW PATH] line 45 column 12
  Function: processRequest(data, callback)
  Reason: parameter 'data' type UNKNOWN
    → Could not determine type at call site
  Impact: ~120 Variant operations per invocation
  Fix: add type hint 'let data: Buffer'

[SLOW PATH] line 78 column 8
  Function: data + transform
  Reason: operator '+' receives 'string | number' (merged type)
    → Type merger at line 72 assigns both types
  Impact: ~8 Variant operations per invocation
  Fix: refactor to separate numeric and string paths

[FAST PATH] 142 of 150 call sites (94.7%)
  Functions with static binding: processData, handleConnection,
  parseHeaders, writeResponse, logAccess

Summary:
  Slow Path call sites: 2
  Estimated Variant operations: ~128 per request
  Recommended annotations: 1

The analyzer helps developers identify exactly where dynamic types degrade performance. Each warning includes a concrete suggestion to promote the variable to the Fast Path, aligning with Dryad's philosophy of progressive performance — add types only where they matter.

↑ Back to top

§15 Programming Paradigms

Dryad is a multi-paradigm language that supports object-oriented, functional, and procedural programming styles, often mixable within the same file. No paradigm is enforced; the developer chooses the approach best suited to the problem.

Definition 15.1

Dryad supports full OOP: encapsulation via access modifiers (public, private, protected), single inheritance via extends, polymorphism via method overriding, and abstraction via classes and interfaces. Static members belong to the class rather than instances.

Definition 15.2

Functional features include: first-class functions (functions as values), higher-order functions (functions accepting or returning functions), closures (lexical scope capture), arrow functions (concise syntax with implicit return for single-expression bodies), and partial immutability via const declarations.

Definition 15.3

Procedural programming is supported through top-level functions (no class wrapper required), global and local variables, structured control flow (loops, conditionals), and no mandatory object-oriented constructs. This paradigm is ideal for scripts, automation tasks, and small-to-medium programs.

Three Paradigms in One Language
// Procedural: top-level function, no classes
function greet(name: string) {
  return "Hello, " ++ name;
}

// Functional: higher-order with closure
function makeMultiplier(factor: number) {
  return (x: number) => x * factor;
}
let double = makeMultiplier(2);

// OOP: class with inheritance
class Animal {
  constructor(public name: string) {}
  speak(): string { return "..."; }
}
class Dog extends Animal {
  speak(): string { return this.name ++ " says woof!"; }
}

console.log(greet("World"));  // Hello, World
console.log(double(5));       // 10
console.log(new Dog("Rex").speak());  // Rex says woof!

All three paradigms are first-class citizens. A Dryad program may freely mix them without ceremony.

↑ Back to top

§16 Formal Grammar (BNF)

The following BNF grammar defines the core syntax of Dryad. This is a simplified subset covering the essential language constructs. The grammar is context-free and designed to be LL(1) parsable for most constructs.

Core BNF Grammar
       ::= *
     ::= 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 

      ::= "let"  [":" ] ["=" ] ";"
    ::= "const"  [":" ] "="  ";"

 ::= ["async"] "function" 
                    "("  ")" [":" ]
                    "{" * "}"

    ::= "class"  ["extends" ]
                    "{" * "}"

    ::= 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 
                  | 

   ::=   
    ::=  
     ::=  "("  ")"
   ::=  "." 
                  |  "["  "]"
                  |  "::" 
   ::= "("  ")" "=>" 
                  | "("  ")" "=>" "{" * "}"

          ::= "number" | "string" | "bool" | "any"
                  |  "[]"
                  | "("  ")"
                  | "fn" "("  ")" "->" 

The BNF specification above is definitive for the core language. Extensions (guards in match, computed object properties, decorators, template strings) are defined in the full theoretical foundation document.

↑ Back to top

§17 Limitations and Performance Considerations

The following limitations and trade-offs are recognized as part of Dryad's design. They inform the scope of applicability and guide future development priorities.

LimitationImpactMitigation
Interpreter SpeedSlower than compiled languages for CPU-heavy workloads.Use bytecode VM via set_compile_mode(true) on hot paths.
Dynamic TypingNo compile-time type error detection in default mode.Opt-in 'use strict types' for static validation in AOT.
FFI ComplexityComplex types passed as opaque pointers across FFI boundary.Use primitive types at the boundary; wrap in Dryad code.
Single-Threaded InterpreterBase interpreter runs on a single thread.Thread functions execute on real OS threads with isolation.
Module VersioningNo built-in package manager for dependency resolution.Package manager is on the roadmap.
AOT ExperimentalNot all language features support AOT compilation.Use interpreter/bytecode for development; AOT for stable paths.
Deep RecursionRisk of stack overflow with unbounded recursion.Prefer iterative patterns; set explicit recursion limits.
GC PressureFrequent GC cycles with large array/object allocations.Use Buffer for raw data; minimize allocations in tight loops.
Property 17.1

Dryad is suitable for: scripting, automation, prototyping, and small-to-medium applications. It is unsuitable for: computationally intensive workloads, hard real-time systems, and performance-critical production pipelines — at least until the AOT compiler matures. Identified bottlenecks include deep call stacks, large array operations, and frequent garbage collection.

↑ Back to top

§18 Proposed Architectural Refinements

The following refinements are proposed for future versions of Dryad. They represent architectural decisions that have been researched but not yet implemented. Each entry describes the motivation, approach, and expected impact.

RefinementMotivationApproachExpected Impact
Direct-Threaded Bytecode VMCentral dispatch loop is a predictable bottleneck — each opcode incurs fetch, decode, and jump-back overhead.Each opcode handler appends the address of the next handler; dispatch becomes a single jmp *next. Eliminates the decode step entirely.~40% reduction in dispatch overhead; 15-25% overall VM speedup on control-flow-heavy code.
Generational GCReference counting alone cannot reclaim cycles. A full mark-and-sweep pause scales with live heap size.Add a young generation (nursery) with bump allocation and fast scavenge. Tenured objects remain RC-managed. Cycle detection runs only on the old generation.Eliminates unbounded pause times. Throughput improvement proportional to allocation rate.
JIT Compilation for Hot PathsBytecode VM is ~10x slower than native for tight loops. Entire-program AOT loses the interactive development loop.Add a lightweight JIT tier: count loop back-edges; after a threshold (e.g., 1000 iterations), compile the loop body to native and patch the bytecode stream with a jump to the compiled fragment.Up to 100x speedup on hot loops (matching AOT) while preserving interactive edit-run turnaround.
WASM BackendDryad cannot currently run in the browser or in WASM-hosted environments.Lower the IR to WebAssembly (MVP + sign extension + mutable globals). The AOT compiler gains a third backend emitting .wasm binaries. The runtime's intrinsics are mapped to WASI equivalents.Enables browser-based Dryad playground, serverless deployments, and plugin systems.
Algebraic Data TypesCurrent class system provides no discriminated unions or exhaustiveness checking.Add syntax: type Option<T> = Some(T) | None. Pattern matching gains exhaustiveness verification. The AOT compiler can represent ADTs as tagged unions with optimal layout.Safer error handling (no null pointer patterns), compiler-verified match coverage, smaller memory footprint for sum types.
Linear Types for Resource ManagementFile handles, sockets, and buffers rely on GC finalization for cleanup, producing non-deterministic release.Add linear type qualifier. Linear values must be consumed exactly once. The compiler inserts release calls at each consumption point. Runtime asserts track violations in debug mode.Deterministic resource cleanup. Zero overhead in AOT mode. Enables safe manual memory management without a borrow checker.
Trait / Interface SystemStructural typing for classes is ad-hoc; there is no way to express interface contracts polymorphically.Introduce trait declarations with required method signatures. Classes declare impl Trait for Class. The compiler generates vtable-based dispatch for trait objects; monomorphization for concrete implementations.Enables generic programming with compile-time safety. Eliminates runtime type checks for interface dispatch in AOT mode.
Package Manager and RegistryNo built-in dependency resolution — users manually copy files or use ad-hoc scripts.Implement dryad pkg subcommand with dryad.toml manifest. Registry supports versioned publishing with semantic versioning. Lockfile provides reproducible builds.Ecosystem enabler. Reduces friction for third-party library adoption. Enables dryad init and dryad test workflows.
Property 18.1

Each proposed refinement is evaluated against the following criteria: (i) backward compatibility — must not break existing Dryad code; (ii) implementation cost — estimated person-weeks for a production-quality implementation; (iii) risk — potential for destabilizing existing subsystems; (iv) ecosystem impact — how many users benefit. Refinements with high ecosystem impact and low risk are prioritized. The table above is ordered approximately by ascending complexity.

↑ Back to top