Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ tasks.json
scsi1.raw
scsi2.raw
cdrom4.iso
Cargo.lock
Cargo.lock
*.log
60 changes: 60 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Claude Instructions

## INVARIANTS (non-negotiable)

These are hard rules. Violating any of these is a session failure.

- **MCP TOOLS BEFORE EVERYTHING**: `search` first!! `search` everything before you do it so you don't flail around.`report_error` before attempting any fix. `check_compat` before writing any compat function. `search` before inventing any technique. NO EXCEPTIONS. Do not skip these because you think you already know the answer — your training data for IRIX is outdated and wrong. **ENFORCED AT TWO LEVELS**: (1) The knowledge MCP tracks MCP tool calls — warning at turn 3, blocking at turn 6+ (nudge_escalation_threshold=2). (2) A Claude Code PreToolUse hook tracks built-in tool calls (Edit, Write, Bash) — warning after 8 calls without MCP search, **Edit/Write BLOCKED after 20 calls**. The hook catches the blind spot where the MCP nudge system can't see built-in tools. Both levels reset when you call search/report_error/check_compat.
- **NO FIXES OUTSIDE MOGRIX RULES**: Every fix goes into `rules/`, `compat/`, or `patches/`. If you `sed` a file during debugging, that fix MUST end up in a YAML rule. If it doesn't, you have failed.
- **NO INLINE C IN YAML**: C files go in `patches/packages/<pkg>/`, referenced via `add_source`. No heredocs generating .c/.h files in `prep_commands`.
- **`add_rule` IMMEDIATELY AFTER FIX CONFIRMED**: The moment a build passes after a fix, call `add_rule` with `file_path` pointing to the authoritative rule file. Do not batch to session end — context pressure causes deferred `add_rule` calls to be dropped.
- **DB IS CACHE, FILES ARE AUTHORITATIVE**: Rule files (`rules/packages/*.yaml`, `rules/generic.yaml`, `compat/catalog.yaml`, `rules/methods/*.md`) are the source of truth. `add_rule` must include `file_path`.
- **DELEGATE LONG DEBUGS**: >2 failed fix attempts for the same error → stop and spawn a sub-agent with `Task()`. Pass it the error text, file paths, and tell it to use MCP tools first. Never let debug trace flood parent context.
- **REDIRECT BUILD OUTPUT**: Never let rpmbuild output flood context. Log to file. Use sub-agents (`Task(model="haiku")`) for reading large build logs.
- **INVOCATION**: `uv run mogrix <command>`. No other invocation method works.

---

## Session Protocol

1. Call `session_start` MCP tool
2. Work — use MCP tools for every error, every symbol, every lookup
3. `add_rule` immediately after each confirmed fix
4. Call `session_handoff` MCP tool before ending

---

## MCP Tool Quick Reference

These are your primary interface. Use them before reading files, before grepping, before guessing.

| When | Tool | What it does |
|------|------|--------------|
| Hit any error | `report_error` | Logs error AND auto-searches rules+compat+errors in one call |
| Need to look something up | `search` (or `knowledge_query`) | FTS5 search across all knowledge, rules, errors, negative knowledge |
| Confirmed a fix | `add_rule` | Stores the fix with `file_path` to authoritative rule file |
| Learned something | `add_knowledge` (or `report_finding`) | Stores findings, decisions, insights |
| Found a dead end | `add_negative` | Stores anti-patterns so they're never repeated |
| Session start | `session_start` | Context summary, last handoff, active tasks |
| Session end | `session_handoff` | Snapshot state for next session |


## Context Management

**Tuned for 1M context (Opus 4.6).** Sessions can safely run 400+ turns. Compaction/handoff urgency is low. Focus is on knowledge capture quality, not checkpoint frequency.

- **Sub-agents for investigation**: Any task requiring >200 lines of output gets a sub-agent. `Task(model="haiku")` for build log reading. Sub-agent investigates and returns a concise summary; parent applies the fix.
- **Re-orientation check every 8 tool calls**: Am I using MCP tools? Am I freestyling a fix that's probably already documented? Have I stored my findings? If unsure, call `session_start`.
- **Store knowledge continuously**: `report_error` when you hit it → fix it → build passes → `add_rule` right then. Don't accumulate findings to store later. The nudge system fires a store reminder after 6 turns without a store.
- **Checkpoint at 30 turns**: `save_snapshot` or `session_handoff` to reset the checkpoint counter. Mandatory stop at 60 turns (enforced, blocks all tools).
- **Batch builds**: Max 2-3 background agents, each with its own rpmbuild directory. Only the orchestrator updates rule files. See `rules/methods/task-tracking.md`.

