# EigenScript Syntax Guide

## Variables and Assignment

Assignment uses `is`. It is **outward-mutable**: if a variable exists in a
parent scope, assignment updates that binding. If not found, it creates a
new local.

```eigenscript
x is 42
name is "hello"
data is [1, 2, 3, 4, 5]
```

Use `local name is expr` when a binding must be created or updated only in
the current evaluator scope, even if a parent scope already has a name with
the same identifier. It does not create a new block scope for `if` or `loop`;
it binds whichever environment is active for that statement.

```eigenscript
name is "outer"
define example as:
    local name is "inner"
    return name

result is example of null   # "inner"
name                         # still "outer"
```

### Numeric Semantics

Numbers are finite by construction. EigenScript does not expose `NaN` or
`Inf` values to user code:

```eigenscript
1 / 1e-320       # 1e+308
sqrt of -1       # 0
exp of 999999    # 1e+308
asin of 5        # 1.5708
num of "nan"     # 0
```

Exact division or modulo by zero emits a warning and returns `0`. Results
that would overflow to infinity saturate at `+/-1e308`; `NaN` collapses to
`0`; domain-limited functions clamp inputs where appropriate.

### Compound Assignment

All arithmetic, bitwise, and shift operators have compound forms:

```eigenscript
x += 3          # x is x + 3
x -= 1          # x is x - 1
x *= 2          # x is x * 2
flags |= 0x80   # flags is flags | 0x80
flags &= 0x0F   # flags is flags & 0x0F
val <<= 4       # val is val << 4
```

These work on variables, dot-access, and index-access:

```eigenscript
obj.score += 10
arr[i] *= 2
buf[0] ^= 0xFF
```

Available: `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`

## Functions

### Named Parameters (recommended)

Define functions with named parameters in parentheses:

```eigenscript
define add(a, b) as:
    return a + b

result is add of [3, 4]    # 7
```

```eigenscript
define greet(name, age) as:
    return f"Hello {name}, you are {age}"

print of (greet of ["Jon", 30])
```

The argument list is unpacked into the named parameters automatically.
`n` is still available as the raw argument for backward compatibility.

### Classic Style

Functions can also be defined without named parameters. The argument is `n`:

```eigenscript
define square as:
    return n * n

result is square of 5    # 25
```

For multiple arguments in classic style, pass a list and unpack manually:
```eigenscript
define add_three as:
    return n[0] + n[1] + n[2]

result is add_three of [10, 20, 30]    # 60
```

### No arguments

Pass `null`:
```eigenscript
define greet as:
    print of "Hello!"

greet of null
```

## String Interpolation

Prefix a string with `f` to enable expression interpolation with `{...}`:

```eigenscript
name is "World"
x is 42
print of f"Hello {name}!"
print of f"x = {x}, doubled = {x * 2}"
print of f"list length: {len of items}"
```

Expressions inside `{}` are evaluated and converted to strings. Use `\{` and
`\}` for literal braces.

## Interactive REPL

Run `eigenscript` with no arguments to enter the REPL:

```
$ eigenscript
EigenScript 0.6.0
Type 'exit' or Ctrl-D to quit.

eigs> x is 42
eigs> print of f"x = {x}"
x = 42
eigs> define double(n) as:
...       return n * 2
...
eigs> double of 21
=> 42
```

Multi-line input (functions, loops, conditionals) is detected automatically
when a line ends with `:`. A blank line ends the block.

## Conditionals

```eigenscript
if x > 0:
    print of "positive"
elif x == 0:
    print of "zero"
else:
    print of "negative"
```

## Loops

**While loop:**
```eigenscript
loop while counter < limit:
    counter is counter + 1
```

**For loop** — iterates over a list:
```eigenscript
for i in range of 10:
    print of i

for item in items:
    print of item
```

## Error Handling

```eigenscript
try:
    x is items[100]
catch err:
    print of f"Caught: {err}"
```

