Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

rask logo

Safety without the pain.

Rask is a systems programming language that sits between Rust and Go:

  • Rust’s safety guarantees without lifetime annotations
  • Go’s simplicity without garbage collection

Status: Design phase with working interpreter (no compiler yet)

Quick Look

func search_file(path: string, pattern: string) -> () or IoError {
    const file = try fs.open(path)
    ensure file.close()

    for line in file.lines() {
        if line.contains(pattern): println(line)
    }
}

No lifetime annotations. No borrow checker fights. No GC pauses.

Core Ideas

  • Value semantics - Everything is a value, no hidden sharing
  • Single ownership - Deterministic cleanup, no GC
  • Scoped borrowing - Temporary access that can’t escape
  • Handles over pointers - Validated indices for graphs and cycles
  • Linear resources - Files and sockets must be explicitly consumed
  • No function coloring - I/O just works, no async/await split

Get Started

Design Philosophy

Want to understand the “why” behind Rask’s design choices?

Getting Started

Welcome to Rask! This section will help you get started with the language.

Note: Rask is in early development. Currently only the interpreter is available (no compiler yet).

What You’ll Learn

Status

Rask is in the design phase with a working interpreter. Three of the five litmus test programs run:

  • Grep clone ✓
  • Game loop ✓
  • Text editor ✓
  • HTTP server (blocked on network I/O)
  • Embedded sensor (blocked on SIMD)

Next Steps

After completing this section, continue to the Language Guide to learn core concepts.

Installation

Note: Rask is in early development. Currently only the interpreter is available.

Prerequisites

  • Rust toolchain (for building from source)
  • Git

Building from Source

git clone https://github.com/dritory/rask.git
cd rask/compiler
cargo build --release

The rask binary will be in compiler/target/release/rask.

Verify Installation

Run a test to verify the interpreter works:

./target/release/rask --version

Running Examples

The repository includes working examples:

./target/release/rask ../examples/hello_world.rk

Next Steps

Your First Program

Create a file called hello.rk:

func main() {
    println("Hello, Rask!")
}

Run it:

rask hello.rk

Output:

Hello, Rask!

What’s Happening?

  • func main() is the program entry point
  • println() is a builtin for printing with newline

Variables

Let’s try variables:

func main() {
    const name = "Rask"
    const year = 2025
    println(format("Hello from {} in {}!", name, year))
}
  • const creates an immutable binding
  • let creates a mutable binding (for values you’ll reassign)
  • Types are inferred, but you can write them explicitly: const year: i64 = 2025

Functions

func greet(name: string) {
    println(format("Hello, {}!", name))
}

func main() {
    greet("World")
}

Functions that return values need explicit return:

func add(a: i32, b: i32) -> i32 {
    return a + b
}

func main() {
    const result = add(2, 3)
    println(format("2 + 3 = {}", result))
}

Next: Explore the Guide

Continue to Basic Syntax →

Language Guide

This guide covers the essential features of Rask.

Note: Rask is in active design. This guide covers what’s currently implemented in the interpreter. For detailed specifications, see the Formal Specifications.

Contents

Learning Path

  1. Start with Basic Syntax to understand the fundamentals
  2. Learn Ownership - this is the key insight that makes Rask work
  3. Explore Collections and Handles for working with data structures
  4. Master Error Handling for robust programs
  5. Discover Concurrency for parallel execution

Examples

Prefer learning by example? See the Examples section for complete working programs.

Basic Syntax

Placeholder: Minimal content for now. See examples and formal specs for details.

Variables

const x = 42          // Immutable binding
let y = 0             // Mutable binding
y = 5                 // Reassignment

When to use:

  • const - binding won’t be reassigned (even if value is mutated via methods)
  • let - binding will be reassigned (e.g., let x = 0; x = 1)

Functions

func add(a: i32, b: i32) -> i32 {
    return a + b      // Explicit return required
}

Functions require explicit return for values (unlike Rust’s implicit returns).

Control Flow

if x > 0 {
    println("positive")
} else {
    println("zero or negative")
}

// Inline if (expression context)
const sign = if x > 0: "+" else: "-"

for i in 0..10 {
    println(i)
}

match result {
    Result.Ok(v) => println(v),
    Result.Err(e) => println("Error: {}", e),
}

Types

