Skip to main content
Dryft is an experimental stack-based concatenative programming language with a simple single-pass compiler architecture. This guide explains how the compiler works internally.

Overview

Dryft features:
  • Stack-based concatenative programming model
  • Pure and impure function distinction (functions vs actions)
  • Linear types and stack-based resource management
  • Full optional type inference
  • Single-pass compilation
  • Pluggable backend system

Build Graph

The compilation process follows this pipeline:

Pipeline Stages

  1. Source Files - .dry source files and standard library
  2. Frontend - Single-pass parser and semantic analyzer
  3. Backend - Code generator for target platform
  4. IR - Intermediate representation (C code, assembly, etc.)
  5. Assembler - Platform assembler/compiler (GCC, NASM, etc.)
  6. Objects - Compiled object files from Dryft code and native bindings
  7. Linker - Links all objects into final executable
  8. Interpreter - Executes the resulting program

Frontend Architecture

The frontend is implemented in src/frontend.rs and provides two main functions:

compile()

The core compilation function (frontend.rs:27) that performs single-pass compilation:
pub fn compile(backend: &mut Box<dyn Backend>, code: &str) -> CompileState
How it works:
  1. Character-by-character processing - The source code is drained one character at a time (line 45)
  2. Token accumulation - Characters are accumulated into words until whitespace is encountered
  3. Immediate processing - Each token is processed immediately via handle_token()
  4. State tracking - The CompileState struct maintains all compilation state
Key features:
  • Comment handling - #comment# syntax (lines 65-69)
  • String literals - "text" syntax (lines 70-79)
  • Annotations - (annotation) syntax for metadata (lines 80-86)
  • File inclusion - Tracks file stack for include keyword (lines 94-104)
  • Line tracking - Maintains line and file information for error reporting (lines 54-62)

compile_full()

A convenience wrapper (frontend.rs:112) that compiles and finalizes the output:
pub fn compile_full(mut backend: Box<dyn Backend>, code: &str) -> String
This calls compile() and then invokes the backend’s complete() method to add runtime boilerplate.

Token Handling

The handle_token() function (frontend.rs:119) is the heart of the compiler. It:
  1. Recognizes token types - Keywords, numbers, variables, function calls, etc.
  2. Manages definition contexts - Tracks whether we’re inside a function, loop, etc.
  3. Performs type checking - Validates stack types match operation requirements
  4. Emits backend code - Calls appropriate backend methods to generate output

Definition Stack

The defnstack tracks nested contexts:
  • Function - Pure functions (declared with fun:)
  • Action - Impure actions (declared with act:)
  • Then - Conditional blocks
  • Elect - When/elect blocks (switch-like)
  • Loop - Infinite loops
  • Variable - Variable declarations
  • Linkin - External function bindings
  • Include - File inclusion
  • Module - Module definitions

Token Matching

The massive match statement (line 225) handles all token types: Keywords for definitions:
  • fun: / :fun - Define functions
  • act: / :act - Define actions
  • then: / :then - Conditional blocks
  • elect: / :elect or when: / :when - Switch-like constructs
  • loop: / :loop or cycle: / :cycle - Infinite loops
  • var: - Variable declaration
Control flow:
  • break - Exit loops
  • return - Return from functions
  • ; or end - End current block
Literals and values:
  • Integer regex match (line 473)
  • String literals (handled separately)
  • true / false - Boolean literals
Variables:
  • $varname - Read variable (line 463)
  • varname! - Write variable (line 478)
Operators:
  • Arithmetic: +, -, *, /, mod
  • Stack ops: ^ (copy), v (drop), swap
  • Comparison: =?, !=?, >?, <?, >=?, =<?
  • Logic: not, both?, either?, xor
Special:
  • linkin - Link external functions (line 329)
  • include - Include other files (line 334)
  • Method calls - Any previously defined function/action name

Compile State

The CompileState struct (src/state.rs) maintains all compiler state:

Core Fields

  • out: Option<String> - Final compiled output
  • methods: HashMap<String, Method> - All defined functions and actions
  • word: String - Current token being accumulated

Stacks

  • defnstack - Definition context stack (what kind of block we’re in)
  • metastack - Metadata for current definition (e.g., function names)
  • bodystack - Code bodies for nested definitions
  • varscopes - Variable scopes (nested HashMap stack)
  • typestack - Type state for type checking
  • voidstack - Input types for functions/actions

State Flags

  • iscomment / isstring / isannotation - Parser mode flags
  • linenumber / tokenumber - Position tracking
  • current_file / file_stack - File inclusion tracking

Backend Interface

The backend system (src/backends.rs) defines the Backend trait that all code generators implement. The frontend calls backend methods to emit target-specific code. Key backend methods:
  • Code generation: create_function(), create_loop_block(), etc.
  • Stack operations: push_integer(), fun_add(), etc.
  • Finalization: complete() wraps code with runtime

Backend Selection

Backends are selected via the select() function (backends.rs:90):
pub fn select(name: &str) -> Box<dyn Backend> {
    match name {
        "C99" => Box::new(c99::C99Backend {}),
        "x86" => Box::new(x86::Nasm64Backend {}),
        other => panic!("Invalid backend {other}"),
    }
}

Type System

Dryft has a simple type system defined in state.rs:

Value Types

pub enum ValueTypes {
    Number,          // Integer values
    Text,            // String values
    Binary,          // Boolean values
    Method(Vec<ValueTypes>, Vec<ValueTypes>),  // Function types
    Fake,            // Internal use only
}

Method Classes

Functions are classified as:
pub enum MethodClass {
    Function,  // Pure functions (fun:)
    Action,    // Impure actions (act:)
}

Type Checking

The compiler performs stack-based type checking:
  • Each operation declares expected input types via cs.expect_types()
  • Operations push their output types via cs.push_type()
  • Type mismatches cause compilation errors
  • Actions are tracked to ensure they’re not called in pure contexts

Error Handling

Errors are reported with file and line information:
  • cs.token_line and cs.token_file track where each token originates
  • cs.throw_error() reports errors with context
  • The file stack enables accurate error reporting even in included files

Single-Pass Compilation

Dryft uses true single-pass compilation (see comment on line 44 of frontend.rs):
  • Each token is processed immediately as it’s encountered
  • No AST is built
  • Backend code is generated during parsing
  • This keeps the compiler extremely simple but limits certain optimizations

Key Design Decisions

Stack-Based Concatenation

Dryft is concatenative - programs are built by composing functions that transform the stack. The compiler maintains type information about the stack state.

Linear Types

The type system enforces linear resource usage - values must be consumed exactly once (though this is still under development).

Macros

The frontend uses Rust macros heavily for code generation patterns:
  • new_definition! - Start new definition contexts
  • add_method! - Finalize method definitions
  • add_builtin! - Emit builtin operations
  • check_terminator! - Validate block endings
These macros reduce boilerplate in the token handler.

Standard Library

The standard library is written in .dry files and compiled alongside user code. The build profile specifies how to compile and link it (see src/targets/*.toml).

Build docs developers (and LLMs) love