Skip to content

RFC-008: Runtime Concurrency Model and Scheduler Decoupling Design

⚠️ Alignment Note: This document has been aligned with RFC-024 New Concurrency Model. The old whole-program DAG analysis, @block/@eager annotations, and L1/L2/L3 hierarchy models have been replaced by the spawn {} block parallel primitive. DAG analysis now operates only within spawn {} blocks.

References:

Abstract

This document defines key design aspects of the Runtime architecture:

  1. Three-tier Runtime Architecture: Embedded (immediate execution) → Standard (spawn + DAG scheduling) → Full (work-stealing)
  2. Compilation and Execution Separation: Compilation phase is identical across all modes; the difference lies only in runtime execution behavior
  3. Dual Backend Model: VM (development/debugging) and LLVM AOT (production/release) with completely consistent behavior
  4. Scheduler = Static Library: Scheduler linked into exe at AOT compile time, ~200-500KB, no GC
  5. Synchronous is Just a Special Case of Scheduling: num_workers=1 equals synchronous mode

Key Clarification: This is Not Java

Java:   .java → .class → JVM (interpret/JIT + GC)        ← Always needs virtual machine
YaoXiang development: .yx → IR → VM execution (fast iteration, step debugging)
YaoXiang production: .yx → IR → LLVM → Native exe (scheduler linked in)

VM is a development tool, not the essence of runtime. Similar to Go's go run vs go build.
Final exe = your native code + scheduler static library + reflection metadata. No interpreter, no JIT, no GC.

Motivation

Core Tensions

TensionDescription
Transparency vs Controlspawn blocks provide explicit concurrency control, normal code executes sequentially
Core vs Optionalspawn is the core parallel primitive, WorkStealing is an advanced feature for num_workers>1
Single-threaded vs ConcurrentIn single-threaded mode, concurrency manifests as async; synchronous is just a special case of scheduling

Proposal

1. Three-tier Runtime Architecture

┌──────────────────────────────────────────────────────────────────┐
│                    Compilation Phase (identical for all modes)   │
│                                                                  │
│  Source Code → Lexer → Parser → TypeCheck → Codegen → IR        │
│                                                                  │
│  ⚠️ Same syntax parsing, type checking, code generation, IR output│
└──────────────────────────────────────────────────────────────────┘

          ┌────────────────────┼────────────────────┐
          ▼                    ▼                    ▼
┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ 🟢 Embedded      │ │ 🔵 Standard   │ │ 🟣 Full          │
│ Immediate executor│ │ spawn + DAG   │ │ Full scheduler   │
│ Synchronous exec │ │ Concurrency   │ │ Parallel opt     │
│ No spawn support │ │ within spawn  │ │ Work-stealing    │
└──────────────────┘ └───────────────┘ └──────────────────┘
PhaseEmbeddedStandardFull
CompilationSameSameSame
Execution modeSynchronousConcurrent in spawn blocksParallel
Memory footprintLowMediumHigh
ConcurrencyNoneWithin spawn blocksWithin spawn blocks + parallel
spawn support
DAG analysisNoneWithin spawn blocksWithin spawn blocks
WorkStealerNoneNone

Embedded Runtime: Targets WASM/game scripts/rule engines. Immediate executor, no spawn support, high performance, low footprint.

Standard Runtime: Targets web services/data pipelines. Supports spawn {} blocks, performs DAG analysis and auto-concurrency within spawn blocks. num_workers=1 equals single-threaded async.

Full Runtime: Targets scientific computing/large-scale parallelism. Standard + WorkStealer load balancing.

2. Scheduler Decoupling: Generics + Injection

Core principle: VM does not directly depend on concrete schedulers; uses generic parameter [S] to call them.

yaoxiang
# Scheduler interface definition
Scheduler: Type = {
    spawn: (Task) -> TaskId,
    await: (TaskId) -> Result,
    spawn_with_deps: (Task, List(TaskId)) -> TaskId,
    await_all: (List(TaskId)) -> List(Result),
    stats: () -> SchedulerStats,
}