const a: i32 = 42       // Signed integers: i8, i16, i32, i64
const b: u64 = 100      // Unsigned: u8, u16, u32, u64
const c: f64 = 3.14     // Floats: f32, f64
const d: bool = true    // Boolean
const s: string = "hi"  // String (lowercase!)

Next Steps

Ownership and Memory

Placeholder: Brief overview. For detailed specifications, see the ownership and borrowing specs.

Core Principles

Rask’s memory model is built on three principles:

  1. Single ownership - Every value has one owner
  2. Move semantics - Assigning/passing transfers ownership
  3. Scoped borrowing - Temporary access that can’t escape

Ownership Transfer

const s1 = string.new("hello")
const s2 = s1              // s1 moved to s2, s1 is now invalid
// println(s1)             // Error: s1 has been moved

Borrowing

Borrowing gives temporary access without transferring ownership:

func print_len(s: string) {
    println(s.len())       // Borrows s
}

const text = string.new("hello")
print_len(text)            // text is borrowed
println(text)              // text still valid here

The borrow lasts only for the function call - text remains valid after.

Copy vs Move

Small types (≤16 bytes) copy implicitly:

const x: i32 = 42
const y = x                // Copy (i32 is small)
println(x)                 // x still valid

Large types move:

const v1 = Vec.new()
const v2 = v1              // Move (Vec is large)
// println(v1.len())       // Error: v1 has been moved

To keep access, explicitly clone:

const v1 = Vec.new()
const v2 = v1.clone()      // Explicit copy
println(v1.len())          // Both valid
println(v2.len())

Why No Storable References?

Rask doesn’t allow storing references in structs or returning them. This eliminates lifetime annotations:

  • ✗ No 'a lifetime parameters
  • ✗ No borrow checker fights
  • ✓ Simple ownership rules
  • ✓ Predictable behavior

For graphs and cycles, use handles instead of references.

Next Steps

Collections and Handles

Placeholder: Brief overview. For detailed specifications, see the collections and pools specs.

Three Collection Types

Vec - Ordered, indexed access:

const v = Vec.new()
try v.push(1)
try v.push(2)
const first = v[0]         // Copy out (if T: Copy)

Map<K,V> - Key-value lookup:

const m = Map.new()
try m.insert("key", "value")
const val = m.get("key")   // Returns Option<V>

Pool<T> - Handle-based storage for graphs:

const pool = Pool.new()
const h1 = try pool.insert(Node.new())
const h2 = try pool.insert(Node.new())
h1.next = h2               // Store handle, not reference

Why Handles?

References can’t be stored in Rask (no lifetime annotations). For graphs, cycles, and entity systems, use Pool\<T\> with handles:

struct Node {
    value: i32,
    next: Option<Handle<Node>>,
}

const pool = Pool.new()
const h1 = try pool.insert(Node { value: 1, next: None })
const h2 = try pool.insert(Node { value: 2, next: Some(h1) })

Handles are validated at runtime:

  • Pool ID check (right pool?)
  • Generation check (still valid?)
  • Index bounds check

Iteration

for i in vec {
    println(vec[i])        // Index iteration
}

for h in pool {
    println(pool[h].value) // Handle iteration
}

for i in 0..10 {
    println(i)             // Range iteration
}

Next Steps

Error Handling

Placeholder: Brief overview. For detailed specifications, see the error types spec.

Result Types

Operations that can fail return Result<T, E>, or using shorthand: T or E

func parse_number(s: string) -> i64 or ParseError {
    // Implementation
}

const result = parse_number("42")
match result {
    Result.Ok(n) => println("Parsed: {}", n),
    Result.Err(e) => println("Error: {}", e),
}

Error Propagation with try

The try operator extracts the success value or returns early with the error:

func process() -> () or Error {
    const file = try fs.open("data.txt")  // Returns Error if fails
    const data = try file.read()          // Returns Error if fails
    process_data(data)
}

Without try, you’d need nested matches for every fallible operation.

Resource Cleanup with ensure

func read_file(path: string) -> string or IoError {
    const file = try fs.open(path)
    ensure file.close()           // Runs at scope exit, even on error

    const data = try file.read()  // Can use try after ensure
    return data
}

The ensure keyword guarantees cleanup runs even if try returns early.

Optional Values

Option<T> or T? for values that may be absent:

const m = Map.new()
try m.insert("key", 42)

const val = m.get("key")      // Returns Option<i64>
if val is Some(v) {
    println("Found: {}", v)
} else {
    println("Not found")
}