Errors inside a `try` block are caught and bound to the variable after
`catch`: runtime errors bind their message string; a `throw`n value
binds **unchanged**, so errors can carry data:

```eigenscript
try:
    throw of {"kind": "validation", "field": "age"}
catch e:
    print of e.kind    # "validation"
```

Without a `try` block, an error is fatal: it prints to stderr with a
stack trace (every frame from the failure to the top level) and the
program exits non-zero.

Use `throw` to raise errors from user code:

```eigenscript
define safe_divide(a, b) as:
    if b == 0:
        throw of "division by zero"
    return a / b

try:
    result is safe_divide of [10, 0]
catch err:
    print of f"Error: {err}"
```

Try/catch blocks can be nested. Errors re-thrown in a catch block are caught
by the outer try.

## Closures

Functions capture their defining environment:

```eigenscript
define make_adder(x) as:
    define adder(y) as:
        return x + y
    return adder

add5 is make_adder of 5
print of (add5 of 10)    # 15
```

This works for factory patterns, callbacks, and higher-order programming.

## Operators

**Arithmetic:** `+`, `-`, `*`, `/`, `%`
**Comparison:** `==`, `!=`, `<`, `>`, `<=`, `>=`
**Logical:** `and`, `or`, `not`
**String:** `+` (concatenation when *both* sides are strings)

### Type rules

- **`+`** adds two numbers or concatenates two strings. It does **not**
  coerce across types — `"n=" + 42` raises. To build text from mixed
  types use an f-string (`f"n={count}"`) or `str of`.
- **Ordering** (`<`, `>`, `<=`, `>=`) requires both operands to be the
  same comparable type (number/number or string/string). Comparing mixed
  types (e.g. `"3" < 4`) raises a runtime error rather than silently
  returning false — catch it with `try`/`catch` if you mean it.
- **Equality** (`==`, `!=`) never coerces. Numbers, strings, and null
  compare by value; lists and dicts compare *structurally* (so
  `[1,2] == [1,2]` is true and `match` works on list/dict patterns);
  functions compare by identity. Operands of different types are simply
  not equal — `"3" == 3` is `false`, never an error.

### `of` binds tighter than arithmetic

Function application with `of` binds tighter than `+`/`-`/`*`/`/`. So
`len of xs - 1` means `(len of xs) - 1`, which is usually what you want.
The flip side: `sqrt of x + 1` means `(sqrt of x) + 1`, *not*
`sqrt of (x + 1)`. When the argument is itself an arithmetic expression,
parenthesize it:

```eigenscript
y is sqrt of (x + 1)      # not  sqrt of x + 1
print of (total + tax)    # not  print of total + tax  (would print total, then error)
```

## Lists

```eigenscript
items is [1, 2, 3, 4, 5]
print of items[0]         # 1
print of (len of items)   # 5
append of [items, 6]      # mutates items
```

Note: list literals must be on a single line.

## Dictionaries

Dictionary literals use `{}` with string keys:

```eigenscript
config is {"host": "localhost", "port": 8080, "debug": 1}
```

**Dot access:**
```eigenscript
print of config.host       # "localhost"
print of config.port       # 8080
```

**Bracket access:**
```eigenscript
key is "host"
print of config["host"]    # "localhost"
print of config[key]       # "localhost"
```

**Nested dictionaries:**
```eigenscript
app is {"db": {"host": "localhost", "port": 5432}, "name": "myapp"}
print of app.db.host       # "localhost"
```

**Builtins:**
```eigenscript
print of (keys of config)          # ["host", "port", "debug"]
print of (values of config)        # ["localhost", 8080, 1]
print of (has_key of [config, "host"])   # 1
dict_set of [config, "timeout", 30]      # mutates config
dict_remove of [config, "debug"]         # mutates config
```

## eval

The `eval` builtin executes a string as EigenScript code and returns the result:

```eigenscript
result is eval of "1 + 2"          # 3
eval of "print of 42"              # prints 42
code is "x is 10\nprint of x"
eval of code
```

Evaluated code runs in the caller's current scope. At top level this is the
global scope; inside a function, new names stay in that function's scope.

## Modules (import / load_file)

`import name` loads a module into a **namespace** — a dict named
`name` holding the module's top-level definitions. It resolves
`lib/name.eigs` (the standard library) first, then `name.eigs`
relative to your script. Nothing leaks into the global scope; names
starting with `_` stay private to the module.

```eigenscript
import math
result is math.clamp of [15, 0, 10]    # 10
import shapes                          # your shapes.eigs, next to the script
a is shapes.area of 2
```

`load_file` is the non-namespaced form — it executes a file directly
in the current scope:

```eigenscript
load_file of "lib/math.eigs"
print of (abs of -5)
```

The loaded file's definitions are added to the global environment.

### Path resolution

For non-absolute paths, `load_file` searches (in order):

1. The path as given, relative to the current working directory.
2. `<script_dir>/<path>` — relative to the script being executed.
3. `<script_dir>/../<path>` — relative to the script's parent directory.
4. `<executable_dir>/../<path>` — relative to the EigenScript binary.
5. `<executable_dir>/../lib/eigenscript/<path>` — installed stdlib layout.
6. `~/.local/lib/eigenscript/<path>` — user-local stdlib fallback.

The third step is what lets a script in `examples/` pick up `lib/foo.eigs`
without the caller having to `cd` to the repository root. The executable
relative steps let external projects use the source-tree or installed stdlib
without copying `lib/*.eigs` into each project. `..` segments
embedded in the `load_file` argument itself are resolved by the OS
normally — there is no sandbox, so a script can read any file the
invoking user can read.

Absolute paths (starting with `/`) are used verbatim with no fallback.

## Interrogatives — Ask Your Code

Every value in EigenScript tracks its own observer state. Interrogatives
are the query interface — zero cost when you don't ask:

| Keyword | Returns | Example |
|---------|---------|---------|
| `what is x` | Current value (scalar), or length (list/string) | `what is loss` → `55.0` |
| `who is x` | Variable name as string | `who is loss` → `"loss"` |
| `when is x` | Observation age (number of assignments) | `when is loss` → `4` |
| `where is x` | Entropy (information content) | `where is loss` → `0.832` |
| `why is x` | dH (rate of change, negative = improving) | `why is loss` → `-0.15` |
| `how is x` | Stability score (0 = unstable, 1 = stable) | `how is loss` → `0.95` |

```eigenscript
loss is 100.0
loss is 80.0
loss is 55.0

print of (what is loss)    # 55
print of (why is loss)     # negative — loss is decreasing
print of (how is loss)     # high — change is consistent
print of (when is loss)    # 3 — three assignments
```

Use interrogatives for debugging, convergence detection, or understanding
runtime behavior without writing logging code.

### Temporal Interrogatives — Ask About the Past

Two forms reach backward through a variable's assignment history.
Both work on top-level (global) bindings, are always on — no
environment variable or debug build required — and return `null`
on a miss rather than erroring.

**`prev of x`** — the value `x` held immediately before its most
recent assignment:

```eigenscript
loss is 100.0
loss is 80.0
print of (prev of loss)    # 100 — the value before the latest assign
```

Returns `null` when `x` has been assigned fewer than two times. Note
the connector: `prev` uses `of` (like a function call), while the six
observer interrogatives use `is`.

**`at <expr>` qualifier** — any value-returning interrogative can be
pinned to a source line. The operand is a full expression evaluated at
query time; the answer is the last value bound at or before that line:

```eigenscript
x is 1        # line 1
x is 2        # line 2
x is 3        # line 3

print of (what is x at 2)    # 2
print of (prev of x at 3)    # value before the assign at/before line 3
print of (when is x at 2)    # assignment count up to line 2
print of (who is x at 2)     # "x"
print of (where is x at 2)   # entropy as of the line-2 assign
print of (why is x at 2)     # dH as of the line-2 assign
```