# Single-threaded scheduler
SingleThreadScheduler: Scheduler = {
    spawn: (task) => { task_queue.push(task); generate_task_id() },
    await: (task_id) => { ... },
    spawn_with_deps: (task, deps) => { ... },
    await_all: (task_ids) => { ... },
    stats: () => { queue_size: task_queue.len() },
}

# Multi-threaded scheduler
MultiThreadScheduler: Scheduler = {
    spawn: (task) => { work_queue.push(task); generate_task_id() },
    await: (task_id) => { wait_for_completion(task_id) },
    spawn_with_deps: (task, deps) => { ... },
    await_all: (task_ids) => { ... },
    stats: () => { workers: get_worker_stats() },
}

# VM uses scheduler via generics
create_vm: [S: Scheduler](scheduler: S) -> VM = (scheduler) => {
    VM(scheduler: scheduler, memory: create_memory(), dag: create_dag())
}

Key Points:

  • Compile-time polymorphism with zero runtime overhead
  • No trait objects needed
  • Generic type constraint [S: Scheduler] is already defined in RFC-011

3. Synchronous = Special Case of Scheduling

❌ Misconception: Disable the scheduler
✅ Correct: Use a scheduler with a single worker

num_workers = 1 → Single-threaded async scheduling
num_workers > 1 → Multi-threaded parallel scheduling

Same scheduler interface, just different configuration. No special cases.

4. Status of DAG

Important Change: DAG analysis no longer operates on the whole program; it is now limited to within spawn {} blocks. Normal code (outside spawn blocks) executes sequentially and does not require DAG analysis.

Layerspawn SupportDAG Analysis ScopeDescription
Core RuntimeWithin spawn blocksCore concurrency
Standard RuntimeWithin spawn blocksspawn + DAG scheduling
Embedded RuntimeNoneImmediate execution, no concurrency

5. Bottom-up Execution Model (Within spawn Blocks)

Important Change: Bottom-up DAG analysis is only performed within spawn {} blocks and no longer analyzes the entire program.

User code (concurrent within spawn block):
    (a, b) = spawn {
        fetch(url0),
        fetch(url1)
    }
    print(a)

Compile-time analysis (bottom-up within spawn block):
    fetch(url0) and fetch(url1) have no mutual dependency → can execute in parallel
    print(a) outside spawn block → sequential execution, waiting for spawn to complete

Runtime scheduling (starting from leaves within spawn block):
    fetch(url0) ┐
                ├→ parallel execution
    fetch(url1) ┘
    print(a)                       ← outside spawn block, sequential execution

Key Points:

  • Bottom-up dependency analysis is limited to within spawn {} blocks
  • Tasks without dependencies within a spawn block execute in parallel
  • Code outside spawn blocks executes sequentially, waiting for spawn blocks to complete

6. Compilation Model: Dual Backend + Statically Linked Runtime

6.1 Two Backends, One Behavior

                      ┌─────────────────────┐
                      │   Compile Frontend   │
                      │   (unified)          │
                      │   Lexer → Parser     │
                      │   → TypeCheck        │
                      │   → DAG analysis     │
                      │   within spawn blocks│
                      │   → Escape analysis  │
                      │   → Cycle detection  │
                      └──────────┬──────────┘

                    ┌────────────┴────────────┐
                    ▼                         ▼
        ┌───────────────────┐     ┌───────────────────┐
        │   VM Backend      │     │   LLVM Backend     │
        │   (Development)   │     │   (Production)     │
        │                   │     │                   │
        │   Generate IR/    │     │   Generate native  │
        │   bytecode        │     │   code             │
        │   VM interprets   │     │   Link runtime     │
        │   Supports step   │     │   static lib       │
        │   debugging       │     │   Output .exe      │
        │   Fast iteration  │     │   Zero interpreter │
        │                   │     │   overhead         │
        └───────────────────┘     └───────────────────┘
                 │                         │
                 ▼                         ▼
          Behavior identical          Behavior identical

VM Backend: Used during development. Modify code → run immediately → step debugging → fast iteration. Behavior is completely consistent with final exe.

