Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
62bc2a7
refactor(cli): update main package path resolution to include workdir…
emil14 Dec 27, 2025
c11f38d
refactor(parser): remove switch statement support from parser and rel…
emil14 Dec 27, 2025
bb40fee
refactor(e2e): add end-to-end test for classify_int and refactor main…
emil14 Dec 30, 2025
36b59cf
refactor(e2e): simplify Main function and improve Handler logic in ma…
emil14 Dec 30, 2025
04179de
feat: introduce `Race` router, `Enrich` component, `Neva-to-Mermaid` …
emil14 Jan 3, 2026
aa7a8bb
feat: introduce Neva to Mermaid workflow, refactor standard library c…
emil14 Jan 3, 2026
8f8a8a6
feat: introduce new builtin control flow components, enhance compiler…
emil14 Jan 3, 2026
e19b398
feat: Introduce `streams.Filter` module, refactor `Reduce`'s accumula…
emil14 Jan 4, 2026
af43d1b
feat: introduce `streams.Map` component and enhance compiler analysis…
emil14 Jan 4, 2026
c345bce
feat: Add Neva to Mermaid workflow, refactor examples to use `streams…
emil14 Jan 4, 2026
9d605bc
feat: Add Neva to Mermaid workflow, update examples, and refine compi…
emil14 Jan 5, 2026
571d12c
feat: introduce State component, refine component signatures and comp…
emil14 Jan 5, 2026
5fba86a
fix: Correct DI type constraint collection by using parent-specific s…
emil14 Jan 5, 2026
a96988f
feat: introduce `state` runtime function for managing and outputting …
emil14 Jan 5, 2026
353f2bc
feat: add race runtime func
emil14 Jan 5, 2026
0788bfc
feat: add `if` conditional router and register it in the functions re…
emil14 Jan 5, 2026
551f369
feat: Add error handling to `Print` and `Scanln` functions with a new…
emil14 Jan 5, 2026
09b3863
refactor(runtime): remove accumulator function and its registration
emil14 Jan 6, 2026
f3b69cd
refactor: replace internal `state` runtime function with new `accumul…
emil14 Jan 7, 2026
d156d1a
refactor: explicitly handle errors from `fmt.Scanln` components.
emil14 Jan 7, 2026
f5a908d
feat: add missing ? to fmt Print usage
emil14 Jan 7, 2026
aa1b4a3
refactor: explicitly connect `scanln`'s `res` output to `println`'s `…
emil14 Jan 7, 2026
7338c62
refactor: simplify `add_numbers_from_stdin` example's graph and `App`…
emil14 Jan 7, 2026
5a9966b
docs: add self-improvement protocol and renumber sections; fix: ensur…
emil14 Jan 7, 2026
ee532f5
refactor: change App function's stop return type from int to any
emil14 Jan 7, 2026
8bdeccd
feat: Implement `Switch.case` port detection for pattern matching, re…
emil14 Jan 7, 2026
80328fd
feat: Implement pattern matching analysis for Switch.case ports and s…
emil14 Jan 7, 2026
baa3a46
tmp
emil14 Jan 8, 2026
d6d5b33
feat(analyzer): catch switch connections to handle union differently;…
emil14 Jan 8, 2026
b596ade
Merge branch 'main' into remove_switch
emil14 Jan 10, 2026
a8302e6
fix(runtime:funcs:switch): do not unbox union for `else`; feat(std): …
emil14 Jan 11, 2026
0bbe33e
feat(e2e): add couple small tests for switch and tagged unions with a…
emil14 Jan 12, 2026
63fe26c
feat(docs): style guide and agents
emil14 Jan 14, 2026
b12b2a7
feat(analyzer): add switch_logic.go for resolving switch:case[i] outp…
emil14 Jan 14, 2026
c4ce372
feat(analyzer:network): integrate switch logic (tmp way)
emil14 Jan 14, 2026
2fbe13f
refactor(analyzer:network): add some comments explaining why do we do…
emil14 Jan 14, 2026
18fc60b
refactor(analyzer): remove unused errors
emil14 Jan 15, 2026
4663fce
refactor(analyzer:switch): minore movement; refactor(runtime): add co…
emil14 Jan 16, 2026
16329ce
refactor(std,runtime:funcs): remove if.go and implement `If` in neva …
emil14 Jan 16, 2026
c224b6e
fix(runtime:funcs:registry): add missing "if" removal
emil14 Jan 16, 2026
1f28a28
fix
emil14 Jan 17, 2026
85eb6e9
upd(docs:qna): add overloading section; refactor(std:streams): remove…
emil14 Jan 18, 2026
2b2d23e
fix(examples:switch): use msg from switch to panic, not "...", becaus…
emil14 Jan 19, 2026
1c33cee
refactor(examples:switch): add comments
emil14 Jan 19, 2026
079e9a4
upd(ai)
emil14 Jan 19, 2026
f3007e6
fix(desugarer): add missing handling of implicit fanin created by des…
emil14 Jan 19, 2026
2db4897
refactor(desugarer:network): move implicit fanin handling to network …
emil14 Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 296 additions & 0 deletions .agent/session/switch_remove_summary.md

Large diffs are not rendered by default.

176 changes: 176 additions & 0 deletions .agent/session/workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Switch Removal: Rationale for Analyzer, IR, and Backend Changes

This document explains why the recent changes were made, using concrete Neva and Go examples, and highlights open questions about necessity. The goal is to make the intent explicit and to capture the edge case that triggered the runtime deadlock.

## 1) Why Switch Coverage Is a Separate Analyzer Pass

### The problem being solved

Switch coverage rules depend on **global usage**, not on a single connection. For example:

```neva
switch Switch<string>
---
'Alice' -> switch:case[0]
// else not connected
```

Whether `switch:else` must be used depends on **what `T` is** and **what case ports are connected** across the entire network. This is not local to one `analyzeConnection` call.

### Why not enforce in `analyzeConnection`

`analyzeConnection` is connection-local: it sees one sender-to-receiver link at a time. It does not know:

- Which other `case[i]` slots were used
- Whether `switch:else` is used somewhere else
- Whether `T` resolves to `bool` vs `union` vs other

So if we tried to enforce coverage there, we’d need **cross-connection state** (a node-level aggregator) or risk false positives/negatives.

### Why place it after `analyzeNetPortsUsage`

Coverage checks depend on the **final port-usage map** (`nodesUsage`) built during connection analysis. This map already detects:

- which slots are used for array ports
- whether `switch:else` is connected

That makes it the natural data source for Switch coverage rules. In other words:

- `analyzeConnection`: validate individual links
- `analyzeNetPortsUsage`: validate global port usage
- `analyzeSwitchUsage` (new): validate global Switch coverage

It is conceptually a **post-pass over the usage graph**.

### Could it be inside `analyzeNetPortsUsage`?

Yes, and that would also be reasonable. The current placement is a separate call immediately after `analyzeNetPortsUsage`. This keeps the logic isolated and easier to reason about, but it could be merged into `analyzeNetPortsUsage` if you prefer a single global-pass function.

### Example: Switch<bool> coverage needs global information

```neva
switch Switch<bool>
---
true -> switch:case[0]
false -> switch:case[1]
```

Coverage can be proven only if both literals are used; this needs the usage map of all `case` slots, which is not visible in a single connection check.

## 2) Why Switch Example Programs Were Adjusted

### What changed

The `switch` examples now forward `switch:else` directly to `:err` and define `err` as `any`:

```neva
// We keep :err as any to forward the unmatched string to Panic.
def App(start any) (stop any, err any) {
switch Switch<string>
---
switch:else -> :err
}
```

This was done so the user-facing behavior matches test expectations such as:

```
Enter the name: panic: Bob
```

If `switch:else` goes through `errors.New`, the panic message becomes the error object (struct) instead of the original string. The tests expect the raw string. This change is semantically consistent and improves the examples.

## 3) The Deadlock: Root Cause and the IR/Backend Changes

### The runtime symptom

The `examples/switch_fan_out` test deadlocked on the else path (e.g., input `Bob`). Trace shows:

```
sent | app/switch:else | "Bob"
recv | app/err_new:data | "..."
sent | app/err_new:res | {"text": "..."}
```

But **panic never receives** the error, so the program stalls.

### The underlying issue (edge case)

This was triggered by **fan-in to the virtual outport `:err`**.

Neva network:

```neva
[switch:else, scanln:err, println:err] -> :err
```

This is legal and common. It means *multiple senders converge on one virtual port*.

When IR is reduced and Go channels are generated, the virtual port (`out:err`) was accidentally split into **multiple channels**, one per sender. The `panic` node only listened to one of them. If another sender fired (like `err_new` after `switch:else`), the message went to a different channel and got stuck with no receiver.

### Why this didn’t show up before

It appears the old `switch` syntax path avoided the specific fan-in shape (or the paths weren’t exercised), so the bug stayed hidden. The new switch-as-node form routes else as a normal outport, which surfaced the existing flaw.

So yes: **this is an edge case that existed but was not triggered** prior to the switch refactor.

### Fix 1: Graph reduction must not collapse fan-in targets

Change in `internal/compiler/ir/graph_reduction.go`:

- Graph reduction now stops at any receiver with multiple incoming connections.
- This prevents collapsing multiple senders into independent channels.

Example:

```neva
[print:err, scanln:err] -> :err
```

This must preserve a shared receiver node instead of “flattening” to:

```
print:err -> panic
scanln:err -> panic
```

Without fan-in preservation, each becomes its own channel and panic only listens to one.

### Fix 2: Backend must reuse channels for virtual ports

Change in `internal/compiler/backend/golang/backend.go`:

- Virtual ports (`in`/`out`) do not correspond to runtime funcs, so they are **special**.
- When multiple connections target the same virtual port, they must **reuse the same channel**.

Example:

```neva
[scanln:err, err_new:res] -> :err
:err -> panic
```

Now all fan-in senders share a single channel feeding `panic`.

### Are these changes necessary?

Given the deadlock trace, yes — at least one of these changes is required:

- **Option A**: Make graph reduction fan-in aware (current fix).
- **Option B**: Allow reduction but fix backend channel assignment to merge fan-in targets.

The fix currently implements **both**, which is arguably conservative. If you want a narrower change, we can drop one of them and rely on the other, but we would need to verify that all fan-in cases still behave correctly.

## 4) Summary of Rationale

- **Switch coverage pass** is global by nature and requires full port usage. It is not a local connection check.
- **Switch examples** were updated to match expected user-facing output and align with `panic` behavior.
- **Graph reduction and Go backend** changes address a fan-in-to-virtual-port edge case that surfaced with switch refactor; this deadlock existed but was previously untested.

## 5) Open Questions / Follow-ups

- Do we want `analyzeSwitchUsage` folded into `analyzeNetPortsUsage` for cohesion?
- Should we keep both graph reduction and backend channel fixes, or narrow to one?
- Do we want a focused test case for fan-in to `:err` to lock this behavior?

If you want, I can add a minimal e2e test specifically for the virtual `:err` fan-in to prove the necessity of the fix.
138 changes: 138 additions & 0 deletions .agent/tasks/refactor_analyzer_network_task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Refactor Task: Analyzer Network Senders

## Problem Statement

The current implementation of sender analysis in the Neva compiler's analyzer is fragmented and contains significant logic duplication, specifically regarding `Union` senders.

## Identified Issues

### 1. Duplicated Logic for Union Senders

The logic for resolving union types, validating tags, and handling wrapped data exists in two places:

- `analyzeSender` in `internal/compiler/analyzer/senders.go`
- `getResolvedSenderType` in `internal/compiler/analyzer/network.go`

A comment in `network.go` (line 612) acknowledges this duplication:
_"logic of getting type for union sender partially duplicates logic of validating it... but it should be possible to refactor"_

### 2. Ambiguous Reachability and Ownership

In the primary connection analysis flow, the `Union` block in `getResolvedSenderType` is mathematically unreachable because `analyzeSender` handles the union and returns early. It is only called recursively by `getResolvedSenderType` itself when resolving predecessors of struct selectors.

### 3. Connection Analysis Is Not Local (Switch Coupling)

The current implementation passes the full `net` into `analyzeConnection`, so the claim that it is "connection-local" is inaccurate. In practice, connection analysis implicitly relies on switch-aware logic that is spread across the analyzer:

- **Node analysis**: Overload resolution depends on knowing the resolved type of `switch:case[i]` outports when the receiver is an overloaded node (e.g., `add`), so switch output typing bleeds into node analysis.
- **Connection analysis (normal path)**: `hasSwitchCaseReceiver` is used to compute `isPatternMatchingContext`, which later affects union sender handling (e.g., the `sender.Union.Data == nil` case). The result is also threaded into `getResolvedSenderType`, even though it appears unused per the existing refactor note.
- **Connection analysis (network loop)**: The `for i, sender := range analyzedSenders { ... }` loop inside `analyzeNormalConnection` contains additional switch-dependent validation and typing logic.

This makes connection analysis effectively network-scoped rather than local, and the switch-specific behavior interferes with overload resolution and type checking. It also means any refactor must preserve a delicate set of interactions across these phases.

Minimal dependency sketch (current flow, with functions):

```
nodes.go
getInterfaceAndOverloadingIndexForNode
getNodeOverloadVersionAndIndex
^ |
| needs switch:case[i] typing | requires resolved iface(s)
| for overloaded receivers v
network.go
analyzeConnection
analyzeNormalConnection
|
| calls to resolve types
v
senders.go
analyzeSender <--> getResolvedSenderType (network.go)
^ |
| hasSwitchCaseReceiver / | relies on node ifaces
| isPatternMatchingContext v
nodes.go
deriveNodeConstraintsFromNetwork
```

### 4. Indirect Recursion loop

There is a complex loop between `analyzeSender` and `getResolvedSenderType`:

- `analyzeSender` calls `getResolvedSenderType` (for non-union senders).
- `getResolvedSenderType` calls `analyzeSender` (to resolve wrapped union data).
- `analyzeSender` calls itself (recursive union data analysis).

### 5. Computational Inefficiency in Chains

When analyzing struct selector chains (e.g., `node:port -> .field1 -> .field2`), the `getResolvedSenderType` function re-resolves the entire preceding chain for every link. For a chain of length $N$, this leads to $O(N^2)$ type resolutions because previous results are not cached or passed through.

## Proposed Resolution

1. **Unify Type Resolution**: Consolidate all "get type" logic into `getResolvedSenderType`. `analyzeSender` should rely on this function for types and focus on validation.
2. **Flatten Recursion**: Refactor the relationship between `analyzeSender` and `getResolvedSenderType` to remove indirect recursion.
3. **Optimized Chain Analysis**: Modify the connection analysis to pass the resolved type of the previous link forward, eliminating the $O(N^2)$ re-resolution of chain predecessors.

## Additional Idea: Decouple Overload Resolution From Network Analysis

### Background: Current Cycle

Overload resolution for nodes lives in `getInterfaceAndOverloadingIndexForNode` and `getNodeOverloadVersionAndIndex` in `internal/compiler/analyzer/nodes.go`. The current flow looks like this:

- **Node analysis** needs a resolved component interface to validate node instantiation (ports, type params, `#autoports`, `#extern`, etc.).
- **Overload selection** depends on **network usage**: `deriveNodeConstraintsFromNetwork` inspects how the node is wired (senders/receivers) to infer constraints and select the best overload.
- **Network analysis** in turn needs resolved node interfaces to type-check connections.