**MCP enforcement thresholds** (mcm-engine.yaml):
- Store reminder: 6 turns
- Checkpoint: 30 turns
- Mandatory stop: 60 turns (+10 grace)
- Nudge escalation: 2 ignores → blocking
- MCP-first enforcement: warning at turn 3, blocks at turn ~7 if no search/report_error/check_compat called

---

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ developer_ip7 = [] # CP0 Compare/timer calibration stats and debug prints
# Lightning: pedal-to-the-metal build — disables breakpoint checks and traceback buffer updates.
# Incompatible with interactive debugging. For end-user / benchmarking builds only.
lightning = []
# Cranelift-based JIT compiler for MIPS → native translation.
jit = ["cranelift-codegen", "cranelift-frontend", "cranelift-jit", "cranelift-module", "cranelift-native", "target-lexicon"]

[dependencies]
clap = { version = "4", features = ["derive"] }
Expand All @@ -31,6 +33,12 @@ serde = { version = "1.0.228", features = ["derive"] }
toml = "1.0.3"
parking_lot = "0.12"
spin = "0.10.0"
cranelift-codegen = { version = "0.116", optional = true }
cranelift-frontend = { version = "0.116", optional = true }
cranelift-jit = { version = "0.116", optional = true }
cranelift-module = { version = "0.116", optional = true }
cranelift-native = { version = "0.116", optional = true }
target-lexicon = { version = "0.13", optional = true }

[target.'cfg(not(windows))'.dependencies]
libc = "0.2"
Expand All @@ -39,6 +47,7 @@ libc = "0.2"
lto = "fat"
codegen-units = 1
panic = "abort"
debug = 1

# Developer profile: release optimizations + debug symbols. Default build target.
# Enables the "developer" feature flag for dev-only tooling.
Expand Down
2 changes: 1 addition & 1 deletion iris.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ prom = "prom.bin"

# Window scale factor: 1 = native resolution, 2 = 2× for HiDPI/4K monitors.
# Can also be set with the --2x command-line flag (CLI takes precedence).
scale = 2
scale = 1

# RAM bank sizes in MB.
# Each bank must be 0 (absent), 8, 16, 32, 64, or 128.
Expand Down
75 changes: 75 additions & 0 deletions jit-diag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/bin/bash
# JIT diagnostic launcher — runs emulator and captures output for analysis
# Usage: ./jit-diag.sh [mode]
# mode: "jit" — JIT enabled (default)
# "verify" — JIT with verification
# "nojit" — interpreter only through JIT dispatch
# "interp" — pure interpreter (no JIT feature, baseline)
# "perf" — perf profile, interpreter only (text report for analysis)
# "perf-jit" — perf profile with JIT enabled
#
# All IRIS_JIT_* env vars are passed through automatically:
# IRIS_JIT_MAX_TIER=0 ./jit-diag.sh jit
# IRIS_JIT_PROBE=500 IRIS_JIT_PROBE_MIN=100 ./jit-diag.sh jit

MODE="${1:-jit}"
OUTFILE="jit-diag-$(date +%Y%m%d-%H%M%S)-${MODE}.log"

# Collect all IRIS_JIT_* env vars for display and passthrough
JIT_VARS=$(env | grep '^IRIS_JIT_' | tr '\n' ' ')

echo "=== IRIS JIT Diagnostic ===" | tee "$OUTFILE"
echo "Mode: $MODE" | tee -a "$OUTFILE"
echo "Date: $(date)" | tee -a "$OUTFILE"
echo "Host: $(uname -m) $(uname -s) $(uname -r)" | tee -a "$OUTFILE"
echo "Rust: $(rustc --version)" | tee -a "$OUTFILE"
[ -n "$JIT_VARS" ] && echo "Env: $JIT_VARS" | tee -a "$OUTFILE"
echo "" | tee -a "$OUTFILE"