LLVM Backend: Used for release. AOT compile to native code, scheduler linked as static library. No interpreter, no JIT.

6.2 Scheduler = Static Library, Not Virtual Machine

Internal structure of final exe:

┌────────────────────────────────────────────┐
│  Your code (native machine code)           │
│  ├── DAG execution plan determined at      │
│  │   compile time                          │
│  ├── Inlined move/ref/clone operations     │
│  └── RAII release code                     │
├────────────────────────────────────────────┤
│  Runtime static lib (~200-500KB)           │
│  ├── Thread pool (fixed size = num_workers)│
│  ├── Event loop (libuv / io_uring)        │
│  ├── Work-stealing queues (Full Runtime)  │
│  ├── Memory allocator (jemalloc/mimalloc) │
│  └── Reflection metadata (loaded on demand,│
│      not resident)                         │
├────────────────────────────────────────────┤
│  No:                                       │
│  ❌ Bytecode interpreter                   │
│  ❌ JIT compiler                           │
│  ❌ GC                                     │
│  ❌ Virtual machine                        │
└────────────────────────────────────────────┘

Comparison:

LanguageJavaGoYaoXiang
Compiled artifactBytecodeNative codeNative code
ExecutionJVM interpret/JITDirect executionDirect execution
Runtime size~200MB (JVM)~1-2MB (with GC)~200-500KB (no GC)
Memory managementGCGCRAII (deterministic)
ReflectionResident memoryResident memoryStored in exe, loaded on demand

6.3 Why Scheduler Performance is Constant

Key Insight: Most work is done at compile time; runtime only does "execution".

Compile time (one-time, not part of runtime):
    ├── Analyze DAG within spawn blocks: who depends on whom
    ├── Topological sort: determine execution order within spawn blocks
    ├── Identify parallelizable tasks: subtrees within spawn blocks with no dependencies
    ├── Escape analysis: ref → Rc or Arc
    ├── Cycle detection: auto-downgrade to Weak or error
    └── Inlining: small functions are directly expanded

Runtime (per execution, data structures are fixed):
    ├── Dispatch tasks to thread pool following compile-time determined DAG order within spawn blocks
    ├── Encounter I/O → suspend current task, event loop takes over
    ├── Task ready → return to ready queue
    └── That's it.

The scheduler itself is a fixed-size data structure: thread pool, event loop, work queue. No dynamic growth, no adaptive re-optimization, no GC scanning. Behavior is completely predictable.

Compile time has already figured out "what to schedule" within spawn blocks; runtime only does "execution". This is different from tokio—tokio dynamically builds Future chains at runtime. YaoXiang's DAG is static and limited to within spawn blocks.

6.4 Reflection: Stored, Not Resident

Reflection metadata is generated at compile time and stored in a separate segment (section) of the exe. It is not loaded at program startup. When reflection is first requested, it is memory-mapped into memory on demand. Similar to:

exe layout:
  .text     ← your code
  .rodata   ← constants
  .reflect  ← reflection metadata (type info, function signatures, etc.)
              mmap loaded on demand, zero memory if not accessed

Trade-off: exe size increases (contains reflection data), but zero memory overhead if not accessed at runtime. First access has loading delay (similar to JIT warm-up), zero overhead afterward.

7. Directory Structure (Updated)

