Nectar Language Reference

This document is the reference for the Nectar programming language. It covers every language construct, from lexical structure to templates, with syntax, semantics, and examples.

Accuracy note: Features marked with (PLANNED) are parsed by the compiler but do not yet have working codegen or runtime support. Do not rely on them for production code. Everything else compiles to working WASM.


Table of Contents

  1. Lexical Structure
  2. Types
  3. Variables
  4. Functions
  5. Components
  6. Stores
  7. Structs and Enums
  8. Traits
  9. Expressions
  10. String Operations
  11. Array Operations and Iterators
  12. Error Handling (Result, Option, ?)
  13. Statements
  14. Patterns
  15. Modules
  16. Templates
  17. Agents
  18. Routers
  19. Contracts
  20. Pages
  21. Forms
  22. Channels
  23. Auth
  24. Payment
  25. Upload
  26. Db
  27. Cache
  28. Embed
  29. Pdf
  30. App (PWA)
  31. Theme
  32. Breakpoints
  33. Animations
  34. Testing

Lexical Structure

Comments

Nectar supports single-line comments with //:

// This is a comment
let x: i32 = 42; // inline comment

Keywords

The following identifiers are reserved keywords in Nectar:

Category Keywords
Declarations fn, component, struct, enum, impl, trait, store, agent, router, mod, use, pub, test, lazy
Variables let, mut, signal, own, ref
Control Flow if, else, match, for, in, while, return, yield
Async/Concurrency async, await, fetch, spawn, channel, select, parallel, stream, suspend
AI prompt, tool
Routing route, fallback, guard, navigate, layout, outlet
Components render, style, transition, animate
Accessibility a11y, manual, hybrid
Stores action, effect, computed
Domain Keywords page, form, field, contract, auth, payment, upload, db, cache, embed, pdf, app, theme, crypto
Domain Sub-blocks meta, schema, permissions, manifest, offline, push, query, mutation, gesture, breakpoint, fluid
Animations spring, keyframes, stagger
Error Handling try, catch
Testing assert, assert_eq, expect
Values true, false, self, Self
Types i32, i64, f32, f64, u32, u64, bool, String, secret
Other as, where, derive, Link, must_use, chunk, atomic, virtual
Component Blocks skeleton, error_boundary, Fragment (parsed as special identifiers, not reserved tokens)

Operators and Symbols

Symbol Meaning
+, -, *, /, % Arithmetic
==, !=, <, >, <=, >= Comparison
&&, \|\|, ! Logical
= Assignment
+=, -=, *=, /= Compound assignment
&, &mut Borrow / mutable borrow
-> Return type arrow
=> Fat arrow (match arms, routes)
:: Path separator
. Field access / method call
? Error propagation (try operator)
\| Closure parameter delimiter
, Separator
: Type annotation / key-value separator
; Statement terminator
( ), { }, [ ], < > Grouping / blocks / arrays / generics

Literals

Integers are written as decimal numbers and are typed as i64 by default:

let x = 42;
let y = -7;
let big = 1000000;

Floating-point numbers use a decimal point and are typed as f64 by default:

let pi = 3.14159;
let neg = -2.5;

Strings are double-quoted:

let greeting = "Hello, world!";

Format strings are prefixed with f and support {expression} interpolation:

let name = "Nectar";
let msg = f"Hello {name}, you have {count} messages";

Booleans:

let yes = true;
let no = false;

Lifetimes

Lifetimes are annotated with a leading apostrophe and are used in reference types and generic parameters:

fn first<'a>(items: &'a [i32]) -> &'a i32 {
    return items[0];
}

The special lifetime 'static denotes a reference that lives for the entire program duration.


Types

Nectar has a rich type system combining primitive types, compound types, and ownership-aware reference types.

Primitive Types

Type Description Size
i32 32-bit signed integer 4 bytes
i64 64-bit signed integer 8 bytes
u32 32-bit unsigned integer 4 bytes
u64 64-bit unsigned integer 8 bytes
f32 32-bit floating point 4 bytes
f64 64-bit floating point 8 bytes
bool Boolean (true/false) 1 byte
String UTF-8 string variable

Arrays

Arrays use bracket syntax and hold elements of a single type:

let numbers: [i32] = [1, 2, 3, 4, 5];
let names: [String] = ["Alice", "Bob"];
let empty: [f64] = [];

Tuples

Tuples combine a fixed number of values of potentially different types:

let pair: (i32, String) = (42, "hello");
let triple: (bool, f64, String) = (true, 3.14, "pi");

Tuple elements are accessed by index using .0, .1, etc.:

let x = pair.0;    // 42
let s = pair.1;    // "hello"

Option

Option<T> represents a value that may or may not be present:

let found: Option<User> = None;
let found: Option<User> = Some(user);

Result

Result<T, E> represents an operation that may succeed with T or fail with E:

fn parse(input: String) -> Result<i32, String> {
    // ...
}

Reference Types

References provide borrowed access to values without taking ownership:

let r: &i32 = &x;           // immutable borrow
let mr: &mut i32 = &mut x;  // mutable borrow
let lr: &'a i32 = &x;       // lifetime-annotated borrow
let lmr: &'a mut i32 = &mut x; // lifetime-annotated mutable borrow

Generic Types

Generic types are parameterized with angle brackets:

let items: Vec<i32> = vec_new();
let map: HashMap<String, User> = hash_map_new();

Function Types

Function types describe callable signatures:

let callback: fn(i32) -> bool = |x| x > 0;

Self and Self Type

Within impl blocks and component methods, self refers to the current instance and Self refers to the enclosing type.


Variables

Let Bindings

Variables are introduced with let. They are immutable by default:

let name = "Nectar";
let count: i32 = 0;

Mutable Variables

Add mut to make a variable mutable:

let mut counter: i32 = 0;
counter = counter + 1;

Signal Variables

Signals are reactive variables that automatically trigger re-renders when their value changes. They are used inside components and stores:

signal count: i32 = 0;
signal name: String = "default";

Type Annotations

Type annotations follow the variable name after a colon. They are optional when the type can be inferred:

let x: i32 = 42;      // explicit type
let y = 42;            // inferred as i64
let z: f64 = 3.14;    // explicit type

Ownership

Nectar uses an ownership system inspired by Rust. Every value has a single owner, and ownership can be transferred (moved) or borrowed:

let a = "hello";
let b = a;           // a is moved to b; a can no longer be used

let c = "world";
let d = &c;          // d borrows c immutably; c is still valid
let e = &mut c;      // e borrows c mutably; no other borrows allowed

The own keyword can explicitly mark owned transfer:

let data = own create_data();

Destructuring

Destructuring is supported in match arms for tuples, structs, and arrays:

// Tuple destructuring in match
match get_point() {
    (0, 0) => { /* origin */ },
    (x, y) => { /* use x, y */ },
}

// Struct destructuring in match
match user {
    User { name, age, .. } => { /* use name, age */ },
}

// Array destructuring in match
match items {
    [first, second, ..] => { /* use first, second */ },
}

Note: let-binding destructuring (let (x, y) = expr;) is parsed but not yet in codegen. Use match arms for destructuring.


Functions

Basic Functions

Functions are declared with the fn keyword:

fn greet(name: String) -> String {
    return f"Hello, {name}!";
}

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

Visibility

Functions can be made public with pub:

pub fn api_handler(request: Request) -> Response {
    // accessible from other modules
}

Async Functions (PLANNED)

Prefix fn with async for asynchronous functions. Parsed by the compiler but no async runtime exists yet:

async fn fetch_data(url: String) -> String {
    let response = await fetch(url);
    return response.json();
}

Generic Functions

Functions can have type parameters. The compiler monomorphizes generic functions – specializing them for each concrete type they are called with:

fn identity<T>(value: T) -> T {
    return value;
}

fn first<'a, T>(items: &'a [T]) -> &'a T {
    return items[0];
}

When called with identity(42), the compiler generates identity__i32. When called with identity("hello"), it generates identity__String.

Where Clauses (Trait Bounds)

Constrain type parameters with where:

fn print_all<T>(items: [T]) where T: Display {
    for item in items {
        println(item.to_string());
    }
}

Self Parameters

Methods take self as their first parameter, with optional borrowing:

fn method(self)              // takes ownership
fn method(&self)             // immutable borrow
fn method(&mut self)         // mutable borrow

Return Type

The return type follows ->. Functions without an explicit return type return the unit type (). The last expression in a function body is implicitly returned:

fn double(x: i32) -> i32 {
    x * 2   // implicit return
}

Components

Components are first-class UI primitives in Nectar. They combine state, behavior, and rendering into a single declaration.

Basic Component

component Hello(name: String) {
    render {
        <div>
            <h1>"Hello from Nectar!"</h1>
            <p>{name}</p>
        </div>
    }
}

Props

Props are declared as parameters in parentheses after the component name. They are immutable by default and can have default values:

component Button(label: String, disabled: bool = false) {
    render {
        <button disabled={disabled}>{label}</button>
    }
}

State (let)

Local state is declared with let or let mut inside the component body:

component Counter(initial: i32) {
    let mut count: i32 = initial;

    // ...
}

Reactive State (signal)

Signals are reactive state variables that automatically update the DOM when changed:

component UserProfile(id: String) {
    signal user_name: String = "Loading...";

    // When user_name changes, the DOM updates automatically
    render {
        <span>{self.user_name}</span>
    }
}

Methods

Components can define methods for event handling and business logic:

component Counter(initial: i32) {
    let mut count: i32 = initial;

    fn increment(&mut self) {
        self.count = self.count + 1;
    }

    fn decrement(&mut self) {
        self.count = self.count - 1;
    }

    render {
        <div>
            <span>{self.count}</span>
            <button on:click={self.increment}>"+1"</button>
            <button on:click={self.decrement}>"-1"</button>
        </div>
    }
}

Scoped Styles

CSS styles are scoped to the component automatically. Styles never leak to parent or sibling components:

component Card() {
    style {
        .card {
            padding: "16px";
            border-radius: "8px";
            box-shadow: "0 2px 8px rgba(0,0,0,0.1)";
        }
        .card h2 {
            color: "#1e293b";
            margin-bottom: "8px";
        }
    }

    render {
        <div class="card">
            <h2>"My Card"</h2>
        </div>
    }
}

Canvas Mode Styles (Honeycomb)

In canvas mode (nectar build --canvas), CSS is not used. Instead, Honeycomb’s layout engine uses typed style properties set as inline attributes:

<div style="direction: vertical; padding: 16px; gap: 12px; background-color: #18181b;">
    <span style="font-size: 15px; font-weight: 600; color: #fafafa;">"Title"</span>
    <button style="width: fill; height: 44px; background-color: #ffa11e; border-radius: 8px;">
        "Click me"
    </button>
</div>

Canvas layout properties: direction (vertical/horizontal/layer), width, height, padding, gap, align, justify, wrap, min-width, max-width, min-height, max-height, scroll, position, z-index, display

Canvas visual properties: background-color, color, font-size, font-weight, line-height, border-radius, border-width, border-color, opacity, text-decoration, text-overflow

Canvas sizing values: fill (stretch to parent), hug (shrink to content), Npx (fixed pixels)

Key differences from CSS: no display: flex (everything is flex by default), no margin (use padding + gap), no CSS grid (use wrap: true), no media queries, no pseudo-classes. See CLAUDE.md for the full comparison table.

Critical Styles

When building with nectar build --ssr --critical-css, the compiler automatically determines which component styles are critical (needed for the initial above-the-fold render) and which can be deferred.

By default, all non-lazy component styles are treated as critical. Lazy component styles are deferred unless the component is the first route target in a router.

The following built-in utility classes are always inlined as critical CSS:

These can be used directly in component templates to provide instant loading feedback during SSR hydration:

component UserProfile(id: u32) {
    state user: Option<User> = None;

    render {
        <div class="profile">
            {match self.user {
                Some(u) => <span>{u.name}</span>,
                None => <div class="nectar-skeleton nectar-skeleton-text" />,
            }}
        </div>
    }
}

Transitions

Declare CSS transitions on component properties:

component FadeBox() {
    transition {
        opacity: "0.3s ease";
        transform: "0.5s cubic-bezier(0.4, 0, 0.2, 1)";
    }

    render {
        <div class="fade-box">"Content"</div>
    }
}

Error Boundaries

Error boundaries catch rendering errors and display fallback UI:

component SafeWidget() {
    error_boundary {
        fallback {
            <div class="error">"Something went wrong."</div>
        }
        {
            <RiskyComponent />
        }
    }

    render {
        <div>"Widget content"</div>
    }
}