This introduces a circular dependency: nodes -> network -> nodes. The current implementation sidesteps this by doing a lighter network scan during node analysis (derive usage constraints early), then doing the full network validation later.

### Proposed Split

Treat node analysis as **two separate responsibilities**:

1. **Instantiation validation** (lightweight):
- Resolve entity reference.
- Validate number and shape of type arguments (`TypeArgs`, `#autoports` preconditions).
- Validate DI arguments and syntactic correctness (but do not select a specific overload).
- Return *candidate* overloads for later filtering.

2. **Overload selection** (network-driven):
- During network analysis, use usage constraints (senders/receivers, chaining, inferred port types) to choose the best overload.
- The "node interface" used by network analysis becomes a *set* of possible interfaces rather than a single resolved one.

### Concrete Data Shape Change

Instead of `nodesIfaces map[string]foundInterface`, return something like:

- `map[string][]foundInterface` or
- `map[string]nodeCandidates` where `nodeCandidates` contains:
- list of candidate interfaces (one per overload)
- precomputed metadata (e.g., overload index, `#extern` flag, `#autoports` eligibility)
- resolved type args (if any)

This would keep the node analysis deterministic while deferring overload selection until network constraints are available.

### How This Helps

- Eliminates the "double check" on the network during node analysis.
- Removes the implicit coupling between `analyzeNodes` and `analyzeNetwork`.
- Makes overload resolution a first-class constraint solving step rather than a side-effect of node analysis.

