Introduction

Welcome to Circuit - a node-based runtime engine for building applications with computational blocks written in Rust that runs anywhere.

What is Circuit?

Circuit is a flexible, cross-platform runtime engine that allows you to create applications using a visual node-based architecture. Write your computational blocks in Rust once, and run them on Swift (iOS/macOS), Kotlin (Android), and Web/React platforms.

Key Features

  • Universal Blocks: Write computational blocks in Rust once, use everywhere
  • Declarative Language: Define blocks and flows using .block and .flow files
  • Cross-Platform: Run on iOS, macOS, Android, and Web (via WebAssembly)
  • Type-Safe: Strong typing with Rust's safety guarantees
  • Visual Flow: Node-based execution graphs for clear data flow
  • Extensible: Easy to add custom blocks for your use cases
  • Zero-Copy FFI: Efficient cross-language communication
  • Cycle Detection: Automatic validation of graph structures
  • Topological Execution: Optimized execution order
  • Comprehensive Testing: Unit, integration, and WASM tests included

Why Circuit?

Traditional cross-platform development often requires writing the same logic multiple times for different platforms. Circuit solves this by:

  1. Write Once, Run Anywhere: Write your computational blocks in Rust and automatically use them across all platforms
  2. Visual Programming: Define complex data flows using an intuitive node-based system
  3. Type Safety: Leverage Rust's type system for runtime safety across all platforms
  4. Performance: Compiled Rust code ensures optimal performance on every platform
  5. Maintainability: Update logic in one place and deploy everywhere

Use Cases

Circuit is perfect for:

  • Data Processing Pipelines: Transform and process data through reusable blocks
  • Business Logic: Share complex business rules across mobile and web
  • Machine Learning: Build ML inference pipelines that work everywhere
  • Game Logic: Create game mechanics once, deploy to all platforms
  • Computational Apps: Any application with complex computational requirements

Quick Example

Here's a simple calculator flow that demonstrates Circuit's declarative syntax:

flow calculator {
    description "Simple calculator: (5 + 3) * 2 = 16"

    node const5: core.constant { value = 5 }
    node const3: core.constant { value = 3 }
    node add: math.add
    node multiply: math.multiply

    connect const5.value -> add.a
    connect const3.value -> add.b
    connect add.result -> multiply.a
}

This flow creates a calculation graph that:

  1. Creates two constant values (5 and 3)
  2. Adds them together
  3. Multiplies the result by another value

Next Steps

Community and Support

Circuit is actively developed and welcomes contributions. If you encounter issues or have questions:

  • GitHub Issues: Report bugs or request features
  • Documentation: This comprehensive guide covers all aspects of Circuit
  • Examples: Check the examples/ directory for working code samples

Let's get started building with Circuit!

Quick Start

Get up and running with Circuit in minutes!

Prerequisites

Before you begin, ensure you have:

  • Rust installed (1.70 or later)
  • Basic familiarity with Rust syntax
  • A text editor or IDE

Installation

1. Install Rust

If you haven't already, install Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

2. Add Circuit to Your Project

Add Circuit to your Cargo.toml:

[dependencies]
circuit-core = { path = "../circuit-core" }
circuit-lang = { path = "../circuit-lang" }

Or clone the repository:

git clone https://github.com/blankly-app/circuit.git
cd circuit

Your First Program

Let's create a simple calculator using Circuit's Rust API:

use circuit_core::{blocks::*, *};
use std::sync::Arc;

fn main() -> Result<()> {
    // Create the engine
    let mut engine = Engine::new();

    // Register built-in blocks
    engine.register_block(Arc::new(AddBlock))?;
    engine.register_block(Arc::new(MultiplyBlock))?;
    engine.register_block(Arc::new(ConstantBlock))?;

    // Create a graph
    let mut graph = Graph::new(
        "calculator".to_string(),
        "Simple Calculator".to_string()
    );

    // Add nodes
    let const5 = graph.add_node(Node::new(
        "const5".to_string(),
        "core.constant".to_string(),
        vec![("value".to_string(), Value::Int(5))].into_iter().collect(),
    ));

    let const3 = graph.add_node(Node::new(
        "const3".to_string(),
        "core.constant".to_string(),
        vec![("value".to_string(), Value::Int(3))].into_iter().collect(),
    ));

    let add = graph.add_node(Node::new(
        "add".to_string(),
        "math.add".to_string(),
        HashMap::new(),
    ));

    // Connect nodes
    graph.connect(&const5, "value", &add, "a")?;
    graph.connect(&const3, "value", &add, "b")?;

    // Load and execute
    engine.load_graph(graph)?;
    let results = engine.execute_graph("calculator")?;

    println!("Result: {:?}", results);

    Ok(())
}

Run it:

cargo run

Using the Declarative Language

Circuit's declarative language makes it even easier. Create a file calculator.flow:

flow calculator {
    description "Simple calculator: (5 + 3) * 2 = 16"

    node const5: core.constant { value = 5 }
    node const3: core.constant { value = 3 }
    node const2: core.constant { value = 2 }

    node add: math.add
    node multiply: math.multiply

    connect const5.value -> add.a
    connect const3.value -> add.b
    connect add.result -> multiply.a
    connect const2.value -> multiply.b
}