case "$MODE" in
jit)
echo "Running: IRIS_JIT=1 ${JIT_VARS}cargo run --release --features jit,lightning" | tee -a "$OUTFILE"
IRIS_JIT=1 cargo run --release --features jit,lightning 2>&1 | tee -a "$OUTFILE"
;;
verify)
echo "Running: IRIS_JIT=1 IRIS_JIT_VERIFY=1 ${JIT_VARS}cargo run --release --features jit,lightning" | tee -a "$OUTFILE"
IRIS_JIT=1 IRIS_JIT_VERIFY=1 cargo run --release --features jit,lightning 2>&1 | tee -a "$OUTFILE"
;;
nojit)
echo "Running: cargo run --release --features jit,lightning (no IRIS_JIT)" | tee -a "$OUTFILE"
cargo run --release --features jit,lightning 2>&1 | tee -a "$OUTFILE"
;;
interp)
echo "Running: cargo run --release --features lightning (no jit feature)" | tee -a "$OUTFILE"
cargo run --release --features lightning 2>&1 | tee -a "$OUTFILE"
;;
perf)
PERFREPORT="perf-report-$(date +%Y%m%d-%H%M%S).txt"
echo "Building (profiling profile, no jit feature)..." | tee -a "$OUTFILE"
cargo build --profile profiling --features lightning 2>&1 | tee -a "$OUTFILE"
echo "--- Press Ctrl-C when you have enough samples ---"
perf record -F 99 --call-graph dwarf -o perf.data -- ./target/profiling/iris
echo "Processing perf data..." | tee -a "$OUTFILE"
perf report --stdio --no-children -i perf.data > "$PERFREPORT" 2>&1
echo "Perf report saved to: $PERFREPORT"
;;
perf-jit)
PERFREPORT="perf-report-jit-$(date +%Y%m%d-%H%M%S).txt"
echo "Building (profiling profile, jit feature)..." | tee -a "$OUTFILE"
cargo build --profile profiling --features jit,lightning 2>&1 | tee -a "$OUTFILE"
echo "--- Press Ctrl-C when you have enough samples ---"
IRIS_JIT=1 perf record -F 99 --call-graph dwarf -o perf.data -- ./target/profiling/iris
echo "Processing perf data..." | tee -a "$OUTFILE"
perf report --stdio --no-children -i perf.data > "$PERFREPORT" 2>&1
echo "Perf report saved to: $PERFREPORT"
;;
*)
echo "Unknown mode: $MODE"
echo "Usage: $0 [jit|verify|nojit|interp|perf|perf-jit]"
exit 1
;;
esac

echo "" >> "$OUTFILE"
echo "=== Exit code: $? ===" >> "$OUTFILE"
echo "Output saved to: $OUTFILE"
52 changes: 52 additions & 0 deletions jit_overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# IRIS Adaptive JIT — How We Taught an Emulator to Learn

## The Problem

IRIS emulates an SGI Indy (MIPS R4400) well enough to boot IRIX 6.5 to a graphical desktop. But the interpreter tops out at ~30 MIPS on x86_64. We wanted a Cranelift-based JIT compiler to go faster.

