github-actions commited on
Commit
8b58cfa
·
1 Parent(s): bc450e0

Auto deploy from GitHub

Browse files
Files changed (33) hide show
  1. space/Dockerfile +5 -2
  2. space/space/space/Dockerfile +19 -6
  3. space/space/space/space/space/space/program_generator.py +116 -0
  4. space/space/space/space/space/space/space/reverse_pass/README.md +57 -0
  5. space/space/space/space/space/space/space/reverse_pass/compiler_optimization_grpo.ipynb +850 -0
  6. space/space/space/space/space/space/space/reverse_pass/toyir_rl_support.py +473 -0
  7. space/space/space/space/space/space/space/space/reverse_pass/reversepass_new_eval_baseline.ipynb +0 -0
  8. space/space/space/space/space/space/space/space/space/app.py +26 -2
  9. space/space/space/space/space/space/space/space/space/space/space/runtime_core.py +316 -0
  10. space/space/space/space/space/space/space/space/space/space/space/space/Dockerfile +19 -0
  11. space/space/space/space/space/space/space/space/space/space/space/space/README.md +50 -5
  12. space/space/space/space/space/space/space/space/space/space/space/space/app.py +162 -177
  13. space/space/space/space/space/space/space/space/space/space/space/space/requirements.txt +2 -24
  14. space/space/space/space/space/space/space/space/space/space/space/space/space/compiler_optimization_grpo.ipynb +959 -0
  15. space/space/space/space/space/space/space/space/space/space/space/space/space/space/role2_deliverable3_training_loop (2) (1) (1).ipynb +0 -0
  16. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/DockerFile +34 -0
  17. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/README.md +9 -1
  18. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/requirements.txt +24 -0
  19. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/app.py +182 -0
  20. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/compiler_tetris (1).ipynb +651 -0
  21. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/metahack1 (1).ipynb +0 -0
  22. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/.DS_Store +0 -0
  23. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/metahack1 (1).ipynb +1379 -0
  24. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/metahack1.ipynb +484 -0
  25. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/.github/workflows/deploy.yml +30 -0
  26. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/README.md +1 -11
  27. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/.gitattributes +35 -0
  28. space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/README.md +11 -0
  29. space/space/space/space/space/space/space/space/space/space/train.py +208 -0
  30. space/space/space/space/space/space/space/space/untitled folder.zip +3 -0
  31. space/space/space/space/space/space/write_colab_synth_notebook.py +345 -0
  32. space/space/space/space/write_colab_synth_notebook.py +28 -22
  33. write_colab_synth_notebook.py +143 -12
space/Dockerfile CHANGED
@@ -16,7 +16,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
16
  NVIDIA_VISIBLE_DEVICES=all \
17
  NVIDIA_DRIVER_CAPABILITIES=compute,utility
18
 
19
- # Python + basic build deps (kept minimal)
20
  RUN apt-get update && apt-get install -y --no-install-recommends \
21
  python3.11 python3.11-venv python3-pip \
22
  git ca-certificates \
@@ -25,8 +25,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
25
  COPY requirements.txt .
26
  RUN python3.11 -m pip install --upgrade pip && python3.11 -m pip install -r requirements.txt
27
 
 
 
 
 
28
  COPY . .
29
 
30
  EXPOSE 7860
31
-
32
  CMD ["python3.11", "app.py"]
 
16
  NVIDIA_VISIBLE_DEVICES=all \
17
  NVIDIA_DRIVER_CAPABILITIES=compute,utility
18
 
19
+ # Python + basic deps
20
  RUN apt-get update && apt-get install -y --no-install-recommends \
21
  python3.11 python3.11-venv python3-pip \
22
  git ca-certificates \
 
25
  COPY requirements.txt .
26
  RUN python3.11 -m pip install --upgrade pip && python3.11 -m pip install -r requirements.txt
27
 
28
+ # CUDA-enabled PyTorch (so `torch.cuda.is_available()` is True on GPU Spaces)
29
+ RUN python3.11 -m pip install --no-cache-dir \
30
+ torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
31
+
32
  COPY . .
33
 
34
  EXPOSE 7860
 
35
  CMD ["python3.11", "app.py"]
space/space/space/Dockerfile CHANGED
@@ -1,19 +1,32 @@
1
  # Optional: use Hugging Face Space SDK = docker (set `sdk: docker` in README.md).
2
- # Default README uses Gradio SDK and does not require this image.
 
 
 
 
3
 
4
- FROM python:3.11-slim
5
 
6
  WORKDIR /app
7
 
8
- ENV PYTHONUNBUFFERED=1 \
 
9
  PIP_NO_CACHE_DIR=1 \
10
- GRADIO_SERVER_NAME=0.0.0.0
 
 
 
 
 
 
 
 
11
 
12
  COPY requirements.txt .
13
- RUN pip install --upgrade pip && pip install -r requirements.txt
14
 
15
  COPY . .
16
 
17
  EXPOSE 7860
18
 
19
- CMD ["python", "app.py"]
 
1
  # Optional: use Hugging Face Space SDK = docker (set `sdk: docker` in README.md).
2
+ #
3
+ # IMPORTANT:
4
+ # - If your Space is on CPU hardware, `torch.cuda.is_available()` will be False no matter what.
5
+ # - If your Space is on GPU hardware, you must use a CUDA-enabled base image (below),
6
+ # otherwise CUDA won't be visible inside the container.
7
 
8
+ FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
9
 
10
  WORKDIR /app
11
 
12
+ ENV DEBIAN_FRONTEND=noninteractive \
13
+ PYTHONUNBUFFERED=1 \
14
  PIP_NO_CACHE_DIR=1 \
15
+ GRADIO_SERVER_NAME=0.0.0.0 \
16
+ NVIDIA_VISIBLE_DEVICES=all \
17
+ NVIDIA_DRIVER_CAPABILITIES=compute,utility
18
+
19
+ # Python + basic build deps (kept minimal)
20
+ RUN apt-get update && apt-get install -y --no-install-recommends \
21
+ python3.11 python3.11-venv python3-pip \
22
+ git ca-certificates \
23
+ && rm -rf /var/lib/apt/lists/*
24
 
25
  COPY requirements.txt .
26
+ RUN python3.11 -m pip install --upgrade pip && python3.11 -m pip install -r requirements.txt
27
 
28
  COPY . .
29
 
30
  EXPOSE 7860
31
 
32
+ CMD ["python3.11", "app.py"]
space/space/space/space/space/space/program_generator.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Random Toy-IR list-of-dicts for GRPO / Deliverable2 / runtime_core.
3
+
4
+ Schema matches `runtime_core.SAMPLE_PROGRAM` (op, args, dest, type) — the same
5
+ shape `Deliverable2_Formatter` and `CompilerOptimizationEnv` expect.
6
+
7
+ Note: `metahack1 (1).ipynb` uses a different TAC shape (CONST/src1/STORE).
8
+ Use that notebook's generators only if you add a separate converter; this
9
+ module is self-contained for training stack compatibility.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import copy
15
+ import json
16
+ import random
17
+ from typing import List
18
+
19
+ # Hand-written seeds (same as Colab / train.py short list) — kept in sync for regression.
20
+ _BUILTIN_EXTRA: List[List[dict]] = [
21
+ [
22
+ {"op": "const", "dest": "a", "args": ["1"], "type": "int"},
23
+ {"op": "add", "dest": "b", "args": ["a", "a"], "type": "int"},
24
+ {"op": "ret", "args": ["b"]},
25
+ ],
26
+ [
27
+ {"op": "const", "dest": "x", "args": ["2"], "type": "int"},
28
+ {"op": "const", "dest": "y", "args": ["4"], "type": "int"},
29
+ {"op": "mul", "dest": "z", "args": ["x", "y"], "type": "int"},
30
+ {"op": "const", "dest": "k", "args": ["1"], "type": "int"},
31
+ {"op": "add", "dest": "w", "args": ["z", "k"], "type": "int"},
32
+ {"op": "ret", "args": ["w"]},
33
+ ],
34
+ ]
35
+
36
+
37
+ def random_toy_ir_program(rng: random.Random) -> List[dict]:
38
+ """
39
+ One valid program: consts v0.., then a chain of add/mul on existing names, then ret.
40
+ All ops use the mock-engine-friendly list schema.
41
+ """
42
+ n_const = rng.randint(2, 5)
43
+ n_arith = rng.randint(1, 5)
44
+ progs: List[dict] = []
45
+ for i in range(n_const):
46
+ progs.append(
47
+ {
48
+ "op": "const",
49
+ "dest": f"v{i}",
50
+ "args": [str(rng.randint(0, 20))],
51
+ "type": "int",
52
+ }
53
+ )
54
+ available = [f"v{i}" for i in range(n_const)]
55
+ nxt = n_const
56
+ for _j in range(n_arith):
57
+ a = rng.choice(available)
58
+ b = rng.choice(available)
59
+ opn = rng.choice(["add", "mul"])
60
+ d = f"v{nxt}"
61
+ nxt += 1
62
+ progs.append({"op": opn, "dest": d, "args": [a, b], "type": "int"})
63
+ available.append(d)
64
+ progs.append({"op": "ret", "args": [available[-1]]})
65
+ return progs
66
+
67
+
68
+ def build_training_program_corpus(
69
+ n_total: int = 120,
70
+ seed: int = 42,
71
+ *,
72
+ include_builtins: bool = True,
73
+ ) -> List[List[dict]]:
74
+ """
75
+ Return `n_total` programs for GRPO. Optionally prepend SAMPLE_PROGRAM + 2 hand-written IRs
76
+ (when include_builtins), then fill with random_toy_ir_program, deduplicating by JSON key.
77
+
78
+ Typical range: set `n_total` between 50 and 200 in the notebook.
79
+ """
80
+ if n_total < 1:
81
+ raise ValueError("n_total must be >= 1")
82
+
83
+ rng = random.Random(seed)
84
+ out: List[List[dict]] = []
85
+ seen: set[str] = set()
86
+
87
+ def _add(p: List[dict]) -> None:
88
+ k = json.dumps(p, sort_keys=True)
89
+ if k in seen:
90
+ return
91
+ seen.add(k)
92
+ out.append(copy.deepcopy(p))
93
+
94
+ if include_builtins:
95
+ from runtime_core import SAMPLE_PROGRAM
96
+
97
+ for p in (SAMPLE_PROGRAM, *_BUILTIN_EXTRA):
98
+ if len(out) >= n_total:
99
+ break
100
+ _add(p)
101
+
102
+ # Fill with random programs (dedupe by full JSON; allow dup if generator keeps colliding)
103
+ guard = 0
104
+ while len(out) < n_total:
105
+ guard += 1
106
+ if guard > 200_000:
107
+ out.append(random_toy_ir_program(rng))
108
+ continue
109
+ cand = random_toy_ir_program(rng)
110
+ k = json.dumps(cand, sort_keys=True)
111
+ if k in seen:
112
+ continue
113
+ seen.add(k)
114
+ out.append(cand)
115
+
116
+ return out
space/space/space/space/space/space/space/reverse_pass/README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Compiler Phase-Ordering RLVR (Toy-IR)
2
+
3
+ Toy-IR compiler phase-ordering environment for RL with verifiable rewards (RLVR), including reverse passes to escape local optima.
4
+
5
+ ## What Is Implemented
6
+
7
+ - Reverse passes are available in the pass library:
8
+ - `expand_constant`
9
+ - `duplicate_computation`
10
+ - Action routing and validation include reverse passes.
11
+ - Prompt guidance includes reverse-pass descriptions and usage intent.
12
+ - Reward shaping is terminal-weighted with RLVR hard gate:
13
+ - non-equivalent -> `-1000.0`
14
+ - terminal -> `((original_cycles - current_cycles) / original_cycles) * 100.0`
15
+ - non-terminal -> `-0.1`
16
+ - Episode termination:
17
+ - `STOP`/`done`, or
18
+ - hard cap at 5 steps.
19
+ - Reverse-pass instrumentation logs every 50 episodes/completions:
20
+ - `reverse_pass_episodes`
21
+ - `expand_constant_count`
22
+ - `duplicate_computation_count`
23
+
24
+ ## Verified So Far
25
+
26
+ - Smoke test runs end-to-end without runtime errors.
27
+ - Rewards are scalar floats (not NaN) in tested rollouts.
28
+ - Forced reverse-pass episodes show expected behavior:
29
+ - small negative intermediate rewards (`-0.1`)
30
+ - terminal reward depends on final outcome (positive if chain beats baseline, negative if not)
31
+
32
+ ## Pending (GPU Required)
33
+
34
+ Comparative training runs are still pending and require a GPU environment with:
35
+
36
+ - `torch`
37
+ - `trl`
38
+ - `unsloth`
39
+ - `wandb`
40
+
41
+ Required fair comparison:
42
+
43
+ 1. Baseline run: reverse passes disabled (`baseline_no_reverse`)
44
+ 2. Reverse run: reverse passes enabled (`with_reverse_passes`)
45
+ 3. Same episode budget for both runs
46
+ 4. Compare reward/cycle curves and reverse-pass usage metrics in WandB
47
+
48
+ ## Team Handoff Status
49
+
50
+ - Reverse-pass feature: shipped
51
+ - Training wiring + reward shaping + instrumentation: shipped
52
+ - Comparative training: pending in GPU environment
53
+ - README: this document
54
+
55
+ ## Suggested Final Pre-Training Sanity Check
56
+
57
+ Run one smoke/equivalence check on a real Role 3 generated curriculum program (not just mock IR) to confirm verifier behavior in-pipeline before long training.
space/space/space/space/space/space/space/reverse_pass/compiler_optimization_grpo.ipynb ADDED
@@ -0,0 +1,850 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "title-cell",
6
+ "metadata": {},
7
+ "source": [
8
+ "# 🔧 Compiler Optimization RL Environment\n",
9
+ "### OpenEnv Hackathon 2026 — Theme #2: Long-Horizon Planning\n",
10
+ "\n",
11
+ "**What this notebook does:**\n",
12
+ "1. Defines a fully OpenEnv-compliant `CompilerOptimizationEnv`\n",
13
+ "2. Loads `Qwen2.5-3B-Instruct` via Unsloth (4-bit QLoRA)\n",
14
+ "3. Trains with GRPO (TRL) — LLM learns to pick compiler passes that reduce CPU cycles\n",
15
+ "4. Runs a smoke-test with a mock engine so you can verify reward logic without real hardware\n",
16
+ "5. Plots reward curves\n",
17
+ "\n",
18
+ "---\n",
19
+ "**Stack:** `unsloth` · `trl` · `openenv` · `wandb` · `matplotlib`\n",
20
+ "\n",
21
+ "> **Runtime:** Google Colab T4 GPU recommended. For the smoke-test only, CPU is fine."
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "markdown",
26
+ "id": "install-header",
27
+ "metadata": {},
28
+ "source": [
29
+ "## 📦 Cell 1 — Install Dependencies"
30
+ ]
31
+ },
32
+ {
33
+ "cell_type": "code",
34
+ "execution_count": null,
35
+ "id": "install-cell",
36
+ "metadata": {},
37
+ "outputs": [],
38
+ "source": [
39
+ "# Install all required packages\n",
40
+ "# Unsloth must be installed before trl to get the right CUDA kernels\n",
41
+ "!pip install \"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\" --quiet\n",
42
+ "!pip install trl datasets transformers accelerate peft bitsandbytes --quiet\n",
43
+ "!pip install wandb matplotlib --quiet\n",
44
+ "\n",
45
+ "# Optional: install openenv for production use\n",
46
+ "# !pip install openenv --quiet\n",
47
+ "\n",
48
+ "print(\"✅ All packages installed\")"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "markdown",
53
+ "id": "env-header",
54
+ "metadata": {},
55
+ "source": [
56
+ "## 🌍 Cell 2 — CompilerOptimizationEnv (OpenEnv-Compliant)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "env-cell",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "# CompilerOptimizationEnv, PASSES, reverse passes, terminal-weighted reward (toyir_rl_support)\n",
67
+ "import os\n",
68
+ "import sys\n",
69
+ "\n",
70
+ "if os.getcwd() not in sys.path:\n",
71
+ " sys.path.insert(0, os.getcwd())\n",
72
+ "\n",
73
+ "from toyir_rl_support import (\n",
74
+ " MCPEnvironment,\n",
75
+ " StepResult,\n",
76
+ " EpisodeStats,\n",
77
+ " CompilerOptimizationEnv,\n",
78
+ " PASSES,\n",
79
+ " MOCK_PASSES,\n",
80
+ " compute_shaped_reward,\n",
81
+ " rollout_shaped_return,\n",
82
+ " log_reverse_pass_stats_for_completion,\n",
83
+ " ensure_import_path,\n",
84
+ " MockEngine,\n",
85
+ " MOCK_ENGINE,\n",
86
+ " SAMPLE_PROGRAM,\n",
87
+ ")\n",
88
+ "\n",
89
+ "ensure_import_path()\n",
90
+ "print(\"✅ Loaded CompilerOptimizationEnv, PASSES, and reward helpers from toyir_rl_support\")\n"
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "markdown",
95
+ "id": "smoke-header",
96
+ "metadata": {},
97
+ "source": [
98
+ "## 🧪 Cell 3 — Smoke Test (No GPU / Real Engine Needed)\n",
99
+ "Validates the entire reward pipeline with a mock engine. Run this before spending compute."
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "code",
104
+ "execution_count": null,
105
+ "id": "smoke-test-cell",
106
+ "metadata": {},
107
+ "outputs": [],
108
+ "source": [
109
+ "# Smoke test (MockEngine; same PASSES as training)\n",
110
+ "engine = MockEngine()\n",
111
+ "env = CompilerOptimizationEnv(\n",
112
+ " engine, MOCK_PASSES, max_steps=CompilerOptimizationEnv.MAX_EPISODE_STEPS\n",
113
+ ")\n",
114
+ "obs = env.reset(SAMPLE_PROGRAM)\n",
115
+ "\n",
116
+ "print(\"═\" * 55)\n",
117
+ "print(\"SMOKE TEST\")\n",
118
+ "print(\"═\" * 55)\n",
119
+ "print(f\"Initial state:\\n{obs}\")\n",
120
+ "print(f\"\\nBaseline cycles : {env.previous_cycles}\")\n",
121
+ "print(f\"Available actions: {env.available_actions()}\")\n",
122
+ "print()\n",
123
+ "\n",
124
+ "actions_to_try = [\n",
125
+ " \"constant_folding\",\n",
126
+ " \"dead_code_elimination\",\n",
127
+ " \"peephole_optimization\", # no-op\n",
128
+ " \"hallucinated_pass\", # invalid — but won't kill episode yet\n",
129
+ " \"constant_folding\",\n",
130
+ " \"dead_code_elimination\",\n",
131
+ " \"expand_constant\",\n",
132
+ " \"STOP\",\n",
133
+ "]\n",
134
+ "\n",
135
+ "for action in actions_to_try:\n",
136
+ " result = env.step(action)\n",
137
+ " tag = \"✗\" if result.reward < 0 else \"✓\"\n",
138
+ " print(f\"{tag} '{action}'\")\n",
139
+ " print(f\" reward={result.reward:+.2f} done={result.done} is_terminal={result.info.get('is_terminal')}\")\n",
140
+ " relevant = {k: v for k, v in result.info.items()\n",
141
+ " if k in (\"delta_pct\", \"error\", \"no_op\", \"reason\",\n",
142
+ " \"terminal_bonus\", \"episode_stats\", \"is_terminal\")}\n",
143
+ " if relevant:\n",
144
+ " print(f\" info: {relevant}\")\n",
145
+ " print()\n",
146
+ " if result.done:\n",
147
+ " break\n",
148
+ "\n",
149
+ "print(\"✅ Smoke test passed\")\n"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "markdown",
154
+ "id": "reward-plot-header",
155
+ "metadata": {},
156
+ "source": [
157
+ "## 📊 Cell 4 — Visualise Reward Across a Mock Episode"
158
+ ]
159
+ },
160
+ {
161
+ "cell_type": "code",
162
+ "execution_count": null,
163
+ "id": "reward-plot-cell",
164
+ "metadata": {},
165
+ "outputs": [],
166
+ "source": [
167
+ "import matplotlib.pyplot as plt\n",
168
+ "import matplotlib.ticker as ticker\n",
169
+ "\n",
170
+ "# Run a full episode and collect data\n",
171
+ "env = CompilerOptimizationEnv(MockEngine(), MOCK_PASSES, max_steps=CompilerOptimizationEnv.MAX_EPISODE_STEPS)\n",
172
+ "env.reset(SAMPLE_PROGRAM)\n",
173
+ "\n",
174
+ "sequence = [\n",
175
+ " \"constant_folding\", \"dead_code_elimination\", \"peephole_optimization\",\n",
176
+ " \"expand_constant\", \"peephole_optimization\",\n",
177
+ "]\n",
178
+ "\n",
179
+ "rewards, cycle_counts, actions_log = [], [], []\n",
180
+ "for act in sequence:\n",
181
+ " r = env.step(act)\n",
182
+ " rewards.append(r.reward)\n",
183
+ " cycle_counts.append(env.previous_cycles)\n",
184
+ " actions_log.append(act)\n",
185
+ " if r.done:\n",
186
+ " break\n",
187
+ "\n",
188
+ "steps = list(range(1, len(rewards) + 1))\n",
189
+ "\n",
190
+ "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)\n",
191
+ "fig.suptitle(\"Compiler Optimization Episode — Mock Engine\", fontsize=14, fontweight=\"bold\")\n",
192
+ "\n",
193
+ "# Reward per step\n",
194
+ "colors = [\"#2ecc71\" if r >= 0 else \"#e74c3c\" for r in rewards]\n",
195
+ "ax1.bar(steps, rewards, color=colors, edgecolor=\"white\", linewidth=0.5)\n",
196
+ "ax1.axhline(0, color=\"grey\", linewidth=0.8, linestyle=\"--\")\n",
197
+ "ax1.set_ylabel(\"Reward\")\n",
198
+ "ax1.set_title(\"Reward per Step (green = positive, red = negative)\")\n",
199
+ "ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter(\"%.1f\"))\n",
200
+ "\n",
201
+ "# Cycle count over time\n",
202
+ "ax2.plot(steps, cycle_counts, marker=\"o\", color=\"#3498db\", linewidth=2, markersize=6)\n",
203
+ "ax2.set_xlabel(\"Step\")\n",
204
+ "ax2.set_ylabel(\"CPU Cycles\")\n",
205
+ "ax2.set_title(\"CPU Cycle Count Over Episode (lower = better)\")\n",
206
+ "ax2.set_xticks(steps)\n",
207
+ "ax2.set_xticklabels(\n",
208
+ " [a.replace(\"_\", \"\\n\") for a in actions_log],\n",
209
+ " fontsize=7,\n",
210
+ ")\n",
211
+ "\n",
212
+ "plt.tight_layout()\n",
213
+ "plt.savefig(\"episode_reward_curve.png\", dpi=150, bbox_inches=\"tight\")\n",
214
+ "plt.show()\n",
215
+ "print(\"📈 Plot saved as episode_reward_curve.png\")\n"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "markdown",
220
+ "id": "model-header",
221
+ "metadata": {},
222
+ "source": [
223
+ "## 🤖 Cell 5 — Load Model with Unsloth (QLoRA 4-bit)\n",
224
+ "> **Requires T4 GPU.** Skip to Cell 9 if you only want to test the environment."
225
+ ]
226
+ },
227
+ {
228
+ "cell_type": "code",
229
+ "execution_count": null,
230
+ "id": "model-cell",
231
+ "metadata": {},
232
+ "outputs": [],
233
+ "source": [
234
+ "import torch\n",
235
+ "from unsloth import FastLanguageModel\n",
236
+ "\n",
237
+ "MODEL_NAME = \"unsloth/Qwen2.5-3B-Instruct\" # swap to 7B if VRAM allows\n",
238
+ "MAX_SEQ_LEN = 1024\n",
239
+ "LORA_RANK = 16\n",
240
+ "\n",
241
+ "model, tokenizer = FastLanguageModel.from_pretrained(\n",
242
+ " model_name = MODEL_NAME,\n",
243
+ " max_seq_length = MAX_SEQ_LEN,\n",
244
+ " dtype = None, # auto bf16/fp16\n",
245
+ " load_in_4bit = True,\n",
246
+ ")\n",
247
+ "\n",
248
+ "model = FastLanguageModel.get_peft_model(\n",
249
+ " model,\n",
250
+ " r = LORA_RANK,\n",
251
+ " target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n",
252
+ " \"gate_proj\", \"up_proj\", \"down_proj\"],\n",
253
+ " lora_alpha = LORA_RANK * 2,\n",
254
+ " lora_dropout = 0.0,\n",
255
+ " bias = \"none\",\n",
256
+ " use_gradient_checkpointing = \"unsloth\",\n",
257
+ " random_state = 42,\n",
258
+ ")\n",
259
+ "\n",
260
+ "print(f\"✅ Loaded {MODEL_NAME} with QLoRA rank={LORA_RANK}\")\n",
261
+ "print(f\" GPU memory allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB\")"
262
+ ]
263
+ },
264
+ {
265
+ "cell_type": "markdown",
266
+ "id": "prompt-header",
267
+ "metadata": {},
268
+ "source": [
269
+ "## 💬 Cell 6 — System Prompt & Dataset Builder"
270
+ ]
271
+ },
272
+ {
273
+ "cell_type": "code",
274
+ "execution_count": null,
275
+ "id": "prompt-cell",
276
+ "metadata": {},
277
+ "outputs": [],
278
+ "source": [
279
+ "import json\n",
280
+ "import re\n",
281
+ "from datasets import Dataset\n",
282
+ "\n",
283
+ "\n",
284
+ "class Deliverable2_Formatter:\n",
285
+ " 'State translation + robust action-array extraction for Role 2.'\n",
286
+ "\n",
287
+ " @staticmethod\n",
288
+ " def translate_state(raw_json: list) -> str:\n",
289
+ " pseudo_assembly = []\n",
290
+ " for i, instruction in enumerate(raw_json):\n",
291
+ " if not isinstance(instruction, dict):\n",
292
+ " pseudo_assembly.append(f\"{i}. NOP\")\n",
293
+ " continue\n",
294
+ " op = str(instruction.get(\"op\", \"UNKNOWN\")).upper()\n",
295
+ " args = \", \".join(str(arg) for arg in instruction.get(\"args\", []))\n",
296
+ " dest = instruction.get(\"dest\", \"\")\n",
297
+ " if dest:\n",
298
+ " pseudo_assembly.append(f\"{i}. {dest} = {op} {args}\".rstrip())\n",
299
+ " else:\n",
300
+ " pseudo_assembly.append(f\"{i}. {op} {args}\".rstrip())\n",
301
+ " return \"\\n\".join(pseudo_assembly) if pseudo_assembly else \"; (empty program)\"\n",
302
+ "\n",
303
+ " @staticmethod\n",
304
+ " def extract_action_array(llm_output: str) -> list:\n",
305
+ " text = (llm_output or \"\").strip()\n",
306
+ " if not text:\n",
307
+ " raise ValueError(\"Invalid JSON format\")\n",
308
+ "\n",
309
+ " try:\n",
310
+ " parsed = json.loads(text)\n",
311
+ " if isinstance(parsed, list):\n",
312
+ " return parsed\n",
313
+ " except json.JSONDecodeError:\n",
314
+ " pass\n",
315
+ "\n",
316
+ " match = re.search(r\"\\[.*?\\]\", text, re.DOTALL)\n",
317
+ " if match:\n",
318
+ " try:\n",
319
+ " parsed = json.loads(match.group(0))\n",
320
+ " if isinstance(parsed, list):\n",
321
+ " return parsed\n",
322
+ " except json.JSONDecodeError:\n",
323
+ " pass\n",
324
+ "\n",
325
+ " raise ValueError(\"Invalid JSON format\")\n",
326
+ "\n",
327
+ "\n",
328
+ "def build_system_prompt(passes: dict) -> str:\n",
329
+ " lines: list = []\n",
330
+ " for name in sorted(passes.keys()):\n",
331
+ " if name == \"expand_constant\":\n",
332
+ " lines.append(\n",
333
+ " \" - expand_constant: Splits a constant assignment into runtime arithmetic (e.g., CONST 8 becomes CONST 3 + ADD 5). May temporarily increase cycles but can enable forward passes to find better optimization chains. Use sparingly when standard passes seem stuck.\"\n",
334
+ " )\n",
335
+ " elif name == \"duplicate_computation\":\n",
336
+ " lines.append(\n",
337
+ " \" - duplicate_computation: Creates a redundant copy of a binary operation with a fresh variable. May temporarily increase cycles but can enable alternative dead code elimination paths. Use when redundancy might unlock further simplification.\"\n",
338
+ " )\n",
339
+ " else:\n",
340
+ " lines.append(f\" - {name}\")\n",
341
+ " action_block = \"\\n\".join(lines)\n",
342
+ " return (\n",
343
+ " \"You are a compiler optimization agent. Your goal is to reduce \"\n",
344
+ " \"CPU cycle count by applying optimization passes to the program below.\\n\\n\"\n",
345
+ " f\"Available actions:\\n{action_block}\\n\"\n",
346
+ " \" - STOP (emit this in the array to stop; optional alias: \\\"done\\\". Stop early if no further improvement is possible.)\\n\\n\"\n",
347
+ " \"Rules:\\n\"\n",
348
+ " \" • Output only a JSON array of pass names (example: [\\\"constant_folding\\\"]).\\n\"\n",
349
+ " \" • No explanation, no markdown, no extra text.\\n\"\n",
350
+ " \" • Do not invent actions not listed above.\\n\"\n",
351
+ " \" • Maximum 5 passes per response (hard episode cap: 5 optimization steps or STOP).\\n\"\n",
352
+ " )\n",
353
+ "\n",
354
+ "\n",
355
+ "def build_dataset(\n",
356
+ " programs: list,\n",
357
+ " engine,\n",
358
+ " passes: dict,\n",
359
+ ") -> Dataset:\n",
360
+ " # Each row = one episode; GRPO samples K completions per row\n",
361
+ " env = CompilerOptimizationEnv(\n",
362
+ " engine, passes, max_steps=CompilerOptimizationEnv.MAX_EPISODE_STEPS\n",
363
+ " )\n",
364
+ " system_prompt = build_system_prompt(passes)\n",
365
+ "\n",
366
+ " rows = []\n",
367
+ " for prog in programs:\n",
368
+ " translated_state = Deliverable2_Formatter.translate_state(prog)\n",
369
+ " prompt = [\n",
370
+ " {\"role\": \"system\", \"content\": system_prompt},\n",
371
+ " {\"role\": \"user\", \"content\": f\"Current program:\\n{translated_state}\\n\\nChoose optimization passes:\"},\n",
372
+ " ]\n",
373
+ " rows.append({\"prompt\": prompt, \"program_json\": json.dumps(prog)})\n",
374
+ "\n",
375
+ " return Dataset.from_list(rows)\n",
376
+ "\n",
377
+ "\n",
378
+ "# --- Demo: build dataset from mock programs -----------------------------------\n",
379
+ "def make_mock_programs(n: int = 20) -> list:\n",
380
+ " # Generate N random mock IR programs\n",
381
+ " import random\n",
382
+ " ops = [\"add\", \"mul\", \"sub\", \"const\", \"load\"]\n",
383
+ " progs = []\n",
384
+ " for _ in range(n):\n",
385
+ " length = random.randint(4, 12)\n",
386
+ " prog = [\n",
387
+ " {\n",
388
+ " \"op\": random.choice(ops),\n",
389
+ " \"dest\": f\"v{i}\",\n",
390
+ " \"args\": [f\"v{max(0, i-1)}\"],\n",
391
+ " \"type\": \"int\",\n",
392
+ " }\n",
393
+ " for i in range(length)\n",
394
+ " ]\n",
395
+ " prog.append({\"op\": \"ret\", \"args\": [f\"v{length-1}\"]})\n",
396
+ " progs.append(prog)\n",
397
+ " return progs\n",
398
+ "\n",
399
+ "\n",
400
+ "mock_programs = make_mock_programs(n=30)\n",
401
+ "train_dataset = build_dataset(mock_programs, MockEngine(), MOCK_PASSES)\n",
402
+ "\n",
403
+ "print(f\"✅ Dataset built: {len(train_dataset)} episodes\")\n",
404
+ "print(f\" Sample prompt keys: {list(train_dataset[0].keys())}\")\n"
405
+ ]
406
+ },
407
+ {
408
+ "cell_type": "markdown",
409
+ "id": "reward-fn-header",
410
+ "metadata": {},
411
+ "source": [
412
+ "## 🎯 Cell 7 — Reward Function Factory (for GRPO)"
413
+ ]
414
+ },
415
+ {
416
+ "cell_type": "code",
417
+ "execution_count": null,
418
+ "id": "reward-fn-cell",
419
+ "metadata": {},
420
+ "outputs": [],
421
+ "source": [
422
+ "import json\n",
423
+ "from typing import Any\n",
424
+ "\n",
425
+ "# Rollout + terminal weighting; reverse-pass W&B window logging\n",
426
+ "from toyir_rl_support import (\n",
427
+ " CompilerOptimizationEnv,\n",
428
+ " MOCK_PASSES,\n",
429
+ " rollout_shaped_return,\n",
430
+ " log_reverse_pass_stats_for_completion,\n",
431
+ " SAMPLE_PROGRAM,\n",
432
+ ")\n",
433
+ "\n",
434
+ "EP_CAP = CompilerOptimizationEnv.MAX_EPISODE_STEPS\n",
435
+ "\n",
436
+ "\n",
437
+ "def _normalize_action_seq(actions: list, cap: int, passes: dict) -> list:\n",
438
+ " out: list = []\n",
439
+ " for x in actions[:cap]:\n",
440
+ " raw = str(x).strip()\n",
441
+ " s_low = raw.lower()\n",
442
+ " if s_low in (\"stop\", \"done\"):\n",
443
+ " out.append(\"done\" if s_low == \"done\" else \"STOP\")\n",
444
+ " continue\n",
445
+ " key = None\n",
446
+ " for k in passes:\n",
447
+ " if k.lower() == s_low:\n",
448
+ " key = k\n",
449
+ " break\n",
450
+ " if key is None:\n",
451
+ " out.append(raw)\n",
452
+ " else:\n",
453
+ " out.append(key)\n",
454
+ " return out\n",
455
+ "\n",
456
+ "\n",
457
+ "def make_reward_fn(engine, passes, max_steps: int = EP_CAP):\n",
458
+ " # TRL GRPO reward: terminal-weighted `rollout_shaped_return` + reverse-pass logging\n",
459
+ " _cap = min(int(max_steps), EP_CAP)\n",
460
+ "\n",
461
+ " def reward_fn(prompts, completions, **kwargs):\n",
462
+ " programs = kwargs.get(\"program_json\", [None] * len(completions))\n",
463
+ " rewards: list = []\n",
464
+ "\n",
465
+ " for completion, prog_json in zip(completions, programs):\n",
466
+ " raw = completion if isinstance(completion, str) else completion[0][\"content\"]\n",
467
+ " if prog_json is None:\n",
468
+ " rewards.append(CompilerOptimizationEnv.INVALID_ACTION_PENALTY)\n",
469
+ " continue\n",
470
+ " program = (\n",
471
+ " json.loads(prog_json) if isinstance(prog_json, str) else list(prog_json)\n",
472
+ " )\n",
473
+ " if not program:\n",
474
+ " rewards.append(CompilerOptimizationEnv.INVALID_ACTION_PENALTY)\n",
475
+ " continue\n",
476
+ " try:\n",
477
+ " actions = Deliverable2_Formatter.extract_action_array(raw)\n",
478
+ " except ValueError:\n",
479
+ " rewards.append(CompilerOptimizationEnv.INVALID_ACTION_PENALTY)\n",
480
+ " continue\n",
481
+ " if not actions:\n",
482
+ " rewards.append(CompilerOptimizationEnv.INVALID_ACTION_PENALTY)\n",
483
+ " continue\n",
484
+ " norm = _normalize_action_seq(actions, _cap, passes)\n",
485
+ " tr, n_e, n_d, _hs, _hc, ok = rollout_shaped_return(\n",
486
+ " program, norm, engine, passes\n",
487
+ " )\n",
488
+ " if not ok:\n",
489
+ " rewards.append(-1000.0)\n",
490
+ " else:\n",
491
+ " log_reverse_pass_stats_for_completion(n_e, n_d)\n",
492
+ " rewards.append(tr)\n",
493
+ " return rewards\n",
494
+ "\n",
495
+ " return reward_fn\n",
496
+ "\n",
497
+ "\n",
498
+ "reward_fn = make_reward_fn(MockEngine(), MOCK_PASSES, max_steps=EP_CAP)\n",
499
+ "print(\"✅ Reward function factory ready\")\n",
500
+ "\n",
501
+ "# Quick sanity check\n",
502
+ "test_completions = [\n",
503
+ " '[\"constant_folding\"]',\n",
504
+ " 'Here is my plan: [\"dead_code_elimination\", \"peephole_optimization\", \"done\"]',\n",
505
+ " \"hallucinated_pass\",\n",
506
+ "]\n",
507
+ "test_programs = [json.dumps(SAMPLE_PROGRAM)] * 3\n",
508
+ "test_rewards = reward_fn(\n",
509
+ " prompts=[\"\"] * 3,\n",
510
+ " completions=test_completions,\n",
511
+ " program_json=test_programs,\n",
512
+ ")\n",
513
+ "print(\"\\nReward sanity check:\")\n",
514
+ "for act, rew in zip(test_completions, test_rewards):\n",
515
+ " print(f\" {act!r} → {rew:+.2f}\")\n"
516
+ ]
517
+ },
518
+ {
519
+ "cell_type": "markdown",
520
+ "id": "trainer-header",
521
+ "metadata": {},
522
+ "source": [
523
+ "## 🚀 Cell 8 — GRPO Trainer Config & Training"
524
+ ]
525
+ },
526
+ {
527
+ "cell_type": "code",
528
+ "execution_count": null,
529
+ "id": "trainer-cell",
530
+ "metadata": {},
531
+ "outputs": [],
532
+ "source": [
533
+ "from trl import GRPOConfig, GRPOTrainer\n",
534
+ "\n",
535
+ "# Optional W&B — comment out if not using\n",
536
+ "try:\n",
537
+ " import wandb\n",
538
+ " wandb.init(project=\"openenv-compiler-opt\", name=\"grpo-qwen2.5-3b\")\n",
539
+ " REPORT_TO = \"wandb\"\n",
540
+ "except Exception:\n",
541
+ " REPORT_TO = \"none\"\n",
542
+ "\n",
543
+ "\n",
544
+ "grpo_config = GRPOConfig(\n",
545
+ " # ── Generation ────────────────────────────────────────────────────────\n",
546
+ " num_generations = 4, # K rollouts per prompt for group-relative advantage\n",
547
+ " max_new_tokens = 16, # Actions are 1 word; don't waste context\n",
548
+ " temperature = 0.9,\n",
549
+ " top_p = 0.95,\n",
550
+ "\n",
551
+ " # ── Optimisation ──────────────────────────────────────────────────────\n",
552
+ " learning_rate = 5e-6,\n",
553
+ " per_device_train_batch_size = 2,\n",
554
+ " gradient_accumulation_steps = 4, # effective batch = 8\n",
555
+ " num_train_epochs = 3,\n",
556
+ " max_grad_norm = 0.5,\n",
557
+ "\n",
558
+ " # ── GRPO-specific ─────────────────────────────────────────────────────\n",
559
+ " beta = 0.04, # KL penalty; keeps policy near reference\n",
560
+ "\n",
561
+ " # ── Logging / checkpointing ───────────────────────────────────────────\n",
562
+ " output_dir = \"./grpo_compiler_checkpoints\",\n",
563
+ " logging_steps = 10,\n",
564
+ " save_steps = 100,\n",
565
+ " report_to = REPORT_TO,\n",
566
+ "\n",
567
+ " # ── Reproducibility ───────────────────────────────────────────────────\n",
568
+ " seed = 42,\n",
569
+ ")\n",
570
+ "\n",
571
+ "trainer = GRPOTrainer(\n",
572
+ " model = model,\n",
573
+ " tokenizer = tokenizer,\n",
574
+ " config = grpo_config,\n",
575
+ " train_dataset = train_dataset,\n",
576
+ " reward_funcs = reward_fn,\n",
577
+ ")\n",
578
+ "\n",
579
+ "print(\"✅ Trainer configured\")\n",
580
+ "print(f\" num_generations (K) = {grpo_config.num_generations}\")\n",
581
+ "print(f\" effective batch size = \"\n",
582
+ " f\"{grpo_config.per_device_train_batch_size * grpo_config.gradient_accumulation_steps}\")\n",
583
+ "print(f\" KL beta = {grpo_config.beta}\")\n",
584
+ "print()\n",
585
+ "print(\"Starting training... (this will take a while on T4)\")\n",
586
+ "trainer.train()"
587
+ ]
588
+ },
589
+ {
590
+ "cell_type": "markdown",
591
+ "id": "save-header",
592
+ "metadata": {},
593
+ "source": [
594
+ "## 💾 Cell 9 — Save Model"
595
+ ]
596
+ },
597
+ {
598
+ "cell_type": "code",
599
+ "execution_count": null,
600
+ "id": "save-cell",
601
+ "metadata": {},
602
+ "outputs": [],
603
+ "source": [
604
+ "SAVE_PATH = \"./grpo_compiler_final\"\n",
605
+ "\n",
606
+ "model.save_pretrained(SAVE_PATH)\n",
607
+ "tokenizer.save_pretrained(SAVE_PATH)\n",
608
+ "\n",
609
+ "print(f\"✅ Model saved to {SAVE_PATH}\")\n",
610
+ "\n",
611
+ "# Optional: push to HuggingFace Hub\n",
612
+ "# model.push_to_hub(\"your-hf-username/compiler-opt-grpo\")\n",
613
+ "# tokenizer.push_to_hub(\"your-hf-username/compiler-opt-grpo\")"
614
+ ]
615
+ },
616
+ {
617
+ "cell_type": "markdown",
618
+ "id": "curriculum-header",
619
+ "metadata": {},
620
+ "source": [
621
+ "## 📈 Cell 10 — Curriculum Callback & Reward Tracking"
622
+ ]
623
+ },
624
+ {
625
+ "cell_type": "code",
626
+ "execution_count": null,
627
+ "id": "curriculum-cell",
628
+ "metadata": {},
629
+ "outputs": [],
630
+ "source": [
631
+ "class CurriculumCallback:\n",
632
+ " \"\"\"\n",
633
+ " Tracks rolling mean reward and promotes curriculum level\n",
634
+ " when the agent has mastered the current difficulty.\n",
635
+ "\n",
636
+ " Usage: call .record(reward) after every episode.\n",
637
+ " Read .level to get current difficulty (1=easy, 2=medium, 3=hard).\n",
638
+ " \"\"\"\n",
639
+ " def __init__(self, reward_threshold: float = 5.0, window: int = 50):\n",
640
+ " self.threshold = reward_threshold\n",
641
+ " self.window = window\n",
642
+ " self._history = []\n",
643
+ " self.level = 1\n",
644
+ " self._promotions = []\n",
645
+ "\n",
646
+ " def record(self, reward: float, step: int = None):\n",
647
+ " self._history.append(reward)\n",
648
+ " if len(self._history) >= self.window:\n",
649
+ " mean = sum(self._history[-self.window:]) / self.window\n",
650
+ " if mean >= self.threshold and self.level < 3:\n",
651
+ " self.level += 1\n",
652
+ " self._promotions.append((step or len(self._history), self.level))\n",
653
+ " print(f\"[Curriculum] ▲ Promoted to level {self.level} \"\n",
654
+ " f\"(rolling mean={mean:.2f})\")\n",
655
+ "\n",
656
+ " def plot(self):\n",
657
+ " import matplotlib.pyplot as plt\n",
658
+ " import numpy as np\n",
659
+ "\n",
660
+ " history = self._history\n",
661
+ " steps = list(range(len(history)))\n",
662
+ " window = self.window\n",
663
+ " rolling = [\n",
664
+ " sum(history[max(0,i-window):i+1]) / min(i+1, window)\n",
665
+ " for i in steps\n",
666
+ " ]\n",
667
+ "\n",
668
+ " fig, ax = plt.subplots(figsize=(10, 4))\n",
669
+ " ax.plot(steps, history, alpha=0.3, color=\"#3498db\", label=\"Episode reward\")\n",
670
+ " ax.plot(steps, rolling, color=\"#e74c3c\", linewidth=2,\n",
671
+ " label=f\"Rolling mean (w={window})\")\n",
672
+ " ax.axhline(self.threshold, linestyle=\"--\", color=\"grey\",\n",
673
+ " linewidth=1, label=f\"Promotion threshold ({self.threshold})\")\n",
674
+ " for step, level in self._promotions:\n",
675
+ " ax.axvline(step, color=\"green\", linewidth=1.5, linestyle=\":\")\n",
676
+ " ax.text(step, ax.get_ylim()[1]*0.9, f\" L{level}\",\n",
677
+ " color=\"green\", fontsize=9)\n",
678
+ " ax.set_xlabel(\"Episode\")\n",
679
+ " ax.set_ylabel(\"Reward\")\n",
680
+ " ax.set_title(\"Training Reward + Curriculum Progression\")\n",
681
+ " ax.legend()\n",
682
+ " plt.tight_layout()\n",
683
+ " plt.savefig(\"curriculum_reward_curve.png\", dpi=150)\n",
684
+ " plt.show()\n",
685
+ " print(\"📈 Saved curriculum_reward_curve.png\")\n",
686
+ "\n",
687
+ "\n",
688
+ "# ── Demo: simulate 200 episodes of improving reward ──────────────────────────\n",
689
+ "import random\n",
690
+ "cb = CurriculumCallback(reward_threshold=5.0, window=50)\n",
691
+ "for ep in range(200):\n",
692
+ " # Simulate reward slowly improving\n",
693
+ " synthetic_reward = -5 + ep * 0.08 + random.gauss(0, 2)\n",
694
+ " cb.record(synthetic_reward, step=ep)\n",
695
+ "\n",
696
+ "cb.plot()"
697
+ ]
698
+ },
699
+ {
700
+ "cell_type": "markdown",
701
+ "id": "inference-header",
702
+ "metadata": {},
703
+ "source": [
704
+ "## 🔍 Cell 11 — Inference: Before vs After Training"
705
+ ]
706
+ },
707
+ {
708
+ "cell_type": "code",
709
+ "execution_count": null,
710
+ "id": "inference-cell",
711
+ "metadata": {},
712
+ "outputs": [],
713
+ "source": [
714
+ "def run_inference_episode(model, tokenizer, engine, passes, program, max_steps=5):\n",
715
+ " \"\"\"Run inference and execute parsed pass arrays until episode termination.\"\"\"\n",
716
+ " FastLanguageModel.for_inference(model)\n",
717
+ "\n",
718
+ " _ms = min(int(max_steps), CompilerOptimizationEnv.MAX_EPISODE_STEPS)\n",
719
+ " env = CompilerOptimizationEnv(engine, passes, max_steps=_ms)\n",
720
+ " obs = env.reset(program)\n",
721
+ " system_prompt = build_system_prompt(passes)\n",
722
+ "\n",
723
+ " actions_chosen, rewards_earned = [], []\n",
724
+ " done = False\n",
725
+ "\n",
726
+ " print(f\"\\nBaseline cycles: {env.previous_cycles}\")\n",
727
+ " print(f\"Initial state:\\n{obs}\\n\")\n",
728
+ "\n",
729
+ " while not done:\n",
730
+ " messages = [\n",
731
+ " {\"role\": \"system\", \"content\": system_prompt},\n",
732
+ " {\"role\": \"user\", \"content\": f\"Current program:\\n{obs}\\n\\nChoose optimization passes:\"},\n",
733
+ " ]\n",
734
+ " inputs = tokenizer.apply_chat_template(\n",
735
+ " messages,\n",
736
+ " tokenize=True,\n",
737
+ " add_generation_prompt=True,\n",
738
+ " return_tensors=\"pt\",\n",
739
+ " ).to(model.device)\n",
740
+ "\n",
741
+ " with torch.no_grad():\n",
742
+ " outputs = model.generate(\n",
743
+ " input_ids=inputs,\n",
744
+ " max_new_tokens=32,\n",
745
+ " temperature=0.1,\n",
746
+ " do_sample=True,\n",
747
+ " )\n",
748
+ "\n",
749
+ " raw_output = tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True).strip()\n",
750
+ "\n",
751
+ " try:\n",
752
+ " parsed_actions = Deliverable2_Formatter.extract_action_array(raw_output)\n",
753
+ " except ValueError:\n",
754
+ " parsed_actions = []\n",
755
+ "\n",
756
+ " if not parsed_actions:\n",
757
+ " result = env.step(\"__invalid__\")\n",
758
+ " actions_chosen.append(\"__invalid__\")\n",
759
+ " rewards_earned.append(result.reward)\n",
760
+ " obs = result.observation\n",
761
+ " done = result.done\n",
762
+ " print(f\"Step {len(actions_chosen)}: invalid output '{raw_output}' → reward={result.reward:+.2f}\")\n",
763
+ " continue\n",
764
+ "\n",
765
+ " for action in parsed_actions[:CompilerOptimizationEnv.MAX_EPISODE_STEPS]:\n",
766
+ " a = str(action).strip()\n",
767
+ " al = a.lower()\n",
768
+ " if al in (\"done\", \"stop\"):\n",
769
+ " result = env.step(\"done\" if al == \"done\" else \"STOP\")\n",
770
+ " actions_chosen.append(al)\n",
771
+ " rewards_earned.append(result.reward)\n",
772
+ " obs = result.observation\n",
773
+ " done = result.done\n",
774
+ " print(f\"Step {len(actions_chosen)}: stop ('{a}') → reward={result.reward:+.2f}\")\n",
775
+ " break\n",
776
+ " action = al\n",
777
+ " if action not in passes:\n",
778
+ " for k in passes:\n",
779
+ " if k.lower() == al:\n",
780
+ " action = k\n",
781
+ " break\n",
782
+ " result = env.step(action)\n",
783
+ " actions_chosen.append(action)\n",
784
+ " rewards_earned.append(result.reward)\n",
785
+ " obs = result.observation\n",
786
+ " done = result.done\n",
787
+ " print(f\"Step {len(actions_chosen)}: '{action}' → reward={result.reward:+.2f}\")\n",
788
+ " if done:\n",
789
+ " break\n",
790
+ "\n",
791
+ " summary = env._episode_summary()\n",
792
+ " print(f\"\\n{'─'*40}\")\n",
793
+ " print(f\"Total improvement: {summary['total_improvement_pct']:.1f}%\")\n",
794
+ " print(f\"Final cycles: {summary['final_cycles']} (was {summary['baseline_cycles']})\")\n",
795
+ " return summary\n",
796
+ "\n",
797
+ "\n",
798
+ "# Uncomment after training:\n",
799
+ "# summary = run_inference_episode(\n",
800
+ "# model, tokenizer, MockEngine(), MOCK_PASSES, SAMPLE_PROGRAM\n",
801
+ "# )\n",
802
+ "\n",
803
+ "print(\"✅ Inference cell ready. Uncomment the last block after training to run.\")"
804
+ ]
805
+ },
806
+ {
807
+ "cell_type": "markdown",
808
+ "id": "tips-header",
809
+ "metadata": {},
810
+ "source": [
811
+ "---\n",
812
+ "## 📝 Notes & Tips\n",
813
+ "\n",
814
+ "| What | Why it matters |\n",
815
+ "|------|----------------|\n",
816
+ "| `num_generations=4` | GRPO needs K≥2 rollouts per prompt to compute group-relative advantage. K=4 balances diversity vs. compute. |\n",
817
+ "| `beta=0.04` | KL penalty keeping policy close to reference. Too low → mode collapse. Too high → no learning. |\n",
818
+ "| `max_new_tokens=16` | Actions are one word. This prevents wasted computation and keeps the model from adding explanations. |\n",
819
+ "| Equivalence / semantic failure | The shaped reward function returns `−1000.0` when a candidate fails verification (RLVR hard gate). |\n",
820
+ "| Terminal reward | Final cycle savings (scaled) are given at `STOP`, step cap (5), or at end of a pass list; non-terminal steps use a small constant cost. |\n",
821
+ "| Soft invalid-action termination | 3 consecutive invalid actions → end. Single mistakes don't kill the episode; the agent can recover. |\n",
822
+ "| `deepcopy` on all pass inputs | Role 3's passes mutate dicts in-place. Without this, `original_program` gets corrupted and the verifier fails spuriously. |\n",
823
+ "\n",
824
+ "**Next steps:**\n",
825
+ "- Replace `MockEngine` with Role 1's real engine\n",
826
+ "- Replace `MOCK_PASSES` with Role 3's real passes\n",
827
+ "- Push environment to HuggingFace Spaces: `openenv init && openenv deploy`\n",
828
+ "- Add W&B sweep to tune `beta`, `learning_rate`, `num_generations`"
829
+ ]
830
+ }
831
+ ],
832
+ "metadata": {
833
+ "accelerator": "GPU",
834
+ "colab": {
835
+ "gpuType": "T4",
836
+ "provenance": []
837
+ },
838
+ "kernelspec": {
839
+ "display_name": "Python 3",
840
+ "language": "python",
841
+ "name": "python3"
842
+ },
843
+ "language_info": {
844
+ "name": "python",
845
+ "version": "3.10.0"
846
+ }
847
+ },
848
+ "nbformat": 4,
849
+ "nbformat_minor": 5
850
+ }
space/space/space/space/space/space/space/reverse_pass/toyir_rl_support.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Toy-IR RL training support: PASSES, mock engine, environment, terminal-weighted shaped reward.
3
+ Used by training notebooks. Do not import toy_vm / verifier / reward_utils here.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+ import json
10
+ import os
11
+ import sys
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ # Allow notebooks in subdirs to import when cwd is set to project root
16
+ _ROOT = os.path.dirname(os.path.abspath(__file__))
17
+
18
+
19
+ @dataclass
20
+ class StepResult:
21
+ observation: str
22
+ reward: float
23
+ done: bool
24
+ info: Dict[str, Any] = field(default_factory=dict)
25
+
26
+
27
+ @dataclass
28
+ class EpisodeStats:
29
+ steps_taken: int = 0
30
+ total_reward: float = 0.0
31
+ passes_applied: List[str] = field(default_factory=list)
32
+ invalid_actions: int = 0
33
+ no_ops: int = 0
34
+ baseline_cycles: int = 0
35
+ final_cycles: int = 0
36
+
37
+ @property
38
+ def total_improvement_pct(self) -> float:
39
+ if self.baseline_cycles == 0:
40
+ return 0.0
41
+ return ((self.baseline_cycles - self.final_cycles) / self.baseline_cycles) * 100.0
42
+
43
+
44
+ class MCPEnvironment:
45
+ def reset(self, *args, **kwargs):
46
+ raise NotImplementedError
47
+
48
+ def step(self, *args, **kwargs):
49
+ raise NotImplementedError
50
+
51
+ def state(self):
52
+ raise NotImplementedError
53
+
54
+
55
+ def _op_u(ins: dict) -> str:
56
+ return str(ins.get("op", "")).upper()
57
+
58
+
59
+ def _first_numeric(ins: dict) -> Optional[int]:
60
+ for k in ("src1", "src2"):
61
+ v = ins.get(k)
62
+ if isinstance(v, int) and v > 0:
63
+ return v
64
+ args = ins.get("args", [])
65
+ if args:
66
+ a0 = args[0]
67
+ try:
68
+ if isinstance(a0, int):
69
+ return a0
70
+ return int(str(a0), 0)
71
+ except (ValueError, TypeError):
72
+ return None
73
+ v = ins.get("src1")
74
+ if isinstance(v, int):
75
+ return v
76
+ return None
77
+
78
+
79
+ # --- forward / reverse passes (list[dict] IR) ---------------------------------
80
+
81
+
82
+ def constant_folding(program: List[dict]) -> List[dict]:
83
+ p = copy.deepcopy(program)
84
+ return p[:-1] if len(p) > 1 else p
85
+
86
+
87
+ def dead_code_elimination(program: List[dict]) -> List[dict]:
88
+ p = copy.deepcopy(program)
89
+ return p[:-1] if len(p) > 2 else p
90
+
91
+
92
+ def peephole_optimization(program: List[dict]) -> List[dict]:
93
+ return copy.deepcopy(program)
94
+
95
+
96
+ def expand_constant(program: List[dict]) -> List[dict]:
97
+ """
98
+ Deoptimization: split one CONST into two consts + ADD to raise cycle count sometimes,
99
+ giving forward passes a different surface (reverse exploration).
100
+ """
101
+ p = copy.deepcopy(program)
102
+ for i, ins in enumerate(p):
103
+ if _op_u(ins) not in ("CONST",):
104
+ continue
105
+ v = _first_numeric(ins) if _first_numeric(ins) is not None else None
106
+ if v is None and ins.get("args"):
107
+ try:
108
+ v = int(ins["args"][0])
109
+ except (ValueError, TypeError, IndexError, KeyError):
110
+ v = None
111
+ if v is None or v < 2:
112
+ continue
113
+ d = str(ins.get("dest", "t0"))
114
+ h, rest = v // 2, v - (v // 2)
115
+ repl = [
116
+ {**{k: ins[k] for k in ins if k in ("type",)}, "op": "const", "dest": f"{d}_a", "args": [str(h)]},
117
+ {**{k: ins[k] for k in ins if k in ("type",)}, "op": "const", "dest": f"{d}_b", "args": [str(rest)]},
118
+ {
119
+ "op": "add",
120
+ "dest": d,
121
+ "args": [f"{d}_a", f"{d}_b"],
122
+ **{k: ins[k] for k in ins if k in ("type",) and k not in ("op", "dest", "args")},
123
+ },
124
+ ]
125
+ p[i : i + 1] = repl
126
+ return p
127
+ return p
128
+
129
+
130
+ def duplicate_computation(program: List[dict]) -> List[dict]:
131
+ """
132
+ Deoptimization: duplicate a binary op into a new destination (redundant work).
133
+ """
134
+ p = copy.deepcopy(program)
135
+ for i, ins in enumerate(p):
136
+ ou = _op_u(ins)
137
+ if ou in ("ADD", "MUL", "SUB", "DIV"):
138
+ dest = str(ins.get("dest", "t0"))
139
+ dup = copy.deepcopy(ins)
140
+ dup["dest"] = f"{dest}_d"
141
+ p.insert(i + 1, dup)
142
+ return p
143
+ return p
144
+
145
+
146
+ PASSES: Dict[str, Any] = {
147
+ "constant_folding": constant_folding,
148
+ "dead_code_elimination": dead_code_elimination,
149
+ "peephole_optimization": peephole_optimization,
150
+ "expand_constant": expand_constant,
151
+ "duplicate_computation": duplicate_computation,
152
+ }
153
+
154
+ MOCK_PASSES = PASSES # alias for existing notebook references
155
+
156
+
157
+ class MockEngine:
158
+ """Stub: cycle count = instruction list length; always equivalent."""
159
+
160
+ def execute_and_count_cycles(self, program: List[dict]) -> int:
161
+ return len(program)
162
+
163
+ def verify_equivalence(self, original, candidate) -> bool:
164
+ return True
165
+
166
+
167
+ # Back-compat: some notebooks use these names
168
+ MOCK_ENGINE = MockEngine()
169
+
170
+
171
+ # --- Shaped reward (user-specified) -------------------------------------------
172
+
173
+
174
+ def compute_shaped_reward(
175
+ equivalent: bool,
176
+ is_terminal: bool,
177
+ original_cycles: int,
178
+ current_cycles: int,
179
+ ) -> float:
180
+ # RLVR hard gate — broken optimizations get massive penalty
181
+ if not equivalent:
182
+ return -1000.0
183
+
184
+ # Terminal reward: large signal proportional to final cycle reduction
185
+ # This is what makes reverse passes learnable — agent gets rewarded for the END state, not the path
186
+ if is_terminal:
187
+ if original_cycles == 0:
188
+ return 0.0
189
+ relative_savings = (original_cycles - current_cycles) / original_cycles
190
+ return relative_savings * 100.0
191
+
192
+ # Per-step: tiny constant cost to discourage infinite thrashing
193
+ # CRITICAL: do NOT punish cycle increases here. Per-step cycle-delta reward kills reverse-pass exploration.
194
+ return -0.1
195
+
196
+
197
+ # --- Environment --------------------------------------------------------------
198
+
199
+
200
+ class CompilerOptimizationEnv(MCPEnvironment):
201
+ TIME_TAX: float = 1.0
202
+ NO_OP_PENALTY: float = -2.0
203
+ INVALID_ACTION_PENALTY: float = -5.0
204
+ MAX_INVALID_ACTIONS: int = 3
205
+ TERMINAL_BONUS_SCALE: float = 0.5
206
+ MAX_EPISODE_STEPS: int = 5 # hard cap (Change 4)
207
+
208
+ def __init__(
209
+ self,
210
+ role1_engine: Any,
211
+ role3_passes: Dict[str, Any],
212
+ max_steps: int = 5,
213
+ curriculum_level: int = 1,
214
+ ):
215
+ self.engine = role1_engine
216
+ self.passes = role3_passes
217
+ self.max_steps = max(1, int(max_steps))
218
+ self.curriculum_level = curriculum_level
219
+ self._valid_actions: frozenset = frozenset(self.passes.keys()) | {"STOP", "done", "DONE"}
220
+ self._stats: Optional[EpisodeStats] = None
221
+ self.original_program: Optional[List[dict]] = None
222
+ self.current_program: Optional[List[dict]] = None
223
+ self.previous_cycles = 0
224
+ self._consecutive_invalid = 0
225
+
226
+ def reset(self, new_program_json: List[dict]) -> str:
227
+ self.original_program = copy.deepcopy(new_program_json)
228
+ self.current_program = copy.deepcopy(new_program_json)
229
+ self.previous_cycles = self._safe_count_cycles(self.current_program)
230
+ self._consecutive_invalid = 0
231
+ self._stats = EpisodeStats(
232
+ baseline_cycles=self.previous_cycles,
233
+ final_cycles=self.previous_cycles,
234
+ )
235
+ return self.state()
236
+
237
+ def state(self) -> str:
238
+ return self._program_to_pseudoasm(self.current_program)
239
+
240
+ def step(self, action_string: str) -> StepResult:
241
+ assert self._stats is not None, "Call reset() before step()."
242
+ aup = str(action_string).upper()
243
+ is_stop = aup in ("STOP", "DONE")
244
+ if is_stop:
245
+ self._stats.steps_taken += 1
246
+ orig = int(self._stats.baseline_cycles)
247
+ cur = self._safe_count_cycles(self.current_program)
248
+ ok = bool(self.engine.verify_equivalence(self.original_program, self.current_program))
249
+ reward = compute_shaped_reward(ok, True, orig, cur)
250
+ self._stats.total_reward += reward
251
+ return StepResult(
252
+ self.state(),
253
+ reward,
254
+ True,
255
+ {
256
+ "reason": "stop",
257
+ "is_terminal": True,
258
+ "stop_token": aup,
259
+ "episode_stats": self._episode_summary(),
260
+ },
261
+ )
262
+ if action_string not in self.passes:
263
+ return self._handle_invalid_action(action_string)
264
+
265
+ self._stats.steps_taken += 1
266
+ candidate = self.passes[action_string](copy.deepcopy(self.current_program))
267
+ if not self.engine.verify_equivalence(self.original_program, candidate):
268
+ return self._handle_semantic_violation()
269
+ new_cycles = self._safe_count_cycles(candidate)
270
+ reward, info = self._compute_reward(action_string, new_cycles)
271
+ self.current_program = candidate
272
+ self.previous_cycles = new_cycles
273
+ self._stats.final_cycles = new_cycles
274
+ self._stats.total_reward += reward
275
+ self._stats.passes_applied.append(action_string)
276
+ self._consecutive_invalid = 0
277
+ done = self._stats.steps_taken >= self.max_steps
278
+ if done:
279
+ info["reason"] = "max_steps_reached"
280
+ info["episode_stats"] = self._episode_summary()
281
+ return StepResult(self.state(), reward, done, info)
282
+
283
+ def _compute_reward(self, action: str, new_cycles: int) -> Tuple[float, dict]:
284
+ info: Dict[str, Any] = {"action": action}
285
+ orig = int(self._stats.baseline_cycles)
286
+ is_term = self._stats.steps_taken >= self.max_steps
287
+ if new_cycles == self.previous_cycles and not is_term:
288
+ self._stats.no_ops += 1
289
+ info["no_op"] = True
290
+ reward = compute_shaped_reward(True, is_term, orig, new_cycles)
291
+ info["is_terminal"] = is_term
292
+ info["new_cycles"] = new_cycles
293
+ return reward, info
294
+
295
+ def _compute_crash_penalty(self) -> float:
296
+ return -1000.0
297
+
298
+ def _handle_invalid_action(self, action: str) -> StepResult:
299
+ self._consecutive_invalid += 1
300
+ self._stats.invalid_actions += 1
301
+ done = self._consecutive_invalid >= self.MAX_INVALID_ACTIONS
302
+ info: Dict[str, Any] = {
303
+ "error": f"Unknown action: '{action}'",
304
+ "valid_actions": sorted(self._valid_actions),
305
+ "consecutive_invalid": self._consecutive_invalid,
306
+ }
307
+ if done:
308
+ info["reason"] = "too_many_invalid_actions"
309
+ info["episode_stats"] = self._episode_summary()
310
+ return StepResult(self.state(), self.INVALID_ACTION_PENALTY, done, info)
311
+
312
+ def _handle_semantic_violation(self) -> StepResult:
313
+ return StepResult(
314
+ self.state(),
315
+ self._compute_crash_penalty(),
316
+ True,
317
+ {
318
+ "error": "Semantic equivalence check FAILED.",
319
+ "reason": "semantic_violation",
320
+ "episode_stats": self._episode_summary(),
321
+ },
322
+ )
323
+
324
+ @staticmethod
325
+ def _program_to_pseudoasm(program: Optional[List[dict]]) -> str:
326
+ if not program:
327
+ return "; (empty program)"
328
+ lines = []
329
+ for i, instr in enumerate(program):
330
+ op = instr.get("op", "NOP")
331
+ args = instr.get("args", [])
332
+ dest = instr.get("dest")
333
+ typ = instr.get("type", "")
334
+ arg_str = ", ".join(str(a) for a in args)
335
+ type_hint = f":{typ}" if typ else ""
336
+ if dest:
337
+ lines.append(f" {i:>3}: {dest}{type_hint} = {op} {arg_str}")
338
+ else:
339
+ lines.append(f" {i:>3}: {op} {arg_str}")
340
+ return "\n".join(lines)
341
+
342
+ def _safe_count_cycles(self, program: List[dict]) -> int:
343
+ return max(0, int(self.engine.execute_and_count_cycles(program)))
344
+
345
+ def _episode_summary(self) -> dict:
346
+ s = self._stats
347
+ return {
348
+ "steps": s.steps_taken,
349
+ "total_reward": round(s.total_reward, 3),
350
+ "passes_applied": s.passes_applied,
351
+ "invalid_actions": s.invalid_actions,
352
+ "no_ops": s.no_ops,
353
+ "baseline_cycles": s.baseline_cycles,
354
+ "final_cycles": s.final_cycles,
355
+ "total_improvement_pct": round(s.total_improvement_pct, 3),
356
+ }
357
+
358
+ def available_actions(self) -> List[str]:
359
+ return sorted(self._valid_actions)
360
+
361
+
362
+ SAMPLE_PROGRAM: List[dict] = [
363
+ {"op": "const", "dest": "x", "args": ["5"], "type": "int"},
364
+ {"op": "const", "dest": "y", "args": ["3"], "type": "int"},
365
+ {"op": "add", "dest": "z", "args": ["x", "y"], "type": "int"},
366
+ {"op": "mul", "dest": "w", "args": ["z", "x"], "type": "int"},
367
+ {"op": "ret", "args": ["w"]},
368
+ ]
369
+
370
+
371
+ # --- Rollout for GRPO reward (terminal-weighted shaping) ------------------------
372
+
373
+
374
+ def rollout_shaped_return(
375
+ program: List[dict],
376
+ action_names: List[str],
377
+ engine: Any,
378
+ passes: Optional[Dict[str, Any]] = None,
379
+ ) -> Tuple[float, int, int, bool, bool, bool]:
380
+ """
381
+ Returns:
382
+ (total_shaped_reward, n_expand_constant, n_duplicate, hit_stop, hit_step_cap, parse_ok)
383
+ If parse_ok is False (unknown pass in sequence), first value is -1000.0.
384
+ Sums per-step `env.step` shaped rewards, adds a terminal `compute_shaped_reward` when
385
+ the action list ends without an explicit STOP (or already terminal from step cap).
386
+ """
387
+ passes = passes or PASSES
388
+ cap = CompilerOptimizationEnv.MAX_EPISODE_STEPS
389
+ env = CompilerOptimizationEnv(engine, passes, max_steps=cap)
390
+ env.reset(program)
391
+ total = 0.0
392
+ n_e = 0
393
+ n_d = 0
394
+ hit_stop = False
395
+ hit_cap = False
396
+ for raw in action_names:
397
+ a = str(raw).strip()
398
+ if not a:
399
+ continue
400
+ aup = a.upper()
401
+ if aup in ("STOP", "DONE"):
402
+ r = env.step(a)
403
+ total += r.reward
404
+ hit_stop = True
405
+ return total, n_e, n_d, hit_stop, hit_cap, True
406
+ if a not in passes:
407
+ return -1000.0, n_e, n_d, hit_stop, hit_cap, False
408
+ if a == "expand_constant":
409
+ n_e += 1
410
+ elif a == "duplicate_computation":
411
+ n_d += 1
412
+ r = env.step(a)
413
+ total += r.reward
414
+ if r.info.get("reason") == "semantic_violation":
415
+ return -1000.0, n_e, n_d, hit_stop, hit_cap, True
416
+ if r.done and r.info.get("reason") == "max_steps_reached":
417
+ hit_cap = True
418
+ return total, n_e, n_d, hit_stop, hit_cap, True
419
+ ok = bool(engine.verify_equivalence(env.original_program, env.current_program))
420
+ cur = int(engine.execute_and_count_cycles(env.current_program or []))
421
+ orig = int(env._stats.baseline_cycles)
422
+ total += compute_shaped_reward(ok, True, orig, cur)
423
+ return total, n_e, n_d, hit_stop, hit_cap, True
424
+
425
+
426
+ # Global window for logging (50-episode / completion windows)
427
+ _REVERSE_LOG_WINDOW: List[dict] = []
428
+
429
+
430
+ def log_reverse_pass_stats_for_completion(
431
+ used_expand: int,
432
+ used_dup: int,
433
+ ) -> None:
434
+ """Call once per training completion. Logs every 50 'episodes' (completions)."""
435
+ global _REVERSE_LOG_WINDOW
436
+ any_r = (used_expand + used_dup) > 0
437
+ _REVERSE_LOG_WINDOW.append(
438
+ {
439
+ "any_reverse": any_r,
440
+ "expand": used_expand,
441
+ "dup": used_dup,
442
+ }
443
+ )
444
+ if len(_REVERSE_LOG_WINDOW) < 50:
445
+ return
446
+ w = _REVERSE_LOG_WINDOW
447
+ _REVERSE_LOG_WINDOW = []
448
+ reverse_pass_episodes = sum(1 for e in w if e["any_reverse"])
449
+ expand_constant_count = sum(e["expand"] for e in w)
450
+ duplicate_computation_count = sum(e["dup"] for e in w)
451
+ payload = {
452
+ "reverse_pass_episodes": reverse_pass_episodes,
453
+ "expand_constant_count": expand_constant_count,
454
+ "duplicate_computation_count": duplicate_computation_count,
455
+ }
456
+ try:
457
+ import wandb
458
+
459
+ if wandb.run is not None:
460
+ wandb.log(payload)
461
+ except Exception:
462
+ pass
463
+ print(
464
+ f"[reverse_pass/50] reverse_pass_episodes={reverse_pass_episodes} "
465
+ f"expand_constant_count={expand_constant_count} "
466
+ f"duplicate_computation_count={duplicate_computation_count}"
467
+ )
468
+
469
+
470
+ def ensure_import_path() -> None:
471
+ d = _ROOT
472
+ if d and d not in sys.path:
473
+ sys.path.insert(0, d)
space/space/space/space/space/space/space/space/reverse_pass/reversepass_new_eval_baseline.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
space/space/space/space/space/space/space/space/space/app.py CHANGED
@@ -23,6 +23,7 @@ from runtime_core import (
23
  MockEngine,
24
  SAMPLE_PROGRAM,
25
  )
 
26
 
27
 
28
  DEFAULT_IR = json.dumps(SAMPLE_PROGRAM, indent=2)
@@ -113,8 +114,9 @@ def build_demo() -> gr.Blocks:
113
  # Compiler optimization (Toy-IR) — interactive demo
114
 
115
  This Space runs **CPU-only** demos: **Deliverable 2** state translation and action parsing,
116
- plus the **CompilerOptimizationEnv** smoke loop from your notebooks. Full GRPO / Unsloth
117
- training belongs on Colab or a GPU Space.
 
118
  """
119
  ).strip()
120
  )
@@ -154,6 +156,28 @@ def build_demo() -> gr.Blocks:
154
  out_ep = gr.Textbox(label="Log", lines=20)
155
  gr.Button("Run episode", variant="primary").click(run_episode, [ir_ep, acts, ms], [out_ep])
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  gr.Markdown(
158
  "Source notebooks in the parent repo: `compiler_optimization_grpo.ipynb`, "
159
  "`role2_deliverable3_training_loop (2) (1).ipynb`, `compiler_tetris (1).ipynb`, `metahack1 (1).ipynb`."
 
23
  MockEngine,
24
  SAMPLE_PROGRAM,
25
  )
26
+ from train import run_toy_training
27
 
28
 
29
  DEFAULT_IR = json.dumps(SAMPLE_PROGRAM, indent=2)
 
114
  # Compiler optimization (Toy-IR) — interactive demo
115
 
116
  This Space runs **CPU-only** demos: **Deliverable 2** state translation and action parsing,
117
+ plus the **CompilerOptimizationEnv** loop. A **toy REINFORCE** tab trains a tiny
118
+ stateless policy over the mock passes (see `train.py`). Full **GRPO + LLM + Unsloth**
119
+ still belongs on Colab or a GPU machine.
120
  """
121
  ).strip()
122
  )
 
156
  out_ep = gr.Textbox(label="Log", lines=20)
157
  gr.Button("Run episode", variant="primary").click(run_episode, [ir_ep, acts, ms], [out_ep])
158
 
159
+ with gr.Tab("Toy training (REINFORCE)"):
160
+ gr.Markdown(
161
+ textwrap.dedent(
162
+ """
163
+ Trains a **stateless** categorical policy over the three mock passes
164
+ (`constant_folding`, `dead_code_elimination`, `loop_unrolling`) using
165
+ **REINFORCE** in pure Python. Same logic as: `python train.py --episodes 50`.
166
+
167
+ This is a CPU smoke run, not an LLM. For real GRPO, use your project notebooks
168
+ on a GPU.
169
+ """
170
+ ).strip()
171
+ )
172
+ tr_ep = gr.Slider(5, 200, value=50, step=1, label="episodes")
173
+ tr_ms = gr.Slider(2, 20, value=8, step=1, label="max_steps per episode")
174
+ tr_seed = gr.Number(value=0, label="random seed", precision=0)
175
+ tr_lr = gr.Slider(0.01, 0.5, value=0.15, step=0.01, label="learning rate")
176
+ out_tr = gr.Textbox(label="Training log", lines=18)
177
+ gr.Button("Run training", variant="primary").click(
178
+ run_toy_training, [tr_ep, tr_ms, tr_seed, tr_lr], [out_tr]
179
+ )
180
+
181
  gr.Markdown(
182
  "Source notebooks in the parent repo: `compiler_optimization_grpo.ipynb`, "
183
  "`role2_deliverable3_training_loop (2) (1).ipynb`, `compiler_tetris (1).ipynb`, `metahack1 (1).ipynb`."
space/space/space/space/space/space/space/space/space/space/space/runtime_core.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shared runtime for the Gradio Space: mock compiler env + Deliverable 2 formatting.
3
+ Sourced from `compiler_optimization_grpo.ipynb` and
4
+ `role2_deliverable3_training_loop (2) (1) (1).ipynb`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import json
11
+ import re
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ # --- Deliverable 2 (LLM-facing pseudo-asm + pass-array parsing) -----------------
16
+
17
+
18
+ class Deliverable2_Formatter:
19
+ @staticmethod
20
+ def translate_state(raw_json: list) -> str:
21
+ """Translate raw JSON IR into compact pseudo-assembly."""
22
+ if not isinstance(raw_json, list) or not raw_json:
23
+ return "; (empty program — 0 instructions)"
24
+
25
+ pseudo_assembly: list[str] = []
26
+ for i, instruction in enumerate(raw_json):
27
+ if not isinstance(instruction, dict):
28
+ pseudo_assembly.append(f"{i}. NOP")
29
+ continue
30
+
31
+ op = str(instruction.get("op", "UNKNOWN")).upper()
32
+ args = ", ".join(str(arg) for arg in instruction.get("args", []))
33
+ dest = instruction.get("dest", "")
34
+ if dest:
35
+ line = f"{i}. {dest} = {op} {args}".rstrip()
36
+ else:
37
+ line = f"{i}. {op} {args}".rstrip()
38
+ pseudo_assembly.append(line)
39
+
40
+ return "\n".join(pseudo_assembly)
41
+
42
+ @staticmethod
43
+ def extract_action_array(llm_output: str) -> list:
44
+ """Best-effort extraction of JSON pass arrays from noisy LLM output."""
45
+ text = (llm_output or "").strip()
46
+ if not text:
47
+ raise ValueError("Invalid JSON format")
48
+
49
+ try:
50
+ parsed = json.loads(text)
51
+ if isinstance(parsed, list):
52
+ return parsed
53
+ except json.JSONDecodeError:
54
+ pass
55
+
56
+ cleaned = re.sub(r"```(?:json)?", "", text, flags=re.IGNORECASE).replace("```", "").strip()
57
+ if cleaned != text:
58
+ try:
59
+ parsed = json.loads(cleaned)
60
+ if isinstance(parsed, list):
61
+ return parsed
62
+ except json.JSONDecodeError:
63
+ pass
64
+
65
+ match = re.search(r"\[.*?\]", text, re.DOTALL)
66
+ if match:
67
+ candidate = match.group(0)
68
+ try:
69
+ parsed = json.loads(candidate)
70
+ if isinstance(parsed, list):
71
+ return parsed
72
+ except json.JSONDecodeError:
73
+ try:
74
+ parsed = json.loads(candidate.replace("'", '"'))
75
+ if isinstance(parsed, list):
76
+ return parsed
77
+ except json.JSONDecodeError:
78
+ pass
79
+
80
+ raise ValueError("Invalid JSON format")
81
+
82
+
83
+ # --- OpenEnv-style compiler environment (mock engine) --------------------------
84
+
85
+
86
+ class MCPEnvironment:
87
+ """Minimal stub. In production: `from openenv import MCPEnvironment`."""
88
+
89
+ def reset(self, *args, **kwargs):
90
+ raise NotImplementedError
91
+
92
+ def step(self, *args, **kwargs):
93
+ raise NotImplementedError
94
+
95
+ def state(self):
96
+ raise NotImplementedError
97
+
98
+
99
+ @dataclass
100
+ class StepResult:
101
+ observation: str
102
+ reward: float
103
+ done: bool
104
+ info: Dict[str, Any] = field(default_factory=dict)
105
+
106
+
107
+ @dataclass
108
+ class EpisodeStats:
109
+ steps_taken: int = 0
110
+ total_reward: float = 0.0
111
+ passes_applied: List[str] = field(default_factory=list)
112
+ invalid_actions: int = 0
113
+ no_ops: int = 0
114
+ baseline_cycles: int = 0
115
+ final_cycles: int = 0
116
+
117
+ @property
118
+ def total_improvement_pct(self) -> float:
119
+ if self.baseline_cycles == 0:
120
+ return 0.0
121
+ return ((self.baseline_cycles - self.final_cycles) / self.baseline_cycles) * 100.0
122
+
123
+
124
+ class CompilerOptimizationEnv(MCPEnvironment):
125
+ TIME_TAX: float = 1.0
126
+ NO_OP_PENALTY: float = -2.0
127
+ INVALID_ACTION_PENALTY: float = -5.0
128
+ MAX_INVALID_ACTIONS: int = 3
129
+ TERMINAL_BONUS_SCALE: float = 0.5
130
+
131
+ def __init__(
132
+ self,
133
+ role1_engine,
134
+ role3_passes: Dict[str, Any],
135
+ max_steps: int = 10,
136
+ curriculum_level: int = 1,
137
+ ):
138
+ self.engine = role1_engine
139
+ self.passes = role3_passes
140
+ self.max_steps = max_steps
141
+ self.curriculum_level = curriculum_level
142
+ self._valid_actions = frozenset(self.passes.keys())
143
+
144
+ self._stats: Optional[EpisodeStats] = None
145
+ self.original_program = None
146
+ self.current_program = None
147
+ self.previous_cycles = 0
148
+ self._consecutive_invalid = 0
149
+
150
+ def reset(self, new_program_json: List[Dict]) -> str:
151
+ self.original_program = copy.deepcopy(new_program_json)
152
+ self.current_program = copy.deepcopy(new_program_json)
153
+ self.previous_cycles = self._safe_count_cycles(self.current_program)
154
+ self._consecutive_invalid = 0
155
+ self._stats = EpisodeStats(
156
+ baseline_cycles=self.previous_cycles,
157
+ final_cycles=self.previous_cycles,
158
+ )
159
+ return self.state()
160
+
161
+ def state(self) -> str:
162
+ assert self.current_program is not None
163
+ return self._program_to_pseudoasm(self.current_program)
164
+
165
+ def step(self, action_string: str) -> StepResult:
166
+ assert self._stats is not None, "Call reset() before step()."
167
+ self._stats.steps_taken += 1
168
+
169
+ if action_string not in self._valid_actions:
170
+ return self._handle_invalid_action(action_string)
171
+
172
+ candidate_program = self.passes[action_string](copy.deepcopy(self.current_program))
173
+
174
+ is_valid = self.engine.verify_equivalence(self.original_program, candidate_program)
175
+ if not is_valid:
176
+ return self._handle_semantic_violation()
177
+
178
+ new_cycles = self._safe_count_cycles(candidate_program)
179
+ reward, info = self._compute_reward(action_string, new_cycles)
180
+
181
+ self.current_program = candidate_program
182
+ self.previous_cycles = new_cycles
183
+ self._stats.final_cycles = new_cycles
184
+ self._stats.total_reward += reward
185
+ self._stats.passes_applied.append(action_string)
186
+ self._consecutive_invalid = 0
187
+
188
+ done = self._stats.steps_taken >= self.max_steps
189
+ if done:
190
+ terminal_bonus = self._terminal_bonus()
191
+ reward += terminal_bonus
192
+ info["terminal_bonus"] = terminal_bonus
193
+ info["reason"] = "max_steps_reached"
194
+ info["episode_stats"] = self._episode_summary()
195
+
196
+ return StepResult(self.state(), reward, done, info)
197
+
198
+ def _compute_reward(self, action: str, new_cycles: int) -> Tuple[float, Dict]:
199
+ info: Dict[str, Any] = {"action": action}
200
+ if self.previous_cycles == 0:
201
+ return -self.TIME_TAX, {**info, "note": "zero_baseline"}
202
+
203
+ old_cycles = self.previous_cycles
204
+ delta_pct = ((old_cycles - new_cycles) / old_cycles) * 100.0
205
+
206
+ if new_cycles == old_cycles:
207
+ reward = self.NO_OP_PENALTY
208
+ if self._stats is not None:
209
+ self._stats.no_ops += 1
210
+ info["no_op"] = True
211
+ else:
212
+ reward = delta_pct - self.TIME_TAX
213
+ info["delta_pct"] = round(delta_pct, 3)
214
+
215
+ info["prev_cycles"] = old_cycles
216
+ info["new_cycles"] = new_cycles
217
+ return reward, info
218
+
219
+ def _terminal_bonus(self) -> float:
220
+ if self._stats is None:
221
+ return 0.0
222
+ return max(0.0, self._stats.total_improvement_pct * self.TERMINAL_BONUS_SCALE)
223
+
224
+ def _compute_crash_penalty(self) -> float:
225
+ return -2.0 * (100.0 * self.max_steps)
226
+
227
+ def _handle_invalid_action(self, action: str) -> StepResult:
228
+ self._consecutive_invalid += 1
229
+ if self._stats is not None:
230
+ self._stats.invalid_actions += 1
231
+ done = self._consecutive_invalid >= self.MAX_INVALID_ACTIONS
232
+ info = {
233
+ "error": f"Unknown action: '{action}'",
234
+ "valid_actions": sorted(self._valid_actions),
235
+ "consecutive_invalid": self._consecutive_invalid,
236
+ }
237
+ if done:
238
+ info["reason"] = "too_many_invalid_actions"
239
+ info["episode_stats"] = self._episode_summary()
240
+ return StepResult(self.state(), self.INVALID_ACTION_PENALTY, done, info)
241
+
242
+ def _handle_semantic_violation(self) -> StepResult:
243
+ return StepResult(
244
+ self.state(),
245
+ self._compute_crash_penalty(),
246
+ True,
247
+ {
248
+ "error": "Semantic equivalence check FAILED.",
249
+ "reason": "semantic_violation",
250
+ "episode_stats": self._episode_summary(),
251
+ },
252
+ )
253
+
254
+ @staticmethod
255
+ def _program_to_pseudoasm(program: List[Dict]) -> str:
256
+ if not program:
257
+ return "; (empty program)"
258
+ lines = []
259
+ for i, instr in enumerate(program):
260
+ op = instr.get("op", "NOP")
261
+ args = instr.get("args", [])
262
+ dest = instr.get("dest")
263
+ typ = instr.get("type", "")
264
+ arg_str = ", ".join(str(a) for a in args)
265
+ type_hint = f":{typ}" if typ else ""
266
+ if dest:
267
+ lines.append(f" {i:>3}: {dest}{type_hint} = {op} {arg_str}")
268
+ else:
269
+ lines.append(f" {i:>3}: {op} {arg_str}")
270
+ return "\n".join(lines)
271
+
272
+ def _safe_count_cycles(self, program: List[Dict]) -> int:
273
+ return max(0, int(self.engine.execute_and_count_cycles(program)))
274
+
275
+ def _episode_summary(self) -> Dict:
276
+ s = self._stats
277
+ if s is None:
278
+ return {}
279
+ return {
280
+ "steps": s.steps_taken,
281
+ "total_reward": round(s.total_reward, 3),
282
+ "passes_applied": s.passes_applied,
283
+ "invalid_actions": s.invalid_actions,
284
+ "no_ops": s.no_ops,
285
+ "baseline_cycles": s.baseline_cycles,
286
+ "final_cycles": s.final_cycles,
287
+ "total_improvement_pct": round(s.total_improvement_pct, 3),
288
+ }
289
+
290
+ def available_actions(self) -> List[str]:
291
+ return sorted(self._valid_actions)
292
+
293
+
294
+ class MockEngine:
295
+ """Stub engine: cycles = instruction count, all programs semantically valid."""
296
+
297
+ def execute_and_count_cycles(self, program):
298
+ return len(program)
299
+
300
+ def verify_equivalence(self, original, candidate):
301
+ return True
302
+
303
+
304
+ MOCK_PASSES = {
305
+ "constant_folding": lambda p: p[:-1] if len(p) > 1 else p,
306
+ "dead_code_elimination": lambda p: p[:-1] if len(p) > 2 else p,
307
+ "loop_unrolling": lambda p: p,
308
+ }
309
+
310
+ SAMPLE_PROGRAM = [
311
+ {"op": "const", "dest": "x", "args": ["5"], "type": "int"},
312
+ {"op": "const", "dest": "y", "args": ["3"], "type": "int"},
313
+ {"op": "add", "dest": "z", "args": ["x", "y"], "type": "int"},
314
+ {"op": "mul", "dest": "w", "args": ["z", "x"], "type": "int"},
315
+ {"op": "ret", "args": ["w"]},
316
+ ]
space/space/space/space/space/space/space/space/space/space/space/space/Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optional: use Hugging Face Space SDK = docker (set `sdk: docker` in README.md).
2
+ # Default README uses Gradio SDK and does not require this image.
3
+
4
+ FROM python:3.11-slim
5
+
6
+ WORKDIR /app
7
+
8
+ ENV PYTHONUNBUFFERED=1 \
9
+ PIP_NO_CACHE_DIR=1 \
10
+ GRADIO_SERVER_NAME=0.0.0.0
11
+
12
+ COPY requirements.txt .
13
+ RUN pip install --upgrade pip && pip install -r requirements.txt
14
+
15
+ COPY . .
16
+
17
+ EXPOSE 7860
18
+
19
+ CMD ["python", "app.py"]
space/space/space/space/space/space/space/space/space/space/space/space/README.md CHANGED
@@ -1,9 +1,54 @@
1
  ---
2
- title: Compiler Brain OpenEnv
3
- emoji: 🧠
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: docker
 
7
  app_file: app.py
8
  pinned: false
 
9
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Compiler Tetris — Toy-IR RL Demo
3
+ emoji: 🧩
4
+ colorFrom: gray
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 5.12.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
+
13
+ # Compiler Tetris (Toy-IR) — Hugging Face Space
14
+
15
+ Interactive **CPU** demo for the Meta / OpenEnv-style **compiler phase-ordering** project:
16
+
17
+ - **Deliverable 2 — state translation:** JSON Toy-IR → compact pseudo-assembly for the LLM.
18
+ - **Deliverable 2 — format enforcement:** resilient extraction of a JSON **pass array** from noisy model text.
19
+ - **CompilerOptimizationEnv** (mock Role 1 engine + mock passes): step through passes and inspect rewards.
20
+
21
+ Full **GRPO / Unsloth** training is not run here (heavy GPU + long installs). Use your notebooks on Colab or a GPU Space for training.
22
+
23
+ ## Source notebooks (parent repo)
24
+
25
+ - `compiler_optimization_grpo.ipynb`
26
+ - `role2_deliverable3_training_loop (2) (1).ipynb`
27
+ - `compiler_tetris (1).ipynb`
28
+ - `metahack1 (1).ipynb`
29
+
30
+ ## Deploy this folder as a new Space
31
+
32
+ 1. Create a new Space on Hugging Face (SDK: **Gradio**).
33
+ 2. Upload the contents of this `hf_space/` directory to the Space repository root (`app.py`, `requirements.txt`, `README.md`).
34
+ 3. Optional: add `Dockerfile` only if you switch the Space to **Docker** (see below).
35
+
36
+ ## Optional: Docker SDK instead of Gradio SDK
37
+
38
+ If you want the Space to build from `Dockerfile`, change the YAML header to:
39
+
40
+ ```yaml
41
+ sdk: docker
42
+ ```
43
+
44
+ and remove Gradio-specific keys (`sdk_version`, `app_file`). The container runs `python app.py`, which listens on the `PORT` environment variable provided by Spaces.
45
+
46
+ ## Local run
47
+
48
+ ```bash
49
+ cd hf_space
50
+ pip install -r requirements.txt
51
+ python app.py
52
+ ```
53
+
54
+ Then open `http://127.0.0.1:7860`.
space/space/space/space/space/space/space/space/space/space/space/space/app.py CHANGED
@@ -1,182 +1,167 @@
 
 
 
 
 
 
1
  import json
2
- import logging
3
- import traceback
4
- from typing import Any, Dict, List
5
-
6
- # OpenEnv SDK import
7
- from openenv import MCPEnvironment
8
-
9
- # ==============================================================================
10
- # ROLE 1 & 3 IMPORTS
11
- # TODO: Import the actual execution engine and generator from your teammates
12
- # ==============================================================================
13
- # from engine import execute_tac, verify_equivalence
14
- # from curriculum import generate_level_code
15
-
16
- logging.basicConfig(level=logging.INFO)
17
- logger = logging.getLogger("CompilerEnvServer")
18
-
19
- class CompilerEnv(MCPEnvironment):
20
- """
21
- The OpenEnv Server Wrapper for the Toy-IR Compiler Pass Optimizer.
22
- Acts as the referee between the LLM client and Role 1's Execution Engine.
23
- """
24
-
25
- def __init__(self):
26
- super().__init__()
27
- # State variables
28
- self.raw_json_code = None
29
- self.current_state_string = ""
30
- self.initial_cycles = 0
31
- self.current_step = 0
32
- self.max_steps = 10 # Set a max step limit per episode
33
-
34
- # Cycle Weights (The Physics defined by Role 2)
35
- self.cycle_weights = {
36
- "ADD": 1,
37
- "SUB": 1,
38
- "MUL": 4,
39
- "DIV": 10,
40
- "MEM_LOAD": 20,
41
- "STORE": 20
42
- }
43
-
44
- def reset(self) -> str:
45
- """
46
- Grabs unoptimized code, calculates baseline cycles, and translates the
47
- state for the LLM.
48
- """
49
- self.current_step = 0
50
-
51
- # 1. Grab new unoptimized code (Role 3 integration)
52
- # TODO: Replace with real generator: self.raw_json_code = generate_level_code()
53
- self.raw_json_code = self._mock_generator()
54
-
55
- # 2. Get baseline cycles (Role 1 integration)
56
- # TODO: Replace with real engine: self.initial_cycles, _ = execute_tac(self.raw_json_code, [])
57
- self.initial_cycles = 100
58
-
59
- # 3. Translate to Pseudo-Assembly to prevent Attention Dilution
60
- self.current_state_string = self._translate_state(self.raw_json_code)
61
-
62
- logger.info(f"Environment Reset. Baseline Cycles: {self.initial_cycles}")
63
- return self.state()
64
-
65
- def step(self, action: str) -> Dict[str, Any]:
66
- """
67
- Executes the LLM's chosen optimization pass, verifies math equivalence,
68
- and calculates the reward.
69
- """
70
- self.current_step += 1
71
-
72
- # 1. Parse LLM Action (Regex/JSON robustness)
73
- try:
74
- # Assuming the LLM outputs a single pass name or a list of passes
75
- action_data = json.loads(action)
76
- if isinstance(action_data, str):
77
- action_array = [action_data]
78
- else:
79
- action_array = action_data
80
- format_bonus = 0.1
81
- except json.JSONDecodeError:
82
- # Format Trap
83
- return self._build_step_response(
84
- reward=-2.5,
85
- done=True,
86
- error="Invalid JSON. You must output a valid JSON array of strings."
87
- )
88
-
89
- # 2. Execute Code & Verify Equivalence (Role 1 Integration)
90
- # TODO: new_cycles, optimized_code = execute_tac(self.raw_json_code, action_array)
91
- # TODO: is_valid = verify_equivalence(self.raw_json_code, optimized_code)
92
- new_cycles = 80 # Mock Data
93
- is_valid = True # Mock Data
94
-
95
- # 3. Calculate Reward Physics
96
- if not is_valid:
97
- # Correctness Penalty + Micro-Variance for GRPO
98
- penalty = -2.0 - (len(action_array) * 0.01)
99
- return self._build_step_response(
100
- reward=penalty,
101
- done=True,
102
- error=f"Code equivalence broken by passes: {action_array}"
103
- )
104
-
105
- # Calculate Improvement Ratio + Time Tax (-1.0)
106
- cycle_improvement_ratio = (self.initial_cycles - new_cycles) / self.initial_cycles
107
- time_tax = -0.05 * self.current_step # Small tax to prevent pass spamming
108
- reward = cycle_improvement_ratio + format_bonus + time_tax
109
-
110
- # Update state if sequential, or finish if one-shot
111
- # NOTE: For hackathon speed, we treat this as a One-Shot episode
112
- done = True
113
-
114
- return self._build_step_response(
115
- reward=reward,
116
- done=done,
117
- info={"status": "success", "optimized_cycles": new_cycles}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  )
119
 
120
- def state(self) -> str:
121
- """
122
- Returns the current observation to the LLM.
123
- """
124
- return f"Current Step: {self.current_step}/{self.max_steps}\n\n{self.current_state_string}"
125
-
126
- def _translate_state(self, raw_json: List[Dict]) -> str:
127
- """
128
- Translates raw AST JSON into clean pseudo-assembly.
129
- Strips all UUIDs and AST metadata.
130
- """
131
- pseudo_assembly = []
132
- instruction_count = 1
133
-
134
- for inst in raw_json:
135
- op = inst.get("op", "UNKNOWN")
136
- src1 = inst.get("src1", "")
137
- src2 = inst.get("src2", "")
138
- dest = inst.get("dest", "")
139
-
140
- # Format arguments cleanly
141
- args = f"{src1}" if src2 is None else f"{src1}, {src2}"
142
-
143
- if dest:
144
- line = f"{instruction_count}. {dest} = {op} {args}"
145
- else:
146
- line = f"{instruction_count}. {op} {args}"
147
-
148
- pseudo_assembly.append(line)
149
- instruction_count += 1
150
-
151
- return "\n".join(pseudo_assembly)
152
-
153
- def _build_step_response(self, reward: float, done: bool, error: str = None, info: dict = None) -> Dict[str, Any]:
154
- """Helper to format the standard OpenEnv step return dictionary."""
155
- response = {
156
- "reward": reward,
157
- "done": done,
158
- "state": self.state()
159
- }
160
- if error:
161
- response["error"] = error
162
- if info:
163
- response["info"] = info
164
- return response
165
-
166
- def _mock_generator(self):
167
- """Mock data so the server runs before Role 1 integrates their engine."""
168
- return [
169
- {"op": "CONST", "dest": "a", "src1": 2, "src2": None},
170
- {"op": "CONST", "dest": "b", "src1": 3, "src2": None},
171
- {"op": "ADD", "dest": "c", "src1": "a", "src2": "b"}
172
- ]
173
 
174
  if __name__ == "__main__":
175
- logger.info("Initializing CompilerEnv Server...")
176
- try:
177
- env = CompilerEnv()
178
- # openenv.run() or start() depending on the specific MCP wrapper version
179
- env.start()
180
- except Exception as e:
181
- logger.error(f"Failed to start environment server: {e}")
182
- logger.error(traceback.format_exc())
 
1
+ """
2
+ Hugging Face Spaces entrypoint — Gradio UI for Toy-IR compiler RL demo.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
  import json
8
+ import os
9
+ import sys
10
+ import textwrap
11
+ from pathlib import Path
12
+
13
+ import gradio as gr
14
+
15
+ _ROOT = Path(__file__).resolve().parent
16
+ if str(_ROOT) not in sys.path:
17
+ sys.path.insert(0, str(_ROOT))
18
+
19
+ from runtime_core import (
20
+ CompilerOptimizationEnv,
21
+ Deliverable2_Formatter,
22
+ MOCK_PASSES,
23
+ MockEngine,
24
+ SAMPLE_PROGRAM,
25
+ )
26
+
27
+
28
+ DEFAULT_IR = json.dumps(SAMPLE_PROGRAM, indent=2)
29
+
30
+
31
+ def translate_ir(ir_json: str) -> tuple[str, str]:
32
+ try:
33
+ data = json.loads(ir_json.strip() or "[]")
34
+ except json.JSONDecodeError as e:
35
+ return "", f"Invalid JSON: {e}"
36
+ if not isinstance(data, list):
37
+ return "", "JSON root must be a list of instruction objects."
38
+ d2 = Deliverable2_Formatter.translate_state(data)
39
+ env = CompilerOptimizationEnv(MockEngine(), MOCK_PASSES, max_steps=10)
40
+ env.reset(data)
41
+ internal = env.state()
42
+ return d2, internal
43
+
44
+
45
+ def parse_llm_output(llm_text: str) -> str:
46
+ try:
47
+ arr = Deliverable2_Formatter.extract_action_array(llm_text)
48
+ return json.dumps(arr, indent=2)
49
+ except ValueError as e:
50
+ return str(e)
51
+
52
+
53
+ def parse_action_line(line: str) -> list[str]:
54
+ line = line.strip()
55
+ if not line:
56
+ return []
57
+ try:
58
+ got = Deliverable2_Formatter.extract_action_array(line)
59
+ return [str(x).strip() for x in got]
60
+ except ValueError:
61
+ parts = [p.strip().strip("\"'") for p in line.split(",") if p.strip()]
62
+ return [p.lower() for p in parts]
63
+
64
+
65
+ def run_episode(ir_json: str, actions_multiline: str, max_steps: int) -> str:
66
+ try:
67
+ program = json.loads(ir_json.strip() or "[]")
68
+ except json.JSONDecodeError as e:
69
+ return f"Invalid program JSON: {e}"
70
+ if not isinstance(program, list):
71
+ return "Program must be a JSON list."
72
+
73
+ lines = [ln for ln in actions_multiline.splitlines() if ln.strip()]
74
+ actions: list[str] = []
75
+ for ln in lines:
76
+ actions.extend(parse_action_line(ln))
77
+ if not actions:
78
+ return "No actions parsed. Enter JSON arrays or comma-separated pass names."
79
+
80
+ engine = MockEngine()
81
+ env = CompilerOptimizationEnv(engine, MOCK_PASSES, max_steps=int(max_steps))
82
+ obs0 = env.reset(program)
83
+ log = [
84
+ f"Baseline cycles: {env.previous_cycles}",
85
+ f"Initial observation (env):\n{obs0}",
86
+ "",
87
+ f"Deliverable 2 pseudo-assembly:\n{Deliverable2_Formatter.translate_state(program)}",
88
+ "",
89
+ "--- steps ---",
90
+ ]
91
+ for i, act in enumerate(actions, start=1):
92
+ r = env.step(act)
93
+ log.append(
94
+ f"{i}. {act!r} → reward={r.reward:+.3f} done={r.done} cycles={env.previous_cycles}"
95
+ )
96
+ if r.info:
97
+ slim = {k: v for k, v in r.info.items() if k in ("delta_pct", "error", "no_op", "reason", "terminal_bonus")}
98
+ if slim:
99
+ log.append(f" info: {slim}")
100
+ if r.done:
101
+ break
102
+ log.append("")
103
+ log.append("Episode summary:")
104
+ log.append(json.dumps(env._episode_summary(), indent=2))
105
+ return "\n".join(log)
106
+
107
+
108
+ def build_demo() -> gr.Blocks:
109
+ with gr.Blocks(title="Compiler Tetris — Toy-IR RL Demo") as demo:
110
+ gr.Markdown(
111
+ textwrap.dedent(
112
+ """
113
+ # Compiler optimization (Toy-IR) interactive demo
114
+
115
+ This Space runs **CPU-only** demos: **Deliverable 2** state translation and action parsing,
116
+ plus the **CompilerOptimizationEnv** smoke loop from your notebooks. Full GRPO / Unsloth
117
+ training belongs on Colab or a GPU Space.
118
+ """
119
+ ).strip()
120
+ )
121
+
122
+ with gr.Tabs():
123
+ with gr.Tab("Translate IR (D2)"):
124
+ gr.Markdown("Paste Toy-IR as a JSON **list** of instruction dicts (`op`, `args`, `dest`, …).")
125
+ ir_in = gr.Textbox(label="IR JSON", value=DEFAULT_IR, lines=12, max_lines=24)
126
+ btn_t = gr.Button("Translate", variant="primary")
127
+ out_d2 = gr.Textbox(label="Deliverable2 pseudo-assembly (LLM-facing)", lines=10)
128
+ out_env = gr.Textbox(label="Env internal pseudo-asm (with type hints)", lines=10)
129
+ btn_t.click(translate_ir, [ir_in], [out_d2, out_env])
130
+
131
+ with gr.Tab("Parse LLM output (D2)"):
132
+ gr.Markdown(
133
+ "Paste model output. A JSON array is preferred; bracket extraction handles light noise."
134
+ )
135
+ llm_in = gr.Textbox(
136
+ label="LLM output",
137
+ value='Here is the plan: ["constant_folding", "dead_code_elimination"]',
138
+ lines=4,
139
+ )
140
+ out_parse = gr.Textbox(label="Parsed array or error", lines=6)
141
+ gr.Button("Parse", variant="primary").click(parse_llm_output, [llm_in], [out_parse])
142
+
143
+ with gr.Tab("Env episode (mock engine)"):
144
+ gr.Markdown(
145
+ "One JSON program + actions: each line can be a JSON array or comma-separated names."
146
+ )
147
+ ir_ep = gr.Textbox(label="Program JSON", value=DEFAULT_IR, lines=10)
148
+ acts = gr.Textbox(
149
+ label="Actions (one JSON array or comma-list per line)",
150
+ value='["constant_folding", "dead_code_elimination"]\nloop_unrolling',
151
+ lines=5,
152
+ )
153
+ ms = gr.Slider(1, 20, value=10, step=1, label="max_steps")
154
+ out_ep = gr.Textbox(label="Log", lines=20)
155
+ gr.Button("Run episode", variant="primary").click(run_episode, [ir_ep, acts, ms], [out_ep])
156
+
157
+ gr.Markdown(
158
+ "Source notebooks in the parent repo: `compiler_optimization_grpo.ipynb`, "
159
+ "`role2_deliverable3_training_loop (2) (1).ipynb`, `compiler_tetris (1).ipynb`, `metahack1 (1).ipynb`."
160
  )
161
 
162
+ return demo
163
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  if __name__ == "__main__":
166
+ port = int(os.environ.get("PORT", "7860"))
167
+ build_demo().launch(server_name="0.0.0.0", server_port=port)
 
 
 
 
 
 
space/space/space/space/space/space/space/space/space/space/space/space/requirements.txt CHANGED
@@ -1,24 +1,2 @@
1
- # OpenEnv SDK (Mandatory for the hackathon judging environment)
2
- openenv
3
-
4
- # Core RL Environment
5
- gymnasium
6
-
7
- # Hugging Face Training Stack (Versions matched to your D3 notebook)
8
- trl==0.23.1
9
- transformers==4.57.1
10
- peft
11
- accelerate
12
- bitsandbytes
13
-
14
- # Unsloth for efficient 4-bit QLoRA training on T4 GPUs
15
- # Note: Unsloth often prefers being installed via their specific pip wheel or git,
16
- # but this is standard for a requirements file.
17
- unsloth
18
-
19
- # PyTorch (Will default to standard compatible version if no index is specified)
20
- torch
21
-
22
- # Logging & Visualizations (For Day 2 judging criteria)
23
- wandb
24
- matplotlib
 
1
+ # Hugging Face Spaces Gradio SDK (CPU demo)
2
+ gradio>=4.44.0,<6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
space/space/space/space/space/space/space/space/space/space/space/space/space/compiler_optimization_grpo.ipynb ADDED
@@ -0,0 +1,959 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 5,
4
+ "metadata": {
5
+ "kernelspec": {
6
+ "display_name": "Python 3",
7
+ "language": "python",
8
+ "name": "python3"
9
+ },
10
+ "language_info": {
11
+ "name": "python",
12
+ "version": "3.10.0"
13
+ },
14
+ "colab": {
15
+ "provenance": [],
16
+ "gpuType": "T4"
17
+ },
18
+ "accelerator": "GPU"
19
+ },
20
+ "cells": [
21
+ {
22
+ "cell_type": "markdown",
23
+ "id": "title-cell",
24
+ "metadata": {},
25
+ "source": [
26
+ "# 🔧 Compiler Optimization RL Environment\n",
27
+ "### OpenEnv Hackathon 2026 — Theme #2: Long-Horizon Planning\n",
28
+ "\n",
29
+ "**What this notebook does:**\n",
30
+ "1. Defines a fully OpenEnv-compliant `CompilerOptimizationEnv`\n",
31
+ "2. Loads `Qwen2.5-3B-Instruct` via Unsloth (4-bit QLoRA)\n",
32
+ "3. Trains with GRPO (TRL) — LLM learns to pick compiler passes that reduce CPU cycles\n",
33
+ "4. Runs a smoke-test with a mock engine so you can verify reward logic without real hardware\n",
34
+ "5. Plots reward curves\n",
35
+ "\n",
36
+ "---\n",
37
+ "**Stack:** `unsloth` · `trl` · `openenv` · `wandb` · `matplotlib`\n",
38
+ "\n",
39
+ "> **Runtime:** Google Colab T4 GPU recommended. For the smoke-test only, CPU is fine."
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "markdown",
44
+ "id": "install-header",
45
+ "metadata": {},
46
+ "source": [
47
+ "## 📦 Cell 1 — Install Dependencies"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": null,
53
+ "id": "install-cell",
54
+ "metadata": {},
55
+ "outputs": [],
56
+ "source": [
57
+ "# Install all required packages\n",
58
+ "# Unsloth must be installed before trl to get the right CUDA kernels\n",
59
+ "!pip install \"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\" --quiet\n",
60
+ "!pip install trl datasets transformers accelerate peft bitsandbytes --quiet\n",
61
+ "!pip install wandb matplotlib --quiet\n",
62
+ "\n",
63
+ "# Optional: install openenv for production use\n",
64
+ "# !pip install openenv --quiet\n",
65
+ "\n",
66
+ "print(\"✅ All packages installed\")"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "markdown",
71
+ "id": "env-header",
72
+ "metadata": {},
73
+ "source": [
74
+ "## 🌍 Cell 2 — CompilerOptimizationEnv (OpenEnv-Compliant)"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "code",
79
+ "execution_count": null,
80
+ "id": "env-cell",
81
+ "metadata": {},
82
+ "outputs": [],
83
+ "source": [
84
+ "\"\"\"\n",
85
+ "CompilerOptimizationEnv — OpenEnv-compatible RL Environment\n",
86
+ "OpenEnv Hackathon 2026 | Theme #2: Long-Horizon Planning\n",
87
+ "\n",
88
+ "Key improvements over naive baseline:\n",
89
+ " ✓ Inherits from MCPEnvironment (OpenEnv API compliant)\n",
90
+ " ✓ Dynamic crash_penalty scaled to reward range (not hardcoded -1000)\n",
91
+ " ✓ Soft termination: invalid actions give 3 chances before episode ends\n",
92
+ " ✓ No-op detection: penalises passes that change nothing\n",
93
+ " ✓ Terminal bonus: rewards cumulative improvement, not just greedy steps\n",
94
+ " ✓ StepResult / EpisodeStats dataclasses for clean interfaces\n",
95
+ " ✓ Curriculum level support\n",
96
+ "\"\"\"\n",
97
+ "\n",
98
+ "import copy\n",
99
+ "import math\n",
100
+ "from dataclasses import dataclass, field\n",
101
+ "from typing import Any, Dict, List, Optional, Tuple\n",
102
+ "\n",
103
+ "\n",
104
+ "# ── Stub base class ───────────────────────────────────────────────────────────\n",
105
+ "# In production replace with: from openenv import MCPEnvironment\n",
106
+ "class MCPEnvironment:\n",
107
+ " \"\"\"Minimal stub. In production: `from openenv import MCPEnvironment`\"\"\"\n",
108
+ " def reset(self, *args, **kwargs): raise NotImplementedError\n",
109
+ " def step(self, *args, **kwargs): raise NotImplementedError\n",
110
+ " def state(self): raise NotImplementedError\n",
111
+ "\n",
112
+ "\n",
113
+ "# ── Data classes ──────────────────────────────────────────────────────────────\n",
114
+ "@dataclass\n",
115
+ "class StepResult:\n",
116
+ " observation: str\n",
117
+ " reward: float\n",
118
+ " done: bool\n",
119
+ " info: Dict[str, Any] = field(default_factory=dict)\n",
120
+ "\n",
121
+ "\n",
122
+ "@dataclass\n",
123
+ "class EpisodeStats:\n",
124
+ " steps_taken: int = 0\n",
125
+ " total_reward: float = 0.0\n",
126
+ " passes_applied: List[str] = field(default_factory=list)\n",
127
+ " invalid_actions: int = 0\n",
128
+ " no_ops: int = 0\n",
129
+ " baseline_cycles: int = 0\n",
130
+ " final_cycles: int = 0\n",
131
+ "\n",
132
+ " @property\n",
133
+ " def total_improvement_pct(self) -> float:\n",
134
+ " if self.baseline_cycles == 0:\n",
135
+ " return 0.0\n",
136
+ " return ((self.baseline_cycles - self.final_cycles) / self.baseline_cycles) * 100.0\n",
137
+ "\n",
138
+ "\n",
139
+ "# ── Core Environment ─────────────────────────��────────────────────────────────\n",
140
+ "class CompilerOptimizationEnv(MCPEnvironment):\n",
141
+ " \"\"\"\n",
142
+ " RL environment where an LLM agent sequentially applies compiler\n",
143
+ " optimization passes to minimise CPU cycle count while preserving\n",
144
+ " program semantics.\n",
145
+ "\n",
146
+ " State : Pseudo-assembly representation of the current IR (text)\n",
147
+ " Actions: Named compiler passes (strings) from the `passes` dictionary\n",
148
+ " Reward : % cycle improvement per step − time_tax, with terminal bonus\n",
149
+ " Done : Semantic violation | max steps reached | too many invalid actions\n",
150
+ " \"\"\"\n",
151
+ "\n",
152
+ " # Hyperparameters\n",
153
+ " TIME_TAX: float = 1.0\n",
154
+ " NO_OP_PENALTY: float = -2.0\n",
155
+ " INVALID_ACTION_PENALTY: float = -5.0\n",
156
+ " MAX_INVALID_ACTIONS: int = 3\n",
157
+ " TERMINAL_BONUS_SCALE: float = 0.5\n",
158
+ "\n",
159
+ " def __init__(\n",
160
+ " self,\n",
161
+ " role1_engine,\n",
162
+ " role3_passes: Dict[str, Any],\n",
163
+ " max_steps: int = 10,\n",
164
+ " curriculum_level: int = 1,\n",
165
+ " ):\n",
166
+ " self.engine = role1_engine\n",
167
+ " self.passes = role3_passes\n",
168
+ " self.max_steps = max_steps\n",
169
+ " self.curriculum_level = curriculum_level\n",
170
+ " self._valid_actions = frozenset(self.passes.keys())\n",
171
+ "\n",
172
+ " # Episode state\n",
173
+ " self._stats: Optional[EpisodeStats] = None\n",
174
+ " self.original_program = None\n",
175
+ " self.current_program = None\n",
176
+ " self.previous_cycles = 0\n",
177
+ " self._consecutive_invalid = 0\n",
178
+ "\n",
179
+ " # ── OpenEnv API ───────────────────────────────────────────────────────────\n",
180
+ " def reset(self, new_program_json: List[Dict]) -> str:\n",
181
+ " self.original_program = copy.deepcopy(new_program_json)\n",
182
+ " self.current_program = copy.deepcopy(new_program_json)\n",
183
+ " self.previous_cycles = self._safe_count_cycles(self.current_program)\n",
184
+ " self._consecutive_invalid = 0\n",
185
+ " self._stats = EpisodeStats(\n",
186
+ " baseline_cycles=self.previous_cycles,\n",
187
+ " final_cycles=self.previous_cycles,\n",
188
+ " )\n",
189
+ " return self.state()\n",
190
+ "\n",
191
+ " def state(self) -> str:\n",
192
+ " return self._program_to_pseudoasm(self.current_program)\n",
193
+ "\n",
194
+ " def step(self, action_string: str) -> StepResult:\n",
195
+ " assert self._stats is not None, \"Call reset() before step().\"\n",
196
+ " self._stats.steps_taken += 1\n",
197
+ "\n",
198
+ " if action_string not in self._valid_actions:\n",
199
+ " return self._handle_invalid_action(action_string)\n",
200
+ "\n",
201
+ " candidate_program = self.passes[action_string](\n",
202
+ " copy.deepcopy(self.current_program)\n",
203
+ " )\n",
204
+ "\n",
205
+ " is_valid = self.engine.verify_equivalence(self.original_program, candidate_program)\n",
206
+ " if not is_valid:\n",
207
+ " return self._handle_semantic_violation()\n",
208
+ "\n",
209
+ " new_cycles = self._safe_count_cycles(candidate_program)\n",
210
+ " reward, info = self._compute_reward(action_string, new_cycles)\n",
211
+ "\n",
212
+ " self.current_program = candidate_program\n",
213
+ " self.previous_cycles = new_cycles\n",
214
+ " self._stats.final_cycles = new_cycles\n",
215
+ " self._stats.total_reward += reward\n",
216
+ " self._stats.passes_applied.append(action_string)\n",
217
+ " self._consecutive_invalid = 0\n",
218
+ "\n",
219
+ " done = self._stats.steps_taken >= self.max_steps\n",
220
+ " if done:\n",
221
+ " terminal_bonus = self._terminal_bonus()\n",
222
+ " reward += terminal_bonus\n",
223
+ " info[\"terminal_bonus\"] = terminal_bonus\n",
224
+ " info[\"reason\"] = \"max_steps_reached\"\n",
225
+ " info[\"episode_stats\"] = self._episode_summary()\n",
226
+ "\n",
227
+ " return StepResult(self.state(), reward, done, info)\n",
228
+ "\n",
229
+ " # ── Reward logic ──────────────────────────────────────────────────────────\n",
230
+ " def _compute_reward(self, action: str, new_cycles: int) -> Tuple[float, Dict]:\n",
231
+ " info: Dict[str, Any] = {\"action\": action}\n",
232
+ " if self.previous_cycles == 0:\n",
233
+ " return -self.TIME_TAX, {**info, \"note\": \"zero_baseline\"}\n",
234
+ "\n",
235
+ " delta_pct = ((self.previous_cycles - new_cycles) / self.previous_cycles) * 100.0\n",
236
+ "\n",
237
+ " if new_cycles == self.previous_cycles:\n",
238
+ " reward = self.NO_OP_PENALTY\n",
239
+ " self._stats.no_ops += 1\n",
240
+ " info[\"no_op\"] = True\n",
241
+ " else:\n",
242
+ " reward = delta_pct - self.TIME_TAX\n",
243
+ " info[\"delta_pct\"] = round(delta_pct, 3)\n",
244
+ "\n",
245
+ " info[\"prev_cycles\"] = self.previous_cycles\n",
246
+ " info[\"new_cycles\"] = new_cycles\n",
247
+ " return reward, info\n",
248
+ "\n",
249
+ " def _terminal_bonus(self) -> float:\n",
250
+ " return max(0.0, self._stats.total_improvement_pct * self.TERMINAL_BONUS_SCALE)\n",
251
+ "\n",
252
+ " def _compute_crash_penalty(self) -> float:\n",
253
+ " # 2× best possible episode reward — always catastrophic, never overwhelming\n",
254
+ " return -2.0 * (100.0 * self.max_steps)\n",
255
+ "\n",
256
+ " # ── Error handlers ────────────────────────────────────────────────────────\n",
257
+ " def _handle_invalid_action(self, action: str) -> StepResult:\n",
258
+ " self._consecutive_invalid += 1\n",
259
+ " self._stats.invalid_actions += 1\n",
260
+ " done = self._consecutive_invalid >= self.MAX_INVALID_ACTIONS\n",
261
+ " info = {\n",
262
+ " \"error\": f\"Unknown action: '{action}'\",\n",
263
+ " \"valid_actions\": sorted(self._valid_actions),\n",
264
+ " \"consecutive_invalid\": self._consecutive_invalid,\n",
265
+ " }\n",
266
+ " if done:\n",
267
+ " info[\"reason\"] = \"too_many_invalid_actions\"\n",
268
+ " info[\"episode_stats\"] = self._episode_summary()\n",
269
+ " return StepResult(self.state(), self.INVALID_ACTION_PENALTY, done, info)\n",
270
+ "\n",
271
+ " def _handle_semantic_violation(self) -> StepResult:\n",
272
+ " return StepResult(\n",
273
+ " self.state(),\n",
274
+ " self._compute_crash_penalty(),\n",
275
+ " True,\n",
276
+ " {\n",
277
+ " \"error\": \"Semantic equivalence check FAILED.\",\n",
278
+ " \"reason\": \"semantic_violation\",\n",
279
+ " \"episode_stats\": self._episode_summary(),\n",
280
+ " },\n",
281
+ " )\n",
282
+ "\n",
283
+ " # ── State representation ──────────────────────────────────────────────────\n",
284
+ " @staticmethod\n",
285
+ " def _program_to_pseudoasm(program: List[Dict]) -> str:\n",
286
+ " if not program:\n",
287
+ " return \"; (empty program)\"\n",
288
+ " lines = []\n",
289
+ " for i, instr in enumerate(program):\n",
290
+ " op = instr.get(\"op\", \"NOP\")\n",
291
+ " args = instr.get(\"args\", [])\n",
292
+ " dest = instr.get(\"dest\")\n",
293
+ " typ = instr.get(\"type\", \"\")\n",
294
+ " arg_str = \", \".join(str(a) for a in args)\n",
295
+ " type_hint = f\":{typ}\" if typ else \"\"\n",
296
+ " if dest:\n",
297
+ " lines.append(f\" {i:>3}: {dest}{type_hint} = {op} {arg_str}\")\n",
298
+ " else:\n",
299
+ " lines.append(f\" {i:>3}: {op} {arg_str}\")\n",
300
+ " return \"\\n\".join(lines)\n",
301
+ "\n",
302
+ " # ── Utilities ─────────────────────────────────────────────────────────────\n",
303
+ " def _safe_count_cycles(self, program: List[Dict]) -> int:\n",
304
+ " return max(0, int(self.engine.execute_and_count_cycles(program)))\n",
305
+ "\n",
306
+ " def _episode_summary(self) -> Dict:\n",
307
+ " s = self._stats\n",
308
+ " return {\n",
309
+ " \"steps\": s.steps_taken,\n",
310
+ " \"total_reward\": round(s.total_reward, 3),\n",
311
+ " \"passes_applied\": s.passes_applied,\n",
312
+ " \"invalid_actions\": s.invalid_actions,\n",
313
+ " \"no_ops\": s.no_ops,\n",
314
+ " \"baseline_cycles\": s.baseline_cycles,\n",
315
+ " \"final_cycles\": s.final_cycles,\n",
316
+ " \"total_improvement_pct\": round(s.total_improvement_pct, 3),\n",
317
+ " }\n",
318
+ "\n",
319
+ " def available_actions(self) -> List[str]:\n",
320
+ " return sorted(self._valid_actions)\n",
321
+ "\n",
322
+ "\n",
323
+ "print(\"✅ CompilerOptimizationEnv defined\")"
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "markdown",
328
+ "id": "smoke-header",
329
+ "metadata": {},
330
+ "source": [
331
+ "## 🧪 Cell 3 — Smoke Test (No GPU / Real Engine Needed)\n",
332
+ "Validates the entire reward pipeline with a mock engine. Run this before spending compute."
333
+ ]
334
+ },
335
+ {
336
+ "cell_type": "code",
337
+ "execution_count": null,
338
+ "id": "smoke-test-cell",
339
+ "metadata": {},
340
+ "outputs": [],
341
+ "source": [
342
+ "class MockEngine:\n",
343
+ " \"\"\"Stub engine: cycles = instruction count, all programs semantically valid.\"\"\"\n",
344
+ " def execute_and_count_cycles(self, program):\n",
345
+ " return len(program)\n",
346
+ "\n",
347
+ " def verify_equivalence(self, original, candidate):\n",
348
+ " return True\n",
349
+ "\n",
350
+ "\n",
351
+ "MOCK_PASSES = {\n",
352
+ " \"constant_folding\": lambda p: p[:-1] if len(p) > 1 else p,\n",
353
+ " \"dead_code_elimination\": lambda p: p[:-1] if len(p) > 2 else p,\n",
354
+ " \"loop_unrolling\": lambda p: p, # intentional no-op for testing\n",
355
+ "}\n",
356
+ "\n",
357
+ "SAMPLE_PROGRAM = [\n",
358
+ " {\"op\": \"const\", \"dest\": \"x\", \"args\": [\"5\"], \"type\": \"int\"},\n",
359
+ " {\"op\": \"const\", \"dest\": \"y\", \"args\": [\"3\"], \"type\": \"int\"},\n",
360
+ " {\"op\": \"add\", \"dest\": \"z\", \"args\": [\"x\", \"y\"], \"type\": \"int\"},\n",
361
+ " {\"op\": \"mul\", \"dest\": \"w\", \"args\": [\"z\", \"x\"], \"type\": \"int\"},\n",
362
+ " {\"op\": \"ret\", \"args\": [\"w\"]},\n",
363
+ "]\n",
364
+ "\n",
365
+ "engine = MockEngine()\n",
366
+ "env = CompilerOptimizationEnv(engine, MOCK_PASSES, max_steps=6)\n",
367
+ "obs = env.reset(SAMPLE_PROGRAM)\n",
368
+ "\n",
369
+ "print(\"═\" * 55)\n",
370
+ "print(\"SMOKE TEST\")\n",
371
+ "print(\"═\" * 55)\n",
372
+ "print(f\"Initial state:\\n{obs}\")\n",
373
+ "print(f\"\\nBaseline cycles : {env.previous_cycles}\")\n",
374
+ "print(f\"Available actions: {env.available_actions()}\")\n",
375
+ "print()\n",
376
+ "\n",
377
+ "actions_to_try = [\n",
378
+ " \"constant_folding\",\n",
379
+ " \"dead_code_elimination\",\n",
380
+ " \"loop_unrolling\", # no-op\n",
381
+ " \"hallucinated_pass\", # invalid — but won't kill episode yet\n",
382
+ " \"constant_folding\",\n",
383
+ " \"dead_code_elimination\",\n",
384
+ "]\n",
385
+ "\n",
386
+ "for action in actions_to_try:\n",
387
+ " result = env.step(action)\n",
388
+ " tag = \"✗\" if result.reward < 0 else \"✓\"\n",
389
+ " print(f\"{tag} '{action}'\")\n",
390
+ " print(f\" reward={result.reward:+.2f} done={result.done}\")\n",
391
+ " relevant = {k: v for k, v in result.info.items()\n",
392
+ " if k in (\"delta_pct\", \"error\", \"no_op\", \"reason\",\n",
393
+ " \"terminal_bonus\", \"episode_stats\")}\n",
394
+ " if relevant:\n",
395
+ " print(f\" info: {relevant}\")\n",
396
+ " print()\n",
397
+ " if result.done:\n",
398
+ " break\n",
399
+ "\n",
400
+ "print(\"✅ Smoke test passed\")"
401
+ ]
402
+ },
403
+ {
404
+ "cell_type": "markdown",
405
+ "id": "reward-plot-header",
406
+ "metadata": {},
407
+ "source": [
408
+ "## 📊 Cell 4 — Visualise Reward Across a Mock Episode"
409
+ ]
410
+ },
411
+ {
412
+ "cell_type": "code",
413
+ "execution_count": null,
414
+ "id": "reward-plot-cell",
415
+ "metadata": {},
416
+ "outputs": [],
417
+ "source": [
418
+ "import matplotlib.pyplot as plt\n",
419
+ "import matplotlib.ticker as ticker\n",
420
+ "\n",
421
+ "# Run a full episode and collect data\n",
422
+ "env = CompilerOptimizationEnv(MockEngine(), MOCK_PASSES, max_steps=10)\n",
423
+ "env.reset(SAMPLE_PROGRAM)\n",
424
+ "\n",
425
+ "sequence = [\n",
426
+ " \"constant_folding\", \"dead_code_elimination\", \"loop_unrolling\",\n",
427
+ " \"constant_folding\", \"dead_code_elimination\", \"loop_unrolling\",\n",
428
+ " \"dead_code_elimination\", \"loop_unrolling\", \"constant_folding\",\n",
429
+ " \"dead_code_elimination\",\n",
430
+ "]\n",
431
+ "\n",
432
+ "rewards, cycle_counts, actions_log = [], [], []\n",
433
+ "for act in sequence:\n",
434
+ " r = env.step(act)\n",
435
+ " rewards.append(r.reward)\n",
436
+ " cycle_counts.append(env.previous_cycles)\n",
437
+ " actions_log.append(act)\n",
438
+ " if r.done:\n",
439
+ " break\n",
440
+ "\n",
441
+ "steps = list(range(1, len(rewards) + 1))\n",
442
+ "\n",
443
+ "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)\n",
444
+ "fig.suptitle(\"Compiler Optimization Episode — Mock Engine\", fontsize=14, fontweight=\"bold\")\n",
445
+ "\n",
446
+ "# Reward per step\n",
447
+ "colors = [\"#2ecc71\" if r >= 0 else \"#e74c3c\" for r in rewards]\n",
448
+ "ax1.bar(steps, rewards, color=colors, edgecolor=\"white\", linewidth=0.5)\n",
449
+ "ax1.axhline(0, color=\"grey\", linewidth=0.8, linestyle=\"--\")\n",
450
+ "ax1.set_ylabel(\"Reward\")\n",
451
+ "ax1.set_title(\"Reward per Step (green = positive, red = negative)\")\n",
452
+ "ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter(\"%.1f\"))\n",
453
+ "\n",
454
+ "# Cycle count over time\n",
455
+ "ax2.plot(steps, cycle_counts, marker=\"o\", color=\"#3498db\", linewidth=2, markersize=6)\n",
456
+ "ax2.set_xlabel(\"Step\")\n",
457
+ "ax2.set_ylabel(\"CPU Cycles\")\n",
458
+ "ax2.set_title(\"CPU Cycle Count Over Episode (lower = better)\")\n",
459
+ "ax2.set_xticks(steps)\n",
460
+ "ax2.set_xticklabels(\n",
461
+ " [a.replace(\"_\", \"\\n\") for a in actions_log],\n",
462
+ " fontsize=7,\n",
463
+ ")\n",
464
+ "\n",
465
+ "plt.tight_layout()\n",
466
+ "plt.savefig(\"episode_reward_curve.png\", dpi=150, bbox_inches=\"tight\")\n",
467
+ "plt.show()\n",
468
+ "print(\"📈 Plot saved as episode_reward_curve.png\")"
469
+ ]
470
+ },
471
+ {
472
+ "cell_type": "markdown",
473
+ "id": "model-header",
474
+ "metadata": {},
475
+ "source": [
476
+ "## 🤖 Cell 5 — Load Model with Unsloth (QLoRA 4-bit)\n",
477
+ "> **Requires T4 GPU.** Skip to Cell 9 if you only want to test the environment."
478
+ ]
479
+ },
480
+ {
481
+ "cell_type": "code",
482
+ "execution_count": null,
483
+ "id": "model-cell",
484
+ "metadata": {},
485
+ "outputs": [],
486
+ "source": [
487
+ "import torch\n",
488
+ "from unsloth import FastLanguageModel\n",
489
+ "\n",
490
+ "MODEL_NAME = \"unsloth/Qwen2.5-3B-Instruct\" # swap to 7B if VRAM allows\n",
491
+ "MAX_SEQ_LEN = 1024\n",
492
+ "LORA_RANK = 16\n",
493
+ "\n",
494
+ "model, tokenizer = FastLanguageModel.from_pretrained(\n",
495
+ " model_name = MODEL_NAME,\n",
496
+ " max_seq_length = MAX_SEQ_LEN,\n",
497
+ " dtype = None, # auto bf16/fp16\n",
498
+ " load_in_4bit = True,\n",
499
+ ")\n",
500
+ "\n",
501
+ "model = FastLanguageModel.get_peft_model(\n",
502
+ " model,\n",
503
+ " r = LORA_RANK,\n",
504
+ " target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n",
505
+ " \"gate_proj\", \"up_proj\", \"down_proj\"],\n",
506
+ " lora_alpha = LORA_RANK * 2,\n",
507
+ " lora_dropout = 0.0,\n",
508
+ " bias = \"none\",\n",
509
+ " use_gradient_checkpointing = \"unsloth\",\n",
510
+ " random_state = 42,\n",
511
+ ")\n",
512
+ "\n",
513
+ "print(f\"✅ Loaded {MODEL_NAME} with QLoRA rank={LORA_RANK}\")\n",
514
+ "print(f\" GPU memory allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB\")"
515
+ ]
516
+ },
517
+ {
518
+ "cell_type": "markdown",
519
+ "id": "prompt-header",
520
+ "metadata": {},
521
+ "source": [
522
+ "## 💬 Cell 6 — System Prompt & Dataset Builder"
523
+ ]
524
+ },
525
+ {
526
+ "cell_type": "code",
527
+ "execution_count": null,
528
+ "id": "prompt-cell",
529
+ "metadata": {},
530
+ "outputs": [],
531
+ "source": [
532
+ "import json\n",
533
+ "from datasets import Dataset\n",
534
+ "\n",
535
+ "\n",
536
+ "def build_system_prompt(env: CompilerOptimizationEnv) -> str:\n",
537
+ " actions = \"\\n\".join(f\" - {a}\" for a in env.available_actions())\n",
538
+ " return (\n",
539
+ " \"You are a compiler optimization agent. Your goal is to reduce \"\n",
540
+ " \"CPU cycle count by applying optimization passes to the program below.\\n\\n\"\n",
541
+ " f\"Available actions (respond with EXACTLY one per turn):\\n{actions}\\n\"\n",
542
+ " \" - done (stop early if no further improvement is possible)\\n\\n\"\n",
543
+ " \"Rules:\\n\"\n",
544
+ " \" • Output only the action name. No explanation, no markdown.\\n\"\n",
545
+ " \" • Do not invent actions not listed above.\\n\"\n",
546
+ " \" • Repeating a pass that does nothing wastes a step.\\n\"\n",
547
+ " )\n",
548
+ "\n",
549
+ "\n",
550
+ "def build_dataset(\n",
551
+ " programs: list,\n",
552
+ " engine,\n",
553
+ " passes: dict,\n",
554
+ ") -> Dataset:\n",
555
+ " \"\"\"\n",
556
+ " Each row = one episode's initial state.\n",
557
+ " GRPO samples K completions (actions) per row to estimate group-relative advantage.\n",
558
+ " \"\"\"\n",
559
+ " env = CompilerOptimizationEnv(engine, passes, max_steps=10)\n",
560
+ " system_prompt = build_system_prompt(env)\n",
561
+ "\n",
562
+ " rows = []\n",
563
+ " for prog in programs:\n",
564
+ " obs = env.reset(prog)\n",
565
+ " prompt = [\n",
566
+ " {\"role\": \"system\", \"content\": system_prompt},\n",
567
+ " {\"role\": \"user\", \"content\": f\"Current program:\\n{obs}\\n\\nChoose an action:\"},\n",
568
+ " ]\n",
569
+ " rows.append({\"prompt\": prompt, \"program_json\": json.dumps(prog)})\n",
570
+ "\n",
571
+ " return Dataset.from_list(rows)\n",
572
+ "\n",
573
+ "\n",
574
+ "# ── Demo: build dataset from mock programs ────────────────────────────────────\n",
575
+ "def make_mock_programs(n: int = 20) -> list:\n",
576
+ " \"\"\"Generate N random mock IR programs for demo purposes.\"\"\"\n",
577
+ " import random\n",
578
+ " ops = [\"add\", \"mul\", \"sub\", \"const\", \"load\"]\n",
579
+ " progs = []\n",
580
+ " for _ in range(n):\n",
581
+ " length = random.randint(4, 12)\n",
582
+ " prog = [\n",
583
+ " {\"op\": random.choice(ops),\n",
584
+ " \"dest\": f\"v{i}\",\n",
585
+ " \"args\": [f\"v{max(0,i-1)}\"],\n",
586
+ " \"type\": \"int\"}\n",
587
+ " for i in range(length)\n",
588
+ " ]\n",
589
+ " prog.append({\"op\": \"ret\", \"args\": [f\"v{length-1}\"]})\n",
590
+ " progs.append(prog)\n",
591
+ " return progs\n",
592
+ "\n",
593
+ "\n",
594
+ "mock_programs = make_mock_programs(n=30)\n",
595
+ "train_dataset = build_dataset(mock_programs, MockEngine(), MOCK_PASSES)\n",
596
+ "\n",
597
+ "print(f\"✅ Dataset built: {len(train_dataset)} episodes\")\n",
598
+ "print(f\" Sample prompt keys: {list(train_dataset[0].keys())}\")"
599
+ ]
600
+ },
601
+ {
602
+ "cell_type": "markdown",
603
+ "id": "reward-fn-header",
604
+ "metadata": {},
605
+ "source": [
606
+ "## 🎯 Cell 7 — Reward Function Factory (for GRPO)"
607
+ ]
608
+ },
609
+ {
610
+ "cell_type": "code",
611
+ "execution_count": null,
612
+ "id": "reward-fn-cell",
613
+ "metadata": {},
614
+ "outputs": [],
615
+ "source": [
616
+ "import re\n",
617
+ "from typing import Any\n",
618
+ "\n",
619
+ "\n",
620
+ "def make_reward_fn(engine, passes, max_steps=10):\n",
621
+ " \"\"\"\n",
622
+ " Returns a reward function compatible with TRL's GRPOTrainer.\n",
623
+ " Each GRPO rollout gets its own fresh env instance — no state leakage.\n",
624
+ "\n",
625
+ " Action parsing strips punctuation the model might add\n",
626
+ " (e.g. 'constant_folding.' → 'constant_folding').\n",
627
+ " \"\"\"\n",
628
+ " def reward_fn(prompts, completions, **kwargs):\n",
629
+ " programs = kwargs.get(\"program_json\", [None] * len(completions))\n",
630
+ " rewards = []\n",
631
+ "\n",
632
+ " for completion, prog_json in zip(completions, programs):\n",
633
+ " # Parse action from model output\n",
634
+ " raw = completion if isinstance(completion, str) else completion[0][\"content\"]\n",
635
+ " action = raw.strip().lower().split()[0] if raw.strip() else \"__invalid__\"\n",
636
+ " action = re.sub(r\"[^a-z0-9_]\", \"\", action) # strip punctuation\n",
637
+ "\n",
638
+ " # Fresh env per rollout\n",
639
+ " env = CompilerOptimizationEnv(\n",
640
+ " role1_engine = engine,\n",
641
+ " role3_passes = passes,\n",
642
+ " max_steps = max_steps,\n",
643
+ " )\n",
644
+ " program = json.loads(prog_json) if prog_json else []\n",
645
+ " env.reset(program)\n",
646
+ "\n",
647
+ " if action == \"done\":\n",
648
+ " rewards.append(0.0) # neutral stop\n",
649
+ " else:\n",
650
+ " result = env.step(action)\n",
651
+ " rewards.append(result.reward)\n",
652
+ "\n",
653
+ " return rewards\n",
654
+ "\n",
655
+ " return reward_fn\n",
656
+ "\n",
657
+ "\n",
658
+ "reward_fn = make_reward_fn(MockEngine(), MOCK_PASSES, max_steps=10)\n",
659
+ "print(\"✅ Reward function factory ready\")\n",
660
+ "\n",
661
+ "# Quick sanity check\n",
662
+ "test_completions = [\"constant_folding\", \"hallucinated_pass\", \"loop_unrolling\"]\n",
663
+ "test_programs = [json.dumps(SAMPLE_PROGRAM)] * 3\n",
664
+ "test_rewards = reward_fn(\n",
665
+ " prompts = [\"\"] * 3,\n",
666
+ " completions = test_completions,\n",
667
+ " program_json = test_programs,\n",
668
+ ")\n",
669
+ "print(\"\\nReward sanity check:\")\n",
670
+ "for act, rew in zip(test_completions, test_rewards):\n",
671
+ " print(f\" '{act}' → {rew:+.2f}\")"
672
+ ]
673
+ },
674
+ {
675
+ "cell_type": "markdown",
676
+ "id": "trainer-header",
677
+ "metadata": {},
678
+ "source": [
679
+ "## 🚀 Cell 8 — GRPO Trainer Config & Training"
680
+ ]
681
+ },
682
+ {
683
+ "cell_type": "code",
684
+ "execution_count": null,
685
+ "id": "trainer-cell",
686
+ "metadata": {},
687
+ "outputs": [],
688
+ "source": [
689
+ "from trl import GRPOConfig, GRPOTrainer\n",
690
+ "\n",
691
+ "# Optional W&B — comment out if not using\n",
692
+ "try:\n",
693
+ " import wandb\n",
694
+ " wandb.init(project=\"openenv-compiler-opt\", name=\"grpo-qwen2.5-3b\")\n",
695
+ " REPORT_TO = \"wandb\"\n",
696
+ "except Exception:\n",
697
+ " REPORT_TO = \"none\"\n",
698
+ "\n",
699
+ "\n",
700
+ "grpo_config = GRPOConfig(\n",
701
+ " # ── Generation ────────────────────────────────────────────────────────\n",
702
+ " num_generations = 4, # K rollouts per prompt for group-relative advantage\n",
703
+ " max_new_tokens = 16, # Actions are 1 word; don't waste context\n",
704
+ " temperature = 0.9,\n",
705
+ " top_p = 0.95,\n",
706
+ "\n",
707
+ " # ── Optimisation ──────────────────────────────────────────────────────\n",
708
+ " learning_rate = 5e-6,\n",
709
+ " per_device_train_batch_size = 2,\n",
710
+ " gradient_accumulation_steps = 4, # effective batch = 8\n",
711
+ " num_train_epochs = 3,\n",
712
+ " max_grad_norm = 0.5,\n",
713
+ "\n",
714
+ " # ── GRPO-specific ─────────────────────────────────────────────────────\n",
715
+ " beta = 0.04, # KL penalty; keeps policy near reference\n",
716
+ "\n",
717
+ " # ── Logging / checkpointing ───────────────────────────────────────────\n",
718
+ " output_dir = \"./grpo_compiler_checkpoints\",\n",
719
+ " logging_steps = 10,\n",
720
+ " save_steps = 100,\n",
721
+ " report_to = REPORT_TO,\n",
722
+ "\n",
723
+ " # ── Reproducibility ───────────────────────────────────────────────────\n",
724
+ " seed = 42,\n",
725
+ ")\n",
726
+ "\n",
727
+ "trainer = GRPOTrainer(\n",
728
+ " model = model,\n",
729
+ " tokenizer = tokenizer,\n",
730
+ " config = grpo_config,\n",
731
+ " train_dataset = train_dataset,\n",
732
+ " reward_funcs = reward_fn,\n",
733
+ ")\n",
734
+ "\n",
735
+ "print(\"✅ Trainer configured\")\n",
736
+ "print(f\" num_generations (K) = {grpo_config.num_generations}\")\n",
737
+ "print(f\" effective batch size = \"\n",
738
+ " f\"{grpo_config.per_device_train_batch_size * grpo_config.gradient_accumulation_steps}\")\n",
739
+ "print(f\" KL beta = {grpo_config.beta}\")\n",
740
+ "print()\n",
741
+ "print(\"Starting training... (this will take a while on T4)\")\n",
742
+ "trainer.train()"
743
+ ]
744
+ },
745
+ {
746
+ "cell_type": "markdown",
747
+ "id": "save-header",
748
+ "metadata": {},
749
+ "source": [
750
+ "## 💾 Cell 9 — Save Model"
751
+ ]
752
+ },
753
+ {
754
+ "cell_type": "code",
755
+ "execution_count": null,
756
+ "id": "save-cell",
757
+ "metadata": {},
758
+ "outputs": [],
759
+ "source": [
760
+ "SAVE_PATH = \"./grpo_compiler_final\"\n",
761
+ "\n",
762
+ "model.save_pretrained(SAVE_PATH)\n",
763
+ "tokenizer.save_pretrained(SAVE_PATH)\n",
764
+ "\n",
765
+ "print(f\"✅ Model saved to {SAVE_PATH}\")\n",
766
+ "\n",
767
+ "# Optional: push to HuggingFace Hub\n",
768
+ "# model.push_to_hub(\"your-hf-username/compiler-opt-grpo\")\n",
769
+ "# tokenizer.push_to_hub(\"your-hf-username/compiler-opt-grpo\")"
770
+ ]
771
+ },
772
+ {
773
+ "cell_type": "markdown",
774
+ "id": "curriculum-header",
775
+ "metadata": {},
776
+ "source": [
777
+ "## 📈 Cell 10 — Curriculum Callback & Reward Tracking"
778
+ ]
779
+ },
780
+ {
781
+ "cell_type": "code",
782
+ "execution_count": null,
783
+ "id": "curriculum-cell",
784
+ "metadata": {},
785
+ "outputs": [],
786
+ "source": [
787
+ "class CurriculumCallback:\n",
788
+ " \"\"\"\n",
789
+ " Tracks rolling mean reward and promotes curriculum level\n",
790
+ " when the agent has mastered the current difficulty.\n",
791
+ "\n",
792
+ " Usage: call .record(reward) after every episode.\n",
793
+ " Read .level to get current difficulty (1=easy, 2=medium, 3=hard).\n",
794
+ " \"\"\"\n",
795
+ " def __init__(self, reward_threshold: float = 5.0, window: int = 50):\n",
796
+ " self.threshold = reward_threshold\n",
797
+ " self.window = window\n",
798
+ " self._history = []\n",
799
+ " self.level = 1\n",
800
+ " self._promotions = []\n",
801
+ "\n",
802
+ " def record(self, reward: float, step: int = None):\n",
803
+ " self._history.append(reward)\n",
804
+ " if len(self._history) >= self.window:\n",
805
+ " mean = sum(self._history[-self.window:]) / self.window\n",
806
+ " if mean >= self.threshold and self.level < 3:\n",
807
+ " self.level += 1\n",
808
+ " self._promotions.append((step or len(self._history), self.level))\n",
809
+ " print(f\"[Curriculum] ▲ Promoted to level {self.level} \"\n",
810
+ " f\"(rolling mean={mean:.2f})\")\n",
811
+ "\n",
812
+ " def plot(self):\n",
813
+ " import matplotlib.pyplot as plt\n",
814
+ " import numpy as np\n",
815
+ "\n",
816
+ " history = self._history\n",
817
+ " steps = list(range(len(history)))\n",
818
+ " window = self.window\n",
819
+ " rolling = [\n",
820
+ " sum(history[max(0,i-window):i+1]) / min(i+1, window)\n",
821
+ " for i in steps\n",
822
+ " ]\n",
823
+ "\n",
824
+ " fig, ax = plt.subplots(figsize=(10, 4))\n",
825
+ " ax.plot(steps, history, alpha=0.3, color=\"#3498db\", label=\"Episode reward\")\n",
826
+ " ax.plot(steps, rolling, color=\"#e74c3c\", linewidth=2,\n",
827
+ " label=f\"Rolling mean (w={window})\")\n",
828
+ " ax.axhline(self.threshold, linestyle=\"--\", color=\"grey\",\n",
829
+ " linewidth=1, label=f\"Promotion threshold ({self.threshold})\")\n",
830
+ " for step, level in self._promotions:\n",
831
+ " ax.axvline(step, color=\"green\", linewidth=1.5, linestyle=\":\")\n",
832
+ " ax.text(step, ax.get_ylim()[1]*0.9, f\" L{level}\",\n",
833
+ " color=\"green\", fontsize=9)\n",
834
+ " ax.set_xlabel(\"Episode\")\n",
835
+ " ax.set_ylabel(\"Reward\")\n",
836
+ " ax.set_title(\"Training Reward + Curriculum Progression\")\n",
837
+ " ax.legend()\n",
838
+ " plt.tight_layout()\n",
839
+ " plt.savefig(\"curriculum_reward_curve.png\", dpi=150)\n",
840
+ " plt.show()\n",
841
+ " print(\"📈 Saved curriculum_reward_curve.png\")\n",
842
+ "\n",
843
+ "\n",
844
+ "# ── Demo: simulate 200 episodes of improving reward ──────────────────────────\n",
845
+ "import random\n",
846
+ "cb = CurriculumCallback(reward_threshold=5.0, window=50)\n",
847
+ "for ep in range(200):\n",
848
+ " # Simulate reward slowly improving\n",
849
+ " synthetic_reward = -5 + ep * 0.08 + random.gauss(0, 2)\n",
850
+ " cb.record(synthetic_reward, step=ep)\n",
851
+ "\n",
852
+ "cb.plot()"
853
+ ]
854
+ },
855
+ {
856
+ "cell_type": "markdown",
857
+ "id": "inference-header",
858
+ "metadata": {},
859
+ "source": [
860
+ "## 🔍 Cell 11 — Inference: Before vs After Training"
861
+ ]
862
+ },
863
+ {
864
+ "cell_type": "code",
865
+ "execution_count": null,
866
+ "id": "inference-cell",
867
+ "metadata": {},
868
+ "outputs": [],
869
+ "source": [
870
+ "def run_inference_episode(model, tokenizer, engine, passes, program, max_steps=10):\n",
871
+ " \"\"\"Run a greedy inference episode and return the action sequence + total improvement.\"\"\"\n",
872
+ " from transformers import TextStreamer\n",
873
+ " FastLanguageModel.for_inference(model)\n",
874
+ "\n",
875
+ " env = CompilerOptimizationEnv(engine, passes, max_steps=max_steps)\n",
876
+ " obs = env.reset(program)\n",
877
+ " system_prompt = build_system_prompt(env)\n",
878
+ "\n",
879
+ " actions_chosen, rewards_earned = [], []\n",
880
+ " done = False\n",
881
+ "\n",
882
+ " print(f\"\\nBaseline cycles: {env.previous_cycles}\")\n",
883
+ " print(f\"Initial state:\\n{obs}\\n\")\n",
884
+ "\n",
885
+ " while not done:\n",
886
+ " messages = [\n",
887
+ " {\"role\": \"system\", \"content\": system_prompt},\n",
888
+ " {\"role\": \"user\", \"content\": f\"Current program:\\n{obs}\\n\\nChoose an action:\"},\n",
889
+ " ]\n",
890
+ " inputs = tokenizer.apply_chat_template(\n",
891
+ " messages,\n",
892
+ " tokenize=True,\n",
893
+ " add_generation_prompt=True,\n",
894
+ " return_tensors=\"pt\",\n",
895
+ " ).to(model.device)\n",
896
+ "\n",
897
+ " with torch.no_grad():\n",
898
+ " outputs = model.generate(\n",
899
+ " input_ids = inputs,\n",
900
+ " max_new_tokens = 16,\n",
901
+ " temperature = 0.1, # greedy-ish for inference\n",
902
+ " do_sample = True,\n",
903
+ " )\n",
904
+ "\n",
905
+ " raw_action = tokenizer.decode(\n",
906
+ " outputs[0][inputs.shape[-1]:], skip_special_tokens=True\n",
907
+ " ).strip()\n",
908
+ " action = re.sub(r\"[^a-z0-9_]\", \"\", raw_action.lower().split()[0])\n",
909
+ "\n",
910
+ " result = env.step(action)\n",
911
+ " actions_chosen.append(action)\n",
912
+ " rewards_earned.append(result.reward)\n",
913
+ " obs = result.observation\n",
914
+ " done = result.done\n",
915
+ "\n",
916
+ " print(f\"Step {len(actions_chosen)}: '{action}' → reward={result.reward:+.2f}\")\n",
917
+ "\n",
918
+ " summary = env._episode_summary()\n",
919
+ " print(f\"\\n{'─'*40}\")\n",
920
+ " print(f\"Total improvement: {summary['total_improvement_pct']:.1f}%\")\n",
921
+ " print(f\"Final cycles: {summary['final_cycles']} (was {summary['baseline_cycles']})\")\n",
922
+ " return summary\n",
923
+ "\n",
924
+ "\n",
925
+ "# Uncomment after training:\n",
926
+ "# summary = run_inference_episode(\n",
927
+ "# model, tokenizer, MockEngine(), MOCK_PASSES, SAMPLE_PROGRAM\n",
928
+ "# )\n",
929
+ "\n",
930
+ "print(\"✅ Inference cell ready. Uncomment the last block after training to run.\")"
931
+ ]
932
+ },
933
+ {
934
+ "cell_type": "markdown",
935
+ "id": "tips-header",
936
+ "metadata": {},
937
+ "source": [
938
+ "---\n",
939
+ "## 📝 Notes & Tips\n",
940
+ "\n",
941
+ "| What | Why it matters |\n",
942
+ "|------|----------------|\n",
943
+ "| `num_generations=4` | GRPO needs K≥2 rollouts per prompt to compute group-relative advantage. K=4 balances diversity vs. compute. |\n",
944
+ "| `beta=0.04` | KL penalty keeping policy close to reference. Too low → mode collapse. Too high → no learning. |\n",
945
+ "| `max_new_tokens=16` | Actions are one word. This prevents wasted computation and keeps the model from adding explanations. |\n",
946
+ "| Dynamic crash penalty | Computed as `−2 × (100 × max_steps)` so it always dominates the best possible episode reward without washing out all other reward signal. |\n",
947
+ "| Terminal bonus | Rewards cumulative improvement, not just greedy single-step gains. Critical for long-horizon tasks. |\n",
948
+ "| Soft invalid-action termination | 3 consecutive invalid actions → end. Single mistakes don't kill the episode; the agent can recover. |\n",
949
+ "| `deepcopy` on all pass inputs | Role 3's passes mutate dicts in-place. Without this, `original_program` gets corrupted and the verifier fails spuriously. |\n",
950
+ "\n",
951
+ "**Next steps:**\n",
952
+ "- Replace `MockEngine` with Role 1's real engine\n",
953
+ "- Replace `MOCK_PASSES` with Role 3's real passes\n",
954
+ "- Push environment to HuggingFace Spaces: `openenv init && openenv deploy`\n",
955
+ "- Add W&B sweep to tune `beta`, `learning_rate`, `num_generations`"
956
+ ]
957
+ }
958
+ ]
959
+ }"
space/space/space/space/space/space/space/space/space/space/space/space/space/space/role2_deliverable3_training_loop (2) (1) (1).ipynb ADDED
The diff for this file is too large to render. See raw diff
 
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/DockerFile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a lightweight Python 3.10 image as the base
2
+ FROM python:3.10-slim
3
+
4
+ # Hugging Face Spaces requires a non-root user for security.
5
+ # We create a user named 'user' with user ID 1000.
6
+ RUN useradd -m -u 1000 user
7
+
8
+ # Set environment variables to ensure Python output is logged immediately
9
+ # and to add the local bin directory to the PATH for pip installations.
10
+ ENV PATH="/home/user/.local/bin:$PATH" \
11
+ PYTHONUNBUFFERED=1 \
12
+ PYTHONDONTWRITEBYTECODE=1
13
+
14
+ # Switch away from root to the new user
15
+ USER user
16
+
17
+ # Set the working directory inside the container
18
+ WORKDIR /home/user/app
19
+
20
+ # Copy the requirements file first to leverage Docker cache layers
21
+ COPY --chown=user requirements.txt .
22
+
23
+ # Upgrade pip and install the dependencies defined by Role 2
24
+ RUN pip install --no-cache-dir --upgrade pip && \
25
+ pip install --no-cache-dir -r requirements.txt
26
+
27
+ # Copy the rest of the application files (app.py, openenv.yaml, engine.py, etc.)
28
+ COPY --chown=user . .
29
+
30
+ # Expose port 7860, which is the default port Hugging Face routes traffic to
31
+ EXPOSE 7860
32
+
33
+ # Start the OpenEnv server using the entrypoint defined in app.py
34
+ CMD ["python", "app.py"]
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/README.md CHANGED
@@ -1 +1,9 @@
1
- # MetaHackathon2026Finals
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Compiler Brain OpenEnv
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenEnv SDK (Mandatory for the hackathon judging environment)
2
+ openenv
3
+
4
+ # Core RL Environment
5
+ gymnasium
6
+
7
+ # Hugging Face Training Stack (Versions matched to your D3 notebook)
8
+ trl==0.23.1
9
+ transformers==4.57.1
10
+ peft
11
+ accelerate
12
+ bitsandbytes
13
+
14
+ # Unsloth for efficient 4-bit QLoRA training on T4 GPUs
15
+ # Note: Unsloth often prefers being installed via their specific pip wheel or git,
16
+ # but this is standard for a requirements file.
17
+ unsloth
18
+
19
+ # PyTorch (Will default to standard compatible version if no index is specified)
20
+ torch
21
+
22
+ # Logging & Visualizations (For Day 2 judging criteria)
23
+ wandb
24
+ matplotlib
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import traceback
4
+ from typing import Any, Dict, List
5
+
6
+ # OpenEnv SDK import
7
+ from openenv import MCPEnvironment
8
+
9
+ # ==============================================================================
10
+ # ROLE 1 & 3 IMPORTS
11
+ # TODO: Import the actual execution engine and generator from your teammates
12
+ # ==============================================================================
13
+ # from engine import execute_tac, verify_equivalence
14
+ # from curriculum import generate_level_code
15
+
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger("CompilerEnvServer")
18
+
19
+ class CompilerEnv(MCPEnvironment):
20
+ """
21
+ The OpenEnv Server Wrapper for the Toy-IR Compiler Pass Optimizer.
22
+ Acts as the referee between the LLM client and Role 1's Execution Engine.
23
+ """
24
+
25
+ def __init__(self):
26
+ super().__init__()
27
+ # State variables
28
+ self.raw_json_code = None
29
+ self.current_state_string = ""
30
+ self.initial_cycles = 0
31
+ self.current_step = 0
32
+ self.max_steps = 10 # Set a max step limit per episode
33
+
34
+ # Cycle Weights (The Physics defined by Role 2)
35
+ self.cycle_weights = {
36
+ "ADD": 1,
37
+ "SUB": 1,
38
+ "MUL": 4,
39
+ "DIV": 10,
40
+ "MEM_LOAD": 20,
41
+ "STORE": 20
42
+ }
43
+
44
+ def reset(self) -> str:
45
+ """
46
+ Grabs unoptimized code, calculates baseline cycles, and translates the
47
+ state for the LLM.
48
+ """
49
+ self.current_step = 0
50
+
51
+ # 1. Grab new unoptimized code (Role 3 integration)
52
+ # TODO: Replace with real generator: self.raw_json_code = generate_level_code()
53
+ self.raw_json_code = self._mock_generator()
54
+
55
+ # 2. Get baseline cycles (Role 1 integration)
56
+ # TODO: Replace with real engine: self.initial_cycles, _ = execute_tac(self.raw_json_code, [])
57
+ self.initial_cycles = 100
58
+
59
+ # 3. Translate to Pseudo-Assembly to prevent Attention Dilution
60
+ self.current_state_string = self._translate_state(self.raw_json_code)
61
+
62
+ logger.info(f"Environment Reset. Baseline Cycles: {self.initial_cycles}")
63
+ return self.state()
64
+
65
+ def step(self, action: str) -> Dict[str, Any]:
66
+ """
67
+ Executes the LLM's chosen optimization pass, verifies math equivalence,
68
+ and calculates the reward.
69
+ """
70
+ self.current_step += 1
71
+
72
+ # 1. Parse LLM Action (Regex/JSON robustness)
73
+ try:
74
+ # Assuming the LLM outputs a single pass name or a list of passes
75
+ action_data = json.loads(action)
76
+ if isinstance(action_data, str):
77
+ action_array = [action_data]
78
+ else:
79
+ action_array = action_data
80
+ format_bonus = 0.1
81
+ except json.JSONDecodeError:
82
+ # Format Trap
83
+ return self._build_step_response(
84
+ reward=-2.5,
85
+ done=True,
86
+ error="Invalid JSON. You must output a valid JSON array of strings."
87
+ )
88
+
89
+ # 2. Execute Code & Verify Equivalence (Role 1 Integration)
90
+ # TODO: new_cycles, optimized_code = execute_tac(self.raw_json_code, action_array)
91
+ # TODO: is_valid = verify_equivalence(self.raw_json_code, optimized_code)
92
+ new_cycles = 80 # Mock Data
93
+ is_valid = True # Mock Data
94
+
95
+ # 3. Calculate Reward Physics
96
+ if not is_valid:
97
+ # Correctness Penalty + Micro-Variance for GRPO
98
+ penalty = -2.0 - (len(action_array) * 0.01)
99
+ return self._build_step_response(
100
+ reward=penalty,
101
+ done=True,
102
+ error=f"Code equivalence broken by passes: {action_array}"
103
+ )
104
+
105
+ # Calculate Improvement Ratio + Time Tax (-1.0)
106
+ cycle_improvement_ratio = (self.initial_cycles - new_cycles) / self.initial_cycles
107
+ time_tax = -0.05 * self.current_step # Small tax to prevent pass spamming
108
+ reward = cycle_improvement_ratio + format_bonus + time_tax
109
+
110
+ # Update state if sequential, or finish if one-shot
111
+ # NOTE: For hackathon speed, we treat this as a One-Shot episode
112
+ done = True
113
+
114
+ return self._build_step_response(
115
+ reward=reward,
116
+ done=done,
117
+ info={"status": "success", "optimized_cycles": new_cycles}
118
+ )
119
+
120
+ def state(self) -> str:
121
+ """
122
+ Returns the current observation to the LLM.
123
+ """
124
+ return f"Current Step: {self.current_step}/{self.max_steps}\n\n{self.current_state_string}"
125
+
126
+ def _translate_state(self, raw_json: List[Dict]) -> str:
127
+ """
128
+ Translates raw AST JSON into clean pseudo-assembly.
129
+ Strips all UUIDs and AST metadata.
130
+ """
131
+ pseudo_assembly = []
132
+ instruction_count = 1
133
+
134
+ for inst in raw_json:
135
+ op = inst.get("op", "UNKNOWN")
136
+ src1 = inst.get("src1", "")
137
+ src2 = inst.get("src2", "")
138
+ dest = inst.get("dest", "")
139
+
140
+ # Format arguments cleanly
141
+ args = f"{src1}" if src2 is None else f"{src1}, {src2}"
142
+
143
+ if dest:
144
+ line = f"{instruction_count}. {dest} = {op} {args}"
145
+ else:
146
+ line = f"{instruction_count}. {op} {args}"
147
+
148
+ pseudo_assembly.append(line)
149
+ instruction_count += 1
150
+
151
+ return "\n".join(pseudo_assembly)
152
+
153
+ def _build_step_response(self, reward: float, done: bool, error: str = None, info: dict = None) -> Dict[str, Any]:
154
+ """Helper to format the standard OpenEnv step return dictionary."""
155
+ response = {
156
+ "reward": reward,
157
+ "done": done,
158
+ "state": self.state()
159
+ }
160
+ if error:
161
+ response["error"] = error
162
+ if info:
163
+ response["info"] = info
164
+ return response
165
+
166
+ def _mock_generator(self):
167
+ """Mock data so the server runs before Role 1 integrates their engine."""
168
+ return [
169
+ {"op": "CONST", "dest": "a", "src1": 2, "src2": None},
170
+ {"op": "CONST", "dest": "b", "src1": 3, "src2": None},
171
+ {"op": "ADD", "dest": "c", "src1": "a", "src2": "b"}
172
+ ]
173
+
174
+ if __name__ == "__main__":
175
+ logger.info("Initializing CompilerEnv Server...")
176
+ try:
177
+ env = CompilerEnv()
178
+ # openenv.run() or start() depending on the specific MCP wrapper version
179
+ env.start()
180
+ except Exception as e:
181
+ logger.error(f"Failed to start environment server: {e}")
182
+ logger.error(traceback.format_exc())
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/compiler_tetris (1).ipynb ADDED
@@ -0,0 +1,651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "b7fdb9ab",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "# ==== CELL 1: Setup ====\n",
11
+ "!pip install -q pytest\n",
12
+ "import os, sys\n",
13
+ "\n",
14
+ "LOCAL_DIR = \"/content/toyir\"\n",
15
+ "DRIVE_DIR = \"/content/drive/MyDrive/env/toyir\"\n",
16
+ "\n",
17
+ "os.makedirs(LOCAL_DIR, exist_ok=True)\n",
18
+ "sys.path.insert(0, LOCAL_DIR)\n",
19
+ "print(\"Workspace ready at\", LOCAL_DIR)\n"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "code",
24
+ "execution_count": null,
25
+ "id": "d0337367",
26
+ "metadata": {},
27
+ "outputs": [],
28
+ "source": [
29
+ "# ==== CELL 1b: Mount Drive + sync workspace ====\n",
30
+ "from google.colab import drive\n",
31
+ "drive.mount('/content/drive')\n",
32
+ "\n",
33
+ "import shutil\n",
34
+ "os.makedirs(DRIVE_DIR, exist_ok=True)\n",
35
+ "\n",
36
+ "# On fresh runtime: pull existing files from Drive -> local\n",
37
+ "for f in os.listdir(DRIVE_DIR):\n",
38
+ " src = os.path.join(DRIVE_DIR, f)\n",
39
+ " if os.path.isfile(src) and f.endswith(\".py\"):\n",
40
+ " shutil.copy2(src, os.path.join(LOCAL_DIR, f))\n",
41
+ "print(\"Synced from Drive:\", sorted(os.listdir(LOCAL_DIR)))\n"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "2c83c529",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "%%writefile /content/toyir/toy_vm.py\n",
52
+ "\"\"\"Toy-IR VM: deterministic, dependency-light execution engine for RLVR.\"\"\"\n",
53
+ "from __future__ import annotations\n",
54
+ "from copy import deepcopy\n",
55
+ "from typing import Any\n",
56
+ "\n",
57
+ "CYCLE_COST: dict[str, int] = {\n",
58
+ " \"CONST\": 1, \"ADD\": 1, \"SUB\": 1, \"MUL\": 3, \"DIV\": 5,\n",
59
+ " \"COPY\": 1, \"LOAD\": 4, \"STORE\": 4, \"NOP\": 0,\n",
60
+ "}\n",
61
+ "OP_ORDER = (\"CONST\", \"ADD\", \"SUB\", \"MUL\", \"DIV\", \"COPY\", \"LOAD\", \"STORE\", \"NOP\")\n",
62
+ "VALID_OPS = frozenset(CYCLE_COST)\n",
63
+ "REQUIRED_KEYS = (\"op\", \"dest\", \"src1\", \"src2\")\n",
64
+ "\n",
65
+ "\n",
66
+ "def _is_str_or_int_or_none(x: Any) -> bool:\n",
67
+ " return x is None or isinstance(x, str) or (isinstance(x, int) and not isinstance(x, bool))\n",
68
+ "\n",
69
+ "\n",
70
+ "def _is_str_or_none(x: Any) -> bool:\n",
71
+ " return x is None or isinstance(x, str)\n",
72
+ "\n",
73
+ "\n",
74
+ "def validate_ir(program: list[dict]) -> dict:\n",
75
+ " errors: list[str] = []\n",
76
+ " for i, ins in enumerate(program):\n",
77
+ " if not isinstance(ins, dict):\n",
78
+ " errors.append(f\"[{i}] not a dict\")\n",
79
+ " continue\n",
80
+ " for k in REQUIRED_KEYS:\n",
81
+ " if k not in ins:\n",
82
+ " errors.append(f\"[{i}] missing key '{k}'\")\n",
83
+ " if \"op\" not in ins:\n",
84
+ " continue\n",
85
+ " op = ins[\"op\"]\n",
86
+ " if op not in VALID_OPS:\n",
87
+ " errors.append(f\"[{i}] invalid op '{op}'\")\n",
88
+ " continue\n",
89
+ " if not _is_str_or_none(ins.get(\"dest\")):\n",
90
+ " errors.append(f\"[{i}] dest must be str|None\")\n",
91
+ " if not _is_str_or_int_or_none(ins.get(\"src1\")):\n",
92
+ " errors.append(f\"[{i}] src1 must be str|int|None\")\n",
93
+ " if not _is_str_or_int_or_none(ins.get(\"src2\")):\n",
94
+ " errors.append(f\"[{i}] src2 must be str|int|None\")\n",
95
+ "\n",
96
+ " # per-op contracts\n",
97
+ " if op in (\"CONST\", \"ADD\", \"SUB\", \"MUL\", \"DIV\", \"COPY\", \"LOAD\"):\n",
98
+ " if not isinstance(ins.get(\"dest\"), str):\n",
99
+ " errors.append(f\"[{i}] {op} requires str dest\")\n",
100
+ " if op in (\"ADD\", \"SUB\", \"MUL\", \"DIV\"):\n",
101
+ " if ins.get(\"src1\") is None or ins.get(\"src2\") is None:\n",
102
+ " errors.append(f\"[{i}] {op} requires src1 and src2\")\n",
103
+ " if op in (\"CONST\", \"COPY\"):\n",
104
+ " if ins.get(\"src1\") is None:\n",
105
+ " errors.append(f\"[{i}] {op} requires src1\")\n",
106
+ " if op in (\"CONST\", \"COPY\", \"LOAD\", \"STORE\"):\n",
107
+ " if ins.get(\"src2\") is not None:\n",
108
+ " errors.append(f\"[{i}] {op} requires src2=None\")\n",
109
+ " if op == \"LOAD\":\n",
110
+ " if not isinstance(ins.get(\"src1\"), str):\n",
111
+ " errors.append(f\"[{i}] LOAD src1 must be str (variable name)\")\n",
112
+ " if op == \"STORE\":\n",
113
+ " if not isinstance(ins.get(\"dest\"), str):\n",
114
+ " errors.append(f\"[{i}] STORE dest must be str (variable name)\")\n",
115
+ " if ins.get(\"src1\") is None:\n",
116
+ " errors.append(f\"[{i}] STORE requires src1\")\n",
117
+ " if op == \"NOP\":\n",
118
+ " if any(ins.get(k) is not None for k in (\"dest\", \"src1\", \"src2\")):\n",
119
+ " errors.append(f\"[{i}] NOP fields must all be None\")\n",
120
+ "\n",
121
+ " return {\"valid\": len(errors) == 0, \"errors\": errors}\n",
122
+ "\n",
123
+ "\n",
124
+ "def _resolve(operand: Any, variables: dict) -> int:\n",
125
+ " if isinstance(operand, str):\n",
126
+ " if operand not in variables:\n",
127
+ " raise KeyError(f\"undefined variable '{operand}'\")\n",
128
+ " return variables[operand]\n",
129
+ " if isinstance(operand, int) and not isinstance(operand, bool):\n",
130
+ " return operand\n",
131
+ " raise TypeError(f\"bad operand: {operand!r}\")\n",
132
+ "\n",
133
+ "\n",
134
+ "def _exec_one(ins: dict, variables: dict, memory: dict) -> int:\n",
135
+ " op = ins[\"op\"]\n",
136
+ " dest = ins[\"dest\"]\n",
137
+ " s1, s2 = ins[\"src1\"], ins[\"src2\"]\n",
138
+ "\n",
139
+ " if op == \"CONST\":\n",
140
+ " variables[dest] = _resolve(s1, variables)\n",
141
+ " elif op == \"ADD\":\n",
142
+ " variables[dest] = _resolve(s1, variables) + _resolve(s2, variables)\n",
143
+ " elif op == \"SUB\":\n",
144
+ " variables[dest] = _resolve(s1, variables) - _resolve(s2, variables)\n",
145
+ " elif op == \"MUL\":\n",
146
+ " variables[dest] = _resolve(s1, variables) * _resolve(s2, variables)\n",
147
+ " elif op == \"DIV\":\n",
148
+ " b = _resolve(s2, variables)\n",
149
+ " if b == 0:\n",
150
+ " raise ZeroDivisionError(\"DIV by zero\")\n",
151
+ " variables[dest] = _resolve(s1, variables) // b\n",
152
+ " elif op == \"COPY\":\n",
153
+ " variables[dest] = _resolve(s1, variables)\n",
154
+ " elif op == \"LOAD\":\n",
155
+ " if not isinstance(s1, str):\n",
156
+ " raise TypeError(\"LOAD src1 must be a variable name\")\n",
157
+ " addr = variables.get(s1)\n",
158
+ " if not (isinstance(addr, int) and not isinstance(addr, bool)):\n",
159
+ " raise TypeError(f\"LOAD address from '{s1}' is not int: {addr!r}\")\n",
160
+ " if addr not in memory:\n",
161
+ " raise KeyError(f\"LOAD from uninitialized addr {addr}\")\n",
162
+ " variables[dest] = memory[addr]\n",
163
+ " elif op == \"STORE\":\n",
164
+ " if not isinstance(dest, str):\n",
165
+ " raise TypeError(\"STORE dest must be a variable name\")\n",
166
+ " addr = variables.get(dest)\n",
167
+ " if not (isinstance(addr, int) and not isinstance(addr, bool)):\n",
168
+ " raise TypeError(f\"STORE address from '{dest}' is not int: {addr!r}\")\n",
169
+ " memory[addr] = _resolve(s1, variables)\n",
170
+ " elif op == \"NOP\":\n",
171
+ " pass\n",
172
+ " else:\n",
173
+ " raise ValueError(f\"non-executable op {op}\")\n",
174
+ " return CYCLE_COST[op]\n",
175
+ "\n",
176
+ "\n",
177
+ "def execute(program: list[dict], initial_state: dict) -> dict:\n",
178
+ " variables: dict = {}\n",
179
+ " memory: dict = {}\n",
180
+ " cycles = 0\n",
181
+ " try:\n",
182
+ " if not isinstance(initial_state, dict):\n",
183
+ " raise TypeError(\"initial_state must be a dict\")\n",
184
+ " state = deepcopy(initial_state)\n",
185
+ " variables = state.get(\"variables\", {})\n",
186
+ " memory = state.get(\"memory\", {})\n",
187
+ " if not isinstance(variables, dict):\n",
188
+ " raise TypeError(\"initial_state['variables'] must be a dict\")\n",
189
+ " if not isinstance(memory, dict):\n",
190
+ " raise TypeError(\"initial_state['memory'] must be a dict\")\n",
191
+ "\n",
192
+ " for ins in program:\n",
193
+ " cycles += _exec_one(ins, variables, memory)\n",
194
+ " except Exception as e:\n",
195
+ " return {\"memory\": memory, \"variables\": variables,\n",
196
+ " \"cycles\": None, \"success\": False, \"error\": str(e)}\n",
197
+ " return {\"memory\": memory, \"variables\": variables,\n",
198
+ " \"cycles\": cycles, \"success\": True, \"error\": None}\n",
199
+ "\n",
200
+ "\n",
201
+ "def count_cycles(program: list[dict]) -> int:\n",
202
+ " total = 0\n",
203
+ " for i, ins in enumerate(program):\n",
204
+ " op = ins.get(\"op\") if isinstance(ins, dict) else None\n",
205
+ " if op not in CYCLE_COST:\n",
206
+ " raise ValueError(f\"count_cycles: invalid or missing op at index {i}: {op!r}\")\n",
207
+ " total += CYCLE_COST[op]\n",
208
+ " return total\n",
209
+ "\n",
210
+ "\n",
211
+ "def profile(program: list[dict]) -> dict:\n",
212
+ " out: dict[str, int] = {\"n_instructions\": len(program)}\n",
213
+ " for op in OP_ORDER:\n",
214
+ " out[f\"n_{op.lower()}\"] = 0\n",
215
+ " for ins in program:\n",
216
+ " key = f\"n_{ins['op'].lower()}\"\n",
217
+ " if key in out:\n",
218
+ " out[key] += 1\n",
219
+ " return out\n",
220
+ "\n",
221
+ "\n",
222
+ "def _fmt_operand(x: Any) -> str:\n",
223
+ " if x is None:\n",
224
+ " return \"\"\n",
225
+ " return str(x)\n",
226
+ "\n",
227
+ "\n",
228
+ "def dump_ir(program: list[dict]) -> str:\n",
229
+ " stores = [ins[\"dest\"] for ins in program if ins[\"op\"] == \"STORE\"]\n",
230
+ " seen, ordered = set(), []\n",
231
+ " for s in stores:\n",
232
+ " if s not in seen:\n",
233
+ " seen.add(s); ordered.append(s)\n",
234
+ " header = \"// OBSERVABLE OUT: \" + (\", \".join(f\"mem[{s}]\" for s in ordered) if ordered else \"(none)\")\n",
235
+ " lines = [header]\n",
236
+ " for ins in program:\n",
237
+ " op = ins[\"op\"]\n",
238
+ " dest, s1, s2 = ins[\"dest\"], ins[\"src1\"], ins[\"src2\"]\n",
239
+ " if op == \"STORE\":\n",
240
+ " lines.append(f\"STORE [{dest}] {_fmt_operand(s1)}\")\n",
241
+ " elif op == \"LOAD\":\n",
242
+ " lines.append(f\"{dest} = LOAD [{_fmt_operand(s1)}]\")\n",
243
+ " elif op == \"NOP\":\n",
244
+ " lines.append(\"NOP\")\n",
245
+ " else:\n",
246
+ " parts = [op, _fmt_operand(s1)]\n",
247
+ " if s2 is not None:\n",
248
+ " parts.append(_fmt_operand(s2))\n",
249
+ " lines.append(f\"{dest} = {' '.join(p for p in parts if p)}\")\n",
250
+ " return \"\\n\".join(lines)\n",
251
+ "\n",
252
+ "\n",
253
+ "def clean_nops(program: list[dict]) -> list[dict]:\n",
254
+ " return [deepcopy(ins) for ins in program if ins[\"op\"] != \"NOP\"]\n"
255
+ ]
256
+ },
257
+ {
258
+ "cell_type": "code",
259
+ "execution_count": null,
260
+ "id": "3c85a58b",
261
+ "metadata": {},
262
+ "outputs": [],
263
+ "source": [
264
+ "%%writefile /content/toyir/verifier.py\n",
265
+ "\"\"\"Strict equivalence verifier - anti-cheat for RL reward hacking.\"\"\"\n",
266
+ "from __future__ import annotations\n",
267
+ "from toy_vm import execute\n",
268
+ "\n",
269
+ "\n",
270
+ "def verify(original_program: list[dict],\n",
271
+ " optimized_program: list[dict],\n",
272
+ " initial_states: list[dict]) -> dict:\n",
273
+ " if not initial_states:\n",
274
+ " raise ValueError(\"verify requires at least one initial_state\")\n",
275
+ "\n",
276
+ " results = []\n",
277
+ " all_match = True\n",
278
+ " for st in initial_states:\n",
279
+ " r_o = execute(original_program, st)\n",
280
+ " r_p = execute(optimized_program, st)\n",
281
+ " mem_o = r_o[\"memory\"] if r_o[\"success\"] else None\n",
282
+ " mem_p = r_p[\"memory\"] if r_p[\"success\"] else None\n",
283
+ "\n",
284
+ " execution_errors = {}\n",
285
+ " if not r_o[\"success\"]:\n",
286
+ " execution_errors[\"original_error\"] = r_o[\"error\"]\n",
287
+ " if not r_p[\"success\"]:\n",
288
+ " execution_errors[\"optimized_error\"] = r_p[\"error\"]\n",
289
+ "\n",
290
+ " if execution_errors:\n",
291
+ " match = False\n",
292
+ " mismatches = [\"__execution_error__\"]\n",
293
+ " else:\n",
294
+ " keys = set(mem_o.keys()) | set(mem_p.keys())\n",
295
+ " mismatches = [k for k in keys if mem_o.get(k) != mem_p.get(k)]\n",
296
+ " match = len(mismatches) == 0\n",
297
+ "\n",
298
+ " if not match:\n",
299
+ " all_match = False\n",
300
+ " results.append({\n",
301
+ " \"initial_state\": st,\n",
302
+ " \"original_memory\": mem_o,\n",
303
+ " \"optimized_memory\": mem_p,\n",
304
+ " \"match\": match,\n",
305
+ " \"mismatches\": sorted(mismatches, key=lambda x: (isinstance(x, str), x)),\n",
306
+ " \"execution_errors\": execution_errors or None,\n",
307
+ " })\n",
308
+ " return {\"equivalent\": all_match, \"results\": results}\n"
309
+ ]
310
+ },
311
+ {
312
+ "cell_type": "code",
313
+ "execution_count": null,
314
+ "id": "cd7f8fa2",
315
+ "metadata": {},
316
+ "outputs": [],
317
+ "source": [
318
+ "%%writefile /content/toyir/reward_utils.py\n",
319
+ "\"\"\"Reward signal: cycle-reduction metric for RL agent.\"\"\"\n",
320
+ "from __future__ import annotations\n",
321
+ "from toy_vm import execute\n",
322
+ "\n",
323
+ "\n",
324
+ "def compute_reduction(original_program: list[dict],\n",
325
+ " optimized_program: list[dict],\n",
326
+ " initial_state: dict) -> dict:\n",
327
+ " r_o = execute(original_program, initial_state)\n",
328
+ " r_p = execute(optimized_program, initial_state)\n",
329
+ "\n",
330
+ " if not (r_o[\"success\"] and r_p[\"success\"]):\n",
331
+ " return {\n",
332
+ " \"original_cycles\": r_o[\"cycles\"],\n",
333
+ " \"optimized_cycles\": r_p[\"cycles\"],\n",
334
+ " \"absolute_savings\": None,\n",
335
+ " \"relative_savings\": None,\n",
336
+ " \"success\": False,\n",
337
+ " \"error\": r_o[\"error\"] or r_p[\"error\"],\n",
338
+ " }\n",
339
+ "\n",
340
+ " oc, pc = r_o[\"cycles\"], r_p[\"cycles\"]\n",
341
+ " rel = 0.0 if oc == 0 else (oc - pc) / oc\n",
342
+ " rel = max(-1.0, min(1.0, rel))\n",
343
+ " return {\n",
344
+ " \"original_cycles\": oc,\n",
345
+ " \"optimized_cycles\": pc,\n",
346
+ " \"absolute_savings\": oc - pc,\n",
347
+ " \"relative_savings\": rel,\n",
348
+ " \"success\": True,\n",
349
+ " \"error\": None,\n",
350
+ " }\n"
351
+ ]
352
+ },
353
+ {
354
+ "cell_type": "code",
355
+ "execution_count": null,
356
+ "id": "4f2a941c",
357
+ "metadata": {},
358
+ "outputs": [],
359
+ "source": [
360
+ "%%writefile /content/toyir/test_vm.py\n",
361
+ "\"\"\"Pytest suite covering VM, verifier, reward, and RL exploit guardrails.\"\"\"\n",
362
+ "from __future__ import annotations\n",
363
+ "import random\n",
364
+ "import pytest\n",
365
+ "from toy_vm import (\n",
366
+ " validate_ir, execute, count_cycles, profile, dump_ir, clean_nops, OP_ORDER,\n",
367
+ ")\n",
368
+ "from verifier import verify\n",
369
+ "from reward_utils import compute_reduction\n",
370
+ "\n",
371
+ "\n",
372
+ "def I(op, dest=None, src1=None, src2=None):\n",
373
+ " return {\"op\": op, \"dest\": dest, \"src1\": src1, \"src2\": src2}\n",
374
+ "\n",
375
+ "\n",
376
+ "def test_const_fold_equivalence():\n",
377
+ " orig = [I(\"CONST\", \"t0\", 5), I(\"CONST\", \"t1\", 7), I(\"ADD\", \"t2\", \"t0\", \"t1\"),\n",
378
+ " I(\"CONST\", \"addr\", 0), I(\"STORE\", \"addr\", \"t2\")]\n",
379
+ " opt = [I(\"CONST\", \"t2\", 12), I(\"CONST\", \"addr\", 0), I(\"STORE\", \"addr\", \"t2\")]\n",
380
+ " res = verify(orig, opt, [{\"variables\": {}, \"memory\": {}}])\n",
381
+ " assert res[\"equivalent\"]\n",
382
+ " red = compute_reduction(orig, opt, {\"variables\": {}, \"memory\": {}})\n",
383
+ " assert red[\"absolute_savings\"] > 0\n",
384
+ "\n",
385
+ "\n",
386
+ "def test_dead_code_invisible():\n",
387
+ " orig = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 3),\n",
388
+ " I(\"MUL\", \"dead\", \"x\", 99), I(\"STORE\", \"addr\", \"x\")]\n",
389
+ " opt = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 3), I(\"STORE\", \"addr\", \"x\")]\n",
390
+ " res = verify(orig, opt, [{\"variables\": {}, \"memory\": {}}])\n",
391
+ " assert res[\"equivalent\"]\n",
392
+ "\n",
393
+ "\n",
394
+ "def test_fold_then_dce():\n",
395
+ " orig = [I(\"CONST\", \"a\", 2), I(\"CONST\", \"b\", 3), I(\"ADD\", \"c\", \"a\", \"b\"),\n",
396
+ " I(\"MUL\", \"d\", \"c\", 10), I(\"CONST\", \"addr\", 0), I(\"STORE\", \"addr\", \"d\")]\n",
397
+ " opt = [I(\"CONST\", \"d\", 50), I(\"CONST\", \"addr\", 0), I(\"STORE\", \"addr\", \"d\")]\n",
398
+ " res = verify(orig, opt, [{\"variables\": {}, \"memory\": {}}])\n",
399
+ " assert res[\"equivalent\"]\n",
400
+ " red = compute_reduction(orig, opt, {\"variables\": {}, \"memory\": {}})\n",
401
+ " assert red[\"original_cycles\"] > red[\"optimized_cycles\"]\n",
402
+ "\n",
403
+ "\n",
404
+ "def test_strength_reduction():\n",
405
+ " orig = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 7),\n",
406
+ " I(\"MUL\", \"y\", \"x\", 2), I(\"STORE\", \"addr\", \"y\")]\n",
407
+ " opt = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 7),\n",
408
+ " I(\"ADD\", \"y\", \"x\", \"x\"), I(\"STORE\", \"addr\", \"y\")]\n",
409
+ " res = verify(orig, opt, [{\"variables\": {}, \"memory\": {}}])\n",
410
+ " assert res[\"equivalent\"]\n",
411
+ " red = compute_reduction(orig, opt, {\"variables\": {}, \"memory\": {}})\n",
412
+ " assert red[\"absolute_savings\"] > 0\n",
413
+ "\n",
414
+ "\n",
415
+ "def test_already_optimal():\n",
416
+ " p = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 1), I(\"STORE\", \"addr\", \"x\")]\n",
417
+ " red = compute_reduction(p, p, {\"variables\": {}, \"memory\": {}})\n",
418
+ " assert red[\"absolute_savings\"] == 0\n",
419
+ " assert red[\"relative_savings\"] == 0.0\n",
420
+ "\n",
421
+ "\n",
422
+ "def test_div_by_zero():\n",
423
+ " p = [I(\"CONST\", \"a\", 10), I(\"CONST\", \"b\", 0), I(\"DIV\", \"c\", \"a\", \"b\")]\n",
424
+ " r = execute(p, {\"variables\": {}, \"memory\": {}})\n",
425
+ " assert r[\"success\"] is False\n",
426
+ " assert \"zero\" in r[\"error\"].lower()\n",
427
+ "\n",
428
+ "\n",
429
+ "def test_hardcoded_exploit_caught():\n",
430
+ " orig = [I(\"CONST\", \"addr\", 0), I(\"LOAD\", \"x\", \"in_addr\"),\n",
431
+ " I(\"MUL\", \"y\", \"x\", 3), I(\"STORE\", \"addr\", \"y\")]\n",
432
+ " opt = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"y\", 42), I(\"STORE\", \"addr\", \"y\")]\n",
433
+ " rng = random.Random(0)\n",
434
+ " states = [{\"variables\": {\"in_addr\": 1}, \"memory\": {1: rng.randint(0, 100)}} for _ in range(5)]\n",
435
+ " res = verify(orig, opt, states)\n",
436
+ " assert res[\"equivalent\"] is False\n",
437
+ " assert any(not r[\"match\"] for r in res[\"results\"])\n",
438
+ "\n",
439
+ "\n",
440
+ "def test_nop_zero_cost():\n",
441
+ " p = [I(\"NOP\"), I(\"CONST\", \"x\", 1), I(\"NOP\")]\n",
442
+ " assert count_cycles(p) == 1\n",
443
+ " r = execute(p, {\"variables\": {}, \"memory\": {}})\n",
444
+ " assert r[\"success\"] and r[\"cycles\"] == 1\n",
445
+ "\n",
446
+ "\n",
447
+ "def test_load_store_non_int_address():\n",
448
+ " p = [I(\"COPY\", \"addr\", \"bad\"), I(\"LOAD\", \"x\", \"addr\")]\n",
449
+ " r = execute(p, {\"variables\": {\"bad\": \"notint\"}, \"memory\": {}})\n",
450
+ " assert r[\"success\"] is False\n",
451
+ "\n",
452
+ "\n",
453
+ "def test_validate_ir_malformed():\n",
454
+ " assert not validate_ir([I(\"FOOBAR\", \"t\", 1, 2)])[\"valid\"]\n",
455
+ " assert not validate_ir([{\"op\": \"ADD\", \"dest\": \"x\"}])[\"valid\"]\n",
456
+ " assert not validate_ir([I(\"CONST\", 123, 1)])[\"valid\"]\n",
457
+ " assert not validate_ir([I(\"STORE\", None, \"x\")])[\"valid\"]\n",
458
+ " assert not validate_ir([I(\"LOAD\", \"x\", 42)])[\"valid\"]\n",
459
+ " assert not validate_ir([I(\"NOP\", \"x\", 1)])[\"valid\"]\n",
460
+ " assert not validate_ir([I(\"ADD\", \"x\", \"a\", None)])[\"valid\"]\n",
461
+ " assert not validate_ir([I(\"LOAD\", \"x\", \"addr\", 9)])[\"valid\"]\n",
462
+ " assert not validate_ir([I(\"STORE\", \"addr\", \"x\", 9)])[\"valid\"]\n",
463
+ " assert validate_ir([I(\"CONST\", \"x\", 1)])[\"valid\"]\n",
464
+ "\n",
465
+ "\n",
466
+ "def test_dump_ir_format():\n",
467
+ " p = [I(\"CONST\", \"t0\", 5), I(\"ADD\", \"t1\", \"t0\", \"a\"),\n",
468
+ " I(\"MUL\", \"t2\", \"t1\", 3), I(\"STORE\", \"addr0\", \"t2\")]\n",
469
+ " s = dump_ir(p)\n",
470
+ " lines = s.split(\"\\n\")\n",
471
+ " assert lines[0] == \"// OBSERVABLE OUT: mem[addr0]\"\n",
472
+ " assert lines[1] == \"t0 = CONST 5\"\n",
473
+ " assert lines[2] == \"t1 = ADD t0 a\"\n",
474
+ " assert lines[3] == \"t2 = MUL t1 3\"\n",
475
+ " assert lines[4] == \"STORE [addr0] t2\"\n",
476
+ " assert len(lines) == 5\n",
477
+ "\n",
478
+ "\n",
479
+ "def test_dump_ir_multi_store_header_order():\n",
480
+ " p = [I(\"CONST\", \"a\", 0), I(\"CONST\", \"b\", 1),\n",
481
+ " I(\"STORE\", \"a\", 5), I(\"STORE\", \"b\", 6), I(\"STORE\", \"a\", 7)]\n",
482
+ " s = dump_ir(p)\n",
483
+ " assert s.split(\"\\n\")[0] == \"// OBSERVABLE OUT: mem[a], mem[b]\"\n",
484
+ "\n",
485
+ "\n",
486
+ "def test_clean_nops():\n",
487
+ " p = [I(\"NOP\"), I(\"CONST\", \"x\", 1), I(\"NOP\"), I(\"CONST\", \"y\", 2), I(\"NOP\")]\n",
488
+ " out = clean_nops(p)\n",
489
+ " assert len(out) == 2\n",
490
+ " assert all(ins[\"op\"] != \"NOP\" for ins in out)\n",
491
+ "\n",
492
+ "\n",
493
+ "def test_verify_mismatches():\n",
494
+ " orig = [I(\"CONST\", \"a0\", 0), I(\"CONST\", \"a1\", 1),\n",
495
+ " I(\"CONST\", \"x\", 5), I(\"CONST\", \"y\", 9),\n",
496
+ " I(\"STORE\", \"a0\", \"x\"), I(\"STORE\", \"a1\", \"y\")]\n",
497
+ " opt = [I(\"CONST\", \"a0\", 0), I(\"CONST\", \"a1\", 1),\n",
498
+ " I(\"CONST\", \"x\", 5), I(\"CONST\", \"y\", 8),\n",
499
+ " I(\"STORE\", \"a0\", \"x\"), I(\"STORE\", \"a1\", \"y\")]\n",
500
+ " res = verify(orig, opt, [{\"variables\": {}, \"memory\": {}}])\n",
501
+ " assert res[\"equivalent\"] is False\n",
502
+ " assert res[\"results\"][0][\"mismatches\"] == [1]\n",
503
+ "\n",
504
+ "\n",
505
+ "def test_profile_counts_and_ordering():\n",
506
+ " p = [I(\"CONST\", \"x\", 1), I(\"ADD\", \"y\", \"x\", 2), I(\"MUL\", \"z\", \"y\", 3)]\n",
507
+ " pr = profile(p)\n",
508
+ " assert pr[\"n_instructions\"] == 3\n",
509
+ " assert pr[\"n_const\"] == 1 and pr[\"n_add\"] == 1 and pr[\"n_mul\"] == 1\n",
510
+ " expected = [\"n_instructions\"] + [f\"n_{op.lower()}\" for op in OP_ORDER]\n",
511
+ " assert list(pr.keys()) == expected\n",
512
+ "\n",
513
+ "\n",
514
+ "def test_no_input_mutation():\n",
515
+ " p = [I(\"CONST\", \"x\", 1), I(\"CONST\", \"addr\", 0), I(\"STORE\", \"addr\", \"x\")]\n",
516
+ " st = {\"variables\": {}, \"memory\": {}}\n",
517
+ " snap = {\"variables\": dict(st[\"variables\"]), \"memory\": dict(st[\"memory\"])}\n",
518
+ " execute(p, st)\n",
519
+ " assert st == snap\n",
520
+ "\n",
521
+ "\n",
522
+ "def test_clean_nops_no_aliasing():\n",
523
+ " p = [I(\"CONST\", \"x\", 1), I(\"NOP\")]\n",
524
+ " out = clean_nops(p)\n",
525
+ " out[0][\"op\"] = \"MUL\"\n",
526
+ " assert p[0][\"op\"] == \"CONST\"\n",
527
+ "\n",
528
+ "\n",
529
+ "def test_verify_rejects_empty_states():\n",
530
+ " with pytest.raises(ValueError):\n",
531
+ " verify([I(\"NOP\")], [I(\"NOP\")], [])\n",
532
+ "\n",
533
+ "\n",
534
+ "def test_verify_surfaces_execution_errors():\n",
535
+ " good = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 1), I(\"STORE\", \"addr\", \"x\")]\n",
536
+ " bad = [I(\"LOAD\", \"x\", \"missing_addr\")]\n",
537
+ " res = verify(good, bad, [{\"variables\": {}, \"memory\": {}}])\n",
538
+ " assert res[\"equivalent\"] is False\n",
539
+ " assert res[\"results\"][0][\"mismatches\"] == [\"__execution_error__\"]\n",
540
+ " assert res[\"results\"][0][\"execution_errors\"][\"optimized_error\"] is not None\n",
541
+ " assert \"original_error\" not in res[\"results\"][0][\"execution_errors\"]\n",
542
+ "\n",
543
+ "\n",
544
+ "def test_execute_rejects_bad_initial_state_shape():\n",
545
+ " r = execute([I(\"CONST\", \"x\", 1)], \"not-a-dict\")\n",
546
+ " assert r[\"success\"] is False\n",
547
+ " assert \"initial_state\" in r[\"error\"]\n",
548
+ "\n",
549
+ "\n",
550
+ "def test_compute_reduction_failure_path():\n",
551
+ " orig = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 1), I(\"STORE\", \"addr\", \"x\")]\n",
552
+ " bad = [I(\"CONST\", \"a\", 1), I(\"CONST\", \"b\", 0), I(\"DIV\", \"c\", \"a\", \"b\")]\n",
553
+ " red = compute_reduction(orig, bad, {\"variables\": {}, \"memory\": {}})\n",
554
+ " assert red[\"success\"] is False\n",
555
+ " assert red[\"relative_savings\"] is None\n",
556
+ " assert red[\"absolute_savings\"] is None\n",
557
+ " assert \"zero\" in red[\"error\"].lower()\n",
558
+ "\n",
559
+ "\n",
560
+ "def test_relative_savings_clamped_on_slowdown():\n",
561
+ " fast = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 1), I(\"STORE\", \"addr\", \"x\")]\n",
562
+ " # slower variant: extra dead MULs balloon the cycle count\n",
563
+ " slow = [I(\"CONST\", \"addr\", 0), I(\"CONST\", \"x\", 1)]\n",
564
+ " slow += [I(\"MUL\", \"junk\", \"x\", 2)] * 50\n",
565
+ " slow += [I(\"STORE\", \"addr\", \"x\")]\n",
566
+ " red = compute_reduction(fast, slow, {\"variables\": {}, \"memory\": {}})\n",
567
+ " assert red[\"absolute_savings\"] < 0\n",
568
+ " assert red[\"relative_savings\"] == -1.0 # clamped\n"
569
+ ]
570
+ },
571
+ {
572
+ "cell_type": "code",
573
+ "execution_count": null,
574
+ "id": "e6b83243",
575
+ "metadata": {},
576
+ "outputs": [],
577
+ "source": [
578
+ "# ==== CELL 6: Push to Drive ====\n",
579
+ "import shutil, os\n",
580
+ "for f in os.listdir(LOCAL_DIR):\n",
581
+ " if f.endswith(\".py\"):\n",
582
+ " shutil.copy2(os.path.join(LOCAL_DIR, f), os.path.join(DRIVE_DIR, f))\n",
583
+ "print(\"Saved to Drive:\", sorted(os.listdir(DRIVE_DIR)))\n"
584
+ ]
585
+ },
586
+ {
587
+ "cell_type": "code",
588
+ "execution_count": null,
589
+ "id": "5675f9a0",
590
+ "metadata": {},
591
+ "outputs": [],
592
+ "source": [
593
+ "# ==== CELL 7: Run tests ====\n",
594
+ "import subprocess\n",
595
+ "res = subprocess.run(\n",
596
+ " [\"python\", \"-m\", \"pytest\", \"-v\", \"test_vm.py\"],\n",
597
+ " cwd=LOCAL_DIR, capture_output=True, text=True,\n",
598
+ ")\n",
599
+ "print(res.stdout)\n",
600
+ "print(res.stderr)\n",
601
+ "assert res.returncode == 0, \"Tests failed\"\n"
602
+ ]
603
+ },
604
+ {
605
+ "cell_type": "code",
606
+ "execution_count": null,
607
+ "id": "1a500bb3",
608
+ "metadata": {},
609
+ "outputs": [],
610
+ "source": [
611
+ "# ==== CELL 8: Smoke demo ====\n",
612
+ "import importlib, toy_vm, verifier, reward_utils\n",
613
+ "importlib.reload(toy_vm); importlib.reload(verifier); importlib.reload(reward_utils)\n",
614
+ "from toy_vm import dump_ir\n",
615
+ "from reward_utils import compute_reduction\n",
616
+ "from verifier import verify\n",
617
+ "\n",
618
+ "orig = [\n",
619
+ " {\"op\":\"CONST\",\"dest\":\"addr\",\"src1\":0,\"src2\":None},\n",
620
+ " {\"op\":\"CONST\",\"dest\":\"a\",\"src1\":2,\"src2\":None},\n",
621
+ " {\"op\":\"CONST\",\"dest\":\"b\",\"src1\":3,\"src2\":None},\n",
622
+ " {\"op\":\"ADD\",\"dest\":\"c\",\"src1\":\"a\",\"src2\":\"b\"},\n",
623
+ " {\"op\":\"MUL\",\"dest\":\"d\",\"src1\":\"c\",\"src2\":2},\n",
624
+ " {\"op\":\"MUL\",\"dest\":\"dead\",\"src1\":\"d\",\"src2\":99},\n",
625
+ " {\"op\":\"STORE\",\"dest\":\"addr\",\"src1\":\"d\",\"src2\":None},\n",
626
+ "]\n",
627
+ "opt = [\n",
628
+ " {\"op\":\"CONST\",\"dest\":\"addr\",\"src1\":0,\"src2\":None},\n",
629
+ " {\"op\":\"CONST\",\"dest\":\"d\",\"src1\":10,\"src2\":None},\n",
630
+ " {\"op\":\"STORE\",\"dest\":\"addr\",\"src1\":\"d\",\"src2\":None},\n",
631
+ "]\n",
632
+ "\n",
633
+ "print(dump_ir(orig)); print(\"---\"); print(dump_ir(opt)); print(\"---\")\n",
634
+ "print(compute_reduction(orig, opt, {\"variables\":{},\"memory\":{}}))\n",
635
+ "print(\"equivalent:\", verify(orig, opt, [{\"variables\":{},\"memory\":{}}])[\"equivalent\"])\n"
636
+ ]
637
+ }
638
+ ],
639
+ "metadata": {
640
+ "kernelspec": {
641
+ "display_name": "Python 3",
642
+ "language": "python",
643
+ "name": "python3"
644
+ },
645
+ "language_info": {
646
+ "name": "python"
647
+ }
648
+ },
649
+ "nbformat": 4,
650
+ "nbformat_minor": 5
651
+ }
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/metahack1 (1).ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/.DS_Store ADDED
Binary file (6.15 kB). View file
 
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/metahack1 (1).ipynb ADDED
@@ -0,0 +1,1379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 0,
4
+ "metadata": {
5
+ "colab": {
6
+ "provenance": [],
7
+ "gpuType": "T4"
8
+ },
9
+ "kernelspec": {
10
+ "name": "python3",
11
+ "display_name": "Python 3"
12
+ },
13
+ "language_info": {
14
+ "name": "python"
15
+ },
16
+ "accelerator": "GPU"
17
+ },
18
+ "cells": [
19
+ {
20
+ "cell_type": "code",
21
+ "source": [
22
+ "## Design Assumptions (do not violate)\n",
23
+ "\n",
24
+ "# 1. **DCE never eliminates STOREs.** They define program output (final mem state).\n",
25
+ "# 2. **Addresses are distinct by construction.** Generator allocates each address variable to a unique integer; no aliasing.\n",
26
+ "# 3. **CF refuses to fold DIV by zero.** If src2 == 0 on a DIV op, leave instruction unchanged.\n",
27
+ "# 4. **Generator never emits DIV by literal zero.** When DIV is generated, src2 is always a non-zero constant or a variable known to be non-zero.\n",
28
+ "# # 5. **Integer arithmetic only.** No floats anywhere — avoids equivalence-check precision issues.\n",
29
+ "# 6. Generator declares `observable_addrs` per program — verifier compares only these mem entries.\n",
30
+ "# 7. State translator annotates observable outputs at top of dump.\n",
31
+ "# 8. Reward distinguishes broken (-1000) from valid-but-worse (small negative) — Harshal's formula.\n",
32
+ "# 9. Multi-input verification: 3-5 random initial states, all must match.\n",
33
+ "# 10. Integer division uses Python floor division (//). Aarush's VM must match."
34
+ ],
35
+ "metadata": {
36
+ "id": "_XI5jT2Ibvrf"
37
+ },
38
+ "execution_count": 2,
39
+ "outputs": []
40
+ },
41
+ {
42
+ "cell_type": "code",
43
+ "execution_count": 11,
44
+ "metadata": {
45
+ "id": "j6CQ327KWXwr"
46
+ },
47
+ "outputs": [],
48
+ "source": [
49
+ "# === TAC Schema v1.0 (LOCKED with Role 1 / Aarush) ===\n",
50
+ "# Reverse passes deferred to stretch goal — not in initial action space.\n",
51
+ "\n",
52
+ "OPS = [\"CONST\", \"ADD\", \"SUB\", \"MUL\", \"DIV\", \"LOAD\", \"STORE\", \"NOP\"]\n",
53
+ "\n",
54
+ "CYCLE_COSTS = {\n",
55
+ " \"CONST\": 1,\n",
56
+ " \"ADD\": 1,\n",
57
+ " \"SUB\": 1,\n",
58
+ " \"MUL\": 3,\n",
59
+ " \"DIV\": 5,\n",
60
+ " \"LOAD\": 4,\n",
61
+ " \"STORE\": 4,\n",
62
+ " \"NOP\": 0,\n",
63
+ "}\n",
64
+ "\n",
65
+ "# Instruction shape: {\"op\": str, \"dest\": str|None, \"src1\": Any, \"src2\": Any}\n",
66
+ "# Operands: str = variable name, int = literal constant, None = unused\n",
67
+ "#\n",
68
+ "# Op semantics:\n",
69
+ "# CONST: dest = src1 (src1 is int literal, src2 = None)\n",
70
+ "# ADD/SUB/MUL/DIV: dest = src1 OP src2 (src1, src2 are var names or int literals)\n",
71
+ "# LOAD: dest = mem[src1] (src1 is a var holding an address)\n",
72
+ "# STORE: mem[dest] = src1 (dest is a var holding an address)\n",
73
+ "# NOP: no-op (all fields None)\n",
74
+ "#\n",
75
+ "# Program output (for equivalence check) = final memory state (mem dict).\n",
76
+ "# Programs ship as: (initial_vars: dict, initial_mem: dict, instructions: list[dict])"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "code",
81
+ "source": [
82
+ "import random\n",
83
+ "\n",
84
+ "def generate_level_1():\n",
85
+ " \"\"\"\n",
86
+ " Generate a Level 1 Toy-IR program.\n",
87
+ "\n",
88
+ " Characteristics:\n",
89
+ " - 4-6 instructions\n",
90
+ " - 2-3 CONST ops with literal values\n",
91
+ " - 1-2 arithmetic ops on those constants (foldable by CF)\n",
92
+ " - 0-1 dead variables (killable by DCE)\n",
93
+ " - Exactly 1 STORE at the end so the program has an observable output\n",
94
+ "\n",
95
+ " Returns:\n",
96
+ " dict with keys: initial_vars, initial_mem, instructions, observable_addrs\n",
97
+ " \"\"\"\n",
98
+ " instructions = []\n",
99
+ " var_counter = 0\n",
100
+ "\n",
101
+ " def new_var():\n",
102
+ " nonlocal var_counter\n",
103
+ " name = f\"v{var_counter}\"\n",
104
+ " var_counter += 1\n",
105
+ " return name\n",
106
+ "\n",
107
+ " # Step 1: Generate 2-3 constant assignments\n",
108
+ " num_consts = random.randint(2, 3)\n",
109
+ " const_vars = []\n",
110
+ " for _ in range(num_consts):\n",
111
+ " var = new_var()\n",
112
+ " value = random.randint(1, 10)\n",
113
+ " instructions.append({\"op\": \"CONST\", \"dest\": var, \"src1\": value, \"src2\": None})\n",
114
+ " const_vars.append(var)\n",
115
+ "\n",
116
+ " # Step 2: Generate 1-2 arithmetic ops using those constants\n",
117
+ " num_arith = random.randint(1, 2)\n",
118
+ " last_result = None\n",
119
+ " for _ in range(num_arith):\n",
120
+ " op = random.choice([\"ADD\", \"MUL\"])\n",
121
+ " src1 = random.choice(const_vars)\n",
122
+ " src2 = random.choice(const_vars)\n",
123
+ " dest = new_var()\n",
124
+ " instructions.append({\"op\": op, \"dest\": dest, \"src1\": src1, \"src2\": src2})\n",
125
+ " last_result = dest\n",
126
+ " const_vars.append(dest)\n",
127
+ "\n",
128
+ " # Step 3: Optionally add 1 dead variable (50% chance)\n",
129
+ " if random.random() < 0.5:\n",
130
+ " dead_var = new_var()\n",
131
+ " dead_value = random.randint(1, 10)\n",
132
+ " instructions.append({\"op\": \"CONST\", \"dest\": dead_var, \"src1\": dead_value, \"src2\": None})\n",
133
+ " # Note: dead_var is intentionally never used — DCE should catch it.\n",
134
+ "\n",
135
+ " # Step 4: Add a STORE at the end so the program has observable output\n",
136
+ " initial_vars = {\"addr0\": 0}\n",
137
+ " instructions.append({\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": last_result, \"src2\": None})\n",
138
+ "\n",
139
+ " initial_mem = {}\n",
140
+ "\n",
141
+ " return {\n",
142
+ " \"initial_vars\": initial_vars,\n",
143
+ " \"initial_mem\": initial_mem,\n",
144
+ " \"instructions\": instructions,\n",
145
+ " \"observable_addrs\": [0], # Aarush's verifier compares only these mem entries\n",
146
+ " }\n",
147
+ "\n",
148
+ "\n",
149
+ "# Sanity-check: generate a few programs and print them\n",
150
+ "for seed in [42, 1, 7, 99]:\n",
151
+ " random.seed(seed)\n",
152
+ " prog = generate_level_1()\n",
153
+ " print(f\"\\n=== seed={seed} ===\")\n",
154
+ " print(f\"initial_vars : {prog['initial_vars']}\")\n",
155
+ " print(f\"initial_mem : {prog['initial_mem']}\")\n",
156
+ " print(f\"observable_addrs : {prog['observable_addrs']}\")\n",
157
+ " print(f\"instructions ({len(prog['instructions'])}):\")\n",
158
+ " for i, instr in enumerate(prog['instructions']):\n",
159
+ " print(f\" {i}: {instr}\")"
160
+ ],
161
+ "metadata": {
162
+ "colab": {
163
+ "base_uri": "https://localhost:8080/"
164
+ },
165
+ "id": "a86vD1OyWcB9",
166
+ "outputId": "2d6d4604-6dc1-4c29-af64-e41cf5717c0d"
167
+ },
168
+ "execution_count": 4,
169
+ "outputs": [
170
+ {
171
+ "output_type": "stream",
172
+ "name": "stdout",
173
+ "text": [
174
+ "\n",
175
+ "=== seed=42 ===\n",
176
+ "initial_vars : {'addr0': 0}\n",
177
+ "initial_mem : {}\n",
178
+ "observable_addrs : [0]\n",
179
+ "instructions (4):\n",
180
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
181
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
182
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v0', 'src2': 'v0'}\n",
183
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
184
+ "\n",
185
+ "=== seed=1 ===\n",
186
+ "initial_vars : {'addr0': 0}\n",
187
+ "initial_mem : {}\n",
188
+ "observable_addrs : [0]\n",
189
+ "instructions (5):\n",
190
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 10, 'src2': None}\n",
191
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 2, 'src2': None}\n",
192
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v1', 'src2': 'v1'}\n",
193
+ " 3: {'op': 'MUL', 'dest': 'v3', 'src1': 'v2', 'src2': 'v1'}\n",
194
+ " 4: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
195
+ "\n",
196
+ "=== seed=7 ===\n",
197
+ "initial_vars : {'addr0': 0}\n",
198
+ "initial_mem : {}\n",
199
+ "observable_addrs : [0]\n",
200
+ "instructions (6):\n",
201
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 3, 'src2': None}\n",
202
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 7, 'src2': None}\n",
203
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 1, 'src2': None}\n",
204
+ " 3: {'op': 'ADD', 'dest': 'v3', 'src1': 'v1', 'src2': 'v2'}\n",
205
+ " 4: {'op': 'CONST', 'dest': 'v4', 'src1': 9, 'src2': None}\n",
206
+ " 5: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
207
+ "\n",
208
+ "=== seed=99 ===\n",
209
+ "initial_vars : {'addr0': 0}\n",
210
+ "initial_mem : {}\n",
211
+ "observable_addrs : [0]\n",
212
+ "instructions (5):\n",
213
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 7, 'src2': None}\n",
214
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 4, 'src2': None}\n",
215
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 10, 'src2': None}\n",
216
+ " 3: {'op': 'ADD', 'dest': 'v3', 'src1': 'v0', 'src2': 'v0'}\n",
217
+ " 4: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n"
218
+ ]
219
+ }
220
+ ]
221
+ },
222
+ {
223
+ "cell_type": "code",
224
+ "source": [
225
+ "def _resolve_operand(operand, known_constants):\n",
226
+ " \"\"\"\n",
227
+ " Given a TAC operand (string variable name or int literal),\n",
228
+ " return its concrete integer value if known, or None if unknown.\n",
229
+ " \"\"\"\n",
230
+ " if isinstance(operand, int):\n",
231
+ " return operand\n",
232
+ " if isinstance(operand, str) and operand in known_constants:\n",
233
+ " return known_constants[operand]\n",
234
+ " return None\n",
235
+ "\n",
236
+ "\n",
237
+ "def _compute(op, v1, v2):\n",
238
+ " \"\"\"Compute the result of a binary arithmetic op on two known integers.\"\"\"\n",
239
+ " if op == \"ADD\": return v1 + v2\n",
240
+ " if op == \"SUB\": return v1 - v2\n",
241
+ " if op == \"MUL\": return v1 * v2\n",
242
+ " if op == \"DIV\": return v1 // v2 # floor division (locked semantics)\n",
243
+ " raise ValueError(f\"_compute called with non-arithmetic op: {op}\")\n",
244
+ "\n",
245
+ "\n",
246
+ "def constant_folding(program):\n",
247
+ " \"\"\"\n",
248
+ " Forward pass that folds constant arithmetic into CONST ops, and\n",
249
+ " propagates known constants into instruction operands.\n",
250
+ "\n",
251
+ " Behavior:\n",
252
+ " - If both sources of an arithmetic op resolve to known integers,\n",
253
+ " replaces the instruction with a CONST holding the computed result.\n",
254
+ " - If only one source is known, still substitutes that known value\n",
255
+ " into the instruction (constant propagation), enabling downstream\n",
256
+ " passes (e.g., peephole) to recognize patterns like ADD-with-0 or MUL-by-1.\n",
257
+ " - Refuses to fold DIV by zero (Design Assumption #3).\n",
258
+ " - Always returns fresh dicts; never aliases input instructions.\n",
259
+ "\n",
260
+ " Args:\n",
261
+ " program: list of TAC instruction dicts (per locked schema).\n",
262
+ "\n",
263
+ " Returns:\n",
264
+ " new list of TAC instruction dicts. Always semantics-preserving.\n",
265
+ " Never raises, never returns None.\n",
266
+ " \"\"\"\n",
267
+ " known_constants = {}\n",
268
+ " new_program = []\n",
269
+ "\n",
270
+ " for instr in program:\n",
271
+ " # Always work on a copy — never alias input dicts\n",
272
+ " instr = instr.copy()\n",
273
+ " op = instr[\"op\"]\n",
274
+ " dest = instr[\"dest\"]\n",
275
+ "\n",
276
+ " if op == \"CONST\":\n",
277
+ " known_constants[dest] = instr[\"src1\"]\n",
278
+ " new_program.append(instr)\n",
279
+ "\n",
280
+ " elif op in (\"ADD\", \"SUB\", \"MUL\", \"DIV\"):\n",
281
+ " # === Constant propagation: substitute known constants into operands ===\n",
282
+ " if isinstance(instr[\"src1\"], str) and instr[\"src1\"] in known_constants:\n",
283
+ " instr[\"src1\"] = known_constants[instr[\"src1\"]]\n",
284
+ " if isinstance(instr[\"src2\"], str) and instr[\"src2\"] in known_constants:\n",
285
+ " instr[\"src2\"] = known_constants[instr[\"src2\"]]\n",
286
+ "\n",
287
+ " # === Try to fold ===\n",
288
+ " v1 = _resolve_operand(instr[\"src1\"], known_constants)\n",
289
+ " v2 = _resolve_operand(instr[\"src2\"], known_constants)\n",
290
+ "\n",
291
+ " if v1 is not None and v2 is not None:\n",
292
+ " # Both operands are known integers\n",
293
+ " if op == \"DIV\" and v2 == 0:\n",
294
+ " # Refuse to fold DIV by zero\n",
295
+ " new_program.append(instr)\n",
296
+ " known_constants.pop(dest, None)\n",
297
+ " else:\n",
298
+ " # Fold: replace with CONST\n",
299
+ " result = _compute(op, v1, v2)\n",
300
+ " new_program.append({\n",
301
+ " \"op\": \"CONST\",\n",
302
+ " \"dest\": dest,\n",
303
+ " \"src1\": result,\n",
304
+ " \"src2\": None,\n",
305
+ " })\n",
306
+ " known_constants[dest] = result\n",
307
+ " else:\n",
308
+ " # Can't fold (at least one operand unknown).\n",
309
+ " # Instruction may still have been mutated by propagation above.\n",
310
+ " new_program.append(instr)\n",
311
+ " known_constants.pop(dest, None)\n",
312
+ "\n",
313
+ " elif op == \"LOAD\":\n",
314
+ " # Memory reads aren't statically resolvable\n",
315
+ " new_program.append(instr)\n",
316
+ " known_constants.pop(dest, None)\n",
317
+ "\n",
318
+ " elif op in (\"STORE\", \"NOP\"):\n",
319
+ " # No dest tracking needed.\n",
320
+ " # Note: we COULD propagate src1 into a STORE for downstream readability,\n",
321
+ " # but the cycle cost is unchanged and the executor handles vars fine.\n",
322
+ " # Leave STORE alone — keeps the code minimal.\n",
323
+ " new_program.append(instr)\n",
324
+ "\n",
325
+ " else:\n",
326
+ " # Unknown op — defensive pass-through\n",
327
+ " new_program.append(instr)\n",
328
+ "\n",
329
+ " return new_program"
330
+ ],
331
+ "metadata": {
332
+ "id": "CGk8Fz4CZA3t"
333
+ },
334
+ "execution_count": 5,
335
+ "outputs": []
336
+ },
337
+ {
338
+ "cell_type": "code",
339
+ "source": [
340
+ "# === Sanity tests for constant_folding ===\n",
341
+ "\n",
342
+ "# Test 1: simple fold — ADD of two CONSTs\n",
343
+ "test1 = [\n",
344
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 3, \"src2\": None},\n",
345
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 5, \"src2\": None},\n",
346
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"},\n",
347
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
348
+ "]\n",
349
+ "result1 = constant_folding(test1)\n",
350
+ "print(\"Test 1 (simple ADD fold):\")\n",
351
+ "for i, instr in enumerate(result1): print(f\" {i}: {instr}\")\n",
352
+ "# Expected: instruction 2 becomes CONST c = 8\n",
353
+ "\n",
354
+ "# Test 2: chained fold — second op uses first op's folded result\n",
355
+ "test2 = [\n",
356
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 3, \"src2\": None},\n",
357
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 5, \"src2\": None},\n",
358
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # → 8\n",
359
+ " {\"op\": \"MUL\", \"dest\": \"d\", \"src1\": \"c\", \"src2\": \"b\"}, # → 8 * 5 = 40\n",
360
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"d\", \"src2\": None},\n",
361
+ "]\n",
362
+ "result2 = constant_folding(test2)\n",
363
+ "print(\"\\nTest 2 (chained fold):\")\n",
364
+ "for i, instr in enumerate(result2): print(f\" {i}: {instr}\")\n",
365
+ "# Expected: c becomes CONST 8, d becomes CONST 40\n",
366
+ "\n",
367
+ "# Test 3: DIV by zero refused\n",
368
+ "test3 = [\n",
369
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 10, \"src2\": None},\n",
370
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 0, \"src2\": None},\n",
371
+ " {\"op\": \"DIV\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # 10/0 — must NOT fold\n",
372
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
373
+ "]\n",
374
+ "result3 = constant_folding(test3)\n",
375
+ "print(\"\\nTest 3 (DIV by zero refused):\")\n",
376
+ "for i, instr in enumerate(result3): print(f\" {i}: {instr}\")\n",
377
+ "# Expected: instruction 2 unchanged (still DIV, not CONST)\n",
378
+ "\n",
379
+ "# Test 4: unknown source can't fold\n",
380
+ "test4 = [\n",
381
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 3, \"src2\": None},\n",
382
+ " {\"op\": \"LOAD\", \"dest\": \"b\", \"src1\": \"addr0\", \"src2\": None}, # b is unknown\n",
383
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # can't fold\n",
384
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
385
+ "]\n",
386
+ "result4 = constant_folding(test4)\n",
387
+ "print(\"\\nTest 4 (LOAD makes b unknown, ADD not folded):\")\n",
388
+ "for i, instr in enumerate(result4): print(f\" {i}: {instr}\")\n",
389
+ "# Expected: instruction 2 unchanged (still ADD)\n",
390
+ "\n",
391
+ "# Test 5: idempotence — running CF on a generated program\n",
392
+ "random.seed(42)\n",
393
+ "prog = generate_level_1()\n",
394
+ "folded = constant_folding(prog[\"instructions\"])\n",
395
+ "print(\"\\nTest 5 (CF on generated Level 1 program, seed=42):\")\n",
396
+ "print(\"Before:\")\n",
397
+ "for i, instr in enumerate(prog[\"instructions\"]): print(f\" {i}: {instr}\")\n",
398
+ "print(\"After:\")\n",
399
+ "for i, instr in enumerate(folded): print(f\" {i}: {instr}\")\n",
400
+ "\n",
401
+ "# Test 6: constant propagation — only one source is known\n",
402
+ "test6 = [\n",
403
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 0, \"src2\": None},\n",
404
+ " {\"op\": \"LOAD\", \"dest\": \"b\", \"src1\": \"addr0\", \"src2\": None}, # b unknown\n",
405
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # a known (=0), b unknown\n",
406
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
407
+ "]\n",
408
+ "result6 = constant_folding(test6)\n",
409
+ "print(\"\\nTest 6 (propagation: a=0 substituted into ADD even though b unknown):\")\n",
410
+ "for i, instr in enumerate(result6): print(f\" {i}: {instr}\")\n",
411
+ "# Expected: instruction 2 is still ADD (can't fold — b unknown), but src1 is now literal 0, not 'a'\n",
412
+ "# This sets up peephole to recognize \"ADD with 0\" later"
413
+ ],
414
+ "metadata": {
415
+ "colab": {
416
+ "base_uri": "https://localhost:8080/"
417
+ },
418
+ "id": "WLKSTVa8fRF8",
419
+ "outputId": "e9480e1e-5d59-4afe-a884-bae0fa4a2192"
420
+ },
421
+ "execution_count": 6,
422
+ "outputs": [
423
+ {
424
+ "output_type": "stream",
425
+ "name": "stdout",
426
+ "text": [
427
+ "Test 1 (simple ADD fold):\n",
428
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 3, 'src2': None}\n",
429
+ " 1: {'op': 'CONST', 'dest': 'b', 'src1': 5, 'src2': None}\n",
430
+ " 2: {'op': 'CONST', 'dest': 'c', 'src1': 8, 'src2': None}\n",
431
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
432
+ "\n",
433
+ "Test 2 (chained fold):\n",
434
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 3, 'src2': None}\n",
435
+ " 1: {'op': 'CONST', 'dest': 'b', 'src1': 5, 'src2': None}\n",
436
+ " 2: {'op': 'CONST', 'dest': 'c', 'src1': 8, 'src2': None}\n",
437
+ " 3: {'op': 'CONST', 'dest': 'd', 'src1': 40, 'src2': None}\n",
438
+ " 4: {'op': 'STORE', 'dest': 'addr0', 'src1': 'd', 'src2': None}\n",
439
+ "\n",
440
+ "Test 3 (DIV by zero refused):\n",
441
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 10, 'src2': None}\n",
442
+ " 1: {'op': 'CONST', 'dest': 'b', 'src1': 0, 'src2': None}\n",
443
+ " 2: {'op': 'DIV', 'dest': 'c', 'src1': 10, 'src2': 0}\n",
444
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
445
+ "\n",
446
+ "Test 4 (LOAD makes b unknown, ADD not folded):\n",
447
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 3, 'src2': None}\n",
448
+ " 1: {'op': 'LOAD', 'dest': 'b', 'src1': 'addr0', 'src2': None}\n",
449
+ " 2: {'op': 'ADD', 'dest': 'c', 'src1': 3, 'src2': 'b'}\n",
450
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
451
+ "\n",
452
+ "Test 5 (CF on generated Level 1 program, seed=42):\n",
453
+ "Before:\n",
454
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
455
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
456
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v0', 'src2': 'v0'}\n",
457
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
458
+ "After:\n",
459
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
460
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
461
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 2, 'src2': None}\n",
462
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
463
+ "\n",
464
+ "Test 6 (propagation: a=0 substituted into ADD even though b unknown):\n",
465
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 0, 'src2': None}\n",
466
+ " 1: {'op': 'LOAD', 'dest': 'b', 'src1': 'addr0', 'src2': None}\n",
467
+ " 2: {'op': 'ADD', 'dest': 'c', 'src1': 0, 'src2': 'b'}\n",
468
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n"
469
+ ]
470
+ }
471
+ ]
472
+ },
473
+ {
474
+ "cell_type": "code",
475
+ "source": [
476
+ "def dead_code_elimination(program):\n",
477
+ " \"\"\"\n",
478
+ " Backward liveness analysis. Replace dead instructions with NOPs,\n",
479
+ " then strip NOPs.\n",
480
+ "\n",
481
+ " Rules:\n",
482
+ " - STOREs are always live (define program output).\n",
483
+ " - Address variables used in STORE/LOAD are always live.\n",
484
+ " - Any instruction whose dest is never read later is dead.\n",
485
+ "\n",
486
+ " Returns a fresh list of instruction dicts. Never raises, never returns None.\n",
487
+ " \"\"\"\n",
488
+ " # Walk backward, build new program in reverse, then re-reverse at the end\n",
489
+ " live = set()\n",
490
+ " new_program_reversed = []\n",
491
+ "\n",
492
+ " for instr in reversed(program):\n",
493
+ " instr = instr.copy() # never alias input\n",
494
+ " op = instr[\"op\"]\n",
495
+ " dest = instr[\"dest\"]\n",
496
+ " src1 = instr[\"src1\"]\n",
497
+ " src2 = instr[\"src2\"]\n",
498
+ "\n",
499
+ " if op == \"STORE\":\n",
500
+ " # STOREs are always kept; mark sources live\n",
501
+ " if isinstance(src1, str): live.add(src1)\n",
502
+ " if isinstance(dest, str): live.add(dest) # address variable\n",
503
+ " new_program_reversed.append(instr)\n",
504
+ "\n",
505
+ " elif op == \"NOP\":\n",
506
+ " # Pass through; will be stripped at the end\n",
507
+ " new_program_reversed.append(instr)\n",
508
+ "\n",
509
+ " elif op in (\"CONST\", \"ADD\", \"SUB\", \"MUL\", \"DIV\", \"LOAD\"):\n",
510
+ " if dest in live:\n",
511
+ " # Live instruction — keep it, mark its sources live\n",
512
+ " live.discard(dest)\n",
513
+ " if isinstance(src1, str): live.add(src1)\n",
514
+ " if isinstance(src2, str): live.add(src2)\n",
515
+ " new_program_reversed.append(instr)\n",
516
+ " else:\n",
517
+ " # Dead — replace with NOP\n",
518
+ " new_program_reversed.append({\n",
519
+ " \"op\": \"NOP\", \"dest\": None, \"src1\": None, \"src2\": None,\n",
520
+ " })\n",
521
+ "\n",
522
+ " else:\n",
523
+ " # Unknown op — defensive pass-through\n",
524
+ " new_program_reversed.append(instr)\n",
525
+ "\n",
526
+ " # Reverse back to forward order, then strip NOPs\n",
527
+ " new_program = list(reversed(new_program_reversed))\n",
528
+ " new_program = [instr for instr in new_program if instr[\"op\"] != \"NOP\"]\n",
529
+ "\n",
530
+ " return new_program"
531
+ ],
532
+ "metadata": {
533
+ "id": "5vo-KgaffTFR"
534
+ },
535
+ "execution_count": 12,
536
+ "outputs": []
537
+ },
538
+ {
539
+ "cell_type": "code",
540
+ "source": [
541
+ "# === Sanity tests for dead_code_elimination ===\n",
542
+ "\n",
543
+ "# Test 1: simple dead variable\n",
544
+ "test1 = [\n",
545
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 5, \"src2\": None},\n",
546
+ " {\"op\": \"CONST\", \"dest\": \"dead\", \"src1\": 99, \"src2\": None}, # never used\n",
547
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"a\", \"src2\": None},\n",
548
+ "]\n",
549
+ "result1 = dead_code_elimination(test1)\n",
550
+ "print(\"Test 1 (kill unused CONST):\")\n",
551
+ "for i, instr in enumerate(result1): print(f\" {i}: {instr}\")\n",
552
+ "# Expected: 'dead' instruction removed, 2 instructions remain\n",
553
+ "\n",
554
+ "# Test 2: chain of dead computation\n",
555
+ "test2 = [\n",
556
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 5, \"src2\": None},\n",
557
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 3, \"src2\": None},\n",
558
+ " {\"op\": \"ADD\", \"dest\": \"x\", \"src1\": \"a\", \"src2\": \"b\"}, # x never used → dead\n",
559
+ " {\"op\": \"CONST\", \"dest\": \"c\", \"src1\": 7, \"src2\": None},\n",
560
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
561
+ "]\n",
562
+ "result2 = dead_code_elimination(test2)\n",
563
+ "print(\"\\nTest 2 (kill unused ADD and its feeders... but only if feeders are also unused):\")\n",
564
+ "for i, instr in enumerate(result2): print(f\" {i}: {instr}\")\n",
565
+ "# Expected: x's ADD killed. a and b also killed (only fed into x, which is dead).\n",
566
+ "# Final: just CONST c=7, STORE.\n",
567
+ "\n",
568
+ "# Test 3: STORE always preserved\n",
569
+ "test3 = [\n",
570
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 5, \"src2\": None},\n",
571
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"a\", \"src2\": None},\n",
572
+ "]\n",
573
+ "result3 = dead_code_elimination(test3)\n",
574
+ "print(\"\\nTest 3 (STORE preserved, feeder kept live):\")\n",
575
+ "for i, instr in enumerate(result3): print(f\" {i}: {instr}\")\n",
576
+ "# Expected: both instructions unchanged\n",
577
+ "\n",
578
+ "# Test 4: variable used by STORE is live, even if defined far above\n",
579
+ "test4 = [\n",
580
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 5, \"src2\": None}, # used by STORE → live\n",
581
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 9, \"src2\": None}, # never used → dead\n",
582
+ " {\"op\": \"CONST\", \"dest\": \"c\", \"src1\": 1, \"src2\": None}, # never used → dead\n",
583
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"a\", \"src2\": None},\n",
584
+ "]\n",
585
+ "result4 = dead_code_elimination(test4)\n",
586
+ "print(\"\\nTest 4 (only 'a' is live, b/c killed):\")\n",
587
+ "for i, instr in enumerate(result4): print(f\" {i}: {instr}\")\n",
588
+ "# Expected: a's CONST + STORE remain. b, c stripped.\n",
589
+ "\n",
590
+ "# Test 5: combine CF + DCE on a generated program\n",
591
+ "random.seed(42)\n",
592
+ "prog = generate_level_1()\n",
593
+ "print(\"\\nTest 5 (CF then DCE on seed=42):\")\n",
594
+ "print(\"Original:\")\n",
595
+ "for i, instr in enumerate(prog[\"instructions\"]): print(f\" {i}: {instr}\")\n",
596
+ "\n",
597
+ "after_cf = constant_folding(prog[\"instructions\"])\n",
598
+ "print(\"After CF:\")\n",
599
+ "for i, instr in enumerate(after_cf): print(f\" {i}: {instr}\")\n",
600
+ "\n",
601
+ "after_dce = dead_code_elimination(after_cf)\n",
602
+ "print(\"After CF + DCE:\")\n",
603
+ "for i, instr in enumerate(after_dce): print(f\" {i}: {instr}\")\n",
604
+ "# Expected: original 4 instructions become much shorter — dead 'v1' eliminated, v2 folded\n",
605
+ "\n",
606
+ "# Test 6: idempotence — running DCE twice gives same result\n",
607
+ "test6 = [\n",
608
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 5, \"src2\": None},\n",
609
+ " {\"op\": \"CONST\", \"dest\": \"dead\", \"src1\": 99, \"src2\": None},\n",
610
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"a\", \"src2\": None},\n",
611
+ "]\n",
612
+ "once = dead_code_elimination(test6)\n",
613
+ "twice = dead_code_elimination(once)\n",
614
+ "print(\"\\nTest 6 (idempotence):\", \"PASS\" if once == twice else \"FAIL\")"
615
+ ],
616
+ "metadata": {
617
+ "colab": {
618
+ "base_uri": "https://localhost:8080/"
619
+ },
620
+ "id": "vv8QRFRlvzdq",
621
+ "outputId": "e1d35874-ff1a-4dec-f43f-e22867dd3911"
622
+ },
623
+ "execution_count": 8,
624
+ "outputs": [
625
+ {
626
+ "output_type": "stream",
627
+ "name": "stdout",
628
+ "text": [
629
+ "Test 1 (kill unused CONST):\n",
630
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 5, 'src2': None}\n",
631
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'a', 'src2': None}\n",
632
+ "\n",
633
+ "Test 2 (kill unused ADD and its feeders... but only if feeders are also unused):\n",
634
+ " 0: {'op': 'CONST', 'dest': 'c', 'src1': 7, 'src2': None}\n",
635
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
636
+ "\n",
637
+ "Test 3 (STORE preserved, feeder kept live):\n",
638
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 5, 'src2': None}\n",
639
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'a', 'src2': None}\n",
640
+ "\n",
641
+ "Test 4 (only 'a' is live, b/c killed):\n",
642
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 5, 'src2': None}\n",
643
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'a', 'src2': None}\n",
644
+ "\n",
645
+ "Test 5 (CF then DCE on seed=42):\n",
646
+ "Original:\n",
647
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
648
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
649
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v0', 'src2': 'v0'}\n",
650
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
651
+ "After CF:\n",
652
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
653
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
654
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 2, 'src2': None}\n",
655
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
656
+ "After CF + DCE:\n",
657
+ " 0: {'op': 'CONST', 'dest': 'v2', 'src1': 2, 'src2': None}\n",
658
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
659
+ "\n",
660
+ "Test 6 (idempotence): PASS\n"
661
+ ]
662
+ }
663
+ ]
664
+ },
665
+ {
666
+ "cell_type": "code",
667
+ "source": [
668
+ "def peephole_optimization(program):\n",
669
+ " \"\"\"\n",
670
+ " Single-instruction peephole rewrites. Replaces expensive ops with\n",
671
+ " cheaper equivalents when operands hit special values (0, 1, 2, self).\n",
672
+ "\n",
673
+ " Patterns (all preserve semantics):\n",
674
+ " MUL by 2 -> ADD x + x\n",
675
+ " MUL by 1 -> CONST (the other operand)\n",
676
+ " MUL by 0 -> CONST 0\n",
677
+ " ADD with 0 -> CONST (the other operand)\n",
678
+ " SUB x - x -> CONST 0\n",
679
+ " DIV by 1 -> CONST (the dividend)\n",
680
+ "\n",
681
+ " Notes:\n",
682
+ " - Operates on individual instructions; no cross-instruction state.\n",
683
+ " - Relies on constant_folding having propagated literal values into operands.\n",
684
+ " - Returns fresh instruction dicts; never aliases inputs.\n",
685
+ "\n",
686
+ " Returns a new list. Never raises, never returns None.\n",
687
+ " \"\"\"\n",
688
+ " new_program = []\n",
689
+ "\n",
690
+ " for instr in program:\n",
691
+ " instr = instr.copy()\n",
692
+ " op = instr[\"op\"]\n",
693
+ " dest = instr[\"dest\"]\n",
694
+ " src1 = instr[\"src1\"]\n",
695
+ " src2 = instr[\"src2\"]\n",
696
+ "\n",
697
+ " # === MUL patterns ===\n",
698
+ " if op == \"MUL\":\n",
699
+ " # MUL by 0 -> CONST 0\n",
700
+ " if src1 == 0 or src2 == 0:\n",
701
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": 0, \"src2\": None})\n",
702
+ " continue\n",
703
+ " # MUL by 1 -> CONST (other operand) if the other operand is a literal,\n",
704
+ " # otherwise leave alone (we don't want to introduce a useless\n",
705
+ " # \"CONST dest = some_var_name\" — that's not a valid CONST).\n",
706
+ " if src1 == 1 and isinstance(src2, int):\n",
707
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": src2, \"src2\": None})\n",
708
+ " continue\n",
709
+ " if src2 == 1 and isinstance(src1, int):\n",
710
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": src1, \"src2\": None})\n",
711
+ " continue\n",
712
+ " # MUL by 2 -> ADD x + x (when one operand is literal 2 and the other is a var)\n",
713
+ " if src1 == 2 and isinstance(src2, str):\n",
714
+ " new_program.append({\"op\": \"ADD\", \"dest\": dest, \"src1\": src2, \"src2\": src2})\n",
715
+ " continue\n",
716
+ " if src2 == 2 and isinstance(src1, str):\n",
717
+ " new_program.append({\"op\": \"ADD\", \"dest\": dest, \"src1\": src1, \"src2\": src1})\n",
718
+ " continue\n",
719
+ " # No pattern matched — keep as is\n",
720
+ " new_program.append(instr)\n",
721
+ "\n",
722
+ " # === ADD patterns ===\n",
723
+ " elif op == \"ADD\":\n",
724
+ " # ADD with 0 -> CONST (other operand) if the other operand is a literal\n",
725
+ " if src1 == 0 and isinstance(src2, int):\n",
726
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": src2, \"src2\": None})\n",
727
+ " continue\n",
728
+ " if src2 == 0 and isinstance(src1, int):\n",
729
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": src1, \"src2\": None})\n",
730
+ " continue\n",
731
+ " # If \"ADD x + 0\" where x is a variable, we'd want a copy — but our schema\n",
732
+ " # has no MOV/COPY op. Leave alone; CF + DCE handle the rest in practice.\n",
733
+ " new_program.append(instr)\n",
734
+ "\n",
735
+ " # === SUB patterns ===\n",
736
+ " elif op == \"SUB\":\n",
737
+ " # SUB x - x -> CONST 0 (only if both sources are the same string variable)\n",
738
+ " if isinstance(src1, str) and isinstance(src2, str) and src1 == src2:\n",
739
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": 0, \"src2\": None})\n",
740
+ " continue\n",
741
+ " # SUB x - 0 with literal 0 on src2 — we'd want a copy; skip (no COPY op)\n",
742
+ " new_program.append(instr)\n",
743
+ "\n",
744
+ " # === DIV patterns ===\n",
745
+ " elif op == \"DIV\":\n",
746
+ " # DIV by 1 -> CONST (dividend) if dividend is a literal\n",
747
+ " if src2 == 1 and isinstance(src1, int):\n",
748
+ " new_program.append({\"op\": \"CONST\", \"dest\": dest, \"src1\": src1, \"src2\": None})\n",
749
+ " continue\n",
750
+ " new_program.append(instr)\n",
751
+ "\n",
752
+ " else:\n",
753
+ " # CONST, LOAD, STORE, NOP — pass through\n",
754
+ " new_program.append(instr)\n",
755
+ "\n",
756
+ " return new_program"
757
+ ],
758
+ "metadata": {
759
+ "id": "IaczDNeLv5PW"
760
+ },
761
+ "execution_count": 9,
762
+ "outputs": []
763
+ },
764
+ {
765
+ "cell_type": "code",
766
+ "source": [
767
+ "# === Sanity tests for peephole_optimization ===\n",
768
+ "\n",
769
+ "# Test 1: MUL by 2 -> ADD x+x\n",
770
+ "test1 = [\n",
771
+ " {\"op\": \"LOAD\", \"dest\": \"y\", \"src1\": \"addr0\", \"src2\": None},\n",
772
+ " {\"op\": \"MUL\", \"dest\": \"x\", \"src1\": \"y\", \"src2\": 2},\n",
773
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
774
+ "]\n",
775
+ "result1 = peephole_optimization(test1)\n",
776
+ "print(\"Test 1 (MUL by 2 -> ADD self):\")\n",
777
+ "for i, instr in enumerate(result1): print(f\" {i}: {instr}\")\n",
778
+ "# Expected: instruction 1 becomes ADD y+y\n",
779
+ "\n",
780
+ "# Test 2: MUL by 0 -> CONST 0\n",
781
+ "test2 = [\n",
782
+ " {\"op\": \"LOAD\", \"dest\": \"y\", \"src1\": \"addr0\", \"src2\": None},\n",
783
+ " {\"op\": \"MUL\", \"dest\": \"x\", \"src1\": \"y\", \"src2\": 0},\n",
784
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
785
+ "]\n",
786
+ "result2 = peephole_optimization(test2)\n",
787
+ "print(\"\\nTest 2 (MUL by 0 -> CONST 0):\")\n",
788
+ "for i, instr in enumerate(result2): print(f\" {i}: {instr}\")\n",
789
+ "# Expected: instruction 1 becomes CONST x = 0\n",
790
+ "\n",
791
+ "# Test 3: MUL by 1 (both literals)\n",
792
+ "test3 = [\n",
793
+ " {\"op\": \"MUL\", \"dest\": \"x\", \"src1\": 7, \"src2\": 1},\n",
794
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
795
+ "]\n",
796
+ "result3 = peephole_optimization(test3)\n",
797
+ "print(\"\\nTest 3 (MUL 7*1 -> CONST 7):\")\n",
798
+ "for i, instr in enumerate(result3): print(f\" {i}: {instr}\")\n",
799
+ "# Expected: instruction 0 becomes CONST x = 7\n",
800
+ "\n",
801
+ "# Test 4: SUB x - x -> CONST 0\n",
802
+ "test4 = [\n",
803
+ " {\"op\": \"LOAD\", \"dest\": \"y\", \"src1\": \"addr0\", \"src2\": None},\n",
804
+ " {\"op\": \"SUB\", \"dest\": \"x\", \"src1\": \"y\", \"src2\": \"y\"},\n",
805
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
806
+ "]\n",
807
+ "result4 = peephole_optimization(test4)\n",
808
+ "print(\"\\nTest 4 (SUB y-y -> CONST 0):\")\n",
809
+ "for i, instr in enumerate(result4): print(f\" {i}: {instr}\")\n",
810
+ "# Expected: instruction 1 becomes CONST x = 0\n",
811
+ "\n",
812
+ "# Test 5: ADD with 0\n",
813
+ "test5 = [\n",
814
+ " {\"op\": \"ADD\", \"dest\": \"x\", \"src1\": 0, \"src2\": 5},\n",
815
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
816
+ "]\n",
817
+ "result5 = peephole_optimization(test5)\n",
818
+ "print(\"\\nTest 5 (ADD 0+5 -> CONST 5):\")\n",
819
+ "for i, instr in enumerate(result5): print(f\" {i}: {instr}\")\n",
820
+ "# Expected: instruction 0 becomes CONST x = 5\n",
821
+ "\n",
822
+ "# Test 6: full pipeline — CF then peephole on a generated program\n",
823
+ "random.seed(7)\n",
824
+ "prog = generate_level_1()\n",
825
+ "print(\"\\nTest 6 (CF then peephole on seed=7):\")\n",
826
+ "print(\"Original:\")\n",
827
+ "for i, instr in enumerate(prog[\"instructions\"]): print(f\" {i}: {instr}\")\n",
828
+ "after_cf = constant_folding(prog[\"instructions\"])\n",
829
+ "print(\"After CF:\")\n",
830
+ "for i, instr in enumerate(after_cf): print(f\" {i}: {instr}\")\n",
831
+ "after_peep = peephole_optimization(after_cf)\n",
832
+ "print(\"After CF + peephole:\")\n",
833
+ "for i, instr in enumerate(after_peep): print(f\" {i}: {instr}\")\n",
834
+ "\n",
835
+ "# Test 7: idempotence\n",
836
+ "test7 = [\n",
837
+ " {\"op\": \"MUL\", \"dest\": \"x\", \"src1\": \"y\", \"src2\": 2},\n",
838
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
839
+ "]\n",
840
+ "once = peephole_optimization(test7)\n",
841
+ "twice = peephole_optimization(once)\n",
842
+ "print(\"\\nTest 7 (idempotence):\", \"PASS\" if once == twice else \"FAIL\")\n",
843
+ "\n",
844
+ "# Test 8: no false fires — vanilla program shouldn't get rewritten\n",
845
+ "test8 = [\n",
846
+ " {\"op\": \"LOAD\", \"dest\": \"y\", \"src1\": \"addr0\", \"src2\": None},\n",
847
+ " {\"op\": \"MUL\", \"dest\": \"x\", \"src1\": \"y\", \"src2\": \"y\"}, # y*y, no peephole pattern\n",
848
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"x\", \"src2\": None},\n",
849
+ "]\n",
850
+ "result8 = peephole_optimization(test8)\n",
851
+ "print(\"\\nTest 8 (no false rewrite on y*y):\")\n",
852
+ "for i, instr in enumerate(result8): print(f\" {i}: {instr}\")\n",
853
+ "# Expected: instruction 1 unchanged (still MUL y*y)"
854
+ ],
855
+ "metadata": {
856
+ "colab": {
857
+ "base_uri": "https://localhost:8080/"
858
+ },
859
+ "id": "DWDUZUQvwXye",
860
+ "outputId": "1d1e327f-5f26-43e4-a6ac-f7a286da3781"
861
+ },
862
+ "execution_count": 10,
863
+ "outputs": [
864
+ {
865
+ "output_type": "stream",
866
+ "name": "stdout",
867
+ "text": [
868
+ "Test 1 (MUL by 2 -> ADD self):\n",
869
+ " 0: {'op': 'LOAD', 'dest': 'y', 'src1': 'addr0', 'src2': None}\n",
870
+ " 1: {'op': 'ADD', 'dest': 'x', 'src1': 'y', 'src2': 'y'}\n",
871
+ " 2: {'op': 'STORE', 'dest': 'addr0', 'src1': 'x', 'src2': None}\n",
872
+ "\n",
873
+ "Test 2 (MUL by 0 -> CONST 0):\n",
874
+ " 0: {'op': 'LOAD', 'dest': 'y', 'src1': 'addr0', 'src2': None}\n",
875
+ " 1: {'op': 'CONST', 'dest': 'x', 'src1': 0, 'src2': None}\n",
876
+ " 2: {'op': 'STORE', 'dest': 'addr0', 'src1': 'x', 'src2': None}\n",
877
+ "\n",
878
+ "Test 3 (MUL 7*1 -> CONST 7):\n",
879
+ " 0: {'op': 'CONST', 'dest': 'x', 'src1': 7, 'src2': None}\n",
880
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'x', 'src2': None}\n",
881
+ "\n",
882
+ "Test 4 (SUB y-y -> CONST 0):\n",
883
+ " 0: {'op': 'LOAD', 'dest': 'y', 'src1': 'addr0', 'src2': None}\n",
884
+ " 1: {'op': 'CONST', 'dest': 'x', 'src1': 0, 'src2': None}\n",
885
+ " 2: {'op': 'STORE', 'dest': 'addr0', 'src1': 'x', 'src2': None}\n",
886
+ "\n",
887
+ "Test 5 (ADD 0+5 -> CONST 5):\n",
888
+ " 0: {'op': 'CONST', 'dest': 'x', 'src1': 5, 'src2': None}\n",
889
+ " 1: {'op': 'STORE', 'dest': 'addr0', 'src1': 'x', 'src2': None}\n",
890
+ "\n",
891
+ "Test 6 (CF then peephole on seed=7):\n",
892
+ "Original:\n",
893
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 3, 'src2': None}\n",
894
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 7, 'src2': None}\n",
895
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 1, 'src2': None}\n",
896
+ " 3: {'op': 'ADD', 'dest': 'v3', 'src1': 'v1', 'src2': 'v2'}\n",
897
+ " 4: {'op': 'CONST', 'dest': 'v4', 'src1': 9, 'src2': None}\n",
898
+ " 5: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
899
+ "After CF:\n",
900
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 3, 'src2': None}\n",
901
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 7, 'src2': None}\n",
902
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 1, 'src2': None}\n",
903
+ " 3: {'op': 'CONST', 'dest': 'v3', 'src1': 8, 'src2': None}\n",
904
+ " 4: {'op': 'CONST', 'dest': 'v4', 'src1': 9, 'src2': None}\n",
905
+ " 5: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
906
+ "After CF + peephole:\n",
907
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 3, 'src2': None}\n",
908
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 7, 'src2': None}\n",
909
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 1, 'src2': None}\n",
910
+ " 3: {'op': 'CONST', 'dest': 'v3', 'src1': 8, 'src2': None}\n",
911
+ " 4: {'op': 'CONST', 'dest': 'v4', 'src1': 9, 'src2': None}\n",
912
+ " 5: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
913
+ "\n",
914
+ "Test 7 (idempotence): PASS\n",
915
+ "\n",
916
+ "Test 8 (no false rewrite on y*y):\n",
917
+ " 0: {'op': 'LOAD', 'dest': 'y', 'src1': 'addr0', 'src2': None}\n",
918
+ " 1: {'op': 'MUL', 'dest': 'x', 'src1': 'y', 'src2': 'y'}\n",
919
+ " 2: {'op': 'STORE', 'dest': 'addr0', 'src1': 'x', 'src2': None}\n"
920
+ ]
921
+ }
922
+ ]
923
+ },
924
+ {
925
+ "cell_type": "code",
926
+ "source": [
927
+ "def dump_ir(program_data):\n",
928
+ " \"\"\"\n",
929
+ " Convert a TAC program into a readable string for the LLM observation.\n",
930
+ "\n",
931
+ " Format:\n",
932
+ " // OBSERVABLE OUT: mem[0], mem[1]\n",
933
+ " 0: v0 = 3 # CONST [1 cycle]\n",
934
+ " 1: v1 = 5 # CONST [1 cycle]\n",
935
+ " 2: v2 = v0 + v1 # ADD [1 cycle]\n",
936
+ " 3: mem[addr0] = v2 # STORE [4 cycles]\n",
937
+ " // TOTAL: 7 cycles\n",
938
+ "\n",
939
+ " Args:\n",
940
+ " program_data: dict with keys 'instructions' and 'observable_addrs',\n",
941
+ " OR a raw list of instructions (legacy).\n",
942
+ "\n",
943
+ " Returns:\n",
944
+ " A multi-line string suitable for inclusion in an LLM prompt.\n",
945
+ " \"\"\"\n",
946
+ " # Accept both forms — full program dict or raw instruction list\n",
947
+ " if isinstance(program_data, dict):\n",
948
+ " instructions = program_data[\"instructions\"]\n",
949
+ " observable_addrs = program_data.get(\"observable_addrs\", [])\n",
950
+ " else:\n",
951
+ " instructions = program_data\n",
952
+ " observable_addrs = []\n",
953
+ "\n",
954
+ " lines = []\n",
955
+ "\n",
956
+ " # Header: observable outputs\n",
957
+ " if observable_addrs:\n",
958
+ " addr_str = \", \".join(f\"mem[{a}]\" for a in observable_addrs)\n",
959
+ " lines.append(f\"// OBSERVABLE OUT: {addr_str}\")\n",
960
+ "\n",
961
+ " # Body: each instruction in human-readable form\n",
962
+ " total_cycles = 0\n",
963
+ " for i, instr in enumerate(instructions):\n",
964
+ " op = instr[\"op\"]\n",
965
+ " dest = instr[\"dest\"]\n",
966
+ " src1 = instr[\"src1\"]\n",
967
+ " src2 = instr[\"src2\"]\n",
968
+ " cost = CYCLE_COSTS.get(op, 0)\n",
969
+ " total_cycles += cost\n",
970
+ "\n",
971
+ " # Render the instruction body\n",
972
+ " if op == \"CONST\":\n",
973
+ " body = f\"{dest} = {src1}\"\n",
974
+ " elif op == \"ADD\":\n",
975
+ " body = f\"{dest} = {src1} + {src2}\"\n",
976
+ " elif op == \"SUB\":\n",
977
+ " body = f\"{dest} = {src1} - {src2}\"\n",
978
+ " elif op == \"MUL\":\n",
979
+ " body = f\"{dest} = {src1} * {src2}\"\n",
980
+ " elif op == \"DIV\":\n",
981
+ " body = f\"{dest} = {src1} // {src2}\"\n",
982
+ " elif op == \"LOAD\":\n",
983
+ " body = f\"{dest} = mem[{src1}]\"\n",
984
+ " elif op == \"STORE\":\n",
985
+ " body = f\"mem[{dest}] = {src1}\"\n",
986
+ " elif op == \"NOP\":\n",
987
+ " body = \"nop\"\n",
988
+ " else:\n",
989
+ " body = f\"<unknown op: {op}>\"\n",
990
+ "\n",
991
+ " cost_label = f\"{cost} cycle\" if cost == 1 else f\"{cost} cycles\"\n",
992
+ " lines.append(f\"{i}: {body:<35} # {op:<6} [{cost_label}]\")\n",
993
+ "\n",
994
+ " # Footer: total cycles\n",
995
+ " lines.append(f\"// TOTAL: {total_cycles} cycles\")\n",
996
+ "\n",
997
+ " return \"\\n\".join(lines)"
998
+ ],
999
+ "metadata": {
1000
+ "id": "yB_yMNOZwaPT"
1001
+ },
1002
+ "execution_count": 13,
1003
+ "outputs": []
1004
+ },
1005
+ {
1006
+ "cell_type": "code",
1007
+ "source": [
1008
+ "# === Sanity tests for state translator ===\n",
1009
+ "\n",
1010
+ "# Test 1: full generated program\n",
1011
+ "random.seed(1)\n",
1012
+ "prog = generate_level_1()\n",
1013
+ "print(\"Test 1 (raw seed=1 program):\")\n",
1014
+ "print(dump_ir(prog))\n",
1015
+ "\n",
1016
+ "# Test 2: after CF\n",
1017
+ "print(\"\\nTest 2 (after CF):\")\n",
1018
+ "folded = constant_folding(prog[\"instructions\"])\n",
1019
+ "prog_after_cf = {**prog, \"instructions\": folded}\n",
1020
+ "print(dump_ir(prog_after_cf))\n",
1021
+ "\n",
1022
+ "# Test 3: after CF + DCE — should show fewer instructions, lower total\n",
1023
+ "print(\"\\nTest 3 (after CF + DCE):\")\n",
1024
+ "optimized = dead_code_elimination(folded)\n",
1025
+ "prog_optimized = {**prog, \"instructions\": optimized}\n",
1026
+ "print(dump_ir(prog_optimized))\n",
1027
+ "\n",
1028
+ "# Test 4: every op type at least once\n",
1029
+ "test4_instructions = [\n",
1030
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 5, \"src2\": None},\n",
1031
+ " {\"op\": \"ADD\", \"dest\": \"b\", \"src1\": \"a\", \"src2\": 3},\n",
1032
+ " {\"op\": \"SUB\", \"dest\": \"c\", \"src1\": \"b\", \"src2\": \"a\"},\n",
1033
+ " {\"op\": \"MUL\", \"dest\": \"d\", \"src1\": \"c\", \"src2\": 2},\n",
1034
+ " {\"op\": \"DIV\", \"dest\": \"e\", \"src1\": \"d\", \"src2\": 4},\n",
1035
+ " {\"op\": \"LOAD\", \"dest\": \"f\", \"src1\": \"addr0\", \"src2\": None},\n",
1036
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"e\", \"src2\": None},\n",
1037
+ " {\"op\": \"NOP\", \"dest\": None, \"src1\": None, \"src2\": None},\n",
1038
+ "]\n",
1039
+ "test4_data = {\n",
1040
+ " \"instructions\": test4_instructions,\n",
1041
+ " \"observable_addrs\": [0, 1],\n",
1042
+ "}\n",
1043
+ "print(\"\\nTest 4 (every op type):\")\n",
1044
+ "print(dump_ir(test4_data))\n",
1045
+ "\n",
1046
+ "# Test 5: empty program shouldn't crash\n",
1047
+ "print(\"\\nTest 5 (empty program):\")\n",
1048
+ "print(dump_ir({\"instructions\": [], \"observable_addrs\": [0]}))"
1049
+ ],
1050
+ "metadata": {
1051
+ "colab": {
1052
+ "base_uri": "https://localhost:8080/"
1053
+ },
1054
+ "id": "SUqa7odp3wB_",
1055
+ "outputId": "c3f44089-213a-4a69-f29f-42b755ed142a"
1056
+ },
1057
+ "execution_count": 14,
1058
+ "outputs": [
1059
+ {
1060
+ "output_type": "stream",
1061
+ "name": "stdout",
1062
+ "text": [
1063
+ "Test 1 (raw seed=1 program):\n",
1064
+ "// OBSERVABLE OUT: mem[0]\n",
1065
+ "0: v0 = 10 # CONST [1 cycle]\n",
1066
+ "1: v1 = 2 # CONST [1 cycle]\n",
1067
+ "2: v2 = v1 + v1 # ADD [1 cycle]\n",
1068
+ "3: v3 = v2 * v1 # MUL [3 cycles]\n",
1069
+ "4: mem[addr0] = v3 # STORE [4 cycles]\n",
1070
+ "// TOTAL: 10 cycles\n",
1071
+ "\n",
1072
+ "Test 2 (after CF):\n",
1073
+ "// OBSERVABLE OUT: mem[0]\n",
1074
+ "0: v0 = 10 # CONST [1 cycle]\n",
1075
+ "1: v1 = 2 # CONST [1 cycle]\n",
1076
+ "2: v2 = 4 # CONST [1 cycle]\n",
1077
+ "3: v3 = 8 # CONST [1 cycle]\n",
1078
+ "4: mem[addr0] = v3 # STORE [4 cycles]\n",
1079
+ "// TOTAL: 8 cycles\n",
1080
+ "\n",
1081
+ "Test 3 (after CF + DCE):\n",
1082
+ "// OBSERVABLE OUT: mem[0]\n",
1083
+ "0: v3 = 8 # CONST [1 cycle]\n",
1084
+ "1: mem[addr0] = v3 # STORE [4 cycles]\n",
1085
+ "// TOTAL: 5 cycles\n",
1086
+ "\n",
1087
+ "Test 4 (every op type):\n",
1088
+ "// OBSERVABLE OUT: mem[0], mem[1]\n",
1089
+ "0: a = 5 # CONST [1 cycle]\n",
1090
+ "1: b = a + 3 # ADD [1 cycle]\n",
1091
+ "2: c = b - a # SUB [1 cycle]\n",
1092
+ "3: d = c * 2 # MUL [3 cycles]\n",
1093
+ "4: e = d // 4 # DIV [5 cycles]\n",
1094
+ "5: f = mem[addr0] # LOAD [4 cycles]\n",
1095
+ "6: mem[addr0] = e # STORE [4 cycles]\n",
1096
+ "7: nop # NOP [0 cycles]\n",
1097
+ "// TOTAL: 19 cycles\n",
1098
+ "\n",
1099
+ "Test 5 (empty program):\n",
1100
+ "// OBSERVABLE OUT: mem[0]\n",
1101
+ "// TOTAL: 0 cycles\n"
1102
+ ]
1103
+ }
1104
+ ]
1105
+ },
1106
+ {
1107
+ "cell_type": "code",
1108
+ "source": [
1109
+ "def generate_level_2():\n",
1110
+ " \"\"\"\n",
1111
+ " Generate a Level 2 Toy-IR program.\n",
1112
+ "\n",
1113
+ " Characteristics:\n",
1114
+ " - 8-12 instructions\n",
1115
+ " - 3-5 CONSTs (some literal, some used in arithmetic)\n",
1116
+ " - 3-5 arithmetic ops (ADD, SUB, MUL, DIV) with chaining\n",
1117
+ " - 1-3 dead variables (DCE opportunities)\n",
1118
+ " - 1 LOAD from initial memory (introduces non-constant variable)\n",
1119
+ " - 1 STORE at the end (observable output)\n",
1120
+ " - Mix of operands designed to hit peephole patterns occasionally:\n",
1121
+ " - MUL by literal 2 / 1 / 0 (not always — diversity matters)\n",
1122
+ " - DIV by non-zero divisor only (Design Assumption #4)\n",
1123
+ "\n",
1124
+ " Returns:\n",
1125
+ " dict with keys: initial_vars, initial_mem, instructions, observable_addrs\n",
1126
+ " \"\"\"\n",
1127
+ " instructions = []\n",
1128
+ " var_counter = 0\n",
1129
+ "\n",
1130
+ " def new_var():\n",
1131
+ " nonlocal var_counter\n",
1132
+ " name = f\"v{var_counter}\"\n",
1133
+ " var_counter += 1\n",
1134
+ " return name\n",
1135
+ "\n",
1136
+ " available_vars = [] # vars that exist and can be used as sources\n",
1137
+ "\n",
1138
+ " # Step 1: 3-5 CONSTs\n",
1139
+ " num_consts = random.randint(3, 5)\n",
1140
+ " for _ in range(num_consts):\n",
1141
+ " var = new_var()\n",
1142
+ " value = random.randint(1, 10)\n",
1143
+ " instructions.append({\"op\": \"CONST\", \"dest\": var, \"src1\": value, \"src2\": None})\n",
1144
+ " available_vars.append(var)\n",
1145
+ "\n",
1146
+ " # Step 2: 1 LOAD from initial memory\n",
1147
+ " # We'll seed initial_mem with something at a fresh address.\n",
1148
+ " load_addr_var = \"addr_in\"\n",
1149
+ " loaded_var = new_var()\n",
1150
+ " instructions.append({\n",
1151
+ " \"op\": \"LOAD\", \"dest\": loaded_var, \"src1\": load_addr_var, \"src2\": None,\n",
1152
+ " })\n",
1153
+ " available_vars.append(loaded_var)\n",
1154
+ "\n",
1155
+ " # Step 3: 3-5 arithmetic ops, chained\n",
1156
+ " num_arith = random.randint(3, 5)\n",
1157
+ " last_result = None\n",
1158
+ " for _ in range(num_arith):\n",
1159
+ " op = random.choice([\"ADD\", \"SUB\", \"MUL\", \"DIV\"])\n",
1160
+ "\n",
1161
+ " # 30% chance to use a literal as src2 to expose peephole opportunities\n",
1162
+ " # (MUL by 2/1/0, etc.)\n",
1163
+ " use_literal_src2 = random.random() < 0.3\n",
1164
+ "\n",
1165
+ " src1 = random.choice(available_vars)\n",
1166
+ " if use_literal_src2:\n",
1167
+ " # For DIV, never emit literal 0 (Design Assumption #4)\n",
1168
+ " if op == \"DIV\":\n",
1169
+ " src2 = random.choice([1, 2, 3, 4]) # safe non-zero divisors\n",
1170
+ " else:\n",
1171
+ " src2 = random.choice([0, 1, 2, 3]) # 0/1/2 hit peephole patterns\n",
1172
+ " else:\n",
1173
+ " src2 = random.choice(available_vars)\n",
1174
+ " # If we sampled a variable for DIV's src2, we can't be sure it's non-zero\n",
1175
+ " # at runtime. To stay safe per Assumption #4, restrict DIV to literal src2.\n",
1176
+ " if op == \"DIV\":\n",
1177
+ " src2 = random.choice([1, 2, 3, 4])\n",
1178
+ "\n",
1179
+ " dest = new_var()\n",
1180
+ " instructions.append({\"op\": op, \"dest\": dest, \"src1\": src1, \"src2\": src2})\n",
1181
+ " available_vars.append(dest)\n",
1182
+ " last_result = dest\n",
1183
+ "\n",
1184
+ " # Step 4: 1-3 extra dead CONSTs sprinkled in (DCE targets)\n",
1185
+ " num_dead = random.randint(1, 3)\n",
1186
+ " for _ in range(num_dead):\n",
1187
+ " dead_var = new_var()\n",
1188
+ " dead_value = random.randint(1, 20)\n",
1189
+ " # Insert at a random position before the (eventual) STORE\n",
1190
+ " insert_pos = random.randint(0, len(instructions))\n",
1191
+ " instructions.insert(insert_pos, {\n",
1192
+ " \"op\": \"CONST\", \"dest\": dead_var, \"src1\": dead_value, \"src2\": None,\n",
1193
+ " })\n",
1194
+ " # Note: dead_var intentionally never used afterward\n",
1195
+ "\n",
1196
+ " # Step 5: STORE the final result\n",
1197
+ " initial_vars = {\n",
1198
+ " \"addr0\": 0,\n",
1199
+ " \"addr_in\": 1, # address that the LOAD reads from\n",
1200
+ " }\n",
1201
+ " initial_mem = {1: random.randint(1, 20)} # seed mem[1] with a random value\n",
1202
+ "\n",
1203
+ " instructions.append({\n",
1204
+ " \"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": last_result, \"src2\": None,\n",
1205
+ " })\n",
1206
+ "\n",
1207
+ " return {\n",
1208
+ " \"initial_vars\": initial_vars,\n",
1209
+ " \"initial_mem\": initial_mem,\n",
1210
+ " \"instructions\": instructions,\n",
1211
+ " \"observable_addrs\": [0],\n",
1212
+ " }"
1213
+ ],
1214
+ "metadata": {
1215
+ "id": "OL6QbL0H3xuP"
1216
+ },
1217
+ "execution_count": 15,
1218
+ "outputs": []
1219
+ },
1220
+ {
1221
+ "cell_type": "code",
1222
+ "source": [
1223
+ "# === Sanity tests for Level 2 generator ===\n",
1224
+ "\n",
1225
+ "# Test 1: spot-check a few seeds\n",
1226
+ "for seed in [42, 1, 7]:\n",
1227
+ " random.seed(seed)\n",
1228
+ " prog = generate_level_2()\n",
1229
+ " print(f\"\\n=== Level 2, seed={seed} ===\")\n",
1230
+ " print(dump_ir(prog))\n",
1231
+ " print(f\"initial_vars: {prog['initial_vars']}\")\n",
1232
+ " print(f\"initial_mem : {prog['initial_mem']}\")\n",
1233
+ "\n",
1234
+ "# Test 2: full pipeline (CF + DCE + peephole) on a Level 2 program\n",
1235
+ "print(\"\\n=== Full optimization pipeline on Level 2 (seed=42) ===\")\n",
1236
+ "random.seed(42)\n",
1237
+ "prog = generate_level_2()\n",
1238
+ "print(\"ORIGINAL:\")\n",
1239
+ "print(dump_ir(prog))\n",
1240
+ "\n",
1241
+ "after_cf = constant_folding(prog[\"instructions\"])\n",
1242
+ "print(\"\\nAFTER CF:\")\n",
1243
+ "print(dump_ir({**prog, \"instructions\": after_cf}))\n",
1244
+ "\n",
1245
+ "after_dce = dead_code_elimination(after_cf)\n",
1246
+ "print(\"\\nAFTER CF + DCE:\")\n",
1247
+ "print(dump_ir({**prog, \"instructions\": after_dce}))\n",
1248
+ "\n",
1249
+ "after_peep = peephole_optimization(after_dce)\n",
1250
+ "print(\"\\nAFTER CF + DCE + PEEPHOLE:\")\n",
1251
+ "print(dump_ir({**prog, \"instructions\": after_peep}))"
1252
+ ],
1253
+ "metadata": {
1254
+ "colab": {
1255
+ "base_uri": "https://localhost:8080/"
1256
+ },
1257
+ "id": "D0XjNetU5gIo",
1258
+ "outputId": "1c35cdfc-a95d-48ff-c7ce-c4c236bbbbdb"
1259
+ },
1260
+ "execution_count": 16,
1261
+ "outputs": [
1262
+ {
1263
+ "output_type": "stream",
1264
+ "name": "stdout",
1265
+ "text": [
1266
+ "\n",
1267
+ "=== Level 2, seed=42 ===\n",
1268
+ "// OBSERVABLE OUT: mem[0]\n",
1269
+ "0: v0 = 2 # CONST [1 cycle]\n",
1270
+ "1: v1 = 1 # CONST [1 cycle]\n",
1271
+ "2: v2 = 5 # CONST [1 cycle]\n",
1272
+ "3: v3 = 4 # CONST [1 cycle]\n",
1273
+ "4: v11 = 19 # CONST [1 cycle]\n",
1274
+ "5: v4 = 4 # CONST [1 cycle]\n",
1275
+ "6: v5 = mem[addr_in] # LOAD [4 cycles]\n",
1276
+ "7: v9 = 18 # CONST [1 cycle]\n",
1277
+ "8: v10 = 8 # CONST [1 cycle]\n",
1278
+ "9: v6 = v4 + v0 # ADD [1 cycle]\n",
1279
+ "10: v7 = v0 // 2 # DIV [5 cycles]\n",
1280
+ "11: v8 = v0 - v3 # SUB [1 cycle]\n",
1281
+ "12: mem[addr0] = v8 # STORE [4 cycles]\n",
1282
+ "// TOTAL: 23 cycles\n",
1283
+ "initial_vars: {'addr0': 0, 'addr_in': 1}\n",
1284
+ "initial_mem : {1: 1}\n",
1285
+ "\n",
1286
+ "=== Level 2, seed=1 ===\n",
1287
+ "// OBSERVABLE OUT: mem[0]\n",
1288
+ "0: v0 = 10 # CONST [1 cycle]\n",
1289
+ "1: v7 = 19 # CONST [1 cycle]\n",
1290
+ "2: v1 = 2 # CONST [1 cycle]\n",
1291
+ "3: v2 = 5 # CONST [1 cycle]\n",
1292
+ "4: v3 = mem[addr_in] # LOAD [4 cycles]\n",
1293
+ "5: v4 = v3 // 2 # DIV [5 cycles]\n",
1294
+ "6: v5 = v3 + v3 # ADD [1 cycle]\n",
1295
+ "7: v6 = v2 + v5 # ADD [1 cycle]\n",
1296
+ "8: mem[addr0] = v6 # STORE [4 cycles]\n",
1297
+ "// TOTAL: 19 cycles\n",
1298
+ "initial_vars: {'addr0': 0, 'addr_in': 1}\n",
1299
+ "initial_mem : {1: 11}\n",
1300
+ "\n",
1301
+ "=== Level 2, seed=7 ===\n",
1302
+ "// OBSERVABLE OUT: mem[0]\n",
1303
+ "0: v0 = 3 # CONST [1 cycle]\n",
1304
+ "1: v1 = 7 # CONST [1 cycle]\n",
1305
+ "2: v2 = 1 # CONST [1 cycle]\n",
1306
+ "3: v3 = 2 # CONST [1 cycle]\n",
1307
+ "4: v4 = mem[addr_in] # LOAD [4 cycles]\n",
1308
+ "5: v5 = v0 + v4 # ADD [1 cycle]\n",
1309
+ "6: v6 = v3 - 3 # SUB [1 cycle]\n",
1310
+ "7: v7 = v4 + 3 # ADD [1 cycle]\n",
1311
+ "8: v10 = 2 # CONST [1 cycle]\n",
1312
+ "9: v8 = v1 + v3 # ADD [1 cycle]\n",
1313
+ "10: v9 = v6 + v0 # ADD [1 cycle]\n",
1314
+ "11: mem[addr0] = v9 # STORE [4 cycles]\n",
1315
+ "// TOTAL: 18 cycles\n",
1316
+ "initial_vars: {'addr0': 0, 'addr_in': 1}\n",
1317
+ "initial_mem : {1: 5}\n",
1318
+ "\n",
1319
+ "=== Full optimization pipeline on Level 2 (seed=42) ===\n",
1320
+ "ORIGINAL:\n",
1321
+ "// OBSERVABLE OUT: mem[0]\n",
1322
+ "0: v0 = 2 # CONST [1 cycle]\n",
1323
+ "1: v1 = 1 # CONST [1 cycle]\n",
1324
+ "2: v2 = 5 # CONST [1 cycle]\n",
1325
+ "3: v3 = 4 # CONST [1 cycle]\n",
1326
+ "4: v11 = 19 # CONST [1 cycle]\n",
1327
+ "5: v4 = 4 # CONST [1 cycle]\n",
1328
+ "6: v5 = mem[addr_in] # LOAD [4 cycles]\n",
1329
+ "7: v9 = 18 # CONST [1 cycle]\n",
1330
+ "8: v10 = 8 # CONST [1 cycle]\n",
1331
+ "9: v6 = v4 + v0 # ADD [1 cycle]\n",
1332
+ "10: v7 = v0 // 2 # DIV [5 cycles]\n",
1333
+ "11: v8 = v0 - v3 # SUB [1 cycle]\n",
1334
+ "12: mem[addr0] = v8 # STORE [4 cycles]\n",
1335
+ "// TOTAL: 23 cycles\n",
1336
+ "\n",
1337
+ "AFTER CF:\n",
1338
+ "// OBSERVABLE OUT: mem[0]\n",
1339
+ "0: v0 = 2 # CONST [1 cycle]\n",
1340
+ "1: v1 = 1 # CONST [1 cycle]\n",
1341
+ "2: v2 = 5 # CONST [1 cycle]\n",
1342
+ "3: v3 = 4 # CONST [1 cycle]\n",
1343
+ "4: v11 = 19 # CONST [1 cycle]\n",
1344
+ "5: v4 = 4 # CONST [1 cycle]\n",
1345
+ "6: v5 = mem[addr_in] # LOAD [4 cycles]\n",
1346
+ "7: v9 = 18 # CONST [1 cycle]\n",
1347
+ "8: v10 = 8 # CONST [1 cycle]\n",
1348
+ "9: v6 = 6 # CONST [1 cycle]\n",
1349
+ "10: v7 = 1 # CONST [1 cycle]\n",
1350
+ "11: v8 = -2 # CONST [1 cycle]\n",
1351
+ "12: mem[addr0] = v8 # STORE [4 cycles]\n",
1352
+ "// TOTAL: 19 cycles\n",
1353
+ "\n",
1354
+ "AFTER CF + DCE:\n",
1355
+ "// OBSERVABLE OUT: mem[0]\n",
1356
+ "0: v8 = -2 # CONST [1 cycle]\n",
1357
+ "1: mem[addr0] = v8 # STORE [4 cycles]\n",
1358
+ "// TOTAL: 5 cycles\n",
1359
+ "\n",
1360
+ "AFTER CF + DCE + PEEPHOLE:\n",
1361
+ "// OBSERVABLE OUT: mem[0]\n",
1362
+ "0: v8 = -2 # CONST [1 cycle]\n",
1363
+ "1: mem[addr0] = v8 # STORE [4 cycles]\n",
1364
+ "// TOTAL: 5 cycles\n"
1365
+ ]
1366
+ }
1367
+ ]
1368
+ },
1369
+ {
1370
+ "cell_type": "code",
1371
+ "source": [],
1372
+ "metadata": {
1373
+ "id": "Vo-9MitD5hwM"
1374
+ },
1375
+ "execution_count": null,
1376
+ "outputs": []
1377
+ }
1378
+ ]
1379
+ }
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/metahack1.ipynb ADDED
@@ -0,0 +1,484 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 0,
4
+ "metadata": {
5
+ "colab": {
6
+ "provenance": [],
7
+ "gpuType": "T4"
8
+ },
9
+ "kernelspec": {
10
+ "name": "python3",
11
+ "display_name": "Python 3"
12
+ },
13
+ "language_info": {
14
+ "name": "python"
15
+ },
16
+ "accelerator": "GPU"
17
+ },
18
+ "cells": [
19
+ {
20
+ "cell_type": "code",
21
+ "source": [
22
+ "## Design Assumptions (do not violate)\n",
23
+ "\n",
24
+ "# 1. **DCE never eliminates STOREs.** They define program output (final mem state).\n",
25
+ "# 2. **Addresses are distinct by construction.** Generator allocates each address variable to a unique integer; no aliasing.\n",
26
+ "# 3. **CF refuses to fold DIV by zero.** If src2 == 0 on a DIV op, leave instruction unchanged.\n",
27
+ "# 4. **Generator never emits DIV by literal zero.** When DIV is generated, src2 is always a non-zero constant or a variable known to be non-zero.\n",
28
+ "# # 5. **Integer arithmetic only.** No floats anywhere — avoids equivalence-check precision issues.\n",
29
+ "# 6. Generator declares `observable_addrs` per program — verifier compares only these mem entries.\n",
30
+ "# 7. State translator annotates observable outputs at top of dump.\n",
31
+ "# 8. Reward distinguishes broken (-1000) from valid-but-worse (small negative) — Harshal's formula.\n",
32
+ "# 9. Multi-input verification: 3-5 random initial states, all must match.\n",
33
+ "# 10. Integer division uses Python floor division (//). Aarush's VM must match."
34
+ ],
35
+ "metadata": {
36
+ "id": "_XI5jT2Ibvrf"
37
+ },
38
+ "execution_count": 17,
39
+ "outputs": []
40
+ },
41
+ {
42
+ "cell_type": "code",
43
+ "execution_count": 13,
44
+ "metadata": {
45
+ "id": "j6CQ327KWXwr"
46
+ },
47
+ "outputs": [],
48
+ "source": [
49
+ "# === TAC Schema v1.0 (LOCKED with Role 1 / Aarush) ===\n",
50
+ "# Reverse passes deferred to stretch goal — not in initial action space.\n",
51
+ "\n",
52
+ "OPS = [\"CONST\", \"ADD\", \"SUB\", \"MUL\", \"DIV\", \"LOAD\", \"STORE\", \"NOP\"]\n",
53
+ "\n",
54
+ "CYCLE_COSTS = {\n",
55
+ " \"CONST\": 1,\n",
56
+ " \"ADD\": 1,\n",
57
+ " \"SUB\": 1,\n",
58
+ " \"MUL\": 3,\n",
59
+ " \"DIV\": 5,\n",
60
+ " \"LOAD\": 4,\n",
61
+ " \"STORE\": 4,\n",
62
+ " \"NOP\": 0,\n",
63
+ "}\n",
64
+ "\n",
65
+ "# Instruction shape: {\"op\": str, \"dest\": str|None, \"src1\": Any, \"src2\": Any}\n",
66
+ "# Operands: str = variable name, int = literal constant, None = unused\n",
67
+ "#\n",
68
+ "# Op semantics:\n",
69
+ "# CONST: dest = src1 (src1 is int literal, src2 = None)\n",
70
+ "# ADD/SUB/MUL/DIV: dest = src1 OP src2 (src1, src2 are var names or int literals)\n",
71
+ "# LOAD: dest = mem[src1] (src1 is a var holding an address)\n",
72
+ "# STORE: mem[dest] = src1 (dest is a var holding an address)\n",
73
+ "# NOP: no-op (all fields None)\n",
74
+ "#\n",
75
+ "# Program output (for equivalence check) = final memory state (mem dict).\n",
76
+ "# Programs ship as: (initial_vars: dict, initial_mem: dict, instructions: list[dict])\n",
77
+ "# 10. Integer division uses Python floor division (//). Aarush's VM must match."
78
+ ]
79
+ },
80
+ {
81
+ "cell_type": "code",
82
+ "source": [
83
+ "import random\n",
84
+ "\n",
85
+ "def generate_level_1():\n",
86
+ " \"\"\"\n",
87
+ " Generate a Level 1 Toy-IR program.\n",
88
+ "\n",
89
+ " Characteristics:\n",
90
+ " - 4-6 instructions\n",
91
+ " - 2-3 CONST ops with literal values\n",
92
+ " - 1-2 arithmetic ops on those constants (foldable by CF)\n",
93
+ " - 0-1 dead variables (killable by DCE)\n",
94
+ " - Exactly 1 STORE at the end so the program has an observable output\n",
95
+ "\n",
96
+ " Returns:\n",
97
+ " dict with keys: initial_vars, initial_mem, instructions, observable_addrs\n",
98
+ " \"\"\"\n",
99
+ " instructions = []\n",
100
+ " var_counter = 0\n",
101
+ "\n",
102
+ " def new_var():\n",
103
+ " nonlocal var_counter\n",
104
+ " name = f\"v{var_counter}\"\n",
105
+ " var_counter += 1\n",
106
+ " return name\n",
107
+ "\n",
108
+ " # Step 1: Generate 2-3 constant assignments\n",
109
+ " num_consts = random.randint(2, 3)\n",
110
+ " const_vars = []\n",
111
+ " for _ in range(num_consts):\n",
112
+ " var = new_var()\n",
113
+ " value = random.randint(1, 10)\n",
114
+ " instructions.append({\"op\": \"CONST\", \"dest\": var, \"src1\": value, \"src2\": None})\n",
115
+ " const_vars.append(var)\n",
116
+ "\n",
117
+ " # Step 2: Generate 1-2 arithmetic ops using those constants\n",
118
+ " num_arith = random.randint(1, 2)\n",
119
+ " last_result = None\n",
120
+ " for _ in range(num_arith):\n",
121
+ " op = random.choice([\"ADD\", \"MUL\"])\n",
122
+ " src1 = random.choice(const_vars)\n",
123
+ " src2 = random.choice(const_vars)\n",
124
+ " dest = new_var()\n",
125
+ " instructions.append({\"op\": op, \"dest\": dest, \"src1\": src1, \"src2\": src2})\n",
126
+ " last_result = dest\n",
127
+ " const_vars.append(dest)\n",
128
+ "\n",
129
+ " # Step 3: Optionally add 1 dead variable (50% chance)\n",
130
+ " if random.random() < 0.5:\n",
131
+ " dead_var = new_var()\n",
132
+ " dead_value = random.randint(1, 10)\n",
133
+ " instructions.append({\"op\": \"CONST\", \"dest\": dead_var, \"src1\": dead_value, \"src2\": None})\n",
134
+ " # Note: dead_var is intentionally never used — DCE should catch it.\n",
135
+ "\n",
136
+ " # Step 4: Add a STORE at the end so the program has observable output\n",
137
+ " initial_vars = {\"addr0\": 0}\n",
138
+ " instructions.append({\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": last_result, \"src2\": None})\n",
139
+ "\n",
140
+ " initial_mem = {}\n",
141
+ "\n",
142
+ " return {\n",
143
+ " \"initial_vars\": initial_vars,\n",
144
+ " \"initial_mem\": initial_mem,\n",
145
+ " \"instructions\": instructions,\n",
146
+ " \"observable_addrs\": [0], # Aarush's verifier compares only these mem entries\n",
147
+ " }\n",
148
+ "\n",
149
+ "\n",
150
+ "# Sanity-check: generate a few programs and print them\n",
151
+ "for seed in [42, 1, 7, 99]:\n",
152
+ " random.seed(seed)\n",
153
+ " prog = generate_level_1()\n",
154
+ " print(f\"\\n=== seed={seed} ===\")\n",
155
+ " print(f\"initial_vars : {prog['initial_vars']}\")\n",
156
+ " print(f\"initial_mem : {prog['initial_mem']}\")\n",
157
+ " print(f\"observable_addrs : {prog['observable_addrs']}\")\n",
158
+ " print(f\"instructions ({len(prog['instructions'])}):\")\n",
159
+ " for i, instr in enumerate(prog['instructions']):\n",
160
+ " print(f\" {i}: {instr}\")"
161
+ ],
162
+ "metadata": {
163
+ "colab": {
164
+ "base_uri": "https://localhost:8080/"
165
+ },
166
+ "id": "a86vD1OyWcB9",
167
+ "outputId": "de3d7093-eaea-420e-a0d7-2bd9451bcf6b"
168
+ },
169
+ "execution_count": 14,
170
+ "outputs": [
171
+ {
172
+ "output_type": "stream",
173
+ "name": "stdout",
174
+ "text": [
175
+ "\n",
176
+ "=== seed=42 ===\n",
177
+ "initial_vars : {'addr0': 0}\n",
178
+ "initial_mem : {}\n",
179
+ "observable_addrs : [0]\n",
180
+ "instructions (4):\n",
181
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
182
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
183
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v0', 'src2': 'v0'}\n",
184
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
185
+ "\n",
186
+ "=== seed=1 ===\n",
187
+ "initial_vars : {'addr0': 0}\n",
188
+ "initial_mem : {}\n",
189
+ "observable_addrs : [0]\n",
190
+ "instructions (5):\n",
191
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 10, 'src2': None}\n",
192
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 2, 'src2': None}\n",
193
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v1', 'src2': 'v1'}\n",
194
+ " 3: {'op': 'MUL', 'dest': 'v3', 'src1': 'v2', 'src2': 'v1'}\n",
195
+ " 4: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
196
+ "\n",
197
+ "=== seed=7 ===\n",
198
+ "initial_vars : {'addr0': 0}\n",
199
+ "initial_mem : {}\n",
200
+ "observable_addrs : [0]\n",
201
+ "instructions (6):\n",
202
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 3, 'src2': None}\n",
203
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 7, 'src2': None}\n",
204
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 1, 'src2': None}\n",
205
+ " 3: {'op': 'ADD', 'dest': 'v3', 'src1': 'v1', 'src2': 'v2'}\n",
206
+ " 4: {'op': 'CONST', 'dest': 'v4', 'src1': 9, 'src2': None}\n",
207
+ " 5: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n",
208
+ "\n",
209
+ "=== seed=99 ===\n",
210
+ "initial_vars : {'addr0': 0}\n",
211
+ "initial_mem : {}\n",
212
+ "observable_addrs : [0]\n",
213
+ "instructions (5):\n",
214
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 7, 'src2': None}\n",
215
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 4, 'src2': None}\n",
216
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 10, 'src2': None}\n",
217
+ " 3: {'op': 'ADD', 'dest': 'v3', 'src1': 'v0', 'src2': 'v0'}\n",
218
+ " 4: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v3', 'src2': None}\n"
219
+ ]
220
+ }
221
+ ]
222
+ },
223
+ {
224
+ "cell_type": "code",
225
+ "source": [
226
+ "def _resolve_operand(operand, known_constants):\n",
227
+ " \"\"\"\n",
228
+ " Given a TAC operand (string variable name or int literal),\n",
229
+ " return its concrete integer value if known, or None if unknown.\n",
230
+ " \"\"\"\n",
231
+ " if isinstance(operand, int):\n",
232
+ " return operand\n",
233
+ " if isinstance(operand, str) and operand in known_constants:\n",
234
+ " return known_constants[operand]\n",
235
+ " return None\n",
236
+ "\n",
237
+ "\n",
238
+ "def _compute(op, v1, v2):\n",
239
+ " \"\"\"Compute the result of a binary arithmetic op on two known integers.\"\"\"\n",
240
+ " if op == \"ADD\": return v1 + v2\n",
241
+ " if op == \"SUB\": return v1 - v2\n",
242
+ " if op == \"MUL\": return v1 * v2\n",
243
+ " if op == \"DIV\": return v1 // v2 # floor division (locked semantics)\n",
244
+ " raise ValueError(f\"_compute called with non-arithmetic op: {op}\")\n",
245
+ "\n",
246
+ "\n",
247
+ "def constant_folding(program):\n",
248
+ " \"\"\"\n",
249
+ " Forward pass that folds constant arithmetic into CONST ops, and\n",
250
+ " propagates known constants into instruction operands.\n",
251
+ "\n",
252
+ " Behavior:\n",
253
+ " - If both sources of an arithmetic op resolve to known integers,\n",
254
+ " replaces the instruction with a CONST holding the computed result.\n",
255
+ " - If only one source is known, still substitutes that known value\n",
256
+ " into the instruction (constant propagation), enabling downstream\n",
257
+ " passes (e.g., peephole) to recognize patterns like ADD-with-0 or MUL-by-1.\n",
258
+ " - Refuses to fold DIV by zero (Design Assumption #3).\n",
259
+ " - Always returns fresh dicts; never aliases input instructions.\n",
260
+ "\n",
261
+ " Args:\n",
262
+ " program: list of TAC instruction dicts (per locked schema).\n",
263
+ "\n",
264
+ " Returns:\n",
265
+ " new list of TAC instruction dicts. Always semantics-preserving.\n",
266
+ " Never raises, never returns None.\n",
267
+ " \"\"\"\n",
268
+ " known_constants = {}\n",
269
+ " new_program = []\n",
270
+ "\n",
271
+ " for instr in program:\n",
272
+ " # Always work on a copy — never alias input dicts\n",
273
+ " instr = instr.copy()\n",
274
+ " op = instr[\"op\"]\n",
275
+ " dest = instr[\"dest\"]\n",
276
+ "\n",
277
+ " if op == \"CONST\":\n",
278
+ " known_constants[dest] = instr[\"src1\"]\n",
279
+ " new_program.append(instr)\n",
280
+ "\n",
281
+ " elif op in (\"ADD\", \"SUB\", \"MUL\", \"DIV\"):\n",
282
+ " # === Constant propagation: substitute known constants into operands ===\n",
283
+ " if isinstance(instr[\"src1\"], str) and instr[\"src1\"] in known_constants:\n",
284
+ " instr[\"src1\"] = known_constants[instr[\"src1\"]]\n",
285
+ " if isinstance(instr[\"src2\"], str) and instr[\"src2\"] in known_constants:\n",
286
+ " instr[\"src2\"] = known_constants[instr[\"src2\"]]\n",
287
+ "\n",
288
+ " # === Try to fold ===\n",
289
+ " v1 = _resolve_operand(instr[\"src1\"], known_constants)\n",
290
+ " v2 = _resolve_operand(instr[\"src2\"], known_constants)\n",
291
+ "\n",
292
+ " if v1 is not None and v2 is not None:\n",
293
+ " # Both operands are known integers\n",
294
+ " if op == \"DIV\" and v2 == 0:\n",
295
+ " # Refuse to fold DIV by zero\n",
296
+ " new_program.append(instr)\n",
297
+ " known_constants.pop(dest, None)\n",
298
+ " else:\n",
299
+ " # Fold: replace with CONST\n",
300
+ " result = _compute(op, v1, v2)\n",
301
+ " new_program.append({\n",
302
+ " \"op\": \"CONST\",\n",
303
+ " \"dest\": dest,\n",
304
+ " \"src1\": result,\n",
305
+ " \"src2\": None,\n",
306
+ " })\n",
307
+ " known_constants[dest] = result\n",
308
+ " else:\n",
309
+ " # Can't fold (at least one operand unknown).\n",
310
+ " # Instruction may still have been mutated by propagation above.\n",
311
+ " new_program.append(instr)\n",
312
+ " known_constants.pop(dest, None)\n",
313
+ "\n",
314
+ " elif op == \"LOAD\":\n",
315
+ " # Memory reads aren't statically resolvable\n",
316
+ " new_program.append(instr)\n",
317
+ " known_constants.pop(dest, None)\n",
318
+ "\n",
319
+ " elif op in (\"STORE\", \"NOP\"):\n",
320
+ " # No dest tracking needed.\n",
321
+ " # Note: we COULD propagate src1 into a STORE for downstream readability,\n",
322
+ " # but the cycle cost is unchanged and the executor handles vars fine.\n",
323
+ " # Leave STORE alone — keeps the code minimal.\n",
324
+ " new_program.append(instr)\n",
325
+ "\n",
326
+ " else:\n",
327
+ " # Unknown op — defensive pass-through\n",
328
+ " new_program.append(instr)\n",
329
+ "\n",
330
+ " return new_program"
331
+ ],
332
+ "metadata": {
333
+ "id": "CGk8Fz4CZA3t"
334
+ },
335
+ "execution_count": 18,
336
+ "outputs": []
337
+ },
338
+ {
339
+ "cell_type": "code",
340
+ "source": [
341
+ "# === Sanity tests for constant_folding ===\n",
342
+ "\n",
343
+ "# Test 1: simple fold — ADD of two CONSTs\n",
344
+ "test1 = [\n",
345
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 3, \"src2\": None},\n",
346
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 5, \"src2\": None},\n",
347
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"},\n",
348
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
349
+ "]\n",
350
+ "result1 = constant_folding(test1)\n",
351
+ "print(\"Test 1 (simple ADD fold):\")\n",
352
+ "for i, instr in enumerate(result1): print(f\" {i}: {instr}\")\n",
353
+ "# Expected: instruction 2 becomes CONST c = 8\n",
354
+ "\n",
355
+ "# Test 2: chained fold — second op uses first op's folded result\n",
356
+ "test2 = [\n",
357
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 3, \"src2\": None},\n",
358
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 5, \"src2\": None},\n",
359
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # → 8\n",
360
+ " {\"op\": \"MUL\", \"dest\": \"d\", \"src1\": \"c\", \"src2\": \"b\"}, # → 8 * 5 = 40\n",
361
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"d\", \"src2\": None},\n",
362
+ "]\n",
363
+ "result2 = constant_folding(test2)\n",
364
+ "print(\"\\nTest 2 (chained fold):\")\n",
365
+ "for i, instr in enumerate(result2): print(f\" {i}: {instr}\")\n",
366
+ "# Expected: c becomes CONST 8, d becomes CONST 40\n",
367
+ "\n",
368
+ "# Test 3: DIV by zero refused\n",
369
+ "test3 = [\n",
370
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 10, \"src2\": None},\n",
371
+ " {\"op\": \"CONST\", \"dest\": \"b\", \"src1\": 0, \"src2\": None},\n",
372
+ " {\"op\": \"DIV\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # 10/0 — must NOT fold\n",
373
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
374
+ "]\n",
375
+ "result3 = constant_folding(test3)\n",
376
+ "print(\"\\nTest 3 (DIV by zero refused):\")\n",
377
+ "for i, instr in enumerate(result3): print(f\" {i}: {instr}\")\n",
378
+ "# Expected: instruction 2 unchanged (still DIV, not CONST)\n",
379
+ "\n",
380
+ "# Test 4: unknown source can't fold\n",
381
+ "test4 = [\n",
382
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 3, \"src2\": None},\n",
383
+ " {\"op\": \"LOAD\", \"dest\": \"b\", \"src1\": \"addr0\", \"src2\": None}, # b is unknown\n",
384
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # can't fold\n",
385
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
386
+ "]\n",
387
+ "result4 = constant_folding(test4)\n",
388
+ "print(\"\\nTest 4 (LOAD makes b unknown, ADD not folded):\")\n",
389
+ "for i, instr in enumerate(result4): print(f\" {i}: {instr}\")\n",
390
+ "# Expected: instruction 2 unchanged (still ADD)\n",
391
+ "\n",
392
+ "# Test 5: idempotence — running CF on a generated program\n",
393
+ "random.seed(42)\n",
394
+ "prog = generate_level_1()\n",
395
+ "folded = constant_folding(prog[\"instructions\"])\n",
396
+ "print(\"\\nTest 5 (CF on generated Level 1 program, seed=42):\")\n",
397
+ "print(\"Before:\")\n",
398
+ "for i, instr in enumerate(prog[\"instructions\"]): print(f\" {i}: {instr}\")\n",
399
+ "print(\"After:\")\n",
400
+ "for i, instr in enumerate(folded): print(f\" {i}: {instr}\")\n",
401
+ "\n",
402
+ "# Test 6: constant propagation — only one source is known\n",
403
+ "test6 = [\n",
404
+ " {\"op\": \"CONST\", \"dest\": \"a\", \"src1\": 0, \"src2\": None},\n",
405
+ " {\"op\": \"LOAD\", \"dest\": \"b\", \"src1\": \"addr0\", \"src2\": None}, # b unknown\n",
406
+ " {\"op\": \"ADD\", \"dest\": \"c\", \"src1\": \"a\", \"src2\": \"b\"}, # a known (=0), b unknown\n",
407
+ " {\"op\": \"STORE\", \"dest\": \"addr0\", \"src1\": \"c\", \"src2\": None},\n",
408
+ "]\n",
409
+ "result6 = constant_folding(test6)\n",
410
+ "print(\"\\nTest 6 (propagation: a=0 substituted into ADD even though b unknown):\")\n",
411
+ "for i, instr in enumerate(result6): print(f\" {i}: {instr}\")\n",
412
+ "# Expected: instruction 2 is still ADD (can't fold — b unknown), but src1 is now literal 0, not 'a'\n",
413
+ "# This sets up peephole to recognize \"ADD with 0\" later"
414
+ ],
415
+ "metadata": {
416
+ "colab": {
417
+ "base_uri": "https://localhost:8080/"
418
+ },
419
+ "id": "WLKSTVa8fRF8",
420
+ "outputId": "27042d38-0044-4814-bbfa-8a2c2d87ee06"
421
+ },
422
+ "execution_count": 20,
423
+ "outputs": [
424
+ {
425
+ "output_type": "stream",
426
+ "name": "stdout",
427
+ "text": [
428
+ "Test 1 (simple ADD fold):\n",
429
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 3, 'src2': None}\n",
430
+ " 1: {'op': 'CONST', 'dest': 'b', 'src1': 5, 'src2': None}\n",
431
+ " 2: {'op': 'CONST', 'dest': 'c', 'src1': 8, 'src2': None}\n",
432
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
433
+ "\n",
434
+ "Test 2 (chained fold):\n",
435
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 3, 'src2': None}\n",
436
+ " 1: {'op': 'CONST', 'dest': 'b', 'src1': 5, 'src2': None}\n",
437
+ " 2: {'op': 'CONST', 'dest': 'c', 'src1': 8, 'src2': None}\n",
438
+ " 3: {'op': 'CONST', 'dest': 'd', 'src1': 40, 'src2': None}\n",
439
+ " 4: {'op': 'STORE', 'dest': 'addr0', 'src1': 'd', 'src2': None}\n",
440
+ "\n",
441
+ "Test 3 (DIV by zero refused):\n",
442
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 10, 'src2': None}\n",
443
+ " 1: {'op': 'CONST', 'dest': 'b', 'src1': 0, 'src2': None}\n",
444
+ " 2: {'op': 'DIV', 'dest': 'c', 'src1': 10, 'src2': 0}\n",
445
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
446
+ "\n",
447
+ "Test 4 (LOAD makes b unknown, ADD not folded):\n",
448
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 3, 'src2': None}\n",
449
+ " 1: {'op': 'LOAD', 'dest': 'b', 'src1': 'addr0', 'src2': None}\n",
450
+ " 2: {'op': 'ADD', 'dest': 'c', 'src1': 3, 'src2': 'b'}\n",
451
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n",
452
+ "\n",
453
+ "Test 5 (CF on generated Level 1 program, seed=42):\n",
454
+ "Before:\n",
455
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
456
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
457
+ " 2: {'op': 'ADD', 'dest': 'v2', 'src1': 'v0', 'src2': 'v0'}\n",
458
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
459
+ "After:\n",
460
+ " 0: {'op': 'CONST', 'dest': 'v0', 'src1': 1, 'src2': None}\n",
461
+ " 1: {'op': 'CONST', 'dest': 'v1', 'src1': 5, 'src2': None}\n",
462
+ " 2: {'op': 'CONST', 'dest': 'v2', 'src1': 2, 'src2': None}\n",
463
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'v2', 'src2': None}\n",
464
+ "\n",
465
+ "Test 6 (propagation: a=0 substituted into ADD even though b unknown):\n",
466
+ " 0: {'op': 'CONST', 'dest': 'a', 'src1': 0, 'src2': None}\n",
467
+ " 1: {'op': 'LOAD', 'dest': 'b', 'src1': 'addr0', 'src2': None}\n",
468
+ " 2: {'op': 'ADD', 'dest': 'c', 'src1': 0, 'src2': 'b'}\n",
469
+ " 3: {'op': 'STORE', 'dest': 'addr0', 'src1': 'c', 'src2': None}\n"
470
+ ]
471
+ }
472
+ ]
473
+ },
474
+ {
475
+ "cell_type": "code",
476
+ "source": [],
477
+ "metadata": {
478
+ "id": "5vo-KgaffTFR"
479
+ },
480
+ "execution_count": null,
481
+ "outputs": []
482
+ }
483
+ ]
484
+ }
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/.github/workflows/deploy.yml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout Repo
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Push to Hugging Face
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: |
20
+ git config --global user.email "github-actions@github.com"
21
+ git config --global user.name "github-actions"
22
+
23
+ git clone https://user:$HF_TOKEN@huggingface.co/spaces/greedybeserk95/Compilertetris space
24
+
25
+ rsync -av --exclude ".git" ./ space/
26
+
27
+ cd space
28
+ git add .
29
+ git commit -m "Auto deploy from GitHub" || echo "No changes"
30
+ git push
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/README.md CHANGED
@@ -1,11 +1 @@
1
- ---
2
- title: Compilertetris
3
- emoji: ⚡
4
- colorFrom: red
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- short_description: an RL environment which optimizes IR code
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # MetaHackathon2026Finals
 
 
 
 
 
 
 
 
 
 
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/space/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Compilertetris
3
+ emoji: ⚡
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: an RL environment which optimizes IR code
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
space/space/space/space/space/space/space/space/space/space/train.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Toy REINFORCE training for CompilerOptimizationEnv (CPU, no PyTorch).
3
+
4
+ This is meant for the Hugging Face Space: fast smoke training that uses the
5
+ same `runtime_core` mock engine and passes as the Gradio demo.
6
+
7
+ For full GRPO + Unsloth + LLM, use your Colab / GPU notebooks
8
+ (`compiler_optimization_grpo.ipynb`, `role2_deliverable3_*`).
9
+
10
+ CLI: python train.py --episodes 50 --max-steps 8 --seed 0
11
+ Or import: from train import run_toy_training; print(run_toy_training(20, 8, 0))
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import math
19
+ import random
20
+ import textwrap
21
+ from typing import List, Sequence, Tuple
22
+
23
+ from runtime_core import (
24
+ CompilerOptimizationEnv,
25
+ MOCK_PASSES,
26
+ MockEngine,
27
+ SAMPLE_PROGRAM,
28
+ )
29
+
30
+ # Additional tiny programs so the policy is not overfit to a single IR.
31
+ TRAINING_PROGRAMS: List[List[dict]] = [
32
+ SAMPLE_PROGRAM,
33
+ [
34
+ {"op": "const", "dest": "a", "args": ["1"], "type": "int"},
35
+ {"op": "add", "dest": "b", "args": ["a", "a"], "type": "int"},
36
+ {"op": "ret", "args": ["b"]},
37
+ ],
38
+ [
39
+ {"op": "const", "dest": "x", "args": ["2"], "type": "int"},
40
+ {"op": "const", "dest": "y", "args": ["4"], "type": "int"},
41
+ {"op": "mul", "dest": "z", "args": ["x", "y"], "type": "int"},
42
+ {"op": "const", "dest": "k", "args": ["1"], "type": "int"},
43
+ {"op": "add", "dest": "w", "args": ["z", "k"], "type": "int"},
44
+ {"op": "ret", "args": ["w"]},
45
+ ],
46
+ ]
47
+
48
+ ACTIONS: Tuple[str, ...] = tuple(sorted(MOCK_PASSES.keys()))
49
+
50
+
51
+ def _softmax(logits: Sequence[float]) -> List[float]:
52
+ m = max(logits) if logits else 0.0
53
+ ex = [math.exp(x - m) for x in logits]
54
+ s = sum(ex) or 1.0
55
+ return [e / s for e in ex]
56
+
57
+
58
+ def _sample_action(rng: random.Random, logits: List[float]) -> Tuple[int, List[float], float]:
59
+ """Return (action index, prob vector, log prob of chosen action)."""
60
+ p = _softmax(logits)
61
+ u = rng.random()
62
+ acc = 0.0
63
+ idx = len(p) - 1
64
+ for i, pi in enumerate(p):
65
+ acc += pi
66
+ if u <= acc:
67
+ idx = i
68
+ break
69
+ log_p = math.log(p[idx] + 1e-12)
70
+ return idx, p, log_p
71
+
72
+
73
+ def _rollout(
74
+ engine: MockEngine,
75
+ program: List[dict],
76
+ max_steps: int,
77
+ logits: List[float],
78
+ rng: random.Random,
79
+ ) -> Tuple[float, List[Tuple[int, List[float], float]]]:
80
+ """
81
+ One episode. Returns total reward and per-step (action index, prob vector, step reward)
82
+ for REINFORCE with returns G_t = sum of rewards from t onward.
83
+ """
84
+ env = CompilerOptimizationEnv(engine, MOCK_PASSES, max_steps=max_steps)
85
+ env.reset(program)
86
+ total = 0.0
87
+ trace: List[Tuple[int, List[float], float]] = []
88
+ for _ in range(max_steps):
89
+ a_idx, pvec, _ = _sample_action(rng, logits)
90
+ action = ACTIONS[a_idx]
91
+ step = env.step(action)
92
+ r = float(step.reward)
93
+ total += r
94
+ trace.append((a_idx, pvec, r))
95
+ if step.done:
96
+ break
97
+ return total, trace
98
+
99
+
100
+ def _reinforce_update(
101
+ logits: List[float], trace: List[Tuple[int, List[float], float]], lr: float
102
+ ) -> None:
103
+ """REINFORCE with Monte Carlo return G_t from each step."""
104
+ G = 0.0
105
+ for t in range(len(trace) - 1, -1, -1):
106
+ a_idx, pvec, r = trace[t]
107
+ G += r
108
+ for i in range(len(logits)):
109
+ delta = 1.0 if i == a_idx else 0.0
110
+ logits[i] += lr * G * (delta - pvec[i])
111
+
112
+
113
+ def run_toy_training(
114
+ episodes: int,
115
+ max_steps: int,
116
+ seed: int = 0,
117
+ lr: float = 0.15,
118
+ ) -> str:
119
+ """
120
+ Train a stateless categorical policy over the three mock passes; print-friendly report.
121
+ """
122
+ if episodes < 1:
123
+ return "episodes must be >= 1"
124
+ if max_steps < 1:
125
+ return "max_steps must be >= 1"
126
+ episodes = int(episodes)
127
+ max_steps = int(max_steps)
128
+ seed = int(seed)
129
+ lr = float(lr)
130
+
131
+ rng = random.Random(seed)
132
+ engine = MockEngine()
133
+ logits = [0.0 for _ in ACTIONS]
134
+
135
+ history: List[Tuple[int, float]] = []
136
+ for ep in range(1, episodes + 1):
137
+ program = TRAINING_PROGRAMS[(ep - 1) % len(TRAINING_PROGRAMS)]
138
+ G, trace = _rollout(engine, program, max_steps, logits, rng)
139
+ if trace:
140
+ _reinforce_update(logits, trace, lr)
141
+ history.append((ep, G))
142
+
143
+ final_p = _softmax(logits)
144
+ lines = [
145
+ "Toy REINFORCE (stateless policy over pass names, CPU, stdlib only)",
146
+ f" episodes={episodes} max_steps={max_steps} seed={seed} lr={lr}",
147
+ f" actions order: {list(ACTIONS)}",
148
+ "",
149
+ f" final logits: {[round(x, 4) for x in logits]}",
150
+ f" final policy: {', '.join(f'{a}={p:.3f}' for a, p in zip(ACTIONS, final_p))}",
151
+ "",
152
+ " return per episode (last 10): " + ", ".join(f"{G:+.1f}" for _, G in history[-10:]),
153
+ "",
154
+ ]
155
+ if history:
156
+ mean_r = sum(G for _, G in history) / len(history)
157
+ lines.append(f" mean return over all episodes: {mean_r:+.3f}")
158
+ lines.append("")
159
+ lines.append(
160
+ textwrap.dedent(
161
+ """
162
+ This does not train an LLM. For GRPO + Qwen + Unsloth, run the project notebooks
163
+ on a GPU machine (e.g. Colab), not the CPU Space.
164
+ """
165
+ ).strip()
166
+ )
167
+ return "\n".join(lines)
168
+
169
+
170
+ def _parse_args() -> argparse.Namespace:
171
+ p = argparse.ArgumentParser(description="Toy REINFORCE on CompilerOptimizationEnv (CPU)")
172
+ p.add_argument("--episodes", type=int, default=50, help="Number of training episodes")
173
+ p.add_argument("--max-steps", type=int, default=8, help="max_steps per env episode")
174
+ p.add_argument("--seed", type=int, default=0, help="RNG seed")
175
+ p.add_argument("--lr", type=float, default=0.15, help="REINFORCE learning rate")
176
+ p.add_argument(
177
+ "--out-json",
178
+ type=str,
179
+ default="",
180
+ help="If set, write a small run summary to this path (e.g. training_log.json).",
181
+ )
182
+ return p.parse_args()
183
+
184
+
185
+ def main() -> None:
186
+ args = _parse_args()
187
+ report = run_toy_training(
188
+ episodes=args.episodes,
189
+ max_steps=args.max_steps,
190
+ seed=args.seed,
191
+ lr=args.lr,
192
+ )
193
+ print(report)
194
+ if args.out_json:
195
+ payload = {
196
+ "episodes": args.episodes,
197
+ "max_steps": args.max_steps,
198
+ "seed": args.seed,
199
+ "lr": args.lr,
200
+ "summary_text": report,
201
+ }
202
+ with open(args.out_json, "w", encoding="utf-8") as f:
203
+ json.dump(payload, f, indent=2)
204
+ print(f"\nWrote {args.out_json}")
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()
space/space/space/space/space/space/space/space/untitled folder.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b29f9a98dd93fb2a14b1d3e8409ce51a756b578debde97af1190991bbb65d203
3
+ size 63307
space/space/space/space/space/space/write_colab_synth_notebook.py ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate Compilertetris_GRPO_synthetic_dataset.ipynb (GRPO + random Toy-IR corpus)."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ ROOT = Path(__file__).resolve().parents[1]
6
+ OUT = ROOT / "Compilertetris_GRPO_synthetic_dataset.ipynb"
7
+
8
+
9
+ def cell_md(s: str) -> dict:
10
+ return {
11
+ "cell_type": "markdown",
12
+ "metadata": {},
13
+ "source": [line + "\n" for line in s.strip().split("\n")],
14
+ }
15
+
16
+
17
+ def cell_code(s: str) -> dict:
18
+ return {
19
+ "cell_type": "code",
20
+ "metadata": {},
21
+ "execution_count": None,
22
+ "outputs": [],
23
+ "source": [line + "\n" for line in s.rstrip().split("\n")],
24
+ }
25
+
26
+ cells: list = []
27
+
28
+ cells.append(cell_md("""
29
+ # Compiler Tetris — GRPO with **synthetic Toy-IR corpus** (Colab)
30
+
31
+ This variant builds **50–200+** training prompts from `program_generator.py` (random const/add/mul chains in the same JSON shape as `runtime_core` / Deliverable 2). The original `metahack1` notebook uses a *different* IR schema; that generator is not mixed in here unless you add a converter.
32
+
33
+ | What | Value |
34
+ |------|--------|
35
+ | Space | `greedybeserk95/Compilertetris` |
36
+ | Code root after clone | `/content/Compilertetris` |
37
+ | LoRA output | `/content/compilertetris_lora` |
38
+ | This run uses **shorter** `TRAIN_STEPS` than the 3-example notebook (see config cell). |
39
+ | **Checkpoints** | Under `output_dir` as `checkpoint-*` (see `CHECKPOINT_EVERY` / `KEEP_LAST_N_CHECKPOINTS` in the GRPO cell) |
40
+
41
+ **Tuning:** In the dataset cell, set `N_TRAIN_PROGRAMS` (e.g. 80, 120, 200).
42
+ """))
43
+
44
+ cells.append(
45
+ cell_md(
46
+ """
47
+ ## Checkpoints
48
+
49
+ The GRPO config uses `save_strategy="steps"` and `save_total_limit` so training writes **periodic checkpoints** under `output_dir` (e.g. `/content/grpo_compilertetris/checkpoint-20`, …) and prunes old ones. After a crash, re-run the setup cells, rebuild `trainer`, then use `resume_from_checkpoint=True` (latest) or a **specific path** (see the cell after training).
50
+ """
51
+ )
52
+ )
53
+
54
+ cells.append(cell_code("""
55
+ # --- Central path config ---
56
+
57
+ HF_SPACE_REPO = "https://huggingface.co/spaces/greedybeserk95/Compilertetris"
58
+ HF_ADAPTER_REPO = "greedybeserk95/Compilertetris-grpo-adapter"
59
+ REPO_DIR = "/content/Compilertetris"
60
+ """))
61
+
62
+ cells.append(cell_code("""
63
+ import os, subprocess
64
+
65
+ if os.path.isdir(REPO_DIR + "/.git"):
66
+ subprocess.check_call(["git", "-C", REPO_DIR, "pull", "--ff-only"], timeout=300)
67
+ else:
68
+ subprocess.check_call(["git", "clone", HF_SPACE_REPO, REPO_DIR], timeout=600)
69
+ print("Repo:", REPO_DIR)
70
+ """))
71
+
72
+ cells.append(cell_code("""!nvidia-smi"""))
73
+
74
+ cells.append(cell_code("""
75
+ !pip install -q "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
76
+ !pip install -q trl datasets transformers accelerate peft bitsandbytes
77
+ """))
78
+
79
+ cells.append(cell_code("""
80
+ import sys, json, os
81
+ sys.path.insert(0, REPO_DIR)
82
+
83
+ import torch
84
+ import unsloth
85
+ from unsloth import FastLanguageModel
86
+ from datasets import Dataset
87
+
88
+ from runtime_core import (
89
+ CompilerOptimizationEnv,
90
+ Deliverable2_Formatter,
91
+ MockEngine,
92
+ MOCK_PASSES,
93
+ )
94
+ from program_generator import build_training_program_corpus
95
+
96
+ print("torch", torch.__version__, "cuda", torch.cuda.is_available())
97
+ print("MOCK_PASSES", list(MOCK_PASSES.keys()))
98
+ """))
99
+
100
+
101
+ def _cell_chatml_synth() -> str:
102
+ p0 = """# Qwen2.5 ChatML + synthetic PROGRAMS
103
+ IM_END = \"<\" + \"|im_end|>\"
104
+ """
105
+ p1 = r'''
106
+ import re
107
+
108
+ # How many training programs (50–200 recommended; includes 3 builtins if include_builtins=True)
109
+ N_TRAIN_PROGRAMS = 120
110
+ RANDOM_SEED = 42
111
+
112
+ SYSTEM_PROMPT = f"""You are a compiler optimization agent.
113
+ You will see Toy-IR as pseudo-assembly (Deliverable-2 text).
114
+
115
+ Output ONLY a valid JSON array of optimization pass names IN ORDER.
116
+ Available passes: {", ".join(repr(p) for p in sorted(MOCK_PASSES))}
117
+ Rules: JSON array only; max 8 passes; you may repeat passes."""
118
+
119
+
120
+ def build_prompt(program_list: list, program_id: int) -> str:
121
+ obs = Deliverable2_Formatter.translate_state(program_list)
122
+ return (
123
+ f"<|im_start|>system\n{SYSTEM_PROMPT}{IM_END}\n"
124
+ f"<|im_start|>user\n#PROGRAM_ID:{program_id}\n{obs}{IM_END}\n"
125
+ f"<|im_start|>assistant\n"
126
+ )
127
+
128
+
129
+ def program_from_prompt(prompt: str) -> list:
130
+ m = re.search(r"#PROGRAM_ID:(\d+)", prompt)
131
+ if m:
132
+ i = int(m.group(1))
133
+ if 0 <= i < len(PROGRAMS):
134
+ return PROGRAMS[i]
135
+ if "#PROGRAM_JSON" in prompt:
136
+ tail = prompt.split("#PROGRAM_JSON", 1)[1]
137
+ if IM_END in tail:
138
+ tail = tail.split(IM_END, 1)[0]
139
+ raw = tail.strip()
140
+ if raw:
141
+ return json.loads(raw)
142
+ raise ValueError("cannot resolve program from prompt (expect #PROGRAM_ID:N or #PROGRAM_JSON)")
143
+
144
+
145
+ PROGRAMS = build_training_program_corpus(
146
+ n_total=N_TRAIN_PROGRAMS,
147
+ seed=RANDOM_SEED,
148
+ include_builtins=True,
149
+ )
150
+ print("Corpus size:", len(PROGRAMS))
151
+
152
+ train_dataset = Dataset.from_dict({
153
+ "prompt": [build_prompt(p, i) for i, p in enumerate(PROGRAMS)],
154
+ })
155
+ print("Dataset rows:", len(train_dataset))
156
+ '''.lstrip("\n")
157
+ return p0 + p1
158
+
159
+
160
+ cells.append(cell_code(_cell_chatml_synth()))
161
+
162
+ cells.append(cell_code("""
163
+ def env_reward_for_completion(prompt: str, completion: str, max_env_steps: int = 8) -> float:
164
+ try:
165
+ program = program_from_prompt(prompt)
166
+ except Exception:
167
+ return -8.0
168
+ try:
169
+ actions = Deliverable2_Formatter.extract_action_array(completion)
170
+ except Exception:
171
+ return -5.0
172
+ actions = [str(a).strip() for a in actions][: max_env_steps]
173
+ if not actions:
174
+ return -4.0
175
+ env = CompilerOptimizationEnv(MockEngine(), MOCK_PASSES, max_steps=max_env_steps)
176
+ env.reset(program)
177
+ total = 0.0
178
+ for a in actions:
179
+ step = env.step(a)
180
+ total += float(step.reward)
181
+ if step.done:
182
+ break
183
+ return float(total)
184
+
185
+
186
+ def make_reward_function(max_env_steps: int = 8):
187
+ def reward_func(prompts: list, completions: list, **kwargs) -> list:
188
+ return [env_reward_for_completion(p, c, max_env_steps) for p, c in zip(prompts, completions)]
189
+ return reward_func
190
+ """))
191
+
192
+ cells.append(cell_code("""
193
+ from trl import GRPOConfig, GRPOTrainer
194
+
195
+ MODEL_NAME = "unsloth/Qwen2.5-3B-Instruct"
196
+ MAX_SEQ_LEN = 1024
197
+ MAX_COMPLETION = 256
198
+
199
+ model, tokenizer = FastLanguageModel.from_pretrained(
200
+ model_name=MODEL_NAME,
201
+ max_seq_length=MAX_SEQ_LEN,
202
+ dtype=None,
203
+ load_in_4bit=True,
204
+ )
205
+ model = FastLanguageModel.get_peft_model(
206
+ model,
207
+ r=16, lora_alpha=16, lora_dropout=0.0, bias="none",
208
+ use_gradient_checkpointing="unsloth", random_state=0,
209
+ )
210
+ """))
211
+
212
+ # Shorter run than 200 steps — scale up after smoke test
213
+ cells.append(
214
+ cell_code(
215
+ """
216
+ NUM_GENERATIONS = 2
217
+ LEARNING_RATE = 2e-5
218
+ # Reduced from 200: increase after you verify loss/reward is stable
219
+ TRAIN_STEPS = 80
220
+
221
+ # --- Checkpoints (TrainingArguments / GRPO) ---
222
+ GRPO_OUTPUT_DIR = "/content/grpo_compilertetris"
223
+ CHECKPOINT_EVERY = 20 # save a checkpoint every N global steps
224
+ KEEP_LAST_N_CHECKPOINTS = 5 # on disk; older folders are deleted
225
+
226
+ grpo_config = GRPOConfig(
227
+ output_dir=GRPO_OUTPUT_DIR,
228
+ learning_rate=LEARNING_RATE,
229
+ per_device_train_batch_size=NUM_GENERATIONS,
230
+ gradient_accumulation_steps=1,
231
+ num_generations=NUM_GENERATIONS,
232
+ max_completion_length=MAX_COMPLETION,
233
+ max_prompt_length=MAX_SEQ_LEN,
234
+ remove_unused_columns=False,
235
+ temperature=0.7,
236
+ max_steps=TRAIN_STEPS,
237
+ logging_steps=5,
238
+ save_strategy="steps",
239
+ save_steps=CHECKPOINT_EVERY,
240
+ save_total_limit=KEEP_LAST_N_CHECKPOINTS,
241
+ seed=0,
242
+ report_to="none",
243
+ use_vllm=False,
244
+ )
245
+
246
+ reward_fn = make_reward_function()
247
+
248
+ trainer = GRPOTrainer(
249
+ model=model,
250
+ processing_class=tokenizer,
251
+ reward_funcs=[reward_fn],
252
+ args=grpo_config,
253
+ train_dataset=train_dataset,
254
+ )
255
+ """
256
+ )
257
+ )
258
+
259
+ cells.append(cell_code("""
260
+ print("Starting GRPO (synthetic corpus)…")
261
+ trainer.train()
262
+ print("Done.")
263
+ """))
264
+
265
+ cells.append(
266
+ cell_code(
267
+ """
268
+ # List on-disk checkpoints (for resume or manual export)
269
+ import glob, os
270
+
271
+ ckpts = sorted(
272
+ glob.glob(os.path.join(GRPO_OUTPUT_DIR, "checkpoint-*")),
273
+ key=lambda p: int(p.split("checkpoint-")[-1]) if p.split("checkpoint-")[-1].isdigit() else 0,
274
+ )
275
+ print("Checkpoints in", GRPO_OUTPUT_DIR, ":", len(ckpts))
276
+ for c in ckpts:
277
+ print(" ", c)
278
+ if ckpts:
279
+ print("Latest:", ckpts[-1])
280
+ """
281
+ )
282
+ )
283
+
284
+ cells.append(
285
+ cell_md(
286
+ """
287
+ **Resume after disconnect / crash** — re-run: clone, pip, imports, dataset, reward, **model load**, and **trainer** cells. Then run **one** of:
288
+
289
+ - `trainer.train(resume_from_checkpoint=True)` — continues from the latest `checkpoint-*` in `output_dir`
290
+ - `trainer.train(resume_from_checkpoint="/content/grpo_compilertetris/checkpoint-40")` — specific step
291
+ """
292
+ )
293
+ )
294
+
295
+ cells.append(
296
+ cell_code(
297
+ """
298
+ # Uncomment to resume from the latest checkpoint (run after re-creating `trainer` in a new session)
299
+ # trainer.train(resume_from_checkpoint=True)
300
+ """
301
+ )
302
+ )
303
+
304
+ cells.append(
305
+ cell_code(
306
+ """
307
+ SAVE_DIR = "/content/compilertetris_lora"
308
+ trainer.model.save_pretrained(SAVE_DIR)
309
+ tokenizer.save_pretrained(SAVE_DIR)
310
+ print("Saved to", SAVE_DIR)
311
+ """
312
+ )
313
+ )
314
+
315
+ cells.append(
316
+ cell_code(
317
+ """
318
+ from huggingface_hub import login, HfApi
319
+
320
+ login()
321
+ HfApi().create_repo(HF_ADAPTER_REPO, exist_ok=True, repo_type="model")
322
+ trainer.model.push_to_hub(HF_ADAPTER_REPO, private=True)
323
+ tokenizer.push_to_hub(HF_ADAPTER_REPO, private=True)
324
+ print("Pushed to https://huggingface.co/" + HF_ADAPTER_REPO)
325
+ """
326
+ )
327
+ )
328
+
329
+ if __name__ == "__main__":
330
+ nb = {
331
+ "nbformat": 4,
332
+ "nbformat_minor": 5,
333
+ "metadata": {
334
+ "colab": {"provenance": [], "gpuType": "T4"},
335
+ "kernelspec": {
336
+ "display_name": "Python 3",
337
+ "language": "python",
338
+ "name": "python3",
339
+ },
340
+ "language_info": {"name": "python"},
341
+ },
342
+ "cells": cells,
343
+ }
344
+ OUT.write_text(json.dumps(nb, indent=2), encoding="utf-8")
345
+ print("Wrote", OUT)
space/space/space/space/write_colab_synth_notebook.py CHANGED
@@ -26,19 +26,21 @@ def cell_code(s: str) -> dict:
26
  cells: list = []
27
 
28
  cells.append(cell_md("""
29
- # Compiler Tetris — GRPO with **synthetic Toy-IR corpus** (Colab)
30
 
31
- This variant builds **50–200+** training prompts from `program_generator.py` (random const/add/mul chains in the same JSON shape as `runtime_core` / Deliverable 2). The original `metahack1` notebook uses a *different* IR schema; that generator is not mixed in here unless you add a converter.
 
 
 
32
 
33
  | What | Value |
34
  |------|--------|
35
  | Space | `greedybeserk95/Compilertetris` |
36
- | Code root after clone | `/content/Compilertetris` |
37
- | LoRA output | `/content/compilertetris_lora` |
38
- | This run uses **shorter** `TRAIN_STEPS` than the 3-example notebook (see config cell). |
39
- | **Checkpoints** | Under `output_dir` as `checkpoint-*` (see `CHECKPOINT_EVERY` / `KEEP_LAST_N_CHECKPOINTS` in the GRPO cell) |
40
 
41
- **Tuning:** In the dataset cell, set `N_TRAIN_PROGRAMS` (e.g. 80, 120, 200).
42
  """))
43
 
44
  cells.append(
@@ -105,8 +107,8 @@ IM_END = \"<\" + \"|im_end|>\"
105
  p1 = r'''
106
  import re
107
 
108
- # How many training programs (50–200 recommended; includes 3 builtins if include_builtins=True)
109
- N_TRAIN_PROGRAMS = 120
110
  RANDOM_SEED = 42
111
 
112
  SYSTEM_PROMPT = f"""You are a compiler optimization agent.
@@ -183,7 +185,7 @@ def env_reward_for_completion(prompt: str, completion: str, max_env_steps: int =
183
  return float(total)
184
 
185
 
186
- def make_reward_function(max_env_steps: int = 8):
187
  def reward_func(prompts: list, completions: list, **kwargs) -> list:
188
  return [env_reward_for_completion(p, c, max_env_steps) for p, c in zip(prompts, completions)]
189
  return reward_func
@@ -192,6 +194,7 @@ def make_reward_function(max_env_steps: int = 8):
192
  cells.append(cell_code("""
193
  from trl import GRPOConfig, GRPOTrainer
194
 
 
195
  MODEL_NAME = "unsloth/Qwen2.5-3B-Instruct"
196
  MAX_SEQ_LEN = 1024
197
  MAX_COMPLETION = 256
@@ -204,37 +207,40 @@ model, tokenizer = FastLanguageModel.from_pretrained(
204
  )
205
  model = FastLanguageModel.get_peft_model(
206
  model,
207
- r=16, lora_alpha=16, lora_dropout=0.0, bias="none",
208
  use_gradient_checkpointing="unsloth", random_state=0,
209
  )
 
210
  """))
211
 
212
- # Shorter run than 200 steps scale up after smoke test
213
  cells.append(
214
  cell_code(
215
  """
216
- NUM_GENERATIONS = 2
217
- LEARNING_RATE = 2e-5
218
- # Reduced from 200: increase after you verify loss/reward is stable
219
- TRAIN_STEPS = 80
 
 
 
220
 
221
- # --- Checkpoints (TrainingArguments / GRPO) ---
222
  GRPO_OUTPUT_DIR = "/content/grpo_compilertetris"
223
- CHECKPOINT_EVERY = 20 # save a checkpoint every N global steps
224
- KEEP_LAST_N_CHECKPOINTS = 5 # on disk; older folders are deleted
225
 
226
  grpo_config = GRPOConfig(
227
  output_dir=GRPO_OUTPUT_DIR,
228
  learning_rate=LEARNING_RATE,
229
  per_device_train_batch_size=NUM_GENERATIONS,
230
- gradient_accumulation_steps=1,
231
  num_generations=NUM_GENERATIONS,
232
  max_completion_length=MAX_COMPLETION,
233
  max_prompt_length=MAX_SEQ_LEN,
234
  remove_unused_columns=False,
235
  temperature=0.7,
236
  max_steps=TRAIN_STEPS,
237
- logging_steps=5,
238
  save_strategy="steps",
239
  save_steps=CHECKPOINT_EVERY,
240
  save_total_limit=KEEP_LAST_N_CHECKPOINTS,
@@ -287,7 +293,7 @@ cells.append(
287
  **Resume after disconnect / crash** — re-run: clone, pip, imports, dataset, reward, **model load**, and **trainer** cells. Then run **one** of:
288
 
289
  - `trainer.train(resume_from_checkpoint=True)` — continues from the latest `checkpoint-*` in `output_dir`
290
- - `trainer.train(resume_from_checkpoint="/content/grpo_compilertetris/checkpoint-40")` — specific step
291
  """
292
  )
293
  )
 
26
  cells: list = []
27
 
28
  cells.append(cell_md("""
29
+ # Compiler Tetris — GRPO, **synthetic Toy-IR** (Colab **T4 ~8–10 h** preset)
30
 
31
+ **Runtime (estimate):** after ~20 steps, read `it/s` in the progress bar. Approximate hours `TRAIN_STEPS / (it/s * 3600)`.
32
+ With **`TRAIN_STEPS = 3600`**: at **0.10 it/s** that is about **10 h**; at **0.12 it/s** about **8.3 h**. If you finish much faster/slower, change **`TRAIN_STEPS`** (or the model size / `NUM_GENERATIONS`).
33
+
34
+ **T4 tips:** 3B + 4bit + below settings fit T4; **7B** or very long `MAX_COMPLETION` can OOM. **High-RAM** runtime in Colab helps. Use **checkpoints** (next cells) in case the session dies.
35
 
36
  | What | Value |
37
  |------|--------|
38
  | Space | `greedybeserk95/Compilertetris` |
39
+ | Code root | `/content/Compilertetris` |
40
+ | LoRA out | `/content/compilertetris_lora` |
41
+ | **Checkpoints** | `/content/grpo_compilertetris/checkpoint-*` |
 
42
 
43
+ `program_generator` Toy-IR in `runtime_core` shape; `metahack1` uses a different schema.
44
  """))
45
 
46
  cells.append(
 
107
  p1 = r'''
108
  import re
109
 
110
+ # T4: 200–300 is a good tradeoff (RAM + diversity)
111
+ N_TRAIN_PROGRAMS = 250
112
  RANDOM_SEED = 42
113
 
114
  SYSTEM_PROMPT = f"""You are a compiler optimization agent.
 
185
  return float(total)
186
 
187
 
188
+ def make_reward_function(max_env_steps: int = 10):
189
  def reward_func(prompts: list, completions: list, **kwargs) -> list:
190
  return [env_reward_for_completion(p, c, max_env_steps) for p, c in zip(prompts, completions)]
191
  return reward_func
 
194
  cells.append(cell_code("""
195
  from trl import GRPOConfig, GRPOTrainer
196
 
197
+ # --- T4-friendly (3B 4-bit). For A100+ you can try Qwen2.5-7B and MAX_COMPLETION=384. ---
198
  MODEL_NAME = "unsloth/Qwen2.5-3B-Instruct"
199
  MAX_SEQ_LEN = 1024
200
  MAX_COMPLETION = 256
 
207
  )
208
  model = FastLanguageModel.get_peft_model(
209
  model,
210
+ r=32, lora_alpha=32, lora_dropout=0.0, bias="none",
211
  use_gradient_checkpointing="unsloth", random_state=0,
212
  )
213
+ print("Model:", MODEL_NAME, "| max_seq", MAX_SEQ_LEN, "| completion cap", MAX_COMPLETION)
214
  """))
215
 
216
+ # ---- T4 / ~8–10 h wall time: tune TRAIN_STEPS after you see it/s in the first minutes ----
217
  cells.append(
218
  cell_code(
219
  """
220
+ # 4 rollouts per prompt: good for GRPO on T4; 6–8 is heavier (slower, more VRAM)
221
+ NUM_GENERATIONS = 4
222
+ LEARNING_RATE = 1.5e-5
223
+ # Target ~8–10 h on T4 when it/s is ~0.10–0.12 (typical for this stack). Re-tune if your it/s differs.
224
+ # hours ≈ TRAIN_STEPS / (it/s * 3600)
225
+ TRAIN_STEPS = 3600
226
+ GRAD_ACCUM = 2
227
 
 
228
  GRPO_OUTPUT_DIR = "/content/grpo_compilertetris"
229
+ CHECKPOINT_EVERY = 200
230
+ KEEP_LAST_N_CHECKPOINTS = 3
231
 
232
  grpo_config = GRPOConfig(
233
  output_dir=GRPO_OUTPUT_DIR,
234
  learning_rate=LEARNING_RATE,
235
  per_device_train_batch_size=NUM_GENERATIONS,
236
+ gradient_accumulation_steps=GRAD_ACCUM,
237
  num_generations=NUM_GENERATIONS,
238
  max_completion_length=MAX_COMPLETION,
239
  max_prompt_length=MAX_SEQ_LEN,
240
  remove_unused_columns=False,
241
  temperature=0.7,
242
  max_steps=TRAIN_STEPS,
243
+ logging_steps=20,
244
  save_strategy="steps",
245
  save_steps=CHECKPOINT_EVERY,
246
  save_total_limit=KEEP_LAST_N_CHECKPOINTS,
 
293
  **Resume after disconnect / crash** — re-run: clone, pip, imports, dataset, reward, **model load**, and **trainer** cells. Then run **one** of:
294
 
295
  - `trainer.train(resume_from_checkpoint=True)` — continues from the latest `checkpoint-*` in `output_dir`
296
+ - `trainer.train(resume_from_checkpoint="/content/grpo_compilertetris/checkpoint-300")` — example path (use an existing `checkpoint-*` folder)
297
  """
298
  )
299
  )
write_colab_synth_notebook.py CHANGED
@@ -36,11 +36,13 @@ With **`TRAIN_STEPS = 3600`**: at **0.10 it/s** that is about **10 h**; at **0.1
36
  | What | Value |
37
  |------|--------|
38
  | Space | `greedybeserk95/Compilertetris` |
39
- | Code root | `/content/Compilertetris` |
40
- | LoRA out | `/content/compilertetris_lora` |
41
- | **Checkpoints** | `/content/grpo_compilertetris/checkpoint-*` |
42
 
43
  `program_generator` — Toy-IR in `runtime_core` shape; `metahack1` uses a different schema.
 
 
44
  """))
45
 
46
  cells.append(
@@ -48,21 +50,49 @@ cells.append(
48
  """
49
  ## Checkpoints
50
 
51
- The GRPO config uses `save_strategy="steps"` and `save_total_limit` so training writes **periodic checkpoints** under `output_dir` (e.g. `/content/grpo_compilertetris/checkpoint-20`, …) and prunes old ones. After a crash, re-run the setup cells, rebuild `trainer`, then use `resume_from_checkpoint=True` (latest) or a **specific path** (see the cell after training).
52
  """
53
  )
54
  )
55
 
56
  cells.append(cell_code("""
57
- # --- Central path config ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  HF_SPACE_REPO = "https://huggingface.co/spaces/greedybeserk95/Compilertetris"
60
  HF_ADAPTER_REPO = "greedybeserk95/Compilertetris-grpo-adapter"
61
- REPO_DIR = "/content/Compilertetris"
 
 
 
 
 
 
 
62
  """))
63
 
64
  cells.append(cell_code("""
65
  import os, subprocess
 
 
 
 
66
 
67
  if os.path.isdir(REPO_DIR + "/.git"):
68
  subprocess.check_call(["git", "-C", REPO_DIR, "pull", "--ff-only"], timeout=300)
@@ -74,16 +104,22 @@ print("Repo:", REPO_DIR)
74
  cells.append(cell_code("""!nvidia-smi"""))
75
 
76
  cells.append(cell_code("""
77
- !pip install -q "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
78
- !pip install -q trl datasets transformers accelerate peft bitsandbytes
 
 
 
 
 
79
  """))
80
 
81
  cells.append(cell_code("""
 
 
82
  import sys, json, os
83
  sys.path.insert(0, REPO_DIR)
84
 
85
  import torch
86
- import unsloth
87
  from unsloth import FastLanguageModel
88
  from datasets import Dataset
89
 
@@ -225,7 +261,7 @@ LEARNING_RATE = 1.5e-5
225
  TRAIN_STEPS = 3600
226
  GRAD_ACCUM = 2
227
 
228
- GRPO_OUTPUT_DIR = "/content/grpo_compilertetris"
229
  CHECKPOINT_EVERY = 200
230
  KEEP_LAST_N_CHECKPOINTS = 3
231
 
@@ -268,6 +304,101 @@ trainer.train()
268
  print("Done.")
269
  """))
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  cells.append(
272
  cell_code(
273
  """
@@ -293,7 +424,7 @@ cells.append(
293
  **Resume after disconnect / crash** — re-run: clone, pip, imports, dataset, reward, **model load**, and **trainer** cells. Then run **one** of:
294
 
295
  - `trainer.train(resume_from_checkpoint=True)` — continues from the latest `checkpoint-*` in `output_dir`
296
- - `trainer.train(resume_from_checkpoint="/content/grpo_compilertetris/checkpoint-300")` — example path (use an existing `checkpoint-*` folder)
297
  """
298
  )
299
  )
@@ -310,7 +441,7 @@ cells.append(
310
  cells.append(
311
  cell_code(
312
  """
313
- SAVE_DIR = "/content/compilertetris_lora"
314
  trainer.model.save_pretrained(SAVE_DIR)
315
  tokenizer.save_pretrained(SAVE_DIR)
316
  print("Saved to", SAVE_DIR)
 
36
  | What | Value |
37
  |------|--------|
38
  | Space | `greedybeserk95/Compilertetris` |
39
+ | Code root | `REPO_DIR` (printed in the “paths” cell) |
40
+ | LoRA out | `LORA_DIR` (printed in the “paths” cell) |
41
+ | **Checkpoints** | `GRPO_OUTPUT_DIR + "/checkpoint-*"` |
42
 
43
  `program_generator` — Toy-IR in `runtime_core` shape; `metahack1` uses a different schema.
44
+
45
+ **Training evidence (loss + reward plots):** the cell *after* `trainer.train()` saves `training_loss_and_reward.png` under `output_dir` and shows it in the notebook — use it in your README / writeup.
46
  """))
47
 
48
  cells.append(
 
50
  """
51
  ## Checkpoints
52
 
53
+ The GRPO config uses `save_strategy="steps"` and `save_total_limit` so training writes **periodic checkpoints** under `output_dir` (e.g. `$GRPO_OUTPUT_DIR/checkpoint-20`, …) and prunes old ones. After a crash, re-run the setup cells, rebuild `trainer`, then use `resume_from_checkpoint=True` (latest) or a **specific path** (see the cell after training).
54
  """
55
  )
56
  )
57
 
58
  cells.append(cell_code("""
59
+ # --- Central path config (works on Colab *and* local / HF runtimes) ---
60
+
61
+ import os
62
+ import tempfile
63
+ from pathlib import Path
64
+
65
+ def _default_workspace_base() -> str:
66
+ # Colab: /content is writable
67
+ c = "/content"
68
+ if os.path.isdir(c) and os.access(c, os.W_OK):
69
+ return str(Path(c) / "work")
70
+
71
+ # Otherwise: a guaranteed-writable temp dir
72
+ return str(Path(tempfile.gettempdir()) / "compilertetris_work")
73
+
74
+
75
+ BASE = os.environ.get("COMPILERTETRIS_BASE", _default_workspace_base())
76
+ os.makedirs(BASE, exist_ok=True)
77
 
78
  HF_SPACE_REPO = "https://huggingface.co/spaces/greedybeserk95/Compilertetris"
79
  HF_ADAPTER_REPO = "greedybeserk95/Compilertetris-grpo-adapter"
80
+ REPO_DIR = str(Path(BASE) / "Compilertetris")
81
+ GRPO_OUTPUT_DIR = str(Path(BASE) / "grpo_compilertetris")
82
+ LORA_DIR = str(Path(BASE) / "compilertetris_lora")
83
+
84
+ print("BASE :", BASE)
85
+ print("REPO_DIR :", REPO_DIR)
86
+ print("GRPO_OUTPUT :", GRPO_OUTPUT_DIR)
87
+ print("LORA_DIR :", LORA_DIR)
88
  """))
89
 
90
  cells.append(cell_code("""
91
  import os, subprocess
92
+ from pathlib import Path
93
+
94
+ # Ensure parent is writable/created
95
+ Path(REPO_DIR).parent.mkdir(parents=True, exist_ok=True)
96
 
97
  if os.path.isdir(REPO_DIR + "/.git"):
98
  subprocess.check_call(["git", "-C", REPO_DIR, "pull", "--ff-only"], timeout=300)
 
104
  cells.append(cell_code("""!nvidia-smi"""))
105
 
106
  cells.append(cell_code("""
107
+ # One-shot install (A100 + CUDA 12.x):
108
+ # - Unsloth's `cu124-ampere-torch260` extra pulls a **Torch 2.6** CUDA stack (fixes `torch.int1` / torchao mismatches)
109
+ # - Pin TRL+Transformers to a Unsloth-tested pair (per Unsloth issue threads around torch2.6 / transformers4.55.x)
110
+ !python -m pip install -U --no-cache-dir \
111
+ "transformers==4.55.4" "trl==0.20.0" \
112
+ matplotlib datasets accelerate peft bitsandbytes safetensors huggingface_hub sentencepiece \
113
+ "unsloth[cu124-ampere-torch260] @ git+https://github.com/unslothai/unsloth.git"
114
  """))
115
 
116
  cells.append(cell_code("""
117
+ # `import unsloth` should come before `transformers` is imported (happens via other imports).
118
+ import unsloth
119
  import sys, json, os
120
  sys.path.insert(0, REPO_DIR)
121
 
122
  import torch
 
123
  from unsloth import FastLanguageModel
124
  from datasets import Dataset
125
 
 
261
  TRAIN_STEPS = 3600
262
  GRAD_ACCUM = 2
263
 
264
+ # GRPO_OUTPUT_DIR is set in the "Central path config" cell
265
  CHECKPOINT_EVERY = 200
266
  KEEP_LAST_N_CHECKPOINTS = 3
267
 
 
304
  print("Done.")
305
  """))
306
 
307
+ cells.append(
308
+ cell_code(
309
+ r"""
310
+ # --- Loss + reward plots (submission): from trainer.state.log_history after train() ---
311
+
312
+ !pip install -q matplotlib
313
+
314
+ import os
315
+ import matplotlib
316
+ matplotlib.use("Agg")
317
+ import matplotlib.pyplot as plt
318
+
319
+ PLOT_DIR = GRPO_OUTPUT_DIR
320
+ os.makedirs(PLOT_DIR, exist_ok=True)
321
+ PNG_PATH = os.path.join(PLOT_DIR, "training_loss_and_reward.png")
322
+
323
+ def _reward_from_log_row(h):
324
+ v = h.get("reward")
325
+ if isinstance(v, (int, float)):
326
+ return float(v), "reward"
327
+ best_k, best_v = None, None
328
+ for k, v in h.items():
329
+ if not isinstance(v, (int, float)):
330
+ continue
331
+ klow = k.replace("-", "/").lower()
332
+ if "reward" not in klow or "std" in klow or "clip" in klow or "per_token" in klow:
333
+ continue
334
+ if "mean" in klow or k == "reward":
335
+ return float(v), k
336
+ if "mean" not in klow and best_k is None:
337
+ best_k, best_v = k, float(v)
338
+ if best_k is not None:
339
+ return best_v, best_k
340
+ return None, None
341
+
342
+ def extract_series(history):
343
+ sl, vl, sr, vr = [], [], [], []
344
+ rlabel = None
345
+ for h in history:
346
+ s = h.get("step")
347
+ if s is None:
348
+ continue
349
+ lo = h.get("loss")
350
+ if isinstance(lo, (int, float)):
351
+ sl.append(s)
352
+ vl.append(float(lo))
353
+ r_val, rk = _reward_from_log_row(h)
354
+ if r_val is not None and rk:
355
+ if rlabel is None:
356
+ rlabel = rk
357
+ if rk == rlabel:
358
+ sr.append(s)
359
+ vr.append(r_val)
360
+ return (sl, vl, "loss"), (sr, vr, rlabel or "reward")
361
+
362
+ (loss_s, loss_v, _lk), (rew_s, rew_v, rew_lab) = extract_series(trainer.state.log_history)
363
+ print("Points — loss:", len(loss_v), "| reward:", len(rew_v))
364
+ if trainer.state.log_history:
365
+ print("Last log row keys (sample):", list(trainer.state.log_history[-1].keys())[:25])
366
+
367
+ fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
368
+ if loss_v:
369
+ ax0.plot(loss_s, loss_v, "b.-", label="loss", linewidth=1, markersize=2)
370
+ ax0.set_ylabel("training loss")
371
+ ax0.set_title("GRPO (this Colab run)")
372
+ ax0.grid(True, alpha=0.3)
373
+ ax0.legend()
374
+ else:
375
+ ax0.text(0.5, 0.5, "No 'loss' in log_history", ha="center", transform=ax0.transAxes)
376
+
377
+ if rew_v:
378
+ ax1.plot(rew_s, rew_v, "g.-", label=rew_lab, linewidth=1, markersize=2)
379
+ ax1.set_ylabel("mean reward" if "mean" in (rew_lab or "") else "reward")
380
+ ax1.set_xlabel("global step")
381
+ ax1.grid(True, alpha=0.3)
382
+ ax1.legend()
383
+ else:
384
+ ax1.text(0.5, 0.5, "No reward column found — see keys above", ha="center", transform=ax1.transAxes)
385
+ ax1.set_xlabel("global step")
386
+
387
+ plt.tight_layout()
388
+ plt.savefig(PNG_PATH, dpi=150, bbox_inches="tight")
389
+ print("Saved:", PNG_PATH)
390
+ try:
391
+ from IPython.display import Image, display
392
+ display(Image(PNG_PATH))
393
+ except Exception as e:
394
+ print("Display:", e)
395
+ finally:
396
+ plt.close("all")
397
+ print("If keys differ: print(trainer.state.log_history[-1])")
398
+ """
399
+ )
400
+ )
401
+
402
  cells.append(
403
  cell_code(
404
  """
 
424
  **Resume after disconnect / crash** — re-run: clone, pip, imports, dataset, reward, **model load**, and **trainer** cells. Then run **one** of:
425
 
426
  - `trainer.train(resume_from_checkpoint=True)` — continues from the latest `checkpoint-*` in `output_dir`
427
+ - `trainer.train(resume_from_checkpoint="<your GRPO_OUTPUT_DIR>/checkpoint-300")` — example (use an existing `checkpoint-*` folder; print `GRPO_OUTPUT_DIR` from the path cell)
428
  """
429
  )
430
  )
 
441
  cells.append(
442
  cell_code(
443
  """
444
+ SAVE_DIR = LORA_DIR
445
  trainer.model.save_pretrained(SAVE_DIR)
446
  tokenizer.save_pretrained(SAVE_DIR)
447
  print("Saved to", SAVE_DIR)