Load and execute it:

use circuit_core::*;
use circuit_lang::{parse_flow, flow_to_graph};
use std::fs;

fn main() -> Result<()> {
    let mut engine = Engine::new();

    // Register blocks (as before)
    // ...

    // Load flow file
    let source = fs::read_to_string("calculator.flow")?;
    let flow = parse_flow(&source)?;
    let graph = flow_to_graph(&flow)?;

    engine.load_graph(graph)?;
    let results = engine.execute_graph("calculator")?;

    println!("Calculator result: {:?}", results);
    Ok(())
}

Running the Examples

Circuit comes with several example flows in the examples/ directory:

# Run the calculator example
cargo run --example calculator

# Run tests to see all examples in action
cargo test -p circuit-lang --test integration_tests

Next Steps

Now that you've seen the basics, explore:

Common Issues

Missing Blocks

If you see "Block not found" errors, make sure you've registered all the blocks you're using:

#![allow(unused)]
fn main() {
engine.register_block(Arc::new(AddBlock))?;
engine.register_block(Arc::new(MultiplyBlock))?;
// etc.
}

Connection Errors

Port names must match exactly. Check your block definitions for the correct input/output port names.

Parse Errors

When using .flow files, ensure:

  • Block types are registered before loading
  • Syntax matches the language specification
  • All connections reference valid ports

Installation

This guide covers installing Circuit for different platforms and use cases.

Rust Development

Prerequisites

  • Rust 1.70 or later
  • Cargo (comes with Rust)

Install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

Verify installation:

rustc --version
cargo --version

Using mise (optional/recommended)

If you prefer to use mise for developer tool/version management, you can use the repo-provided mise.toml and the rust-toolchain file to automatically install and pin the correct Rust version and components.

# Install mise (only if you don't have it)
curl https://mise.run | sh

# Activate mise in your shell (zsh example)
eval "$(~/.local/bin/mise activate zsh)"

# From the repository root, activate the Rust toolchain and install configured tools
cd circuit
mise use rust@1.90.0
mise install

# Verify cargo/rust version (runs inside mise environment)
mise x -- cargo --version

Note: .rust-toolchain.toml is also included and will ensure rustup uses the right channel and components if you're using rustup directly.

Clone Circuit

git clone https://github.com/blankly-app/circuit.git
cd circuit

Build All Packages

# Build all packages in release mode
cargo build --release

# Run tests to verify everything works
cargo test

# Run an example
cargo run --example calculator

Platform-Specific Installation

iOS/macOS (Swift)

Install Additional Targets

# For iOS devices (ARM64)
rustup target add aarch64-apple-ios

# For iOS Simulator (x86_64)
rustup target add x86_64-apple-ios

# For iOS Simulator (ARM64, M1/M2 Macs)
rustup target add aarch64-apple-ios-sim

# For macOS
rustup target add aarch64-apple-darwin  # Apple Silicon
rustup target add x86_64-apple-darwin   # Intel Macs

Build for iOS

# Build the FFI library for iOS
cargo build --release --target aarch64-apple-ios -p circuit-ffi

# The library will be at: target/aarch64-apple-ios/release/libcircuit_ffi.a

See the Swift Integration Guide for detailed Xcode setup.

Android (Kotlin)

Install NDK and Targets

  1. Install Android NDK via Android Studio or standalone

  2. Add Rust targets:

rustup target add aarch64-linux-android   # ARM64
rustup target add armv7-linux-androideabi # ARMv7
rustup target add i686-linux-android      # x86
rustup target add x86_64-linux-android    # x86_64

Configure Cargo for Android

Create or edit ~/.cargo/config.toml:

[target.aarch64-linux-android]
ar = "/path/to/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/path/to/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"

[target.armv7-linux-androideabi]
ar = "/path/to/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/path/to/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi30-clang"

Replace /path/to/ndk with your NDK installation path.

Build for Android

# Build for ARM64 (most common)
cargo build --release --target aarch64-linux-android -p circuit-ffi

# The library will be at: target/aarch64-linux-android/release/libcircuit_ffi.so

See the Kotlin Integration Guide for Android Studio setup.

Web/React (WebAssembly)

Install wasm-pack

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Or with cargo:

cargo install wasm-pack

Build WASM Package

cd circuit-wasm
wasm-pack build --target web --release

# Or for bundlers (webpack, vite, etc.)
wasm-pack build --target bundler --release

This creates a pkg/ directory with:

  • circuit_wasm_bg.wasm - The WebAssembly module
  • circuit_wasm.js - JavaScript bindings
  • circuit_wasm.d.ts - TypeScript definitions
  • package.json - NPM package manifest

Install in Your Web Project

cd your-web-project
npm install ../circuit/circuit-wasm/pkg

Or publish to NPM and install normally.

See the React Integration Guide for detailed web setup.

Development Tools