Skeleton Screens

Skeleton screens define placeholder UI that renders immediately (including during SSR) while the component’s data is loading. The skeleton block is shown first and automatically replaced with the real render content once the component’s signals change from their initial values.

component UserProfile(id: u32) {
    signal user: Option<User> = None;

    skeleton {
        <div class="skeleton">
            <div class="skeleton-avatar" />
            <div class="skeleton-line" style="width: 60%" />
            <div class="skeleton-line" style="width: 40%" />
        </div>
    }

    render {
        <div class="profile">
            <img src={self.user.avatar} />
            <h1>{self.user.name}</h1>
        </div>
    }
}

How it works:

Skeleton blocks are optional. Components without a skeleton block render their render content immediately as before.

Generic Components

Components can accept type parameters with optional trait bounds. Generic monomorphization applies:

component List<T>(items: [T]) where T: Display {
    render {
        <ul>
            {for item in items {
                <li>{item.to_string()}</li>
            }}
        </ul>
    }
}

Accessibility (a11y)

By default, all components get automatic accessibility support — the compiler injects ARIA attributes, roles, keyboard handlers, and focus styles.

// Default: a11y auto (compiler generates everything)
component SearchBox(placeholder: String) {
    render {
        <input type="text" placeholder={placeholder} />
    }
}

// Opt out entirely
component CustomWidget() {
    a11y manual;
    render {
        <div role="slider" aria-valuenow="50" tabindex="0">
            // Developer handles all a11y
        </div>
    }
}

// Hybrid: developer overrides specific attrs, compiler fills the rest
component ToggleButton(active: bool) {
    a11y hybrid;
    render {
        <button aria-pressed={active}>
            // Compiler auto-adds focus styles, keyboard handling
        </button>
    }
}

Layout Primitives

Language-level layout constructs that compile to semantic HTML + CSS at build time. Zero runtime cost — pure compile-time sugar.

component Dashboard() {
    render {
        <Stack gap="24">
            <Row gap="16" align="center">
                <h1>"Dashboard"</h1>
                <Button label="Refresh" />
            </Row>
            <Grid cols="3" gap="16">
                <Card title="Users" />
                <Card title="Revenue" />
                <Card title="Orders" />
            </Grid>
            <Sidebar side="left" width="250">
                <NavMenu />
                <MainContent />
            </Sidebar>
        </Stack>
    }
}

Available layout primitives:

Primitive Compiles to Attributes
<Stack> <section> with flex-direction:column gap
<Row> <div> with flex-direction:row gap, align
<Grid> <div> with display:grid cols, rows, gap
<Center> <div> with margin:0 auto max_width
<Cluster> <div> with flex-wrap:wrap gap
<Sidebar> <div> with flex + sidebar sizing side, width
<Switcher> <div> with flex-wrap + threshold threshold

Lazy Components

Lazy components are only loaded when first rendered, enabling code splitting:

lazy component HeavyChart(data: [f64]) {
    render {
        <canvas />
    }
}

Stores

Stores provide global reactive state management, similar to Redux/Flux patterns. Any component can read from and dispatch actions to a store.

Basic Store

store CounterStore {
    signal count: i32 = 0;
    signal step: i32 = 1;

    action increment(&mut self) {
        self.count = self.count + self.step;
    }

    action decrement(&mut self) {
        self.count = self.count - self.step;
    }

    computed double_count(&self) -> i32 {
        self.count * 2
    }

    effect on_count_change(&self) {
        println(self.count);
    }
}

Signal Fields

Store state is declared with signal. These are reactive: any component reading a signal will automatically re-render when it changes.

signal count: i32 = 0;
signal user: Option<User> = None;

Actions

Actions are methods that mutate store state. They can be synchronous or asynchronous:

// Synchronous action
action increment(&mut self) {
    self.count = self.count + 1;
}

// Async action
async action fetch_user(&mut self, id: u32) {
    let response = await fetch(f"https://api.example.com/users/{id}");
    self.user = response.json();
}

Computed Values

Computed values are derived from signals. They are cached and only recompute when their dependencies change:

computed is_logged_in(&self) -> bool {
    match self.status {
        AuthStatus::LoggedIn(_) => true,
        _ => false,
    }
}

Effects

Effects are side-effect callbacks that run whenever their signal dependencies change:

effect on_auth_change(&self) {
    match self.status {
        AuthStatus::LoggedIn(user) => {
            println(f"User logged in: {user.name}");
        }
        _ => {}
    }
}

Using Stores from Components

Components access store state and dispatch actions using the StoreName:: syntax:

component Dashboard() {
    render {
        <div>
            <p>{f"Count: {CounterStore::get_count()}"}</p>
            <button on:click={CounterStore::increment}>"+"</button>
        </div>
    }
}

Structs and Enums

Struct Definition

Structs group named fields together:

struct User {
    id: u32,
    name: String,
    email: String,
}

pub struct Point<T> {
    pub x: T,
    pub y: T,
}

Fields can be marked pub for public visibility. Structs support lifetimes and generic type parameters:

struct Ref<'a, T> {
    value: &'a T,
}

Struct Initialization

Create struct instances with field-value syntax:

let user = User {
    id: 1,
    name: "Alice",
    email: "alice@example.com",
};

Enum Definition

Enums define a type that can be one of several variants. Variants may carry data:

enum Filter {
    All,
    Active,
    Completed,
}

enum AuthStatus {
    LoggedOut,
    Loading,
    LoggedIn(User),
    Error(String),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Impl Blocks

Add methods to structs and enums with impl:

impl User {
    fn full_name(&self) -> String {
        return f"{self.first_name} {self.last_name}";
    }

    pub fn new(name: String, email: String) -> Self {
        return User { id: 0, name: name, email: email };
    }
}

Trait Implementations

Implement traits for types with impl Trait for Type:

impl Display for User {
    fn to_string(&self) -> String {
        return f"User({self.name})";
    }
}

Traits

Trait Definition

Traits define shared behavior (interfaces). Methods can have default implementations:

trait Display {
    fn to_string(&self) -> String;
}

trait Drawable {
    fn draw(&self);