// Or use the ?? operator for defaults:
const v = m.get("key") ?? 0

Next Steps

Concurrency

Placeholder: Brief overview. For detailed specifications, see the concurrency specs.

No Function Coloring

Functions are just functions - no async/await split:

func fetch_user(id: u64) -> User {
    const response = try http_get(url)  // Pauses task, not thread
    return parse_user(response)
}

Spawning Tasks

func main() {
    with multitasking {
        const h = spawn { fetch_user(1) }
        const user = try h.join()
        println(user.name)
    }
}

Three spawn constructs:

  • spawn { } - Green task (requires with multitasking)
  • spawn_thread { } - Thread from pool (requires with threading)
  • spawn_raw { } - Raw OS thread (no requirements)

Channels

with multitasking {
    const chan = Channel.buffered(10)

    spawn {
        try chan.sender.send(42)
    }.detach()

    const val = try chan.receiver.recv()
    println("Received: {}", val)
}

Channels transfer ownership - no shared mutable state between tasks.

Thread Pools

with threading(4) {
    const results = Vec.new()
    for i in 0..100 {
        const h = spawn_thread { compute(i) }
        try results.push(h)
    }

    for h in results {
        const val = try h.join()
        println(val)
    }
}

Next Steps

Examples

Real Rask programs that demonstrate practical patterns.

All example code is in the repository’s examples/ folder and runs on the current interpreter.

Available Examples

Running Examples

git clone https://github.com/dritory/rask.git
cd rask/compiler
cargo build --release
./target/release/rask ../examples/grep_clone.rk --help

View all examples on GitHub →

What These Demonstrate

Each example showcases key Rask concepts:

Grep Clone

  • CLI argument parsing
  • File I/O with error handling
  • String operations
  • Resource cleanup with ensure

Game Loop

  • Entity-component system using Pool<T>
  • Handle-based indirection
  • Game state management

Text Editor

  • Command pattern for undo/redo
  • File I/O and resource management
  • State transitions

Grep Clone

A command-line tool for searching files with pattern matching.

Full source: grep_clone.rk

Key Concepts Demonstrated

  • CLI argument parsing
  • File I/O with error handling
  • String operations (split, contains, trim)
  • Resource cleanup with ensure
  • Pattern matching with enums

Highlights

Resource Management

func search_file(path: string, pattern: string) -> () or IoError {
    const file = try fs.open(path)
    ensure file.close()  // Guaranteed cleanup

    for line in file.lines() {
        if line.contains(pattern): println(line)
    }
}

The ensure keyword guarantees file.close() runs even on early returns or errors.

Error Handling

enum GrepError {
    NoPattern,
    NoFiles,
    FileError(string),
}

func parse_args(args: Vec<string>) -> Options or GrepError {
    // Returns Result type, caller must handle errors
}

String Processing

for line in file.lines() {
    if case_insensitive {
        if line.to_lowercase().contains(pattern.to_lowercase()) {
            println(line)
        }
    } else {
        if line.contains(pattern) {
            println(line)
        }
    }
}

Running It

rask grep_clone.rk "pattern" file1.txt file2.txt
rask grep_clone.rk -i "case-insensitive" *.txt

What You’ll Learn

  • How to parse command-line arguments in Rask
  • Error handling patterns with Result types
  • Resource management with ensure
  • String manipulation and iteration

View full source →

Game Loop

An entity-component system demonstrating handle-based indirection.

Full source: game_loop.rk

Key Concepts Demonstrated

  • Entity-component system with Pool<T>
  • Handle-based references (no pointers!)
  • Game state management
  • Frame-based update loop

Highlights

Entity Storage

struct Entity {
    pos: Vec2,
    vel: Vec2,
    health: i32,
    target: Option<Handle<Entity>>,  // Handle, not reference!
}

const entities = Pool.new()
const player = try entities.insert(Entity.new())
const enemy = try entities.insert(Entity.new())

// Enemy targets player using handle
entities[enemy].target = Some(player)

Update Loop

func update(delta: f32) with entities: Pool<Entity> {
    for h in entities {
        entities[h].pos.x += entities[h].vel.x * delta
        entities[h].pos.y += entities[h].vel.y * delta

        // Handle AI, collision, etc.
    }
}