Optional: Install cargo-watch for Auto-rebuild

cargo install cargo-watch

Use it during development:

cargo watch -x test
cargo watch -x 'run --example calculator'

Optional: Install cargo-expand for Macro Debugging

cargo install cargo-expand

Useful for inspecting generated code:

cargo expand -p circuit-core

Optional: Install mdBook for Documentation

cargo install mdbook

Build and serve the documentation locally:

cd docs
mdbook serve --open

Verification

Run All Tests

cargo test --all

Build All Targets

# Native
cargo build --release

# iOS
cargo build --release --target aarch64-apple-ios -p circuit-ffi

# Android
cargo build --release --target aarch64-linux-android -p circuit-ffi

# WASM
cd circuit-wasm && wasm-pack build --target web

Run Examples

cargo run --example calculator

Troubleshooting

Linker Errors on macOS

If you encounter linker errors, ensure you have Xcode command line tools:

xcode-select --install

Android NDK Not Found

Ensure ANDROID_NDK_HOME is set:

export ANDROID_NDK_HOME=/path/to/android-ndk

Add to your .bashrc or .zshrc for persistence.

WASM Build Fails

Ensure you have the wasm32 target:

rustup target add wasm32-unknown-unknown

And install wasm-bindgen-cli:

cargo install wasm-bindgen-cli

Next Steps

Your First Flow

This tutorial will walk you through creating your first Circuit flow from scratch.

What You'll Build

We'll create a temperature converter that converts Celsius to Fahrenheit using the formula:

F = (C × 9/5) + 32

Step 1: Create the Flow File

Create a new file called temp_converter.flow:

flow temp_converter {
    description "Converts Celsius to Fahrenheit"

    // Input temperature in Celsius
    node celsius: core.constant {
        value = 25
    }

    // Constants for the formula
    node nine: core.constant {
        value = 9
    }

    node five: core.constant {
        value = 5
    }

    node thirtytwo: core.constant {
        value = 32
    }

    // Calculations: (C × 9 / 5) + 32
    node multiply: math.multiply
    node divide: math.multiply  // We'll use multiply with 1/5 = 0.2
    node add: math.add

    // Connect the graph
    connect celsius.value -> multiply.a
    connect nine.value -> multiply.b
    connect multiply.result -> divide.a
    connect five.value -> divide.b
    connect divide.result -> add.a
    connect thirtytwo.value -> add.b

    // Output the final result
    output add.result
}

Step 2: Understanding the Flow

Let's break down what this flow does:

  1. Constants: We define constant values for:

    • celsius (25) - the input temperature
    • nine (9) - part of the conversion formula
    • five (5) - part of the conversion formula
    • thirtytwo (32) - the offset in the formula
  2. Operations: We create three math operation nodes:

    • multiply - multiplies Celsius by 9
    • divide - divides by 5
    • add - adds 32
  3. Connections: We wire the nodes together to form the calculation pipeline

  4. Output: We expose the final result from the add node

Step 3: Load and Execute

Create a Rust program to execute the flow:

use circuit_core::*;
use circuit_core::blocks::*;
use circuit_lang::{parse_flow, flow_to_graph};
use std::sync::Arc;
use std::fs;

fn main() -> Result<()> {
    // Create engine and register blocks
    let mut engine = Engine::new();
    engine.register_block(Arc::new(AddBlock))?;
    engine.register_block(Arc::new(MultiplyBlock))?;
    engine.register_block(Arc::new(ConstantBlock))?;

    // Load the flow file
    let source = fs::read_to_string("temp_converter.flow")?;
    let flow = parse_flow(&source)?;
    let graph = flow_to_graph(&flow)?;

    // Execute
    let graph_id = graph.id.clone();
    engine.load_graph(graph)?;
    let results = engine.execute_graph(&graph_id)?;

    // Print results
    for (node_id, outputs) in results {
        println!("Node {}: {:?}", node_id, outputs);
    }

    Ok(())
}

Step 4: Run It

cargo run

You should see output showing the intermediate calculations and the final result (25°C = 77°F).

Step 5: Make It Interactive

Now let's make it easier to change the input. Modify your Rust program:

use std::io::{self, Write};

fn main() -> Result<()> {
    // ... (setup code as before)

    // Get temperature from user
    print!("Enter temperature in Celsius: ");
    io::stdout().flush()?;

    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let celsius: f64 = input.trim().parse()
        .map_err(|_| CircuitError::InvalidInput("Invalid number".to_string()))?;

    // Modify the constant node's configuration
    // (In a real app, you'd modify the graph before loading)

    // ... (execution code)

    Ok(())
}

Next Steps

Congratulations! You've created your first Circuit flow. Here's what to explore next:

Create Custom Blocks

Instead of using just constants and math operations, create custom blocks:

block conversion.celsius_to_fahrenheit {
    description "Converts Celsius to Fahrenheit"

    input celsius: Number {
        description "Temperature in Celsius"
    }

    output fahrenheit: Number {
        description "Temperature in Fahrenheit"
    }

    execute {
        fahrenheit = (celsius * 9 / 5) + 32
    }
}