    fn bounds(&self) -> (f64, f64) {
        // default implementation
        return (0.0, 0.0);
    }
}

Generic Traits

Traits can have type parameters:

trait Container<T> {
    fn get(&self, index: i32) -> T;
    fn size(&self) -> i32;
}

Trait Bounds

Use trait bounds to constrain generic type parameters:

fn print_item<T>(item: T) where T: Display {
    println(item.to_string());
}

Trait dispatch is static – the compiler resolves the concrete implementation at compile time. There is no vtable or dynamic dispatch.


Expressions

Nectar is expression-oriented. Most constructs produce a value.

Arithmetic Expressions

let sum = a + b;
let diff = a - b;
let product = a * b;
let quotient = a / b;
let remainder = a % b;
let negated = -x;

Comparison Expressions

a == b    // equal
a != b    // not equal
a < b     // less than
a > b     // greater than
a <= b    // less or equal
a >= b    // greater or equal

Logical Expressions

a && b    // logical AND
a || b    // logical OR
!a        // logical NOT

Assignment Expressions

x = 42;
x += 1;     // desugars to x = x + 1
x -= 1;
x *= 2;
x /= 2;

Field Access and Method Calls

user.name              // field access
user.full_name()       // method call
items.len()            // method call
items.push(42)         // method call with argument

Function Calls

greet("Alice")
add(1, 2)
Module::function(arg)

Index Expressions

items[0]
matrix[i][j]

If/Else Expressions

if/else is an expression that produces a value:

let max = if a > b { a } else { b };

if condition {
    do_something();
}

if x > 0 {
    "positive"
} else {
    "non-positive"
}

Match Expressions

Pattern matching with match:

match status {
    AuthStatus::LoggedIn(user) => show_dashboard(user),
    AuthStatus::Loading => show_spinner(),
    AuthStatus::Error(msg) => show_error(msg),
    _ => show_login(),
}

For Loops

Iterate over collections:

for item in items {
    process(item);
}

for todo in &mut self.todos {
    if todo.id == id {
        todo.done = !todo.done;
    }
}

Iterate over integer ranges:

for i in 0..10 {
    process(i);   // i = 0, 1, 2, ..., 9
}

While Loops

while count < 10 {
    count = count + 1;
}

Break and Continue

break exits the current loop, continue skips to the next iteration:

for item in items {
    if item.skip {
        continue;
    }
    if item.done {
        break;
    }
    process(item);
}

Both compile to WASM br instructions targeting the correct block/loop label.

Closures

Closures (lambdas) are used primarily as arguments to map/filter/reduce:

// With type annotations
let add = |a: i32, b: i32| a + b;

// Without type annotations
let double = |x| x * 2;

// Block body
let process = |item: Item| {
    validate(item);
    save(item);
};

Closures can also be written with fn syntax in certain positions:

items.filter(fn(t: &Todo) -> bool { !t.done })

Closures with environment capture compile to WASM functions in the function table. They work for map/filter/reduce callbacks and event handlers.

Await Expressions (PLANNED)

Await an asynchronous operation. Parsed but no async runtime:

let response = await fetch("https://api.example.com/data");
let data = await process(response);

Fetch Expressions

First-class HTTP communication:

// Simple GET
let response = fetch("https://api.example.com/users");

// With options
let response = fetch("https://api.example.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: json_string,
});

Spawn and Channel Expressions

Concurrency primitives backed by Web Workers and MessageChannel:

// Spawn work on a background thread
spawn {
    heavy_computation()
}

// Create a typed channel
let ch = channel<i32>();

// Send and receive
ch.send(42);
let value = ch.recv();

Parallel Expressions

Run multiple expressions concurrently via Web Workers:

parallel {
    fetch_users(),
    fetch_posts(),
    fetch_comments(),
}

Try/Catch Expressions

Structured error handling:

try {
    let data = parse(input)?;
    process(data);
} catch err {
    log_error(err);
}

Error Propagation (? Operator)

The ? postfix operator unwraps a Result or Option, propagating the error on failure:

fn load_config() -> Result<Config, String> {
    let text = read_file("config.toml")?;
    let config = parse_toml(text)?;
    return Ok(config);
}

Programmatic client-side navigation:

navigate("/user/42");
navigate(f"/posts/{post_id}");

Stream Expressions

Process async data as it arrives via streaming fetch:

for chunk in stream fetch("https://api.example.com/stream") {
    process_chunk(chunk);
}

Suspend Expressions

Show fallback content while loading:

suspend(<LoadingSpinner />) {
    <HeavyComponent />
}

Animate Expressions

Trigger a named animation imperatively:

animate(element, "fadeIn");

Format Strings

Interpolate expressions into strings:

let msg = f"Hello {name}, you have {count} items";
let url = f"https://api.example.com/users/{id}";

Prompt Templates

AI prompt templates with interpolation. The codegen builds the interpolated string and triggers a fetch:

let p = prompt "Summarize the following document: {document}";

Struct Initialization

Construct struct instances inline:

let user = User { name: "Alice", age: 30 };

Borrow and Mutable Borrow

let r = &value;         // immutable borrow
let mr = &mut value;    // mutable borrow

Block Expressions

Blocks are expressions that evaluate to their last expression:

let result = {
    let x = compute();
    let y = transform(x);
    x + y
};

String Operations

Nectar strings support built-in methods that compile directly to WASM. No imports needed.

Length

let s = "hello";
let n = s.len();  // 5

Case Conversion

let upper = s.to_upper();  // "HELLO"
let lower = s.to_lower();  // "hello"

Trimming

let trimmed = s.trim();  // removes leading/trailing whitespace

Splitting

let parts = s.split(",");  // returns [String]

Contains

let has = s.contains("ell");  // true

String Formatting

Use format() to convert values to strings:

let text = format("{}", 42);        // "42"
let msg = format("count: {}", n);   // "count: 5"

Or use format string literals:

let msg = f"Hello {name}, count is {count}";

Note: format() is the more battle-tested path for int-to-string conversion. Format strings (f"...") are parsed and have codegen but are less proven.


Array Operations and Iterators

Array Literals and Indexing

let items = [1, 2, 3, 4, 5];
let first = items[0];         // 1
let third = items[2];         // 3

Length and Push

let n = items.len();          // 5
items.push(6);                // appends to array

Contains

let has = items.contains(3);  // true

Map

Transform each element, returning a new array:

let doubled = items.map(|x| x * 2);
// or with explicit types:
let doubled = items.map(fn(x: i32) -> i32 { x * 2 });