src/
├── lib.rs
├── main.rs
├── backends/                          # Runtime backends
│   ├── common/                        # Shared across all backends (values, heap, opcodes)
│   │   ├── allocator.rs
│   │   ├── heap.rs
│   │   ├── opcode.rs
│   │   └── value.rs
│   ├── dev/                           # REPL + debugger
│   │   ├── debugger.rs
│   │   ├── shell.rs
│   │   └── repl/
│   ├── interpreter/                   # 🟢 Tree-walking interpreter (formerly Embedded/VM)
│   │   ├── ffi.rs
│   │   ├── frames.rs
│   │   ├── registers.rs
│   │   ├── runtime.rs
│   │   └── executor/
│   └── runtime/                       # 🔵 Compiled VM runtime
│       ├── engine.rs
│       ├── facade.rs
│       └── task.rs
├── frontend/                          # Compile frontend (shared by all backends)
│   ├── compiler.rs
│   ├── config.rs
│   ├── pipeline.rs
│   ├── core/
│   │   ├── lexer/
│   │   ├── parser/
│   │   ├── typecheck/
│   │   │   ├── checker.rs
│   │   │   ├── spawn_placement.rs     # ★ spawn block DAG/concurrency analysis
│   │   │   │     (formerly frontend/dag/)
│   │   │   ├── inference/
│   │   │   └── traits/
│   │   └── types/
│   ├── events/
│   ├── module/
│   └── pipeline/
├── middle/                            # Middle-end
│   ├── core/                          # IR & bytecode
│   │   ├── bytecode.rs
│   │   ├── ir.rs                      #   IR definition (shared by VM and LLVM)
│   │   └── ir_gen.rs
│   └── passes/                        # Compilation passes
│       ├── codegen/                   # Code generation (formerly codegen/)
│       ├── lifetime/                  # Lifetime/borrow analysis
│       └── mono/                      # Monomorphization
├── lsp/                               # Language Server
├── formatter/                         # Source code formatter
├── package/                           # Package manager
├── std/                               # Standard library
│   ├── concurrent.rs
│   ├── io.rs
│   ├── list.rs
│   ├── math.rs
│   ├── net.rs
│   ├── string.rs
│   └── weak.rs
└── util/                              # Utilities
    ├── diagnostic/
    ├── i18n/
    └── config/

Directory Mapping (old → new):

Old DirectoryNew LocationDescription
frontend/dag/frontend/core/typecheck/spawn_placement.rsspawn block DAG analysis integrated into type checking
codegen/middle/passes/codegen/Code generation moved to middle-end pass
embedded/backends/interpreter/Tree-walking interpreter
runtime/backends/runtime/Compiled VM runtime
vm/backends/interpreter/Merged with embedded
full/(not implemented)Full Runtime + work-stealing, future release
reflect/(not implemented)Reflection metadata, future release
core/backends/common/Shared values/heap/opcodes

Trade-offs

Advantages

  • Clear layering: Embedded / Standard / Full three tiers
  • Compilation reuse: Frontend code is completely shared
  • Generics decoupling: Compile-time polymorphism, zero overhead
  • Consistency: Synchronous is just a special case of scheduling
  • Embedded-friendly: High performance + low memory + fast startup

Disadvantages

  • Initial complexity: Need to define scheduler interface and multiple runtime variants
  • Compile-time binding: Scheduler type is determined at compile time

Design Decision Log

DecisionDecisionDate
Scheduler decouplingGenerics + injection2025-01-05
Single-threaded modeSynchronous is special case of scheduling2025-01-05
Async implementationDAG naturally supports it2025-01-05
WorkStealerFull Runtime advanced feature2025-01-05
Embedded designImmediate execution, no DAG scheduling2025-01-05
Compilation phaseAll runtimes share same frontend2025-01-05
Runtime layeringEmbedded / Standard / Full2025-01-05
Type constraintsRFC-011 already defines2025-01-25
Dependency graph constructionStatic dependency graph, determined at compile time2025-01-05
Dual backend modelVM (dev/debug) + LLVM AOT (prod), consistent behavior2026-05-11
Scheduler formStatic lib linked into exe, ~200-500KB, no GC2026-05-11
Reflection metadataCompiled into exe separate section, mmap on demand2026-05-11
Scheduler performanceDAG analysis completed at compile time, runtime only executes2026-05-11
DAG scope alignmentDAG analysis limited to spawn blocks, aligned with RFC-0242026-06-05
Three-tier architecture updateEmbedded has no spawn, Standard supports spawn2026-06-05

References


Lifecycle and Disposition

StatusLocationDescription
Draftdocs/design/rfc/Author draft
Under Reviewdocs/design/rfc/Open for community discussion
Accepteddocs/design/accepted/Official design document
Rejecteddocs/design/rfc/Retained in RFC directory