Add More Features

Extend your temperature converter:

  1. Support both directions (C→F and F→C)
  2. Add Kelvin conversion
  3. Add input validation
  4. Create a visual display

Learn More About Flows

Try the Examples

Check out the example flows in examples/flows/:

cargo test -p circuit-lang --test integration_tests

Troubleshooting

"Block not found" error

Make sure all blocks used in your flow are registered:

#![allow(unused)]
fn main() {
engine.register_block(Arc::new(AddBlock))?;
engine.register_block(Arc::new(MultiplyBlock))?;
engine.register_block(Arc::new(ConstantBlock))?;
}

Connection errors

Verify port names match exactly. Use:

  • .value for constant block outputs
  • .a and .b for math block inputs
  • .result for math block outputs

Parse errors

Check your .flow syntax:

  • Node definitions need node id: type { config }
  • Connections need connect from.port -> to.port
  • All statements should be inside the flow { } block

Architecture Overview

Understanding Circuit's architecture will help you build better applications and debug issues more effectively.

High-Level Architecture

Circuit consists of four main layers:

┌─────────────────────────────────────────────────────┐
│         Platform Layer (Swift/Kotlin/JS)            │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────┐  │
│  │     iOS      │  │   Android    │  │   Web    │  │
│  └──────────────┘  └──────────────┘  └──────────┘  │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│          FFI/WASM Bindings Layer                    │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────┐  │
│  │ circuit-ffi  │  │ circuit-ffi  │  │circuit-  │  │
│  │   (Swift)    │  │  (Kotlin)    │  │  wasm    │  │
│  └──────────────┘  └──────────────┘  └──────────┘  │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│              Language Layer                         │
│              ┌──────────────┐                       │
│              │ circuit-lang │                       │
│              │   Parser &   │                       │
│              │  Converter   │                       │
│              └──────────────┘                       │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│               Core Runtime Engine                   │
│              ┌──────────────┐                       │
│              │ circuit-core │                       │
│              │   Engine,    │                       │
│              │  Blocks,     │                       │
│              │   Graphs     │                       │
│              └──────────────┘                       │
└─────────────────────────────────────────────────────┘

Core Components

1. circuit-core: The Runtime Engine

The heart of Circuit, circuit-core provides:

Blocks

The fundamental unit of computation. Each block:

  • Has inputs (data it receives)
  • Has outputs (data it produces)
  • Has configuration (parameters that customize behavior)
  • Implements an execute method (the actual computation)
#![allow(unused)]
fn main() {
pub trait Block: Send + Sync {
    fn metadata(&self) -> BlockMetadata;
    fn execute(&self, context: BlockContext) -> Result<HashMap<String, Value>>;
    fn validate(&self, _config: &HashMap<String, Value>) -> Result<()> { Ok(()) }
}
}

Graphs

A directed acyclic graph (DAG) representing data flow:

  • Nodes: Instances of blocks with unique IDs
  • Connections: Data paths between node ports
  • Cycle Detection: Ensures no circular dependencies
  • Validation: Checks that all connections are valid

Engine

The execution runtime that:

  1. Registers blocks (makes them available)
  2. Loads graphs (prepares them for execution)
  3. Executes graphs in topological order
  4. Routes data between connected nodes
  5. Returns final output values

Values

Type-safe data containers supporting:

  • Null, Bool, Int, Float, String
  • Array, Object (nested structures)
  • Bytes (binary data)
  • Conversion and serialization via serde

2. circuit-lang: The Declarative Language

Provides a human-friendly way to define blocks and flows:

Parser

Uses Pest grammar to parse .block and .flow files into AST (Abstract Syntax Tree).

Converter

Transforms AST into executable Graph objects that the engine can run.

Benefits

  • Declarative: Describe what you want, not how to build it
  • Readable: Clear syntax for non-programmers
  • Maintainable: Easy to modify flows without recompiling

3. circuit-ffi: Native Platform Bridge

C-compatible FFI layer for iOS and Android:

  • C API: Simple functions for create, load, execute, destroy
  • Memory Management: Safe handling of strings and pointers
  • Error Handling: Proper error propagation across FFI boundary
  • Thread Safety: Uses lazy_static for global engine registry

Swift and Kotlin wrappers provide idiomatic APIs on top of the C layer.

4. circuit-wasm: Web Assembly Bridge

WebAssembly bindings for browser and Node.js:

  • wasm-bindgen: Automatic JavaScript bindings
  • Zero-Copy: Efficient data transfer between JS and Rust
  • TypeScript: Full type definitions included
  • Promise-based: Async/await friendly API

Data Flow

Here's how data flows through a Circuit application:

1. Define Flow (.flow file or Rust code)
   │
   ├─> Defines nodes (block instances)
   ├─> Defines connections (data paths)
   └─> Defines configuration (block parameters)

2. Parse/Convert (circuit-lang)
   │
   └─> Converts to Graph object

3. Load Graph (Engine)
   │
   ├─> Validates graph structure
   ├─> Checks for cycles
   └─> Computes topological order