Each entities[h] access is expression-scoped - the borrow ends at the semicolon. This allows mutation between accesses.

Why Handles Work

Unlike references, handles:

  • Can be stored in structs
  • Can form cycles (entity targets another)
  • Are validated at runtime (pool ID + generation)
  • Don’t need lifetime annotations

Running It

rask game_loop.rk

What You’ll Learn

  • How to use Pool<T> for entity systems
  • Handle-based indirection patterns
  • Expression-scoped borrowing for collections
  • Game loop structure in Rask

View full source →

Text Editor

A text editor with undo/redo functionality.

Full source: text_editor.rk

Key Concepts Demonstrated

  • Command pattern for undo/redo
  • File I/O and resource management
  • State transitions
  • Vec usage for history

Highlights

Command Pattern

enum Command {
    Insert(usize, string),
    Delete(usize, usize),
    Replace(usize, usize, string),
}

struct Editor {
    content: string,
    history: Vec<Command>,
    position: usize,
}

Undo/Redo

func undo(editor: Editor) {
    if editor.position > 0 {
        editor.position -= 1
        const cmd = editor.history[editor.position]
        reverse_command(editor, cmd)
    }
}

func redo(editor: Editor) {
    if editor.position < editor.history.len() {
        const cmd = editor.history[editor.position]
        apply_command(editor, cmd)
        editor.position += 1
    }
}

File Operations

func save(editor: Editor, path: string) -> () or IoError {
    const file = try fs.create(path)
    ensure file.close()

    try file.write(editor.content)
}

func load(path: string) -> Editor or IoError {
    const file = try fs.open(path)
    ensure file.close()

    const content = try file.read_to_string()
    return Editor { content, history: Vec.new(), position: 0 }
}

Running It

rask text_editor.rk

What You’ll Learn

  • Command pattern for undo/redo
  • Resource management with files
  • State management in Rask
  • Vec operations for history tracking

View full source →

Reference

Detailed technical documentation for Rask.

For Users vs Implementers

This book provides high-level user-facing documentation. For formal specifications and implementation details, see:

Coming Soon

Once the language stabilizes:

  • Generated API documentation
  • Standard library reference
  • Compiler command-line reference
  • Error code index

Current Status

Rask is in the design phase. The formal specifications are actively being developed and are the canonical source of truth for the language.

For now, refer to:

Formal Specifications

The formal language specifications are maintained in the repository’s specs/ directory. These are detailed technical documents for language implementers and those who want deep understanding.

View Specifications →

Organization

Specs are organized by topic:

  • Types - Type system, generics, traits
  • Memory - Ownership, borrowing, resources
  • Control - Loops, match, comptime
  • Concurrency - Tasks, threads, channels
  • Structure - Modules, packages, builds
  • Stdlib - Standard library APIs

Quick Access

Key specifications:

TopicLink
Ownershipownership.md
Borrowingborrowing.md
Collectionscollections.md
Poolspools.md
Error Typeserror-types.md
Concurrencyasync.md

For Users vs Implementers

  • This Book - User-facing documentation (“How do I use Rask?”)
  • Specs - Formal specifications (“How does Rask work internally?”)

Most Rask users won’t need the specs. If you’re:

  • Building applications → This book is for you
  • Building compilers/tools → Read the specs
  • Curious about internals → Specs provide complete detail

See Also

Playground

Try Rask directly in your browser with our interactive playground!

Open in full screen →

Features

The playground provides:

  • Online editor with syntax highlighting
  • Instant execution - no setup required
  • 🔗 Shareable code snippets via URL
  • 📚 Example programs to explore
  • 🎮 Quick experimentation without installation

How to Use

  1. Write code in the left editor pane
  2. Click “Run” or press Ctrl+Enter to execute
  3. View output in the right pane
  4. Load examples from the dropdown menu
  5. Share your code with the “Share” button

Limitations

The browser-based playground has some limitations compared to local execution:

  • No file I/O - fs module is disabled
  • No networking - net module is disabled
  • No stdin - interactive input not supported
  • Most features work - math, collections, json, pattern matching, etc.

Try These Examples

Click the examples dropdown in the playground to try:

  • Hello World - Basic println and output
  • Collections - Working with Vec, structs, and pattern matching
  • Pattern Matching - Demonstrating match expressions
  • Math Demo - Mathematical operations and calculations

Local Development

For full language features including file I/O and networking:

  1. Install Rask locally
  2. Run the examples
  3. Build real applications

