# nexus-lang

[![build status](https://codeberg.org/krisvanrens/nexus-lang/actions/workflows/nexus-rs.yml/badge.svg?branch=main&style=for-the-badge)](https://codeberg.org/krisvanrens/nexus-lang/actions?workflow=nexus-rs.yml)
[![MIT license](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](./LICENSE)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-3.0-4baaaa.svg?style=for-the-badge)](CODE_OF_CONDUCT.md)

<!-- [![Build status](https://codeberg.org/krisvanrens/nexus-lang/actions/workflows/build-and-test.yml/badge.svg?style=for-the-badge&branch=main)](https://codeberg.org/krisvanrens/nexus-lang/actions/workflows) -->

The Nexus programming language.
A programming language for generating component network descriptions.

---

- [nexus-lang](#nexus-lang)
  - [Getting started](#getting-started)
  - [Native integration with component networks](#native-integration-with-component-networks)
    - [Nodes, connections and groups](#nodes-connections-and-groups)
      - [Nodes](#nodes)
      - [Connections](#connections)
      - [Groups](#groups)
      - [Library imports](#library-imports)
  - [Simplicity](#simplicity)
  - [Safety](#safety)
    - [Using a value uninitialized is an error](#using-a-value-uninitialized-is-an-error)
    - [Block scope braces are strictly required](#block-scope-braces-are-strictly-required)
  - [Tooling](#tooling)
  - [Documentation and tests](#documentation-and-tests)
  - [The component model](#the-component-model)
    - [Operations](#operations)
      - [Operation: component instantiation](#operation-component-instantiation)
      - [Operation: component connection](#operation-component-connection)
      - [Operation: component grouping](#operation-component-grouping)
    - [Network model](#network-model)
  - [Language API](#language-api)
    - [Entry point](#entry-point)
    - [Data model](#data-model)
    - [Network helpers](#network-helpers)
    - [Usage example](#usage-example)
  - [Language backend-end API / FFI](#language-backend-end-api--ffi)
  - [Examples](#examples)
    - [Example 1: general purpose](#example-1-general-purpose)
    - [Example 2: graph description](#example-2-graph-description)
    - [Example 3: functions as arguments](#example-3-functions-as-arguments)
    - [Example 4: program result types](#example-4-program-result-types)
      - [Option 1: no result](#option-1-no-result)
      - [Option 2: a single node result](#option-2-a-single-node-result)
      - [Option 3: a named group result](#option-3-a-named-group-result)
      - [Option 4: an anonymous group result](#option-4-an-anonymous-group-result)
  - [Implementation status](#implementation-status)
    - [Milestone 0: ideation, base setup](#milestone-0-ideation-base-setup)
    - [Milestone 1: language setup](#milestone-1-language-setup)
    - [Milestone 2: foundations I](#milestone-2-foundations-i)
    - [Milestone 3: foundations II](#milestone-3-foundations-ii)
    - [Milestone 4: basics](#milestone-4-basics)
    - [Milestone 5: network features](#milestone-5-network-features)
    - [Milestone 6: language hardening and improvements](#milestone-6-language-hardening-and-improvements)
    - [Milestone 7: initial release preparations](#milestone-7-initial-release-preparations)
    - [Milestone 8: tooling and integration features](#milestone-8-tooling-and-integration-features)
    - [Milestone 9: finalize: examples and secondary integrations](#milestone-9-finalize-examples-and-secondary-integrations)
  - [Language grammar](#language-grammar)
    - [EBNF Notation](#ebnf-notation)
      - [Basic Symbols](#basic-symbols)
      - [Examples](#examples-1)
    - [Grammar Structure](#grammar-structure)
      - [Lexical Grammar (Tokens)](#lexical-grammar-tokens)
      - [Parsing Grammar (Syntax)](#parsing-grammar-syntax)
    - [Reading Tips](#reading-tips)
    - [Lexical grammar](#lexical-grammar)
    - [Parsing grammar](#parsing-grammar)
      - [Operator precedence](#operator-precedence)
    - [Language keywords](#language-keywords)
      - [Base language keywords](#base-language-keywords)
      - [Language type keywords](#language-type-keywords)
    - [Builtin methods](#builtin-methods)
  - [TODO](#todo)
  - [FAQ](#faq)
    - [Why the name?](#why-the-name)
    - [Why hand-code all the parsing etcetera?](#why-hand-code-all-the-parsing-etcetera)

---

Aside a simple base of common general-purpose primitives/control flow/etc. Nexus offers native integration for building a network of components, connecting in-/outputs for components and setting component properties.
The goal of Nexus is to strike a balance between a full-fledged programming language and a declarative network description language.

Nexus is meant to drive another component-network-oriented network system through its API or output, using the Nexus network description as input.

The syntax and most semantics of Nexus are loosely modeled after that of the [Rust programming language](https://github.com/rust-lang/rust).

## Getting started

> **Note**: This section will be expanded as the tooling matures.

Nexus requires a working [Rust toolchain](https://rustup.rs). To build and run:

```bash
git clone https://codeberg.org/krisvanrens/nexus-lang
cd nexus-lang
cargo build --release
```

Run a program from a file:

```bash
cargo run -- -f path/to/program.nxs
```

Run a one-liner from a string:

```bash
cargo run -- -s 'let x = 42; print x;'
```

Launch the interactive REPL by running with no arguments:

```bash
cargo run
```

## Native integration with component networks

Nexus is designed to interface component-network-oriented systems.

Consider this example network:

```mermaid
flowchart LR
    subgraph system
    direction LR
    source --> processor
    processor --> sink
    end
```

This network can be implemented using the following code:

```rust
node type Source    { out output; }
node type Converter { in input; out output; }
node type Sink      { in input; }

// A component group named 'system'.
let system = group {
    // Group member declarations follow the form `self.name = ...`.
    self.source    = node Source;
    self.processor = node Converter;
    self.sink      = node Sink;

    // Operators for defining node in-/output connections.
    source.output    -> processor.input;
    processor.output -> sink.input;
}

system // Final program result named 'system'.
```

> **NOTE**: Member references inside the group body can use bare names (implicit `self.`). Declarations follow the `self.name = ...` form.

### Nodes, connections and groups

A component network is described in terms of three elementary parts:

- **Nodes** (i.e. component instantiations),
- **Connections** (between node in-/outputs),
- **Groups** (i.e. collections of nodes).

Each of these parts can be expressed using Nexus.

#### Nodes

To instantiate a component (i.e. create a node) of type `Converter`:

```rust
node type Converter { in input; out output; }

let c = node Converter;
```

The local handle `c` can be used to address the node within Nexus (e.g. for creating a connection).

#### Connections

To create a connection between nodes, the `->` operator can be used:

```rust
node type Source { out output; }
node type Sink   { in input; }

let ingest = node Source;
let egest  = node Sink;

ingest.output -> egest.input;
```

This connects the `output` port of the `Source` node to the `input` port of the `Sink` node.
Port names and directions are declared in the `node type` definition and validated at interpretation time; referencing an undeclared port or connecting in the wrong direction is a hard error.

Connections are directional (`out`/`inout` → `in`/`inout`); the direction is checked against the declared port schemas of both endpoints.

A single source port can be connected to multiple targets in one statement using a comma-separated target list (fan-out):

```rust
source.output -> sink_a.input, sink_b.input, sink_c.input;
```

This is equivalent to three individual connection statements and produces three distinct connections.

#### Groups

To organize nodes hierarchically, groups can be defined:

```rust
node type Source    { in input; out output; }
node type Converter { in input; out output; }
node type Sink      { in input; out output; }

// Ingest subsystem definition + instantiation:
let ingest = group {
    self.source    = node Source;
    self.converter = node Converter;

    source.output -> converter.input;

    port input  = source.input;
    port output = converter.output;
};

// Egest subsystem definition + instantiation:
let egest = group {
    self.converter = node Converter;
    self.sink      = node Sink;

    converter.output -> sink.input;

    port input  = converter.input;
    port output = sink.output;
};

// Connect both subsystems:
ingest.output -> egest.input;
```

#### Library imports

Node type declarations and other reusable definitions can be shared across files using `use`:

```rust
// components.nxs
node type Reader { in input; out output; }
node type Writer { in input; out output; }

fn default_capacity() -> Number { 64 }
```

```rust
// main.nxs
use components;

let r = node Reader;
let w = node Writer;

r.output -> w.input;
```

`use libname;` loads `libname.nxs` from the same directory as the importing file. Additional search directories can be provided with the `-I` flag. Each file is loaded at most once regardless of how many times it is imported; imports are transitive and name collisions are a hard error.

## Simplicity

Nexus is geared towards simplicity, in the sense that it tries to support a minimal viable set of features required for flexible use as a component network description language.

Node types, port names, port directions, and connection validity are all checked at interpretation time; typos in port names or wrong-direction connections are hard errors.
It is still up to the downstream application consuming the network description to perform any higher-level semantic validation (e.g. type compatibility between connected ports).

## Safety

Nexus is opinionated in some respects, mostly to improve language safety.
The following subsections indicate in what specific ways.

### Using a value uninitialized is an error

Even though Nexus is dynamically typed, it requires a value to be initialized when it's used.
E.g.:

```rust
let x;

let y = x; // Error: Using 'x' uninitialized.
```

and:

```rust
let mut x;

x = true;

let y = x; // OK.
```

### Block scope braces are strictly required

Braces after `if`/`while`/`for`/etc. expressions are strictly required.
E.g.:

```rust
// Error: no block scope braces.
if expr
    do_something();

// OK.
if expr {
    do_something();
}
```

Explicit scope limiting increases esthetic consistency, as well as maintainability.

### Warnings for unused values

The interpreter emits a warning whenever a value is computed but never used:

- A `let`, `const`, or `fn` binding that is declared but never read.
- A `for` loop variable that is never referenced inside the loop body.
- An expression statement whose result is non-unit and discarded.

Warnings do not stop interpretation; they are informational.
Use `_` as the exact binding name to suppress the warning (e.g. `let _ = unused_value;`, `for _ in 0..n { ... }`).

## Tooling

One of the focus points of Nexus is that there should be good tooling.
This has many aspects:

- Nexus should be friendly for building tools for; the API should be simple.
- Nexus should (on the long run) be delivered with tools and examples.

## Documentation and tests

> **Note**: Full language and API documentation is planned. This section will be expanded.

The Nexus implementation ships with a file-based test suite. Tests are plain `.nxs` files organised under `tests/` by pipeline stage and expected outcome:

```
tests/
  scanner/
    pass/    ← programs that must scan successfully
    fail/    ← programs that must produce a scan error
  parser/
    pass/    ← programs that must parse successfully
    fail/    ← programs that must produce a parse error
  interpreter/
    pass/    ← programs that must run without error (typically end with assert statements)
    fail/    ← programs that must produce a runtime error
```

Run the full suite with:

```bash
cargo test
```

Adding a test requires only creating a `.nxs` file in the appropriate directory; no Rust code needed.

## The component model

This section describes the abstract component model used to define networks for.

### Operations

The end result of a Nexus program is a component network, described in terms of three fundamental structures: component declarations (including in- and outputs), component instantiations, connections and groupings.

Because Nexus itself only deals with these simple operations, it is up to the higher-level processing to deal with further validation of the resulting component network.
These validation checks might include:

- Validating component types,
- Validating component in- and outputs (names and directions),
- Checking for invalid component properties,
- Checking for incompatible component in- / output connections,
- ...

#### Operation: component instantiation

```mermaid
flowchart LR
  direction LR
  c
```

#### Operation: component connection

```mermaid
flowchart LR
  direction LR
  c1 --> c2
```

#### Operation: component grouping

```mermaid
flowchart LR
  subgraph group
  direction LR
  c1 --> c2
  end
```

### Network model

The resulting component network is modelled as a tree hierarchy, starting from a virtual root node.

For example, consider this network:

```mermaid
flowchart
  subgraph g1
  direction LR
  c2 --> c3
  end

  subgraph g2
  direction LR
  c4
  end

  c1 --> c5
  c4 --> c5
```

This would lead to the component instantiations / group definitions:

```text
c1
g1/c2
g1/c3
g2/c4
c5
```

And the following component connections:

```text
c1 -> c5
g1/c2 -> g1/c3
g2/c4 -> c5
```

Here `c1`..`c5` are component instantiations ("nodes"), `g1` and `g2` are component groups.

> **NOTE**: In actual coded Nexus, components have in-/outputs between which connections are made (not the components themselves).

## Language API

The `nexus` crate exposes a public Rust API for embedding the interpreter and consuming the resulting network.

### Entry point

```rust
use nexus::{Nexus, Network, Warning, NexusError};

// From source code:
let (network, warnings) = Nexus::run(source_code)?;

// From a file:
let (network, warnings) = Nexus::run_file("network.nxs")?;
```

Both functions return `Result<(Network, Vec<Warning>), NexusError>`.
`NexusError` covers scan, parse, interpret, and I/O failures as a single unified error type.

### Data model

```rust
pub struct Network {
    pub root: Option<Node>,          // None for programs that produce no network
    pub connections: Vec<Connection>, // all connections, globally owned
}

pub struct Node {
    pub id: NodeId,           // stable u64, assigned in declaration order
    pub name: String,         // from the `let` binding, or "root" if anonymous
    pub type_name: String,    // schema name for leaf nodes; "group" for composites
    pub ports: Vec<Port>,
    pub children: Vec<Node>,  // empty for leaf nodes; members for groups
}

pub struct Connection {
    pub from: PortRef,
    pub to: PortRef,
}

pub struct PortRef {
    pub node_id: NodeId,
    pub port_name: String,
}

pub struct Port {
    pub name: String,
    pub direction: PortDirection, // In | Out | InOut
}
```

Groups and leaf nodes are unified into a single `Node` type.
A group is a `Node` with non-empty `children` and `type_name: "group"`.
Connections are owned by the `Network`, not by individual nodes.

### Network helpers

```rust
// Map from NodeId to path string (e.g. "pipeline/source"):
let paths = network.node_paths();

// Map from NodeId to &Node for O(1) lookup:
let index = network.node_index();
```

### Usage example

```rust
use nexus::Nexus;

let (network, warnings) = Nexus::run_file("network.nxs")?;

for w in &warnings {
    eprintln!("warning: {}", w.message);
}

if let Some(root) = &network.root {
    for node in &root.children {
        println!("{} (type: {})", node.name, node.type_name);
    }

    let paths = network.node_paths();
    for conn in &network.connections {
        println!("{}/{} -> {}/{}",
            paths[&conn.from.node_id], conn.from.port_name,
            paths[&conn.to.node_id],   conn.to.port_name);
    }
}
```

## Examples

### Example 1: general purpose

```rust
// Comment.
fn free(a: Number, b: bool, c: Number) -> Number {
    42 + a + if b { c } else { 0 }
}

/* Comment */
fn main() {
    fn local1() {
        return "Hello1".len();
    }

    let local2 = || "Hello2".len();

    let x = free(1, true, local1() + local2());

    print ||{ 17 + x }(); // IIFE.
}

main();
```

### Example 2: graph description

```rust
node type Reader { in input; out output; }
node type Writer { in input; out output; }

// System group factory function.
fn create_system() -> Group {
    group {
        self.source = node Reader;
        self.sink   = node Writer;

        source.output -> sink.input;

        // Group in-/output definitions:
        port input  = source.input;
        port output = sink.output;
    }
}

const NUMBER_OF_SYSTEMS: Number = 4;

let mut app: [Group; NUMBER_OF_SYSTEMS];

// Create four systems in 'app':
for i in 0..NUMBER_OF_SYSTEMS {
    app[i] = create_system();
}

print app; // Print for debugging.

app
```

### Example 3: functions as arguments

```rust
// Function argument prototypes follow: fn(T_args) -> T_return
fn map(array: [Number], f: fn(Number) -> Number) -> [Number] {
    let mut result: [Number; array.len()];
    for i in 0..array.len() {
        result[i] = f(array[i]);
    }

    result
}

fn reduce(array: [Number], init: Number, f: fn(Number, Number) -> Number) -> Number {
    let mut acc = init;
    for element in array {
        acc = f(acc, element);
    }

    acc
}

let data = [1, 2, 3, 4];
let doubled = map(data, |x: Number| -> Number { x * 2 });
let sum = reduce(doubled, 0, |a: Number, b: Number| -> Number { a + b });

assert sum == 20;
```

### Example 4: program result types

#### Option 1: no result

```rust
print "The answer is 42" // Only side-effects, no result.
```

#### Option 2: a single node result

```rust
node type Supernode {}

node Supernode
```

#### Option 3: a named group result

```rust
let app = group {
    // ..define network..
};

app
```

The root node for this program is called `app`.

#### Option 4: an anonymous group result

```rust
group {
    // ..define network..
}
```

The root node for this program is called `root` (as per Nexus' implicit definition).

## Implementation status

### Milestone 0: ideation, base setup

- [x] Lexing/scanning setup.
- [x] Scanner error handling.
- [x] Implement scanning for the complete lexical grammar.
- [x] Interpretation from source file (`.nxs`).
- [x] Command-line REPL setup.

### Milestone 1: language setup

- [x] Parsing setup.
- [x] Parsing error handling.
- [x] Interpretation setup.
- [x] Interpretation error handling.

### Milestone 2: foundations I

- [x] Implement parsing and interpretation for core expressions (basic arithmetic expressions).
- [x] Implement parsing and interpretation for core expressions (basic logic expressions).

### Milestone 3: foundations II

- [x] Implement parsing and interpretation for unscoped constant definitions (through `const`).
- [x] Implement parsing and interpretation for unscoped variables (immutable `let` variable declarations with initialization).
- [x] Implement parsing and interpretation for unscoped variables (mutable `let` variable declarations, assignment statements).
- [x] Implement block expressions and scoping.

### Milestone 4: basics

- [x] Implement parsing and interpretation for control flow and logic (`if`/`else` conditionals, `while` loops).
- [x] Implement parsing and interpretation for ranges and `for .. in` loop expressions.
- [x] Implement parsing and interpretation for functions (function declarations with parameters/return types, function calls, `return` statements).
- [x] Implement parsing and interpretation for homogeneous data structures.
- [x] Implement parsing and interpretation for member access (`.`) and basic builtin methods `len()` for arrays and strings.
- [x] Implement parsing and interpretation for closures and function-types as arguments.

### Milestone 5: network features

- [x] Work out design of network-related features.
- [x] Add member field access parsing and interpretation.
- [x] Implement parsing and interpretation for domain-specific feature: `node` type declarations + registry.
- [x] Implement parsing and interpretation for domain-specific feature: `node` instantiation expressions.
- [x] Implement `Value::PortRef` on `Value::Node` (extend member-access dispatch with schema validation).
- [x] Implement parsing and interpretation for connection statements (`->`) with a schema-agnostic direction validator.
- [x] Implement `group { ... }` bodies with `self.NAME = ...` member bindings (contextual `self` keyword, member map, duplicate-membership and reassignment errors).
- [x] Implement `port NAME = self.member.port;` port declarations with direction inference, schema freeze at scope end, and multi-target fan-out (`port NAME = expr, expr, ...;`).
- [x] Extend `PortRef` production to `Value::Group`; unified node+group connection validation.
- [x] Add traversal and printing of network nodes/groups/connections by path.

### Milestone 6: language hardening and improvements

- [x] Re-evaluate language grammar and design, audit for flaws and potentials misuse.
- [x] Extend language tests with cross-feature tests.
- [x] Build test harness for fuzzing.
- [x] Fix high-priority issues.
- [x] Implicit `.self` for group member references.
- [x] Multi-target connections using `x -> y, z;`.
- [x] Number literal separators (using infix `_`).
- [x] Validate possible error and warning messages, and create a reproducible test program set.

### Milestone 7: initial release preparations

- [x] Implement `use` declarations (file loading, canonical-path dedup, flat-merge).
- [x] Reorganize project directory to have the Rust implementation in the root.
- [x] Design and implement unified language API for integration of Nexus.
- [ ] Publish a 0.7 version on crates.io.

### Milestone 8: tooling and integration features

- [ ] Revise scanner/parser/interpreter APIs where needed (currently all split up).
- [ ] Extend CLI tools for debugging and validation.
- [ ] Add language and integration API documentation.
- [ ] Publish a 0.8 release.

### Milestone 9: finalize: examples and secondary integrations

- [ ] TODO: Determine what other features are required for a v1.0.

## Language grammar

Productions are in [Extended Backus-Naur Form (EBNF)](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form), to be specific: a slight variant on the form [W3C uses for XML](https://www.w3.org/TR/REC-xml/#sec-notation).

### EBNF Notation

The Nexus grammar uses Extended Backus-Naur Form (EBNF). Here's how to read it:

#### Basic Symbols

- `→` means "is defined as",
- `|` means "or" (alternative),
- `( )` groups elements,
- `[ ]` makes elements optional,
- `*` means "zero or more",
- `+` means "one or more",
- `?` means "zero or one" (optional).

#### Examples

```ebnf
var_decl → 'let' ( 'mut' )? ID ( '=' expr )? ';'
```

This reads as:

> "A variable declaration is the keyword `let`, optionally followed by `mut`, then an identifier, optionally followed by `=` and an expression, ending with a semicolon."

```ebnf
arguments → expr ( ',' expr )*
```

This reads as:

> "Arguments are an expression followed by zero or more comma-separated expressions."

### Grammar Structure

#### Lexical Grammar (Tokens)

- `UPPERCASE` rules define individual tokens (keywords, operators, literals),
- These are the building blocks that the parser recognizes.

#### Parsing Grammar (Syntax)

- `lowercase` rules define how tokens combine into language constructs,
- Start from `program` and follow the rules down.

### Reading Tips

1. **Start with `program`** - this is the top-level rule,
2. **Follow the chain** - each rule references other rules,
3. **Terminals in quotes** - `'let'`, `'fn'` are literal keywords,
4. **UPPERCASE tokens** - `ID`, `STRING`, `NUMBER` are defined in lexical grammar,
5. **Precedence matters** - expression rules are ordered by operator precedence (highest to lowest).

### Lexical grammar

```ebnf
ALPHA           → [a-zA-Z]
COMMENT_MULTI   → '/*' [^*]* '*'+ ( [^/*] [^*]* '*'+ )* '/'
COMMENT_SINGLE  → '//' [^\n]*
DELIMITER       → '(' | ')' | '{' | '}' | '[' | ']' | ':' | ';' | ','
DIGIT           → [0-9]
ESCAPE_SEQ      → '\\' ['"\\nrtbf0] | '\\x' HEX_DIGIT{2} | '\\u' HEX_DIGIT{4} | '\\U' HEX_DIGIT{8}
HEX_DIGIT       → [0-9a-fA-F]
ID              → ( ALPHA | '_' ) ( ALPHA | DIGIT | '_' )*
NUMBER          → DIGIT+ ( '.' DIGIT+ )?
OPERATOR_ARITH  → '+' | '-' | '*' | '/' | '%'
OPERATOR_ASSIGN → '+=' | '-=' | '*=' | '/=' | '%='
OPERATOR_COMP   → '==' | '!=' | '<' | '>' | '<=' | '>='
OPERATOR_LOGIC  → '||' | '&&'
OPERATOR_MISC   → '&' | '=' | '.' | '..' | '..=' | '->'
STRING          → '"' ( [^"\\] | ESCAPE_SEQ )* '"'
WHITESPACE      → [ \t\n\r]+
```

#### NOTE 1; Unicode character support for identifiers

For simplicity in the production rules, `ALPHA` is represented here as ASCII alphabetic only.
However, in the Nexus implementation, it means any _alphabetic_ character, as defined by [chapter 4](https://www.unicode.org/versions/Unicode15.0.0/ch04.pdf) of the [Unicode standard](https://www.unicode.org/versions/Unicode15.0.0/).
This means in practice it is possible to define identifiers named '`ŮñĭçøƋɇ`'.
Identifiers are compared as raw byte sequences with no Unicode normalization; NFC and NFD forms of the same visual character are treated as distinct names.

#### NOTE 2; Keywords not valid as identifiers

Language keywords can not be used as an identifier name, see: [Language keywords](#language-keywords).

#### NOTE 3; Number literal format

The number format accepts basic integer (e.g. `42`) / full floating-point number notation only (e.g. `3.14`).

#### NOTE 4; Token disambiguation and longest-match rule

Longest match rule applies; operators are matched before numeric literals.

#### NOTE 5; Discard placeholder `_`

The identifier `_` is a discard placeholder.
It may appear as the binding name in `let` declarations (e.g. `let _ = expr;`); the RHS is evaluated for side effects but the result is never stored; and as the loop variable in `for` loops (e.g. `for _ in expr { ... }`) to indicate the iteration value is intentionally unused.
Using `_` in either position also suppresses the unused-value warning (see NOTE 21).
`_` may be repeated any number of times within a scope.
Bare `_ = expr;` (assignment, not declaration) is a parse error.
Reading `_` gives `UndefinedIdentifier` because nothing is ever bound to it.

### Parsing grammar

```ebnf
program        → decl* ( expr )? EOF

decl           → fn_decl | const_decl | var_decl | use_decl | node_type_decl | stmt
fn_decl        → 'fn' function
const_decl     → 'const' ID ':' type '=' expr ';'
var_decl       → 'let' ( 'mut' )? ID ( ( '=' expr ) | ( ':' type ) | ( ':' type '=' expr ) )? ';'
use_decl       → 'use' ID ';'
node_type_decl → 'node' 'type' ID '{' port_spec* '}'
port_spec      → ( 'in' | 'out' | 'inout' ) ID ( ',' ID )* ';'

group_decl     → port_decl | member_decl | decl
port_decl      → 'port' ID '=' expr ( ',' expr )* ';'
member_decl    → 'self' '.' ID '=' expr ';'

stmt           → expr_stmt | assert | assignment | connect | print | return | block
expr_stmt      → expr ( ';' )?
assignment     → expr ( '=' | '+=' | '-=' | '*=' | '/=' | '%=' ) ( expr | ref ) ';'
assert         → 'assert' expr ( ',' expr )? ';'
connect        → expr '->' expr ( ',' expr )* ';'
print          → 'print' expr? ';'
return         → 'return' expr? ';'
block          → '{' decl* '}'

expr           → range_expr
node_expr      → 'node' ID
group_expr     → 'group' '{' group_decl* '}'
range_expr     → logic_or ( ( '..' | '..=' ) logic_or )?
logic_or       → logic_and ( '||' logic_and )*
logic_and      → equality ( '&&' equality )*
equality       → relational ( ( '==' | '!=' ) relational )*
relational     → additive ( ( '<' | '>' | '<=' | '>=' ) additive )*
additive       → factor ( ( '+' | '-' ) factor )*
factor         → unary ( ( '*' | '/' | '%' ) unary )*
unary          → ( '!' | '+' | '-' ) unary | postfix
postfix        → primary ( ( '.' ID ) | ( '(' arguments ')' ) | ( '[' expr ']' ) )*
primary        → ID | literal | array_literal | closure | control | node_expr | group_expr | block | '(' expr ')'
literal        → NUMBER | STRING | 'true' | 'false'
array_literal  → '[' ( expr ( ',' expr )* )? ']'
closure        → ( '||' | '|' parameters? '|' ) ( '->' type )? ( expr | block )
control        → if | loop | while | for | break | continue
if             → 'if' expr block ( 'else' ( if | block ) )?
loop           → 'loop' block
while          → 'while' expr block
for            → 'for' ( ID | '_' ) 'in' expr block
break          → 'break' expr?
continue       → 'continue'

ref            → '&' expr
function       → ID '(' parameters? ')' ( '->' type )? block
parameters     → parameter ( ',' parameter )*
parameter      → ID ':' type
arguments      → expr ( ',' expr )*
type           → base_type | array_type | fn_type
base_type      → 'bool' | 'Group' | 'Node' | 'Number' | 'String'
array_type     → '[' type ( ';' expr )? ']'
fn_type        → 'fn' '(' ( type ( ',' type )* )? ')' ( '->' type )?
```

#### NOTE 1; References; aliases, dereferencing, and scope

References (`&x`) create scope-qualified aliases to named variables.
Only bare identifiers may be referenced; `&arr[i]` and `&self.foo` are `InvalidRefTarget`.
Reading a reference auto-dereferences to the referent's current value; reference chains (`&&x`, `&r` where `r = &x`) auto-deref through all layers to the underlying non-reference value.
Assigning to a variable that holds a reference writes through to the referent.
References may be created to immutable or const variables (as read-only aliases); writing through such a reference is a runtime error (`AssignToImmutable` / `AssignToConstant`) with the original variable name in the message.
If the referent's scope has been destroyed, using the reference produces an orphaned reference error.
References are tracked by the scope ID of the original binding, so shadowing does not cause a reference to silently retarget.

#### NOTE 2; Uninitialized variables and deferred type inference

Mutable variables may be declared without type indication or initialization value.
In this case, type inference will determine the type of the variable.
Reading a variable before it has been assigned a value is a runtime `UninitializedRead` error.
Iterating over an array with `for..in` where an element is uninitialized (e.g., a `[Group; N]` fill array that was never fully assigned) also raises `UninitializedRead`.
Assigning such an all-uninitialized array to a new variable without a type annotation raises `TypeMismatch`; add an explicit type annotation (e.g. `: [Group]`) to resolve it.

#### NOTE 3a; Number literal format and underscore separators

The number format accepts basic integer (e.g. `42`) and full floating-point notation (e.g. `3.14`) only.
Scientific notation (`1e10`), hex (`0x1F`), and octal/binary literals are not supported; the lexer raises `UnexpectedToken` on the offending character.
Underscore digit separators are supported: `1_000`, `1_000_000`, `3.141_592_653`.
The underscore must appear between digit groups; leading (e.g. `_1` is an identifier), trailing (`1_`), consecutive (`1__0`), immediately before the decimal point (`1_.5`), or immediately after the decimal point (`1._5`) are all scan errors.

#### NOTE 3b; Semicolon rules for expression statements

The semicolon after an expression statement (`expr ';'`) is required for all statements except the final expression of a block, which may omit it to act as the block's result value.
Statements that are not expressions; `let`, `const`, `fn`, `return`, `assert`, `print`, `connect`; always require a terminating `;`.

#### NOTE 4; Token disambiguation; closures, operators, and comments

Disambiguation among logical or `||`, single `|` symbols for closures and empty-parameter closure syntax `||` is resolved using contextual information during lexing.
Longest match rule applies; operators are matched before numeric literals.
Examples: `0..10` scans as `[0, .., 10]`; `0..=10` as `[0, ..=, 10]`; a single `.` as a member-access operator when not followed by another `.`.
Block comments (`/* */`) are **not** nestable: `/* /* */ */` ends at the first `*/`, leaving the second `*/` as a stray token.
The empty-parameter closure form `||` is a single token; `| |` (two `|` separated by whitespace) is a parse error as a closure header.

#### NOTE 5; Port and member declarations; group scope restriction

Port declarations and member declarations are valid only at the top level of a `group { ... }` body (not inside nested blocks, `if`/`for`/`while`, or function bodies declared within the group).
Enforced at parse time.

#### NOTE 6; Variable shadowing and redeclarations

Shadowing or redeclarations of variables works just like in Rust.
This means variable names can be reused as mutable/immutable, or as different types or values regardless of their previous use.
A `node type` name reservation applies only to the current scope; a `let`/`const` with the same name is legal in an inner scope (it shadows the type), but within that inner scope `node TypeName` resolves to the shadowing binding rather than the type, causing `NotANodeType`.
A `const` may be shadowed in an inner scope by another `const` or `let`; re-declaring the same `const` in the **same** scope is `DuplicateDeclaration`.

#### NOTE 7; Function declaration order

Functions have to be declared before they can be called.

#### NOTE 8; Closures; capture semantics and invocation

Closures capture free variables by value: the values are snapshotted at closure-creation time, and mutations to the outer variable after that point are not visible inside the closure.
Each invocation of the closure re-binds the captured variables from the original snapshot, so mutations made _inside_ the closure body do not persist across calls either; successive calls always start from the same captured state.
Identifiers referenced in the closure body that are not yet in scope at creation time are looked up at call time instead; this means a closure can call itself by name once its binding is complete (recursive closures work).
Both closures and named functions match the `fn(Type, ...) -> Type` type syntax, enabling higher-order functions.
A closure without an explicit `-> T` return-type annotation is accepted as any `fn(...) -> T` parameter; if the actual return type is wrong, the error surfaces when the return value is consumed.
A closure body may be an expression or a block; they are semantically equivalent: `|x| x + 1` and `|x| { x + 1 }` produce identical behaviour.
`return` inside a closure exits the closure body only, not any enclosing named function.

#### NOTE 9; Array types; sized, unsized, and element rules

`[Type]` is an unsized array type that accepts arrays of any length.
`[Type; N]` is a sized array type; values must have exactly N elements.
The size is enforced at runtime for function parameters, return types, variable declarations, constant declarations, and full-array variable reassignment (e.g. `a = [1, 2]` on a `[Number; 3]` binding raises `ArraySizeMismatch`).
Element-wise assignment via indexing (`a[i] = v`) does not change the array length and is always allowed.
The size expression `N` in `[Type; N]` is evaluated at declaration time in the current scope; any expression that produces a non-negative integer is legal, including `let`-bound variables.
An empty array literal `[]` without a type annotation is a `TypeMismatch`; add an explicit annotation such as `let a: [Number] = [];`.
All elements of an array literal must share the same type; the first element's type defines the element type and any subsequent mismatch raises `ArrayElementTypeMismatch`.
Negative indices are not supported; `arr[-1]` raises `IndexNotInteger`.
For last-element access, use `arr[arr.len() - 1]`.

#### NOTE 10; Network validation strictness

Nexus is strict about network validation, uniformly for nodes and groups.
Every `node ID` expression must refer to a `node type` declaration in scope (declared locally or imported via `use`); unknown type names are a hard error.
Every port access on a node or group value is validated against the owner's schema; unknown port names are a hard error.
Every connection direction is checked; `out → in`, `out → inout`, `inout → in`, and `inout → inout` are legal; `in → *` and `* → out` are errors.
There is no permissive mode.
The `node` instantiation keyword takes a bare identifier (not a string literal) so that typos are caught by the normal identifier-resolution machinery.

#### NOTE 11; Top-level restrictions for `node type` and `use`

`node_type_decl` and `use_decl` are valid only at file top level, not inside blocks, groups, or function bodies.
This is enforced at parse time, not by the grammar (matching the existing pattern for `port_decl`, NOTE 5).

#### NOTE 12; `use` import semantics and search paths

`use libname;` loads `libname.nxs` from the directory of the importing file (each `use` resolves relative to its own file).
Additional search directories may be configured via the `-I`/`--search-path` CLI flag (repeatable); the file's own directory is searched first.
Each file is loaded at most once per program, tracked by canonical absolute path (symlinks resolved).
All top-level declarations from the imported file; `node type`, `const`, `fn`, etc.; are flat-merged into the importing file's top-level scope; no visibility distinction.
Imports are transitive: if `foo.nxs` imports `bar.nxs`, any file that imports `foo.nxs` also sees `bar`'s declarations.
Name collisions between `use`d decls are an error at load time.

#### NOTE 13; Shared namespace for types and values

Types and values share a single namespace.
Declaring `node type Foo { ... }` reserves the identifier `Foo`; a later `let Foo = ...` in the same scope is `DuplicateDeclaration`.
This matches how built-in type names (`Number`, `String`, `Group`, `Node`) are already reserved.
`Array` is a reserved keyword but is not usable as a standalone type annotation; `let a: Array;` is a parse error; use `[Type]` or `[Type; N]`.
The `Group` base type matches any group value regardless of its port schema; connection-time validation uses the value's actual schema, not the binding's declared type.
Port names within a `node type` declaration must be unique across all directions; `node type Foo { in x; out x; }` raises `DuplicatePortName`.

#### NOTE 14; `Node` base type vs. schema names

The expression-type of a `node ID` expression is `Node` (the base type), regardless of which `node type` was named.
The schema name (e.g., `Foo`) identifies the port set for validation but is not itself a type in the type system; `let x: Foo = node Foo;` raises `TypeMismatch`; write `let x: Node = node Foo;`.
Port-reference validation happens at runtime based on the node's schema name.

#### NOTE 15a; Node naming; direct binding vs. anonymous

A `node ID` expression bound as the **direct** initializer of a `let`/`const` (e.g., `let name = node Foo;`) adopts the binding name as its source-level name.
Indirection; blocks, function or closure returns, array elements, bare expression statements; loses the binding name.
Anonymous `node ID` expressions receive an internal unique name of the form `node_<N>` where `<N>` is a monotonic counter shared with `NodeId` allocation.
Source code cannot refer to an anonymous node by name, but it still participates in the output model with a stable `NodeId`.
The anonymous prefix is constant (`node_`) rather than type-derived because node type names share a single flat namespace; the counter alone carries uniqueness, and a constant prefix makes every anonymous instance visibly a node regardless of schema.
The counter is per-`Interpreter` instance, starts at 0, and increments in evaluation order; `use` imports do not reset it.
Nodes created inside `for` loops receive sequential `node_<N>` names in iteration order.

#### NOTE 15b; Port references; first-class values and identity

Accessing a port on a node or group (e.g. `src.output`) produces a port reference (`PortRef`) value.
The port name is validated against the declared schema; referencing an unknown port is a hard error.
`PortRef` values are first-class: they can be bound to variables (`let p: PortRef = src.output;`), stored in arrays, and captured by closures.
`PortRef`, `Node`, and `Group` values are not equality-comparable; `==`/`!=` on them is `InvalidComparison`.
In general, `==`/`!=` requires both operands to be the same type.
Capturing a `Node`, `Group`, or `PortRef` in a closure preserves its identity; the captured value refers to the same instance as the original binding.
A captured `PortRef` retains its original endpoint identity; the resulting connection is added to the group being evaluated at the connection point (or the top-level connection list if outside all groups).
A closure that _creates_ a node (e.g. `|| node Foo`) produces a new instance on each call.
The same identity rules apply to groups.

#### NOTE 15c; `connect` statement semantics and validation

`connect` (`->`) is a statement, not an expression.
Both operands must be port references; any other type is a hard error.
Legal direction pairings are `out → in`, `out → inout`, `inout → in`, and `inout → inout`; all other combinations are rejected.
The same validation applies uniformly to node-to-node, node-to-group, group-to-node, and group-to-group connections.
Self-loops (connecting two ports on the same node or group) are permitted; the direction rules alone govern legality.
A self-loop appears in the network summary as two endpoints with the same node path but different port names.
Duplicate-connection detection is per-group: the same source/destination pair is rejected within the same group's connection list, but identical structure in two distinct group instances is not a duplicate.
A comma-separated list of targets is supported for fan-out (`a.out -> b.in, c.in, d.in;`); this is equivalent to N individual connection statements and all targets are validated independently.
Chaining (`a -> b -> c;`) is not supported; `->` is not an expression operator.

#### NOTE 16; `group` expression; port schema and identity

A `group { ... }` expression produces a `Group` value with a port schema; the set of ports declared in the body via `port NAME = self.member.port;`.
Port direction is inferred from the referenced member port and carries through unchanged.
The schema is fixed when the closing `}` is reached and governs all subsequent port accesses and connections on that group value, exactly as a `node type` schema governs its instances.
Each `group { ... }` evaluation creates a distinct identity.
Port existence and direction compatibility are validated uniformly for all connection kinds: node-to-node, node-to-group, group-to-group, and group-to-node.

#### NOTE 17; Group member declarations and implicit `self`

Inside a `group { ... }` body, child nodes and sub-groups are bound as members with `self.NAME = expr;`.
The right-hand side must be a `Node` or `Group`; other types are rejected.
Ordinary `let`/`const`/`fn` declarations in the body are plain local bindings and do **not** become group members.
Members may be addressed either as `self.NAME` (explicit) or as a bare `NAME` (implicit `self.`) within the same group body; the bare form resolves to the member only after the normal lexical scope lookup misses, so a local `let x = ...;` in the body shadows a member `self.x` in its lexical scope, while explicit `self.x` continues to refer to the member.
The implicit form is the idiomatic style for references; member _declarations_ always require explicit `self.`.
The contextual `self` keyword is only valid as a path prefix inside a `group { ... }` body.
Member names are unique and immutable; reassigning `self.NAME` is an error.
Port declarations and connection endpoints may use either the explicit `self.member.port` form or the implicit `member.port` form, or a local variable holding a `PortRef` (e.g. `let p = source.output;`).
A closure defined inside a group body may capture a member by bare name; the member value is snapshotted into the closure's captures at creation time, identical to capturing any other free variable.
Member visibility ends at the enclosing `group { ... }`'s closing brace and does **not** leak into the bodies of `fn`s or closures _called_ from within the group.
A given node or group can be a member of at most one group; binding the same value under two different names or in two different groups is an error.
Members and ports occupy separate namespaces: from outside the group, `g.foo` resolves against the port schema only.
An empty group (`group {}`) has an empty port schema; every port access on it raises `UndefinedPort`.
`print` of a `Node` or `Group` value produces a debug representation; for stable structured output, use the network summary returned by `eval()`.

#### NOTE 18; Network summary ordering and path format

The network produced by `eval()` is deterministic regardless of declaration or evaluation order.
Nodes and groups are listed in ascending lexicographic order of their path.
Connections are listed in ascending lexicographic order of their `from` path, with `to` path as a tiebreaker.
This ordering is guaranteed by the implementation and may be relied upon in tests and tooling.
Network paths are composed of `/`-separated identifier segments with **no leading `/`** (e.g. `source`, `pipeline/source`, `pipeline/subsystem/node`).
Since identifiers consist of `[a-zA-Z0-9_]` characters only, segments cannot contain `/` and path collisions between segments are impossible.

#### NOTE 19; `assert` expression semantics

`assert expr;` evaluates `expr` and raises `AssertionFailed` if it is `false`; if it is not a `bool`, raises `ConditionNotBoolean`.
`assert expr, msg;` evaluates `msg` **only** when the assertion fails (lazy evaluation).
If `msg` does not evaluate to a `String`, raises `TypeMismatch`; the message must be a string literal or a string-valued expression.

#### NOTE 20; Top-level result expression

The top-level result of the program is treated as the final program result (excluding any side-effects like print output).
Top-level results can be empty (i.e. no result), or any value.
Values of type `node`/`group`/connections contribute to the network-level output of the program.
Only one top-level trailing semicolon-less expression is allowed; a second one before it (without `;`) is a `MultipleTopLevelResults` error.

#### NOTE 21; Unused value warnings

The interpreter emits **warnings** for unused values: any `let`/`const`/`fn` binding that is never read, a `for` loop variable that is never used inside the body, and any expression statement whose result is non-unit and is discarded.
Warnings do not stop interpretation.
To suppress a warning, use `_` as the exact binding name (e.g. `let _ = expr;`) or as the loop variable (`for _ in expr { ... }`).

#### NOTE 22; Loop semantics; breaks, iteration limit, and continue

A `loop` body must contain at least one `break` statement; this is a syntactic (parse-time) check; a conditional `break` is sufficient.
The iteration limit (`MAX_LOOP_ITERATIONS = 1_000_000`) is **cumulative** across all nested loop constructs (`loop`, `while`, `for`) within one function invocation; deeply nested loops that each stay individually under the limit will still fire `LoopIterationLimitExceeded` once their combined total exceeds it.
The limit resets to 0 on every function call entry.
`continue` carries no value; `continue expr;` is a parse error.

#### NOTE 23; `Number` type and numeric constraints

`Number` is Nexus's single numeric type (64-bit floating-point).
Array indices and range bounds must be non-negative integers: any `Number` whose fractional part is zero and whose value is ≥ 0.
A fractional or negative value raises `IndexNotInteger` or a similar range-bound error.

#### Operator precedence

Operator precedence ordering is similar to Rust and C/C++ precedence levels.

Precedence levels from _high to low_ in order:

|     Operators     | Associativity | Description                                    |
| :---------------: | :-----------: | :--------------------------------------------- |
|  `.` `x()` `x[]`  | Left-to-right | Member access, functional call, array indexing |
|    `!` `+` `-`    | Right-to-left | Unary operators                                |
|    `*` `/` `%`    | Left-to-right | Factor operators                               |
|      `+` `-`      | Left-to-right | Addition and subtraction                       |
| `<` `<=` `>=` `>` | Left-to-right | Relational operators                           |
|     `==` `!=`     | Left-to-right | Equality operators                             |
|       `&&`        | Left-to-right | Logical AND operator                           |
|      `\|\|`       | Left-to-right | Logical OR operator                            |
|    `..` `..=`     | Left-to-right | Range definitions                              |

#### Operator note; Type restrictions and special cases

Unary `-` and `!` apply to `Number` and `bool` respectively.
Unary `+` is restricted to `Number`; applying it to any other type is a runtime `InvalidUnaryPlus` error.
Relational operators (`<` `<=` `>` `>=`) are defined on `Number` (numeric) and `String` (lexicographic / Unicode code-point order).
`==`/`!=` require both operands to be the same type; `Node`, `Group`, and `PortRef` values are never equality-comparable.
String concatenation via `+` is supported for `String + String`, `String + Number`, `Number + String`, `String + bool`, and `bool + String`; the non-string side is stringified via its display representation.
All other type combinations (e.g. `String + Array`) raise `InvalidOp`.
The `->` operator is only valid at statement level as a `connect` statement or as a return-type separator in `fn`/closure syntax; using it in expression position is a parse error.
Range operators (`..`, `..=`) are non-chainable; a second range operator in the same expression without parentheses is a parse error.

### Language keywords

#### Base language keywords

|  Keyword   | Description                                                      |
| :--------: | :--------------------------------------------------------------- |
|  `assert`  | Runtime assertion                                                |
|  `break`   | Loop break (with optional value)                                 |
|  `const`   | Constant declaration                                             |
| `continue` | Loop continuation                                                |
|   `else`   | Conditional expression                                           |
|    `fn`    | Function declaration                                             |
|   `for`    | Loop expression                                                  |
|  `group`   | Component group instantiation                                    |
|    `if`    | Conditional expression                                           |
|    `in`    | `for` loop iterator / input port direction                       |
|  `inout`   | Bidirectional port direction                                     |
|   `let`    | Variable declaration                                             |
|   `loop`   | Unconditional loop expression                                    |
|   `mut`    | Variable mutability specifier                                    |
|   `node`   | Component instantiation / type declaration                       |
|   `out`    | Output port direction                                            |
|   `port`   | Group port declaration                                           |
|  `print`   | Print expression result                                          |
|  `return`  | Return statement                                                 |
|   `self`   | Group member reference (contextual; inside `group { ... }` only) |
|   `type`   | Node type declaration (after `node`)                             |
|   `use`    | External use statement                                           |
|  `while`   | Loop expression                                                  |

#### Language type keywords

| Keyword  | Description                                                                                               |
| :------: | :-------------------------------------------------------------------------------------------------------- |
| `Array`  | Homogeneous array of other types (reserved; not usable as a standalone type; use `[Type]` or `[Type; N]`) |
| `Group`  | Component group                                                                                           |
|  `Node`  | Component                                                                                                 |
| `Number` | Number type                                                                                               |
| `String` | String type                                                                                               |
|  `bool`  | Boolean logic type                                                                                        |

### Builtin methods

Some types have builtin methods:

|   Type   | Method  | Description                                                                                                                                       |
| :------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Array`  | `len()` | Returns the length of the array                                                                                                                   |
| `String` | `len()` | Returns the length of the string (in bytes; a string with multi-byte UTF-8 characters has a byte length greater than its visible character count) |

#### Method note; Built-in methods in v1

In v1, built-in methods are a closed set.
Only `Array` and `String` have built-in methods.
Calling any method on `Number`, `bool`, `Node`, `Group`, or `PortRef` raises `UndefinedMethod`.
`print;` (no expression) emits a blank line.
The interpreter is fail-fast in v1: the first error stops execution; multi-error reporting is future work.

## TODO

- Node and group properties.
- Factory function return type inference (currently requires `-> Group` or `-> Node`).
- Edge types between ports (+ compatibility verification between connected ports).
- Support for integration into a visual IDE / generative tooling.
- FFI? How to deal with FFI of rich Unicode strings? Allow Unicode in Nexus, but restrict to ASCII over FFI boundaries? Limit connection/port/node names to ASCII.
- Tool idea: Nexus to Graphviz Dot description.
- Example integrations and implementations. Nexus to Rust library, Nexus to C++ library etc.
- Debugging commands in REPL tool.
- An LSP implementation.
- A code formatter.

## FAQ

### Why the name?

From the dictionary:

> **Nexus**; _nex·us_; meaning: _connection, link_

Of course this ties back to its place as a component network-description language.

### Why hand-code all the parsing and interpretation? 🤨

Yes.
I know.
There's excellent crates like [`syn`](https://github.com/dtolnay/syn) (for parsing Rust code) and [`pest`](https://github.com/pest-parser/pest) (for writing parsers), and probably many more.
But a large part of the fun of coding up homegrown projects like this is that it's OK to write most of it yourself, learning from the process and enjoying it!
The testing coverage is pretty good, so I'm confident it been done well.