4. Execute Graph (Engine)
   │
   ├─> Executes nodes in topological order
   ├─> Routes data through connections
   └─> Collects final outputs

5. Return Results
   │
   └─> HashMap<String, HashMap<String, Value>>

Topological Execution

Circuit executes nodes in topological order, ensuring:

  1. A node only executes after all its dependencies
  2. Data flows in the correct direction
  3. No deadlocks or circular dependencies

Example

For this flow:

flow example {
    node a: core.constant { value = 1 }
    node b: core.constant { value = 2 }
    node c: math.add
    node d: math.multiply

    connect a.value -> c.a
    connect b.value -> c.b
    connect c.result -> d.a
}

Execution order:

  1. a and b execute first (no dependencies)
  2. c executes next (depends on a and b)
  3. d executes last (depends on c)

Memory Model

Rust Side

  • Blocks: Arc<dyn Block> - Thread-safe, shared references
  • Graphs: Owned by Engine, stored in HashMap
  • Values: Cloned when passed between nodes (cheap for small values)

FFI Boundary

  • Strings: Converted between Rust String and C char*
  • Pointers: Engine handles stored in global registry
  • Memory Safety: Rust manages all allocations

WASM Boundary

  • Values: Serialized as JSON across boundary
  • Optimization: Uses wasm-bindgen for efficient marshaling
  • Memory: WASM linear memory managed by Rust

Error Handling

Circuit uses Rust's Result type throughout:

#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, CircuitError>;

pub enum CircuitError {
    BlockNotFound(String),
    GraphNotFound(String),
    InvalidInput(String),
    InvalidConnection(String),
    CycleDetected,
    ExecutionError(String),
    ParseError(String),
}
}

Errors propagate through the stack and can be caught at the platform layer.

Performance Characteristics

  • Graph Loading: O(N + E) where N = nodes, E = edges
  • Topological Sort: O(N + E)
  • Execution: O(N × B) where B = average block execution time
  • Data Flow: Cloning overhead for Values (typically small)

Optimization Opportunities

  1. Lazy Evaluation: Only execute nodes needed for requested outputs
  2. Parallelization: Execute independent nodes concurrently
  3. Caching: Memoize block results for repeated inputs
  4. Streaming: Support streaming data for large datasets

These are future roadmap items.

Thread Safety

  • Engine: Not thread-safe; use one per thread or protect with Mutex
  • Blocks: Must be Send + Sync (thread-safe)
  • Graphs: Immutable after loading (safe to share)
  • Values: Cloned between nodes (no shared mutation)

Next Steps

Understanding Blocks

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Understanding Blocks in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Creating Flows

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Creating Flows in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

The Declarative Language

Circuit provides a declarative language for defining computational blocks and flow graphs through .block and .flow files.

Overview

The Circuit Language consists of two main file types:

  • .block files: Define reusable computational blocks (similar to function definitions)
  • .flow files: Define complete graphs/workflows that connect blocks together (similar to programs)

Both file types use a simple, readable syntax designed to be easy to write and maintain.

Why Use the Declarative Language?

While you can create blocks and graphs programmatically in Rust, the declarative language offers several advantages:

  1. Readability: Clear, self-documenting syntax
  2. Accessibility: Non-programmers can create and modify flows
  3. Rapid Prototyping: Quickly iterate on designs without recompiling
  4. Visual Mapping: Easy to visualize from the textual representation
  5. Separation of Concerns: Logic (blocks) separate from composition (flows)

File Structure

Block Files

block <qualified.name> {
    description "Human-readable description"

    input <name>: <Type> { ... }
    output <name>: <Type> { ... }
    config <name>: <Type> { ... }

    execute { ... }
}

Flow Files

flow <name> {
    description "Human-readable description"

    node <id>: <block.type> { ... }
    connect <from>.<port> -> <to>.<port>
    output <node>.<port>
}

Syntax Rules

  • Comments: Use // for single-line comments
  • Whitespace: Whitespace is flexible and ignored
  • Identifiers: Must start with a letter, can contain letters, numbers, and underscores
  • Qualified names: Use dots to create namespaces (e.g., math.advanced.fft)
  • Case sensitivity: All identifiers are case-sensitive

Best Practices

  1. Use descriptive names: Choose clear, self-documenting names for blocks, nodes, and ports
  2. Add descriptions: Always include descriptions for blocks and ports
  3. Provide defaults: Set sensible default values for optional inputs and config parameters
  4. Organize blocks: Use qualified names to organize blocks into namespaces (e.g., math., string., io.)
  5. Position nodes: Use position statements in flows for better visualization
  6. Comment your flows: Use the description field to explain what your flow does

Next Steps

Examples

Check out the example files in the repository:

  • examples/blocks/ - Example block definitions
  • examples/flows/ - Example flow definitions

Run the integration tests to see them in action:

cargo test -p circuit-lang --test integration_tests

Block Syntax

Block files define reusable computational units with inputs, outputs, configuration parameters, and execution logic.

Basic Structure