Technical Details

The playground compiles the Rask interpreter to WebAssembly using wasm-pack. Code executes entirely in your browser with no server-side processing.

WASM bundle size: ~200KB gzipped Supported browsers: Chrome, Firefox, Safari (latest versions)

Source Code

The playground is open source:

Feedback

Found a bug or have a suggestion? Open an issue on GitHub!

Contributing

Rask is in active design and development. Contributions welcome!

How to Help

  • Try the interpreter - Run examples, report bugs
  • Review specs - Provide feedback on language design
  • Implement features - Check TODO.md for open tasks
  • Documentation - Improve this book

Getting Started

  1. Read the Design Process to understand Rask’s philosophy
  2. Check out the formal specifications
  3. Explore the CORE_DESIGN.md document
  4. Look at TODO.md for what needs work

Repository

github.com/dritory/rask

Ways to Contribute

Bug Reports

Found a bug? Open an issue.

Include:

  • Code that demonstrates the bug
  • Expected behavior
  • Actual behavior
  • Interpreter output

Feature Suggestions

Have an idea? Open an issue for discussion.

Consider:

  • Does it align with core principles?
  • What’s the tradeoff?
  • How does it affect the litmus tests?

Code Contributions

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Run tests: cd compiler && cargo test
  5. Submit a pull request

Documentation

Improvements to this book are welcome:

  • Fix typos and clarity issues
  • Add examples
  • Improve explanations
  • Expand placeholder sections

Community Guidelines

  • Be respectful and constructive
  • Focus on technical merit
  • Consider tradeoffs and design constraints
  • Test your changes

Questions?

Open a discussion or issue on GitHub.

Design Process

Rask’s design is guided by clear principles and measured against specific metrics.

Design Principles

  1. Safety Without Annotation - Memory safety without lifetime markers
  2. Value Semantics - No hidden sharing or aliasing
  3. No Storable References - References can’t escape scope
  4. Transparent Costs - Major costs visible in code
  5. Local Analysis Only - No whole-program inference
  6. Resource Types - I/O handles must be consumed
  7. Compiler Knowledge is Visible - IDE shows inferred information

Full details: CORE_DESIGN.md

Validation

Rask is validated against test programs that must work naturally:

  1. HTTP JSON API server
  2. grep clone ✓ (implemented)
  3. Text editor with undo ✓ (implemented)
  4. Game loop with entities ✓ (implemented)
  5. Embedded sensor processor

Litmus test: If Rask is longer/noisier than Go for core loops, fix the design.

Metrics

Design decisions are evaluated using concrete metrics:

  • Clone overhead (% of lines with .clone())
  • Handle access cost (nanoseconds)
  • Compile times (seconds per 1000 LOC)
  • Binary size
  • Memory usage

See METRICS.md for the scoring methodology.

Specs and RFCs

Language features are documented as formal specifications in specs/.

Major changes follow an RFC process:

  1. Open an issue for discussion
  2. Draft a specification
  3. Implement in interpreter
  4. Validate against litmus tests
  5. Update metrics
  6. Merge if it improves the design

Tradeoffs

Every design has tradeoffs. Rask makes these intentional choices:

  • More .clone() calls - Better than lifetime annotations (our view)
  • Handle overhead - Better than raw pointers with manual tracking
  • No storable references - Simpler mental model, requires restructuring some patterns
  • Explicit costs - Better than hidden complexity

See CORE_DESIGN.md § Tradeoffs for full discussion.

Contributing to Design

When proposing changes:

  1. Explain the problem - What use case is difficult today?
  2. Show the tradeoff - What does this cost?
  3. Test against litmus tests - Does it make real programs better or worse?
  4. Measure the impact - Update relevant metrics
  5. Consider alternatives - What other approaches exist?

The goal is ergonomics without hidden costs. If a feature hides complexity or breaks transparency, it probably doesn’t belong.

Philosophy

“Safety is a property, not an experience.”

Users shouldn’t think about memory safety—they should just write code. The type system and scope rules make unsafe operations impossible by construction.

“If Rask needs 3+ lines where Go needs 1, question the design.”

Ceremony should be minimal. Explicit costs are good; boilerplate is bad.

“Local analysis only.”

Compilation should scale linearly. No whole-program inference, no escape analysis. Function signatures tell the whole story.

Learn More