First attempt: compile everything. Result: **hang**. Loads and stores in the same compiled block caused Cranelift to generate bad register spill code on x86_64 (only 15 usable registers vs AArch64's 31). Weeks of debugging.

## The Insight

Instead of fixing one bug and praying, make the JIT **fix itself**.

## How It Works

Every compiled block starts at the safest level and earns its way up:

```
Tier 0 (Alu) Pure math + branches. Can't go wrong.
Tier 1 (Loads) Add memory reads. Might hit TLB misses.
Tier 2 (Full) Add memory writes. Full native speed.
```

**Lifecycle of a block:**
1. First seen → compile at Tier 0, mark **speculative**
2. Before each speculative run → snapshot the entire CPU (~2.3 KB)
3. Block runs clean 50 times → **trusted** (no more snapshots)
4. Trusted for 200 runs → **promote** to next tier (speculative again)
5. Block causes 3 exceptions at new tier → **demote** back, recompile

If a speculative block misbehaves, CPU state is rolled back from the snapshot and the interpreter re-runs the instruction correctly. The system never crashes — it just learns that block isn't ready yet.

## Bugs Found Along the Way

1. **SSA register pressure** — Cranelift's exception paths referenced values across block boundaries. Fixed by flushing modified registers before each helper call.

2. **Delay slot skip** *(the real killer)* — MIPS branches have a "delay slot": the instruction after a branch always executes. The JIT's tracer included load instructions in delay slots but the compiler's tier gate silently skipped them. Every branch with a load delay slot (extremely common in MIPS) produced wrong results. One-line fix.

## Profile Cache

Hot block profiles are saved to `~/.iris/jit-profile.bin` on shutdown. Next boot, blocks are pre-compiled at their proven tier — skipping the entire warmup.

## Results

```
Run with IRIS_JIT=0: boots ✓ (interpreter only)
Run with IRIS_JIT=1: boots to graphical desktop ✓
73,015 blocks compiled
4,036 promotions, 6 demotions, 145 rollbacks
0 crashes
```

The JIT is now self-correcting. It starts conservative, learns what's safe, and backs off when it's wrong. The emulator doesn't need us to manually decide what to compile — it figures it out at runtime.
52 changes: 52 additions & 0 deletions mcm-engine.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
project_name: iris
db_path: .claude/knowledge.db
rules_path: rules/
plugins: []
nudges:
# Tuned for 1M context (Opus 4.6) — sessions can safely run 400+ turns.
# Compaction/handoff thresholds are relaxed; enforcement thresholds are tightened.
store_reminder_turns: 6 # was 4 — less aggressive, sessions are longer
checkpoint_turns: 30 # was 10 — 1M context, no rush to checkpoint
mandatory_stop_turns: 320 # was 20 — allows much longer sessions
hyper_focus_threshold: 8 # was 3 — slight increase for longer sessions
rules_check_interval: 4 # was 5 — less frequent but still periodic
# Block tool calls (return error) after mandatory_stop + grace without checkpoint.
mandatory_stop_blocking: true
mandatory_stop_grace: 10 # was 5 — more grace for long sessions
# After N ignored nudges of the same type, escalate to blocking.
# TIGHTER than before — escalate faster when agent ignores nudges.
nudge_escalation_threshold: 2 # was 3

server_name: iris-knowledge

server_instructions: |

BEHAVIORAL MANDATES (non-negotiable):

0. Do not be lazy. Do not cheat. Focus on correctness and precision, not the "quickest way" to solve problems. Carefully examine any potential shortcut and consider how it will impact downstream packages.

1. MUST call `report_error` BEFORE attempting manual fixes for any build/link/runtime error.
It logs the error AND auto-searches rules, errors, and compat catalog in one call.

2. MUST call `check_compat` BEFORE writing compat function implementations.

3. MUST call `add_rule` IMMEDIATELY after confirming a fix works (build passes, test passes).
Do NOT defer add_rule calls to session end — context pressure causes them to be dropped.
Pattern: report_error when you hit it -> fix it -> build passes -> add_rule RIGHT THEN.

4. MUST delegate to a sub-agent after 2 failed fix attempts for the same error.
Long debug sessions destroy parent context.

5. DB is CACHE, files are AUTHORITATIVE. Rule files (rules/packages/*.yaml, rules/generic.yaml,
compat/catalog.yaml, rules/methods/*.md) are the source of truth. When using add_rule,
provide file_path pointing to the authoritative rule file.

Tool quick reference:
- `search` (or `knowledge_query`): Search rules, knowledge, errors, compat
- `report_error`: Log error + auto-search for fixes (THE KILLER FEATURE)
- `check_compat`: Search compat/catalog.yaml for a symbol
- `add_knowledge` (or `report_finding`): Store findings/decisions/insights
- `add_negative`: Store anti-patterns and dead ends
- `add_rule`: Create/index rule after fixing a problem
- `session_start`: Initialize session with context
- `session_handoff`: Snapshot state for next session
8 changes: 5 additions & 3 deletions src/hptimer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,14 @@ fn timer_thread_loop(inner: Arc<Mutex<TimerManagerInner>>, new_timer_added: Arc<

let delay = target - sleep_now;

if delay > Duration::from_millis(2) {
if delay > Duration::from_micros(200) {
// Park with a safe threshold
let park_duration = delay - Duration::from_millis(1);
let park_duration = delay - Duration::from_micros(100);
thread::park_timeout(park_duration);
} else {
std::hint::spin_loop();
// Short sleep instead of spin — yields the core without
// burning CPU while waiting for the timer to fire.
thread::sleep(Duration::from_micros(50));
}
}
} else {
Expand Down
Loading
Loading