block <qualified.name> {
    description "Human-readable description"

    input <name>: <Type> {
        description "Input description"
        default = <value>
    }

    output <name>: <Type> {
        description "Output description"
    }

    config <name>: <Type> {
        description "Config parameter description"
        default = <value>
    }

    execute {
        // Execution statements
    }
}

Block Name

The block name must be a qualified identifier using dot notation:

block math.add { ... }
block string.format { ... }
block myapp.custom.processor { ... }

Best practices:

  • Use namespaces to organize related blocks
  • Keep names descriptive but concise
  • Use lowercase with dots for namespacing

Description

Optional but highly recommended:

block math.square {
    description "Squares a number (x²)"
    ...
}

Inputs

Define input ports that receive data:

input x: Number {
    description "The number to square"
}

input optional_value: Number {
    description "An optional input"
    default = 0
}

Properties:

  • name: Port identifier (required)
  • type: Data type (required)
  • description: Human-readable description (optional)
  • default: Default value if not connected (optional)

Outputs

Define output ports that produce data:

output result: Number {
    description "The squared result"
}

Properties:

  • name: Port identifier (required)
  • type: Data type (required)
  • description: Human-readable description (optional)

Config Parameters

Configuration values set at design-time:

config multiplier: Number {
    description "Multiplication factor"
    default = 1
}

Difference from inputs:

  • Config values are static (set when creating the node)
  • Inputs are dynamic (data flowing through the graph)

Execute Block

Contains the computation logic:

execute {
    result = x * x
}

Supported Operations

Assignment:

output = input
result = calculation

Arithmetic:

result = a + b
result = a - b
result = a * b
result = a / b

Comparisons:

equal = a == b
not_equal = a != b
greater = a > b
less = a < b

Logical:

and_result = a && b
or_result = a || b
not_result = !value

Conditionals:

if condition {
    output = value1
}

Function Calls:

result = pow(base, exponent)
result = sqrt(value)

Member Access:

result = object.property
result = object.method()

Complete Examples

Example 1: Simple Math Block

block math.square {
    description "Squares a number (x²)"

    input x: Number {
        description "The number to square"
    }

    output result: Number {
        description "The squared result"
    }

    execute {
        result = x * x
    }
}

Example 2: Block with Config

block math.power {
    description "Raises a base to an exponent"

    input base: Number {
        description "The base number"
    }

    config exponent: Number {
        description "The exponent"
        default = 2
    }

    output result: Number {
        description "The result of base^exponent"
    }

    execute {
        result = base * base
    }
}

Example 3: String Processing

block string.format {
    description "Formats a string with prefix and suffix"

    input template: String {
        description "The template string"
    }

    config prefix: String {
        description "Prefix to add"
        default = ""
    }

    config suffix: String {
        description "Suffix to add"
        default = ""
    }

    output result: String {
        description "The formatted string"
    }

    execute {
        result = prefix + template + suffix
    }
}

Example 4: Multiple Outputs

block logic.compare {
    description "Compares two numbers"

    input a: Number
    input b: Number

    output equal: Bool
    output greater: Bool
    output less: Bool

    execute {
        equal = a == b
        greater = a > b
        less = a < b
    }
}

Loading Block Definitions

Use the circuit-lang crate to parse block files:

#![allow(unused)]
fn main() {
use circuit_lang::parse_block;

let source = std::fs::read_to_string("math.square.block")?;
let block = parse_block(&source)?;

println!("Block: {}", block.name);
println!("Inputs: {}", block.inputs.len());
println!("Outputs: {}", block.outputs.len());
}

See Also

Flow Syntax

Flow files define complete computational graphs by instantiating blocks and connecting them together.

Basic Structure

flow <name> {
    description "Human-readable description"

    node <id>: <block.type> {
        <config_param> = <value>
        position(<x>, <y>)
    }

    connect <from_node>.<from_port> -> <to_node>.<to_port>

    output <node>.<port>
}

Flow Name

Simple identifier (not qualified like blocks):

flow calculator { ... }
flow data_pipeline { ... }
flow my_workflow { ... }

Description

Optional but recommended:

flow calculator {
    description "Simple calculator: (5 + 3) * 2 = 16"
    ...
}

Node Definitions

Each node is an instance of a block:

node <id>: <block.type> {
    <config_param> = <value>
    position(<x>, <y>)
}

Example:

node const5: core.constant {
    value = 5
    position(100, 100)
}

node add: math.add {
    position(250, 150)
}

Properties:

  • id: Unique identifier for this node instance
  • block.type: The qualified name of the block to instantiate
  • Configuration values are set inline
  • position(x, y) is optional (for visual layout)

Connections

Define data flow between nodes:

connect <from_node>.<from_port> -> <to_node>.<to_port>

Examples:

connect const5.value -> add.a
connect add.result -> multiply.a
connect input.data -> processor.input

Rules:

  • Port names must match the block's input/output definitions
  • Connections create directed edges in the graph
  • A single output can connect to multiple inputs
  • An input can only receive one connection

Output Declarations