Filter

Select elements matching a predicate:

let evens = items.filter(|x| x % 2 == 0);
let active = todos.filter(fn(t: &Todo) -> bool { !t.done });

Reduce

Fold elements into a single value:

let sum = items.reduce(0, |acc, x| acc + x);

Range Expressions

Range expressions produce integer sequences for use in for loops:

for i in 0..10 {
    // i goes from 0 to 9 (exclusive end)
    process(i);
}

for i in start..end {
    // dynamic range
}

Lazy For Loops

For large collections, lazy for renders only the first 20 items initially, then uses IntersectionObserver to load more as the user scrolls:

{lazy for item in self.items {
    <div>{item.name}</div>
}}

This compiles to a batch function in the WASM function table that renders the next 20 items each time the sentinel element becomes visible.


Error Handling

Result Type

Result<T, E> represents an operation that may succeed or fail:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn parse(input: String) -> Result<i32, String> {
    // ...
}

Option Type

Option<T> represents a value that may or may not be present:

let found: Option<User> = None;
let found: Option<User> = Some(user);

The ? Operator

The ? postfix operator unwraps a Result or Option, propagating the error on failure:

fn load_config() -> Result<Config, String> {
    let text = read_file("config.toml")?;
    let config = parse_toml(text)?;
    return Ok(config);
}

Try/Catch

Structured error handling:

try {
    let data = parse(input)?;
    process(data);
} catch err {
    log_error(err);
}

Note: Try/catch has limited codegen. The ? operator is more reliable.

Pattern Matching on Result/Option

match result {
    Ok(value) => use_value(value),
    Err(msg) => show_error(msg),
}

match maybe_user {
    Some(user) => show_profile(user),
    None => show_login(),
}

Note: Matching on Ok/Err/Some/None works well. Custom enum variant matching is more limited.


Statements

Let Statements

Bind a value to a name:

let x = 42;
let mut name: String = "Nectar";
let (a, b) = get_pair();           // PLANNED: tuple destructuring not yet in codegen
let User { name, email, .. } = user; // PLANNED: struct destructuring not yet in codegen

Signal Statements

Declare a reactive signal:

signal count: i32 = 0;
signal visible: bool = true;

Return Statements

Exit a function with an optional value:

return;
return 42;
return Ok(result);

Yield Statements (PLANNED)

Emit a value from a stream. Parsed but no generator runtime:

yield chunk;
yield f"data: {value}\n";

Expression Statements

Any expression can appear as a statement. A trailing semicolon is optional:

process(data);
self.count = self.count + 1;

Patterns

Patterns are used in match arms, let destructuring, and for bindings.

Wildcard Pattern

Matches anything, ignores the value:

_ => default_action(),

Identifier Pattern

Binds the matched value to a name:

x => use_value(x),

Literal Pattern

Matches a specific value:

42 => handle_forty_two(),
"hello" => handle_greeting(),
true => handle_true(),

Variant Pattern

Matches an enum variant, optionally binding inner fields:

Some(value) => use_value(value),
AuthStatus::LoggedIn(user) => show_user(user),
None => show_empty(),

Tuple Pattern

Destructure a tuple in match arms:

match point {
    (0, 0) => handle_origin(),
    (x, _) => use_x_only(x),
}

Struct Pattern

Destructure a struct in match arms, with an optional .. to ignore remaining fields:

match user {
    User { name, age, .. } => greet(name, age),
}

Array Pattern

Destructure an array in match arms:

match items {
    [first, second, ..] => process(first, second),
}

Note: let-binding destructuring (let (x, y) = expr;, let User { name, .. } = user;) is parsed but not yet in codegen. Use match arms for destructuring.


Modules

Module Declaration

Declare an external module (loaded from a separate file):

mod utils;          // loads ./utils.nectar or ./utils/mod.nectar
mod networking;     // loads ./networking.nectar

Declare an inline module:

mod helpers {
    pub fn capitalize(s: String) -> String {
        // ...
    }
}

Use/Import

Import items from other modules:

// Import a single item
use std::string;

// Import with alias
use http::Client as HttpClient;

// Glob import (all public items)
use utils::*;

// Group import
use std::{string, collections, io};

// Group import with aliases
use models::{User, Post as BlogPost};

Visibility

Items are private by default. Mark them pub for public access:

pub struct User { ... }
pub fn create_user(...) { ... }

struct Internal { ... }  // private

Templates

Templates are the JSX-like rendering syntax used in component render blocks.

Elements

HTML elements with static attributes:

<div class="container">
    <h1>"Title"</h1>
    <p>"Paragraph text"</p>
</div>

Self-Closing Elements

<input placeholder="Enter text" />
<br />
<NavBar />

Static Attributes

String-valued attributes:

<div class="card" id="main">
<input type="text" placeholder="Search..." />

Dynamic Attributes

Expression-valued attributes use curly braces:

<div class={dynamic_class}>
<span>{self.count}</span>
<img src={image_url} />

Event Handlers

Event handlers use the on:event syntax:

<button on:click={self.handle_click}>"Click me"</button>
<input on:submit={self.handle_submit} />
<div on:mouseover={self.show_tooltip} />

Two-Way Bindings

The bind:property syntax creates two-way data binding between a signal and a form element:

<input bind:value={search_query} />
<input type="checkbox" bind:checked={is_active} />

Note: Parsed and codegen exists, but less battle-tested than on:click handlers.

ARIA Attributes

Accessibility attributes are first-class:

<button aria-label="Close dialog" aria-expanded={is_open}>
<nav aria-hidden="true">
<div aria-live="polite" aria-describedby="description">

Role Attributes

<div role="button" tabindex="0">
<nav role="navigation">

Text Content

Text content is written as string literals inside elements:

<p>"This is text content."</p>

Expression Interpolation

Expressions inside curly braces render their value:

<span>{self.count}</span>
<p>{f"Total: {items.len()} items"}</p>

Conditional Rendering

{if self.loading {
    <div>"Loading..."</div>
}}

{if show_details {
    <Details data={self.data} />
} else {
    <Summary />
}}

List Rendering

{for item in self.items {
    <li>{item.name}</li>
}}

{for post in PostService::get_posts() {
    <article>
        <h3>{post.title}</h3>
        <p>{post.body}</p>
    </article>
}}

Lazy List Rendering