### Implementation Notes and Risks

- **Constraint propagation**: `deriveNodeConstraintsFromNetwork` currently depends on resolved interfaces. It would need to accept candidate interfaces and filter them iteratively as constraints are gathered.
- **Error reporting**: "no compatible overload" vs "ambiguous overload" should be reported after network analysis (or when constraints become unsatisfiable).
- **Autoports (#autoports)**: This logic currently needs a concrete overload and type args to generate a synthesized interface. If overloads are deferred, the analyzer must either:
- generate interfaces for each candidate up front (if type args are fixed), or
- keep a lazy generator that can be resolved once constraints narrow down.
- **Native component overloads**: `isNativeComponentWithMultipleExterns` currently shortcuts via type args or inferred constraints. This logic should move to the network-driven phase, so it can leverage full usage info and avoid premature pruning.
- **Complexity risk**: If implemented naively, network analysis might have to reason across many candidates. A practical approach would be:
- first filter by static checks (type arg count, `#extern`, interface compatibility with explicit ports),
- then apply dynamic constraints from network senders/receivers.

### Suggested Direction

A good end state is a two-pass approach:

1. **Node candidate pass**: validate instantiation and build candidate overload set.
2. **Network constraint pass**: resolve overloads using full wiring constraints, then validate network strictly using the chosen overloads.

This preserves existing behavior but makes the dependency graph explicit and removes the need for a partial network scan during node analysis. It should also make the overload subsystem easier to test in isolation.
35 changes: 35 additions & 0 deletions .agent/workflows/deep-refactoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
description: A systematic approach to refactoring complex codebases.
---

# Deep Refactoring Workflow

Use this workflow when a component has become too complex to understand or modify safely, or when architectural flaws (e.g. duplicated logic) are identified.

## 1. Phase 1: Knowledge Mapping

The goal is to move from "mystery code" to "well-defined intent".

- **Plain English Specification**: Write a document describing what the function/component is _supposed_ to do in strictly plain English. Do not look at the code yet; use the current problem statement and requirements.
- **Dependency Visualization**: Use Mermaid flowcharts to map out how functions call each other. Identify:
- Indirect recursion loops.
- Points of high coupling.
- Redundant entry points.
- **Data Flow Analysis**: Trace how data (especially complex objects) transforms through the system.

## 2. Phase 2: Decoupling

The goal is to separate _what_ is being done from _how_ and _where_.

- **Extract Pure Logic**: Identify "dirty" functions that do multiple things. Extract the pure logic into standalone, side-effect-free functions, when makes sense.
- **Unify Entry Points**: Ensure there is a single source of truth for specific operations, unless clearly needed.

## 3. Phase 3: Tuning & Validation

The goal is to polish, optimize, and ensure correctness.

- **Complexity Reduction**: Target non-optimal code paths.
- **Readability Pass**: Use the "plain English" descriptions from Phase 1 to rename variables and update comments so they match the intent.
- **Verification**:
- Run existing tests.
- Add/Remove/Change tests when/if needed and/or contributes to code quality.
Loading
Loading