Specify which node outputs are exposed as flow outputs:

output <node>.<port>

Example:

output multiply.result
output final_node.computed_value

Complete Examples

Example 1: Simple Calculator

flow calculator {
    description "Simple calculator: (5 + 3) * 2 = 16"

    node const5: core.constant {
        value = 5
        position(100, 100)
    }

    node const3: core.constant {
        value = 3
        position(100, 200)
    }

    node const2: core.constant {
        value = 2
        position(400, 150)
    }

    node add: math.add {
        position(250, 150)
    }

    node multiply: math.multiply {
        position(550, 150)
    }

    connect const5.value -> add.a
    connect const3.value -> add.b
    connect add.result -> multiply.a
    connect const2.value -> multiply.b

    output multiply.result
}

Example 2: Data Pipeline

flow data_pipeline {
    description "Process data through multiple stages"

    node input: core.constant {
        value = 10
    }

    node stage1: math.multiply {
        position(200, 100)
    }

    node stage2: math.add {
        position(400, 100)
    }

    node stage3: core.debug {
        position(600, 100)
    }

    connect input.value -> stage1.a
    connect stage1.result -> stage2.a
    connect stage2.result -> stage3.value

    output stage3.value
}

Example 3: String Processing

flow string_processing {
    description "Concatenates strings together"

    node hello: core.constant {
        value = "Hello"
    }

    node space: core.constant {
        value = " "
    }

    node world: core.constant {
        value = "World"
    }

    node concat1: string.concat
    node concat2: string.concat

    connect hello.value -> concat1.a
    connect space.value -> concat1.b
    connect concat1.result -> concat2.a
    connect world.value -> concat2.b

    output concat2.result
}

Loading and Executing Flows

Use the circuit-lang crate:

#![allow(unused)]
fn main() {
use circuit_core::*;
use circuit_lang::{parse_flow, flow_to_graph};
use std::fs;

// Parse flow file
let source = fs::read_to_string("calculator.flow")?;
let flow = parse_flow(&source)?;

// Convert to executable graph
let graph = flow_to_graph(&flow)?;

// Execute with engine
let graph_id = graph.id.clone();
engine.load_graph(graph)?;
let results = engine.execute_graph(&graph_id)?;
}

Graph Validation

Circuit automatically validates flows:

  • Cycle Detection: Ensures no circular dependencies
  • Port Validation: Checks that connected ports exist
  • Type Checking: Validates data type compatibility (future)

Position Coordinates

The position(x, y) directive is optional and used for visual graph editors:

node add: math.add {
    position(250, 150)
}
  • Coordinates are arbitrary (typically pixels)
  • Not required for execution
  • Helpful for visual tools and documentation

Common Patterns

Fan-out (One output → Many inputs)

node source: core.constant { value = 5 }
node consumer1: math.add
node consumer2: math.multiply

connect source.value -> consumer1.a
connect source.value -> consumer2.a

Sequential Processing

node step1: processor.first
node step2: processor.second
node step3: processor.third

connect step1.output -> step2.input
connect step2.output -> step3.input

Parallel Processing

node input: core.constant { value = 10 }
node pathA: math.add
node pathB: math.multiply

connect input.value -> pathA.a
connect input.value -> pathB.a

See Also

Type System

Circuit supports a rich type system for data flowing through graphs.

Supported Types

TypeDescriptionExample Values
NumberFloating-point numbers42, 3.14, -10.5
StringText strings"hello", "world"
BoolBoolean valuestrue, false
ArrayOrdered lists[1, 2, 3], ["a", "b"]
ObjectKey-value maps{"key": "value"}
BytesBinary dataRaw byte arrays
AnyAny typeAny of the above
NullNull valuenull

Value Literals

Null

null

Booleans

true
false

Numbers

Circuit uses 64-bit floating-point numbers internally:

42
3.14159
-10.5
0.0
1e6      // Scientific notation

Strings

Double-quoted text:

"hello world"
"multi word string"
"with \"escaped\" quotes"
"line 1\nline 2"   // Escape sequences

Arrays

Ordered collections of values:

[1, 2, 3]
["a", "b", "c"]
[true, false, true]
[]   // Empty array

Arrays can contain mixed types:

[1, "two", true, null]

Objects

Key-value pairs (like JSON objects):

{"name": "Alice", "age": 30}
{"x": 10, "y": 20}
{}   // Empty object

Nested Structures

Arrays and objects can be nested:

{
    "user": "bob",
    "scores": [10, 20, 30],
    "metadata": {
        "active": true,
        "level": 5
    }
}

Type Annotations

In block and flow definitions, specify types:

input count: Number
input name: String
input flag: Bool
input items: Array
input data: Object
input anything: Any

Rust Value Type

In Rust code, all values use the Value enum:

#![allow(unused)]
fn main() {
pub enum Value {
    Null,
    Bool(bool),
    Int(i64),
    Float(f64),
    String(String),
    Array(Vec<Value>),
    Object(HashMap<String, Value>),
    Bytes(Vec<u8>),
}
}

Creating Values