For large collections, lazy for renders only the first 20 items initially and uses IntersectionObserver to load more as the user scrolls:

{lazy for item in self.items {
    <div class="card">{item.name}</div>
}}

The compiler emits a batch function in the WASM function table that renders the next 20 items each time a sentinel element becomes visible. Range-based lazy for (lazy for i in 0..1000) is also supported.

Match in Templates

{match status {
    Some(err) => <div class="error">{err.message}</div>,
    None => <span />,
}}

The <Link> element provides client-side navigation:

<Link to="/">"Home"</Link>
<Link to="/about">"About"</Link>
<Link to={f"/user/{id}"}>"Profile"</Link>

Fragment

Group multiple elements without an extra wrapper node:

<Fragment>
    <h1>"Title"</h1>
    <p>"Content"</p>
</Fragment>

Child Components

Render other components as elements:

<NavBar />
<Counter initial={0} />
<UserCard user={current_user} />

Agents

Agents are first-class constructs for building AI-powered interactions. They wrap LLM communication with tool definitions and reactive UI.

Agent Definition

agent ChatBot {
    prompt system = "You are a helpful coding assistant.";

    signal messages: [Message] = [];
    signal input: String = "";
    signal streaming: bool = false;

    tool search_docs(query: String) -> String {
        let results = await fetch(f"https://api.example.com/search?q={query}");
        return results.json().summary;
    }

    tool run_code(language: String, code: String) -> String {
        let result = await fetch("https://api.example.com/execute", {
            method: "POST",
            body: { language: language, code: code },
        });
        return result.json().output;
    }

    fn send(&mut self) {
        let msg = Message { role: "user", content: self.input };
        self.messages.push(msg);
        self.input = "";
        self.streaming = true;
        ai::chat_stream(self.messages, self.tools);
    }

    render {
        <div class="chat">
            <div class="messages">
                {for msg in self.messages {
                    <div class={msg.role}>{msg.content}</div>
                }}
            </div>
            <input value={self.input} on:submit={self.send} />
        </div>
    }
}

System Prompt

Define the AI’s system prompt:

prompt system = "You are a helpful assistant specializing in data analysis.";

Tools

Tools are functions the AI can call. They have typed parameters and return types:

tool get_weather(city: String) -> String {
    let result = await fetch(f"https://api.example.com/weather?city={city}");
    return result.json().forecast;
}

Routers

Routers map URL paths to components for client-side navigation.

Router Definition

router AppRouter {
    route "/" => Home,
    route "/about" => About,
    route "/user/:id" => UserProfile,
    route "/admin/*" => AdminPanel guard { AuthStore::is_logged_in() },
    fallback => NotFound,
}

Route Patterns

Route Guards

Guards are expressions that must evaluate to true for the route to be accessible:

route "/admin/*" => AdminPanel guard { AuthStore::is_logged_in() },

Fallback Route

The fallback component renders when no route matches:

fallback => NotFound,

Router Layouts

Persistent layout shells where only the outlet content swaps on navigation:

router AppRouter {
    layout {
        <Stack>
            <NavBar />
            <Outlet />
            <Footer />
        </Stack>
    }

    route "/" => Home,
    route "/about" => About,
    route "/settings" => Settings,
    fallback => NotFound,
}

<Outlet /> marks where routed content renders. The surrounding layout (NavBar, Footer) persists across navigation — no re-render, no flicker.

View Transitions

Animate between page navigations with the transition keyword:

router AppRouter {
    transition "fade";  // Default transition for all routes

    route "/" => Home,
    route "/about" => About transition "slide-left",  // Per-route override
    route "/settings" => Settings,
    fallback => NotFound,
}

Transitions are WASM-internal — the animation math and DOM orchestration happen through the command buffer.

Programmatic Navigation

Navigate from code:

navigate("/user/42");

Contracts

Contracts define type-safe API boundaries. The compiler validates that API responses match the contract at compile time, and the WASM runtime validates at the wire level using a built-in JSON parser (no JS JSON.parse).

Contract Definition

contract UserResponse {
    id: i32,
    name: String,
    email: String,
    role: enum { Admin, User, Guest },
    avatar: String?,    // ? = nullable (Option<String>)
}

Fields can be any type, including inline enums. The ? suffix makes a field nullable (wraps in Option<T>).

What the compiler generates

For each contract, the compiler emits: - $__contract_register_<Name> – registers the contract schema (name, fields, types) in WASM memory - $__contract_validate_<Name> – validates a JSON buffer against the contract fields using the WASM JSON parser ($json_parse + $json_get_field) - A deterministic hash of the contract shape for cache invalidation

Using Contracts

// Parse a JSON response against a contract
let user = UserResponse::parse(json_ptr, json_len);

Contract Inference

The contract_infer module can infer contract shapes from fetch response patterns at compile time.

Contract Verification

The contract_verify module validates at compile time that contract field types are consistent across usages.

Contracts can be bound to channel definitions (channel ChatRoom -> ChatMessage) and cache queries (query get_users() : fetch(...) -> UserResponse).


Pages

Pages are SEO-optimized components with meta tags, structured data, and server-rendering support.

Page Definition

page BlogPost(slug: String) {
    signal post: Option<Post> = None;

    meta {
        title: f"Blog - {self.post.title}",
        description: self.post.excerpt,
        og_image: self.post.cover_image,
        og_type: "article",
    }

    schema {
        type: "Article",
        headline: self.post.title,
        author: self.post.author,
    }

    async fn load(&mut self) {
        self.post = Some(await fetch(f"/api/posts/{slug}").json());
    }

    style { .post { max-width: "800px"; margin: "0 auto"; } }

    render {
        <article class="post">
            <h1>{self.post.title}</h1>
            <p>{self.post.body}</p>
        </article>
    }
}

Page Blocks

Build modes: nectar build --ssr for server rendering, nectar build --ssg for static generation.


Forms

Declarative forms with built-in validation. No form libraries needed.

Form Definition

form ContactForm {
    field name: String {
        label: "Full Name",
        placeholder: "Jane Doe",
        required,
        min_length: 2,
        max_length: 100,
    }

    field email: String {
        label: "Email",
        required,
        email,
    }

    field website: Option<String> {
        label: "Website",
        url,
    }

    async fn on_submit(&mut self) {
        await fetch("/api/contact", { method: "POST", body: self.values() });
    }

    render {
        <form on:submit={self.on_submit}>
            {self.render_fields()}
            <button type="submit" disabled={!self.is_valid()}>"Send"</button>
        </form>
    }
}