All seven temporal forms support `at`. The observer-derived three
(`where`/`why`/`how`) answer from per-assign observer snapshots that
are captured only when the compiled program actually contains such a
query — programs that never ask pay nothing. Queries before the first
assignment to the name return `null`.

**`state_at of line`** — the whole-program version: returns a dict
mapping every tracked binding to the value it held at or before `line`
(names not yet assigned by then are omitted). See
[BUILTINS.md](BUILTINS.md).

`prev`, and `at` inside an interrogative, are soft keywords — outside
these positions they parse as ordinary identifiers, so existing code
using them as variable names keeps working.

For recording full execution traces and replaying nondeterministic
runs deterministically, see [TRACE.md](TRACE.md).

## Observer Semantics

The observer system classifies value trajectories automatically:

```eigenscript
status is report of loss   # "improving"
state is observe of loss   # [status, entropy, dH, prev_dH]
```

Six trajectory states: `improving`, `diverging`, `stable`, `equilibrium`,
`oscillating`, `converged`.

**Predicates** — boolean keywords for use in conditions:
```eigenscript
loop while not converged:
    loss is loss * 0.9
    if stable:
        print of "reached stable band"
    if oscillating:
        lr is lr * 0.5   # reduce learning rate
```

The observer tracks every variable. Predicates check the most recently
observed value. `report of x` and `observe of x` let you query any
specific variable.

## Unobserved Blocks — Opting Out

The observer runs on every assignment so interrogations are always
cheap. That cost is unavoidable when the value *might* be observed
later — and wasted when you know a region won't be.

The `unobserved` block is the user-level opt-out:

```eigenscript
unobserved:
    game.px is game.px + game.vx * DT
    game.py is game.py + game.vy * DT
    game.angle is game.angle + DT
```

Inside the block:

- **Numeric mutations land in place.** Local-var and dict-field
  assignments whose right-hand side is pure arithmetic over numbers
  update the existing `Value`'s data, keeping pointer identity. No
  intermediate allocation.
- **Observer tracking is skipped.** Entropy / dH / obs_age on touched
  values stay frozen at whatever the last observed write left them.
  Interrogatives (`why is game.px`, `how is game.px`) will return
  stale answers until the next observed assignment.
- **The last-observer pointer is not updated.** Predicates in scope
  continue to report whatever was last observed outside the block.

Outside the block, normal observed behavior resumes. Nested `unobserved`
blocks compose — the inner block doesn't re-enable observation.

This mirrors what tensor code already does at the C level via
`arena_mark` / `arena_reset` (lifecycle-scoped) and the save-restore
pointer pattern in `numerical_grad_*` (identity-preserving raw swap).
`unobserved` is the same idea at statement scope, visible in user
source.

**When to reach for it:** game loops, physics integrators, and other
hot paths where you'll inspect state via normal reads (`game.px`),
not via interrogatives (`what is game.px`).

## Standard Library

Libraries live in `lib/` and are loaded with `load_file`:

| Module | Functions |
|--------|-----------|
| `lib/math.eigs` | `abs`, `max_val`, `min_val`, `clamp`, `lerp`, `dot` |
| `lib/list.eigs` | `map`, `filter`, `reduce`, `reverse`, `zip`, `flatten` |
| `lib/string.eigs` | `join`, `repeat`, `pad_left` |
| `lib/sanitize.eigs` | `sanitize_text`, `is_garble`, `clean_response` |
| `lib/auth.eigs` | `auth_login`, `auth_check`, `auth_logout` |

See [BUILTINS.md](BUILTINS.md) for the complete builtin reference.

## Limitations

**Single-line collections:**
List and dict literals must be on a single line.

**No break/continue:**
Use flag variables to exit loops early.