#![allow(unused)]
fn main() {
use circuit_core::Value;

let num = Value::Int(42);
let text = Value::String("hello".to_string());
let flag = Value::Bool(true);
let list = Value::Array(vec![
    Value::Int(1),
    Value::Int(2),
    Value::Int(3),
]);
let obj = Value::Object([
    ("key".to_string(), Value::String("value".to_string())),
].iter().cloned().collect());
}

Extracting Values

#![allow(unused)]
fn main() {
// Safe extraction with Option
if let Some(num) = value.as_int() {
    println!("Integer: {}", num);
}

if let Some(text) = value.as_str() {
    println!("String: {}", text);
}

// Check type
if value.is_null() {
    println!("Value is null");
}
}

Type Conversion

#![allow(unused)]
fn main() {
// Convert to JSON
let json = serde_json::to_string(&value)?;

// Parse from JSON
let value: Value = serde_json::from_str(&json)?;
}

Type Checking (Future)

Currently, Circuit performs minimal type checking. Future versions will include:

  • Compile-time type validation: Catch type errors before execution
  • Type inference: Automatically infer types from connections
  • Custom types: Define your own data structures
  • Generics: Blocks that work with multiple types

Best Practices

1. Use Specific Types

Prefer specific types over Any:

// Good
input count: Number

// Less good
input count: Any

2. Document Expected Formats

For Object types, document the expected structure:

input config: Object {
    description "Config object with fields: timeout (Number), retries (Number)"
}

3. Validate in Execute

Add validation in your block's execute method:

#![allow(unused)]
fn main() {
fn execute(&self, context: BlockContext) -> Result<HashMap<String, Value>> {
    let count = context.get_input("count")
        .and_then(|v| v.as_int())
        .ok_or_else(|| CircuitError::InvalidInput("count must be a number".into()))?;

    if count < 0 {
        return Err(CircuitError::InvalidInput("count must be positive".into()));
    }

    // ... rest of logic
}
}

4. Handle Null Values

Check for null before processing:

#![allow(unused)]
fn main() {
match context.get_input("optional_value") {
    Some(Value::Null) | None => {
        // Use default
    }
    Some(value) => {
        // Process value
    }
}
}

Type Coercion

Circuit performs automatic type coercion in some cases:

  • IntFloat: Automatic
  • FloatInt: Truncates (may lose precision)
  • BoolNumber: true = 1, false = 0
  • Any type → String: Via to_string()

Example:

#![allow(unused)]
fn main() {
// These are equivalent internally
Value::Int(42) == Value::Float(42.0)
}

JSON Compatibility

All Circuit values are JSON-compatible:

#![allow(unused)]
fn main() {
use serde_json;

let value = Value::Object(/* ... */);

// Serialize
let json = serde_json::to_string(&value)?;

// Deserialize
let value: Value = serde_json::from_str(&json)?;
}

This makes it easy to:

  • Store graphs and values
  • Transfer data across platforms
  • Integrate with web APIs

Platform-Specific Types

Swift

// Circuit values map to Swift types
Int64 -> Int
Double -> Double
String -> String
Array -> [Any]
Dictionary -> [String: Any]

Kotlin

// Circuit values map to Kotlin types
Int64 -> Long
Double -> Double
String -> String
Array -> List<Any>
Object -> Map<String, Any>

JavaScript/TypeScript

// Circuit values map to JS types
Int64 -> number
Float64 -> number
String -> string
Array -> Array<any>
Object -> Record<string, any>
Bool -> boolean
Null -> null

See Also

Built-in Blocks

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Built-in Blocks in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Creating Custom Blocks

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Creating Custom Blocks in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Using .block Files

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Using .block Files in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Using Rust Code

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Using Rust Code in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

The Graph Engine

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover The Graph Engine in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Values and Types

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Values and Types in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Error Handling

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Error Handling in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Platform Overview

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Platform Overview in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Swift (iOS/macOS)

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Swift (iOS/macOS) in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Kotlin (Android)

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Kotlin (Android) in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

React (Web)

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover React (Web) in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

WebAssembly

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover WebAssembly in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Building from Source

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Building from Source in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

FFI Integration

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover FFI Integration in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Performance Optimization

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Performance Optimization in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Testing Your Blocks

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Testing Your Blocks in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Cross-Compilation

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Cross-Compilation in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Calculator Example

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Calculator Example in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Data Pipeline

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Data Pipeline in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

String Processing

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover String Processing in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Core API

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Core API in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Language API

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Language API in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

WASM API

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover WASM API in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

FFI API

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover FFI API in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Generated Rust Docs

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Generated Rust Docs in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

How to Contribute

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover How to Contribute in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Development Setup

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Development Setup in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request

Code Style

Note: This section is under development. Check back soon for detailed documentation.

Overview

This page will cover Code Style in detail.

Coming Soon

Detailed documentation for this topic is being written.

In the Meantime

Contribute

Help us improve this documentation! If you'd like to contribute, please:

  1. Fork the repository
  2. Add your documentation
  3. Submit a pull request