Built-in Validators

Validator Syntax Purpose
required required or required: "message" Field must not be empty
min_length min_length: 2 Minimum string length
max_length max_length: 100 Maximum string length
pattern pattern: "^[a-z]+$" Regex pattern match
email email Valid email format
url url Valid URL format
validate validate: custom_fn Custom validation function

Automatic Features


Channels

WebSocket connections with automatic reconnection and type-safe messages.

Channel Definition

channel ChatRoom -> ChatMessage {
    url: f"wss://api.example.com/ws/chat",
    reconnect: true,
    heartbeat: 30000,

    on_connect {
        println("Connected");
    }

    on_message {
        ChatStore::add_message(message);
    }

    on_disconnect {
        println("Disconnected");
    }

    fn send_text(&mut self, text: String) {
        self.send(ChatMessage { user: "me", text: text, timestamp: now() });
    }
}

Channel Options


Auth

Declarative OAuth/authentication with session management.

Auth Definition

auth AppAuth {
    provider "google" {
        client_id: env("GOOGLE_CLIENT_ID"),
        scopes: ["openid", "email", "profile"],
    }

    provider "github" {
        client_id: env("GITHUB_CLIENT_ID"),
        scopes: ["user:email"],
    }

    session: "cookie",

    fn on_login(&mut self) { navigate("/dashboard"); }
    fn on_logout(&mut self) { navigate("/"); }
    fn on_error(&mut self) { println("Auth error"); }
}

Auth Options


Payment

PCI-compliant payment processing via sandboxed iframes.

Payment Definition

payment Checkout {
    provider: "stripe",
    public_key: env("STRIPE_PUBLIC_KEY"),
    sandbox: true,

    async fn on_success(&mut self) {
        await fetch("/api/orders/confirm", { method: "POST" });
        navigate("/thank-you");
    }

    fn on_error(&mut self) {
        println("Payment failed");
    }
}

Card data never touches component state. The compiler guarantees payment data isolation through sandboxed iframes.


Upload

File uploads with progress tracking, validation, and chunked transfer.

Upload Definition

upload AvatarUpload {
    endpoint: "/api/upload/avatar",
    max_size: 5242880,
    accept: ["image/png", "image/jpeg", "image/webp"],
    chunked: true,

    fn on_progress(&mut self) { /* track progress */ }
    async fn on_complete(&mut self) { /* handle result */ }
    fn on_error(&mut self) { /* handle error */ }
}

Upload Options


Db

Client-side database abstraction over IndexedDB with declarative schema.

Db Definition

db AppDatabase {
    version: 1,

    store "users" {
        key: "id",
        index "by_email" => "email",
        index "by_name" => "name",
    }

    store "posts" {
        key: "id",
        index "by_author" => "authorId",
    }
}

Store Options


Cache

Data caching with stale-while-revalidate, TTL, and optimistic updates.

Cache Definition

cache ApiCache {
    strategy: "stale-while-revalidate",
    ttl: 300,
    persist: true,
    max_entries: 100,

    query get_users() : fetch("/api/users") -> UserResponse {
        ttl: 60,
        stale: 30,
        invalidate_on: ["user_created"],
    }

    mutation create_user(data: UserInput) : fetch("/api/users", { method: "POST", body: data }) {
        optimistic: true,
        rollback_on_error: true,
        invalidate: ["get_users"],
    }
}

Query Options

Mutation Options


Embed

Third-party script embedding with security controls.

Embed Definition

embed Analytics {
    src: "https://analytics.example.com/script.js",
    loading: "defer",
    sandbox: true,
    integrity: "sha384-abc123...",

    permissions {
        allow: ["analytics"],
        deny: ["dom_access", "network"],
    }
}

Embed Options


Pdf

PDF generation from render blocks.

Pdf Definition

pdf Invoice {
    page_size: "A4",
    orientation: "portrait",
    margins: "2cm",

    render {
        <div class="invoice">
            <h1>"Invoice #1234"</h1>
            <table><tr><td>"Item"</td><td>"$100"</td></tr></table>
        </div>
    }
}

Trigger download: Invoice::download("invoice.pdf");


App (PWA)

Progressive Web App configuration.

App Definition

app MyApp {
    manifest {
        name: "My Application",
        short_name: "MyApp",
        start_url: "/",
        theme_color: "#4a90d9",
        display: "standalone",
    }

    offline {
        precache: ["/", "/about", "/offline"],
        strategy: "cache-first",
        fallback: OfflinePage,
    }

    push {
        vapid_key: env("VAPID_PUBLIC_KEY"),
        on_message: handle_push,
    }
}

App Blocks


Theme

Design tokens for light/dark modes.

Theme Definition

theme AppTheme {
    light {
        bg: "#ffffff",
        text: "#1a1a1a",
        primary: "#4a90d9",
    }

    dark {
        bg: "#1a1a1a",
        text: "#e0e0e0",
        primary: "#6ab0ff",
    }
}

The compiler generates CSS custom properties (--bg, --text, --primary) and a toggle mechanism. Respects prefers-color-scheme by default.

Usage in styles: background: var(--bg); Toggle: AppTheme::toggle(); or AppTheme::set("dark");


Breakpoints

Responsive design breakpoints.

Breakpoints Definition

breakpoints {
    mobile: 320,
    tablet: 768,
    desktop: 1024,
    wide: 1440,
}

Values are pixel widths (integers). Use in styles as @mobile, @tablet, @desktop.


Animations

Three animation primitives.

Spring

Physics-based animation with configurable stiffness, damping, and mass:

spring MenuSlide {
    stiffness: 200,
    damping: 20,
    mass: 1,
    properties: ["transform", "opacity"],
}

Keyframes

CSS keyframe animations with percentage-based frames:

keyframes FadeIn {
    0% { opacity: "0", transform: "translateY(10px)" }
    100% { opacity: "1", transform: "translateY(0)" }
    duration: "0.3s",
    easing: "ease-out",
}

Stagger

Stagger an animation across multiple elements:

stagger ListReveal {
    animation: FadeIn,
    delay: "50ms",
    selector: ".list-item",
}

All animations automatically respect prefers-reduced-motion.


Testing

Test Blocks

Test blocks define named test cases:

test "addition works" {
    let result = add(2, 3);
    assert_eq(result, 5);
}

test "user creation" {
    let user = User::new("Alice", "alice@test.com");
    assert(user.name == "Alice");
    assert_eq(user.email, "alice@test.com");
}

Assertions

assert(condition) – asserts that a condition is true:

assert(x > 0);
assert(list.len() > 0, "list should not be empty");

assert_eq(left, right) – asserts that two values are equal:

assert_eq(result, 42);
assert_eq(name, "Alice", "names should match");

Both assertion forms accept an optional message string as the last argument.

Running Tests

nectar test tests.nectar
nectar test tests.nectar --filter "addition"
nectar test tests.nectar --verbose

Component Testing with the Test Renderer

Nectar includes a built-in test renderer that mounts components into a virtual DOM for testing without a browser. The render() function returns a TestElement with query and interaction methods.

Mounting a Component

test "greeting renders correctly" {
    let el = render(<Greeting name="Nectar" />);
    let heading = el.findByText("Hello, Nectar!");
    assert(heading.exists());
}

Query Methods

Interaction Methods

Simulating User Interaction

test "counter increments on click" {
    let el = render(<Counter />);
    let btn = el.findByText("+1");
    let display = el.findByRole("counter");

    btn.click();
    assert_eq(display.getText(), "1");

    btn.click();
    btn.click();
    assert_eq(display.getText(), "3");
}

After each click() or type() call, the test renderer processes all reactive updates synchronously. Subsequent queries reflect the updated DOM state – no manual flushing is required.

Testing Props and Defaults

component Badge(label: String = "default") {
    render { <span>{self.label}</span> }
}

test "default prop is applied" {
    let el = render(<Badge />);
    assert_eq(el.findByText("default").getText(), "default");
}

test "explicit prop overrides default" {
    let el = render(<Badge label="custom" />);
    assert_eq(el.findByText("custom").getText(), "custom");
}

Testing Conditional and List Rendering

test "conditional rendering" {
    let el = render(<Alert show={true} />);
    assert(el.findByText("Warning!").exists());
}

test "list rendering" {
    let el = render(<ItemList items={["a", "b", "c"]} />);
    assert(el.findByText("a").exists());
    assert(el.findByText("b").exists());
    assert(el.findByText("c").exists());
}

Testing Store Integration

Components that read from stores can be tested end-to-end:

test "store-connected component updates on action" {
    let el = render(<StoreCounter />);
    let btn = el.findByText("+1");

    btn.click();

    let display = el.findByRole("display");
    assert_eq(display.getText(), "Store count: 1");
}

Agent Testing

Agents are testable like components but have additional capabilities for verifying tool registration, tool dispatch, and AI interaction mocking.

Testing Tool Registration

test "agent registers tools" {
    let tools = MyAgent::get_registered_tools();
    assert_eq(tools.len(), 2);
    assert_eq(tools[0].name, "search");
    assert_eq(tools[1].name, "calculate");
}

get_registered_tools() returns metadata about each tool block: name, parameter names and types, and return type.

Testing Tool Dispatch

test "dispatch tool with typed args" {
    let result = await MyAgent::dispatch_tool("search", {
        query: "nectar language",
    });
    // Verify the tool executed correctly
}

dispatch_tool(name, args) invokes a tool by name with a typed argument object, simulating what the runtime does when the AI model requests a tool call.

Mocking AI Responses

The ai::mock_response() and ai::mock_tool_call() functions install canned responses for testing without a real LLM:

test "mock a text response" {
    ai::mock_response("The answer is 42.");
    let response = await ai::chat_complete(messages);
    assert_eq(response.content, "The answer is 42.");
}

test "mock a tool call response" {
    ai::mock_tool_call("get_weather", { city: "Paris" });
    let response = await ai::chat_complete(messages);
    assert_eq(response.tool_calls[0].name, "get_weather");
}

Mocking Streaming Responses

test "mock streaming tokens" {
    ai::mock_stream(["Hello", " ", "world"]);
    let mut text = "";
    for chunk in stream ai::chat_stream(messages) {
        text = text + chunk;
    }
    assert_eq(text, "Hello world");
}

Async Test Patterns

Test blocks support await for testing asynchronous operations:

test "async fetch in tests" {
    let response = await fetch("https://api.example.com/data");
    assert(response.status == 200 || response.status == 0);
}

In the test environment, HTTP imports are stubbed by the test runner. The fetch calls resolve immediately without hitting real endpoints. This allows testing the async control flow without external dependencies.

For sequential async operations:

test "sequential async" {
    let a = await fetch("https://api.example.com/step1");
    let b = await fetch("https://api.example.com/step2");
    assert(true, "both requests completed");
}

Test Organization Best Practices

Use descriptive test names. Test names appear in the output when tests fail. Use full sentences that describe the expected behavior:

// Good
test "counter increments on click" { ... }
test "empty list renders zero total" { ... }
test "login fails with invalid credentials" { ... }

// Avoid
test "test1" { ... }
test "counter" { ... }

One assertion focus per test. Each test should verify one behavior. Multiple assert calls are fine when they verify facets of the same behavior:

test "user creation sets all fields" {
    let user = User::new("Alice", "alice@test.com", 30);
    assert_eq(user.name, "Alice");
    assert_eq(user.email, "alice@test.com");
    assert_eq(user.age, 30);
}

Organize tests near related code. Place test blocks at the bottom of the file after the types and functions they test, or in dedicated test files:

examples/
    todo.nectar              # Application code
    tests.nectar             # Unit tests for logic
    component-tests.nectar   # Component integration tests
    agent-tests.nectar       # Agent behavior tests

Use --filter for focused testing. During development, run only the tests relevant to your current change:

nectar test tests.nectar --filter "fibonacci"
nectar test component-tests.nectar --filter "counter"

Reset shared state between tests. When testing stores or agents, call the clear or reset method at the start of each test to avoid state leaking between tests:

test "store starts fresh" {
    MyStore::reset();
    assert_eq(MyStore::get_count(), 0);
}

Test error paths, not just happy paths. Use Result, Option, and try/catch to verify that error handling works correctly:

test "division by zero returns error" {
    let result = divide(10.0, 0.0);
    match result {
        Result::Ok(_) => assert(false, "should not succeed"),
        Result::Err(e) => assert_eq(e, "division by zero"),
    }
}