惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
Engineering at Meta
Engineering at Meta
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
I
InfoQ
S
SegmentFault 最新的问题
博客园 - 叶小钗
N
Netflix TechBlog - Medium
Y
Y Combinator Blog
IT之家
IT之家
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
月光博客
月光博客
The Cloudflare Blog
U
Unit 42
GbyAI
GbyAI
L
LangChain Blog
Microsoft Azure Blog
Microsoft Azure Blog

博客园 - aiplus

QuickForm CLI 4K DIY行车记录仪 再生制动二次惩罚损失 MuJoCo 免费开源的物理计算内核 https://sim.luwudynamics.ai/ 意识可以靠开会灌输;执行必须靠机制驱动 rig-puppy python 编辑方式 青少年AI编程机构:270°沉浸式数智空间实际价值 & 规避沦为普通大屏风险 270°全景大屏沉浸式数智学习空间 VS 常规智慧教室智能黑板 职场沟通复盘:高效做事,更要有效共情 ai新闻 阿里云平台攻防态势 quickclass生成论文初稿 quickclass课题生成文献检查 公司官网与产品推广方案(含 GEO 落地) rig-puppy mcp服务 相同的商业套路,为什么可以在各个行业搞戈壁徒步 quickclass 草稿 博文阅读密码验证 - 博客园 WorkBuddy+QuickClass联合使用 发展高阶思维,教育何为 键盘检测 RIG-puppy 图形化编程 机器狗应用 rig-puppy 机器狗 maven 下载安装 博文阅读密码验证 - 博客园 一句话生成专业 PPT 阿里云wan2.7-image-pro 试用 博文阅读密码验证 - 博客园 一次真实电商上新决策:用小浣熊完成蓝牙耳机爆款分析与汇报
网页中的 python解释器
aiplus · 2026-09-10 · via 博客园 - aiplus
请用**单个 HTML 文件**(内联 CSS + 原生 JavaScript,无任何外部依赖、无 CDN)制作一个可在浏览器和 Pad 上直接运行的互动探究网页,主题是**「Python 基础语法:看见代码是怎么跑起来的」**。

## 一、核心机制(最关键,必须真实计算)

网页内置一个**用 JavaScript 真正实现的小型 Python 解释器**,它不是播放预设动画,而是**真的解析并执行学生输入的代码**。必须支持以下语法子集:

- 赋值语句 `x = 表达式`,增强赋值 `+= -= *= /=`
- 数据类型自动识别:整数 int、小数 float、字符串 str、布尔 bool、None
- 算术运算符:`+ - * / // % **` 与括号、一元负号
- 比较运算符 `== != > < >= <=`,逻辑运算符 `and or not`
- `print(...)` 输出,可打印多个值(逗号分隔,用空格连接)
- `#` 注释
- `if / elif / else` 分支,靠**缩进**判定代码块
- `for i in range(a, b, step):` 循环(支持 range 单参、双参、三参)
- `while 条件:` 循环(要有最大步数保护,比如 2000 步后提示"循环次数过多")
- 字符串:单双引号、`+` 拼接、`*` 重复、`len()`
- `input()` 不支持,遇到时提示"本实验室暂不支持 input"

**执行模型**:整个解释器用 JS 的 generator 函数实现,每执行一个基本动作(一次赋值、一次条件判断、一次打印、一次循环迭代)就 `yield` 一次,同时输出一个快照:`{ 当前行号, 变量表快照(名/值/类型), 输出行数组, 已执行步数 }`。前端根据这些快照做单步与连续播放——**变量值必须由真实运算得出,不能硬编码**。

**真实错误处理**,报错信息要接近真实 Python:
- 缩进错误 → `IndentationError: expected an indented block (line N)`
- 使用未定义变量 → `NameError: name 'x' is not defined`
- 字符串加数字 → `TypeError: can only concatenate str (not "int") to str`
- 除以零 → `ZeroDivisionError: division by zero`
- 语法错误 → `SyntaxError: invalid syntax (line N)`
报错时高亮出错行,并在控制台输出红色错误信息,执行终止。

## 二、界面布局(宽屏左右分栏,窄屏上下堆叠)

**左栏:代码区**
- 深色背景(#1e2430)等宽字体代码编辑框,左侧有灰色行号栏
- 必须**关闭自动换行**(`wrap="off"`),行高固定 24px,以便精确对齐
- 当前正在执行的那一行,用一条半透明琥珀色高亮横条覆盖(通过 `top = (行号-1)*24 - 滚动偏移` 动态计算),**单步执行时必须能看到高亮条一行一行往下跳**
- 支持简单的语法着色(关键字紫色、数字绿色、字符串橙色、注释灰色)——用 textarea 叠底层 + 高亮层同步滚动实现

**右栏上部:变量面板("内存观察窗")**
- 每个变量一张卡片:变量名 / 当前值 / 类型徽章
- 类型徽章配色:int 蓝色、float 绿色、str 橙色、bool 紫色
- 某变量的值发生变化时,该卡片**高亮闪烁 0.4 秒**,让学生一眼看到"这一步改变了谁"
- 值显示真实精度,字符串带引号显示,便于区分 `5` 和 `"5"`

**右栏下部:输出控制台**
- 白底等宽字体,`print` 的输出按顺序逐行追加

**底部控制条**
- `▶ 连续运行`、`⏭ 单步执行`、`⟲ 重置`
- **速度滑块**:每步间隔 80ms ~ 1200ms,可调
- 步数计数器:`第 N 步`

## 三、探究任务区(不是闯关,是"观察引导")

在代码区上方放一排示例按钮,点击后把对应代码载入编辑器并自动重置。**每个示例下方配一段"观察提示 + 引导性问题"**,用浅色卡片呈现,措辞是引导学生去看、去比较、去改代码:

**示例 1 · 三种除法**
```python
x = 7
y = 2
print(x / y)
print(x // y)
print(x % y)
a = x / y
b = x // y
```
观察提示:单步执行,注意变量面板里 `a` 和 `b` 的类型徽章颜色不一样——一个 int,一个 float。
引导问题:同样是 7 和 2,为什么 `/` 得到 3.5 而 `//` 得到 3?`%` 那个结果代表什么?把 x 改成 8 再跑一遍,`%` 的结果变了吗?

**示例 2 · 循环累加(看变量怎么一步步长大)**
```python
total = 0
for i in range(1, 6):
    total = total + i
    print(i, total)
```
观察提示:把速度调到最慢,盯着变量面板——每走一步,`i` 增加 1,`total` 是**上一次的 total 加上这次的 i**。
引导问题:第 1 次循环后 total 是多少?第 3 次呢?为什么最后一次 print 出来的 total 等于 1+2+3+4+5?如果改成 `range(1, 101)`,你猜 total 会变成多少?先猜,再运行验证。

**示例 3 · 缩进决定代码属于谁**
```python
score = 85
if score >= 60:
    print("及格")
    print("恭喜")
print("检查完毕")
```
观察提示:运行后看输出顺序。
引导问题:把第二个 `print("恭喜")` 前面的 4 个空格删掉,再运行——输出变成什么?为什么?这说明缩进在 Python 里意味着什么?

**示例 4 · 同样的加号,不同的含义**
```python
print(3 + 5)
print("3" + "5")
print("3" * 3)
```
引导问题:`+` 在数字之间做的是什么?在字符串之间做的又是什么?如果写 `print(3 + "5")` 会发生什么?点运行,读一读红色的报错信息,它说清楚问题在哪了吗?

**示例 5 · while 循环**
```python
n = 5
while n > 0:
    print("倒数", n)
    n = n - 1
print("发射")
```
引导问题:如果忘记写 `n = n - 1`,会发生什么?先删掉它运行一次看看(注意保护机制会提示),再想想为什么这一行是不能少的。

**示例 6 · 自由探索**(编辑器默认内容是可自由修改的空白模板 `# 在这里写你的代码`)

## 四、视觉风格与技术要求

- 现代简洁:主色 #2f6fed 蓝,辅助 #f5f7fa 浅灰背景,卡片圆角 12px + 柔和阴影
- 信息层级分明:标题 18px 半粗,正文 14px,代码区等宽字体 Consolas/Menlo
- **"可观察的变化"要最突出**:当前行高亮条、变量值闪烁、类型徽章,这三处视觉反馈要做到位
- 所有控件(按钮、滑块)在鼠标和触摸屏上都好用,按钮最小点击高度 40px
- 禁止使用 `alert()` 弹窗报错,错误显示在控制台面板中
- 页面加载后代码区预置"示例 1",学生点击示例按钮即可切换
- 单文件、可离线运行、可直接放入 iframe 中打开
这是为您生成的一个可直接运行的 HTML 文件,它内置了一个用 JavaScript 实现的小型 Python 解释器,让您能像调试器一样单步执行代码,亲眼看到变量和输出如何一步步变化。
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python 基础语法:看见代码是怎么跑起来的</title>
<style>
  :root{
    --blue:#2f6fed;
    --bg:#f5f7fa;
    --ink:#1f2937;
    --muted:#64748b;
    --code-bg:#1e2430;
    --mono: Consolas, Menlo, Monaco, "Courier New", monospace;
    --line-h: 24px;
    --pad: 12px;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;}
  body{
    background:var(--bg);
    color:var(--ink);
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
    font-size:14px;line-height:1.6;
    padding:16px 18px 46px;
    -webkit-text-size-adjust:100%;
  }
  .topbar{max-width:1420px;margin:0 auto 14px;}
  .topbar h1{font-size:18px;font-weight:600;margin:0 0 6px;letter-spacing:.2px;}
  .topbar p{margin:0;color:#5b6472;font-size:13.5px;}
  .topbar .accent{color:var(--blue);font-weight:600;}

  /* 示例按钮 */
  .examples{max-width:1420px;margin:0 auto 10px;display:flex;flex-wrap:wrap;gap:8px;}
  .ex-btn{
    min-height:40px;padding:0 14px;border-radius:10px;border:1px solid #dbe2ec;background:#fff;
    color:#334155;font-size:13px;cursor:pointer;transition:.16s;font-family:inherit;
  }
  .ex-btn:hover{border-color:var(--blue);color:var(--blue);}
  .ex-btn.active{background:var(--blue);border-color:var(--blue);color:#fff;box-shadow:0 4px 14px rgba(47,111,237,.30);}

  /* 观察提示卡 */
  .hint-card{
    max-width:1420px;margin:0 auto 14px;background:#fff;border-radius:12px;padding:12px 16px;
    box-shadow:0 2px 10px rgba(20,40,80,.06);display:flex;gap:22px;flex-wrap:wrap;
  }
  .hint-block{flex:1 1 320px;min-width:260px;}
  .hint-block b{display:block;font-size:13px;color:var(--blue);margin-bottom:4px;}
  .hint-block p,.hint-block ul{margin:0;font-size:13px;color:#475569;}
  .hint-block ul{padding-left:18px;}
  .hint-block li{margin-bottom:2px;}
  .hint-block code{background:#eef3fb;color:#1d4ed8;padding:1px 5px;border-radius:4px;font-family:var(--mono);font-size:12.5px;}
  .hint-block strong{color:#b45309;}

  /* 主布局 */
  .layout{max-width:1420px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:14px;align-items:start;}
  @media (max-width:920px){.layout{grid-template-columns:1fr;}}

  .panel{background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(20,40,80,.07);overflow:hidden;}
  .panel-head{
    display:flex;justify-content:space-between;align-items:center;gap:10px;
    padding:10px 14px;border-bottom:1px solid #eef1f6;font-size:13px;font-weight:600;color:#334155;
  }
  .panel-head .hint-mini{font-weight:400;color:#94a3b8;font-size:12px;}

  /* 代码编辑器 */
  .editor{display:flex;background:var(--code-bg);height:430px;position:relative;}
  @media (max-width:920px){.editor{height:340px;}}
  .gutter-wrap{width:46px;flex:none;background:#171c26;overflow:hidden;position:relative;}
  .gutter{position:absolute;top:0;left:0;right:0;padding-top:var(--pad);will-change:transform;}
  .gutter .ln{height:var(--line-h);line-height:var(--line-h);text-align:right;padding-right:10px;
    color:#5b6678;font-family:var(--mono);font-size:13px;user-select:none;}
  .code-wrap{position:relative;flex:1;min-width:0;overflow:hidden;}
  #hl{
    position:absolute;top:0;left:0;margin:0;padding:var(--pad);
    font-family:var(--mono);font-size:14px;line-height:var(--line-h);
    white-space:pre;color:#e6edf3;pointer-events:none;
    width:max-content;min-width:100%;z-index:1;will-change:transform;tab-size:4;
  }
  #hl .c-kw{color:#c792ea;}
  #hl .c-num{color:#7ee787;}
  #hl .c-str{color:#ffab70;}
  #hl .c-com{color:#6b7280;font-style:italic;}
  #hl .c-bi{color:#79c0ff;}
  #hl .c-const{color:#f78c6c;}
  #lineHl{
    position:absolute;left:0;width:100%;height:var(--line-h);
    background:rgba(255,193,7,.16);border-left:3px solid #ffc107;
    z-index:0;pointer-events:none;display:none;
  }
  #lineHl.error{background:rgba(239,68,68,.24);border-left-color:#ef4444;}
  #code{
    position:absolute;top:0;left:0;width:100%;height:100%;margin:0;padding:var(--pad);
    background:transparent;color:transparent;caret-color:#ffd479;border:none;outline:none;resize:none;
    font-family:var(--mono);font-size:14px;line-height:var(--line-h);
    white-space:pre;overflow:auto;z-index:2;tab-size:4;
  }
  #code::selection{background:rgba(47,111,237,.45);}
  .exec-note{
    font-family:var(--mono);font-size:12.5px;color:#94a3b8;padding:7px 14px;background:#fafbfd;
    border-top:1px solid #eef1f6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .exec-note.err{color:#dc2626;background:#fff5f5;}

  /* 变量面板 */
  .vars-panel{padding:12px;display:flex;flex-wrap:wrap;gap:10px;min-height:152px;max-height:250px;overflow:auto;background:#fcfdff;}
  .empty{color:#94a3b8;font-size:13px;padding:16px 6px;}
  .var-card{
    position:relative;background:#fff;border:1px solid #eef1f6;border-left:4px solid #cbd5e1;
    border-radius:10px;padding:8px 12px 8px 10px;min-width:116px;
  }
  @keyframes flashCard{
    0%{background:#fff7d6;box-shadow:0 0 0 3px rgba(255,193,7,.55);}
    100%{background:#fff;box-shadow:0 0 0 0 rgba(255,193,7,0);}
  }
  .var-card.flash{animation:flashCard .4s ease-out;}
  .var-name{font-family:var(--mono);font-size:13px;color:#64748b;}
  .var-value{font-family:var(--mono);font-size:17px;font-weight:600;color:#0f172a;margin:2px 0 4px;word-break:break-all;}
  .badge{display:inline-block;font-size:11px;padding:0 7px;border-radius:999px;font-family:var(--mono);line-height:18px;}
  .t-int{border-left-color:#2f6fed;} .b-int{background:#e8f0fe;color:#1d4ed8;}
  .t-float{border-left-color:#16a34a;} .b-float{background:#e7f7ee;color:#15803d;}
  .t-str{border-left-color:#ea580c;} .b-str{background:#fff2e8;color:#c2410c;}
  .t-bool{border-left-color:#9333ea;} .b-bool{background:#f3e8ff;color:#7e22ce;}
  .t-NoneType{border-left-color:#6b7280;} .b-NoneType{background:#f1f5f9;color:#475569;}
  .t-range{border-left-color:#0891b2;} .b-range{background:#e0f7fa;color:#0e7490;}

  /* 控制台 */
  .console{height:214px;overflow:auto;padding:10px 12px;background:#fbfcfe;
    font-family:var(--mono);font-size:13px;line-height:1.55;}
  .out-line{color:#1e2430;white-space:pre-wrap;word-break:break-word;}
  .err-line{color:#dc2626;white-space:pre-wrap;word-break:break-word;}
  .info-line{color:#16a34a;white-space:pre-wrap;}

  /* 控制条 */
  .controlbar{
    max-width:1420px;margin:14px auto 0;background:#fff;border-radius:12px;padding:12px 14px;
    box-shadow:0 2px 12px rgba(20,40,80,.07);display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  }
  .btn{
    min-height:40px;padding:0 16px;border-radius:10px;border:1px solid #dbe2ec;background:#fff;
    color:#334155;font-size:14px;cursor:pointer;font-family:inherit;transition:.16s;
  }
  .btn:hover{border-color:var(--blue);color:var(--blue);}
  .btn.primary{background:var(--blue);border-color:var(--blue);color:#fff;}
  .btn.primary:hover{background:#2560d8;color:#fff;}
  .btn:disabled{opacity:.45;cursor:not-allowed;}
  .speed{display:flex;align-items:center;gap:9px;margin-left:auto;}
  .speed label{font-size:13px;color:#64748b;white-space:nowrap;}
  .speed input[type=range]{width:170px;accent-color:var(--blue);height:36px;cursor:pointer;}
  .speed .val{font-family:var(--mono);font-size:12.5px;color:#475569;min-width:82px;}
  .stepcounter{font-size:13px;color:#475569;}
  .stepcounter b{font-family:var(--mono);color:var(--blue);font-size:15px;}
  @media (max-width:640px){
    .speed{margin-left:0;width:100%;}
    .speed input[type=range]{flex:1;}
  }
</style>
</head>
<body>

<div class="topbar">
  <h1>Python 基础语法 · 看见代码是怎么跑起来的</h1>
  <p>右边那块「<span class="accent">内存观察窗</span>」会随着代码的执行实时变化。点 <b>⏭ 单步执行</b>,让每一行慢慢跑,看清楚变量是什么时候、被改成了什么。</p>
</div>

<div class="examples" id="examples"></div>

<div class="hint-card" id="hintCard"></div>

<div class="layout">
  <!-- 左栏:代码 -->
  <section class="panel">
    <div class="panel-head">
      <span>代码区</span>
      <span class="hint-mini">可自由编辑 · 缩进用 4 个空格 · 支持 if / for / while / print</span>
    </div>
    <div class="editor">
      <div class="gutter-wrap"><div class="gutter" id="gutter"></div></div>
      <div class="code-wrap" id="codeWrap">
        <div id="lineHl"></div>
        <pre id="hl"></pre>
        <textarea id="code" wrap="off" spellcheck="false" autocorrect="off" autocapitalize="off"></textarea>
      </div>
    </div>
    <div class="exec-note" id="execNote">准备就绪 —— 点击「▶ 连续运行」或「⏭ 单步执行」开始</div>
  </section>

  <!-- 右栏 -->
  <section style="display:flex;flex-direction:column;gap:14px;min-width:0;">
    <div class="panel">
      <div class="panel-head">
        <span>内存观察窗(变量)</span>
        <span class="hint-mini">值变化的卡片会闪一下</span>
      </div>
      <div class="vars-panel" id="varsPanel"></div>
    </div>
    <div class="panel">
      <div class="panel-head">
        <span>输出控制台</span>
        <span class="hint-mini">print 的结果逐行出现在这里</span>
      </div>
      <div class="console" id="consoleOut"></div>
    </div>
  </section>
</div>

<div class="controlbar">
  <button class="btn primary" id="btnRun">▶ 连续运行</button>
  <button class="btn" id="btnStep">⏭ 单步执行</button>
  <button class="btn" id="btnReset">⟲ 重置</button>
  <div class="speed">
    <label>速度 快</label>
    <input type="range" id="speed" min="80" max="1200" step="20" value="420">
    <label>慢</label>
    <span class="val" id="speedLabel">420 ms / 步</span>
  </div>
  <div class="stepcounter">第 <b id="stepNum">0</b> 步</div>
</div>

<script>
(function(){
'use strict';

/* =========================================================
   一、值模型(区分 int / float / str / bool / NoneType)
   ========================================================= */
const INT   = v => ({t:'int',      v: Math.trunc(v)});
const FLOAT = v => ({t:'float',    v: v});
const STR   = v => ({t:'str',      v: v});
const BOOL  = v => ({t:'bool',     v: !!v});
const NONE  = () => ({t:'NoneType', v: null});
const RANGE = (start, stop, step) => ({t:'range', v:{start, stop, step}});

function floatRepr(x){
  if (!isFinite(x)) return 'inf';
  if (Number.isInteger(x)) return x.toFixed(1);
  return String(x);
}
function repr(v){
  switch(v.t){
    case 'int':   return String(v.v);
    case 'float': return floatRepr(v.v);
    case 'str':   return "'" + v.v.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n') + "'";
    case 'bool':  return v.v ? 'True' : 'False';
    case 'NoneType': return 'None';
    case 'range': return `range(${v.v.start}, ${v.v.stop}${v.v.step!==1 ? ', '+v.v.step : ''})`;
    default: return String(v.v);
  }
}
function strOf(v){ return v.t === 'str' ? v.v : repr(v); }
function tname(v){ return v.t; }
function isNum(v){ return v.t === 'int' || v.t === 'float' || v.t === 'bool'; }
function num(v){ return v.t === 'bool' ? (v.v ? 1 : 0) : v.v; }
function truthy(v){
  switch(v.t){
    case 'int': case 'float': return v.v !== 0;
    case 'bool': return v.v;
    case 'str': return v.v.length > 0;
    case 'NoneType': return false;
    default: return true;
  }
}
function numBin(a,b,r){ return (a.t === 'float' || b.t === 'float') ? FLOAT(r) : INT(r); }

/* =========================================================
   二、错误类型
   ========================================================= */
class PyError extends Error{
  constructor(pyType, msg, line){
    super(msg);
    this.pyType = pyType;
    this.line = line;
  }
}

/* =========================================================
   三、算术 / 比较运算
   ========================================================= */
function addVals(a,b,line){
  if (a.t === 'str' && b.t === 'str') return STR(a.v + b.v);
  if (a.t === 'str' || b.t === 'str'){
    if (a.t === 'str') throw new PyError('TypeError', `can only concatenate str (not "${tname(b)}") to str`, line);
    throw new PyError('TypeError', `unsupported operand type(s) for +: '${tname(a)}' and '${tname(b)}'`, line);
  }
  if (isNum(a) && isNum(b)) return numBin(a,b, num(a)+num(b));
  throw new PyError('TypeError', `unsupported operand type(s) for +: '${tname(a)}' and '${tname(b)}'`, line);
}
function mulVals(a,b,line){
  if (a.t === 'str' && isNum(b)){
    const n = Math.trunc(num(b));
    return STR(n > 0 ? a.v.repeat(n) : '');
  }
  if (isNum(a) && b.t === 'str'){
    const n = Math.trunc(num(a));
    return STR(n > 0 ? b.v.repeat(n) : '');
  }
  if (isNum(a) && isNum(b)) return numBin(a,b, num(a)*num(b));
  throw new PyError('TypeError', `can't multiply sequence by non-int of type '${tname(b)}'`, line);
}
function applyBinOp(op,a,b,line){
  switch(op){
    case '+': return addVals(a,b,line);
    case '-':
      if (!isNum(a) || !isNum(b)) throw new PyError('TypeError', `unsupported operand type(s) for -: '${tname(a)}' and '${tname(b)}'`, line);
      return numBin(a,b, num(a)-num(b));
    case '*': return mulVals(a,b,line);
    case '/': {
      if (!isNum(a) || !isNum(b)) throw new PyError('TypeError', `unsupported operand type(s) for /: '${tname(a)}' and '${tname(b)}'`, line);
      const bv = num(b);
      if (bv === 0) throw new PyError('ZeroDivisionError', 'division by zero', line);
      return FLOAT(num(a) / bv);
    }
    case '//': {
      if (!isNum(a) || !isNum(b)) throw new PyError('TypeError', `unsupported operand type(s) for //: '${tname(a)}' and '${tname(b)}'`, line);
      const bv = num(b);
      if (bv === 0) throw new PyError('ZeroDivisionError', 'integer division or modulo by zero', line);
      const r = Math.floor(num(a) / bv);
      return (a.t === 'float' || b.t === 'float') ? FLOAT(r) : INT(r);
    }
    case '%': {
      if (!isNum(a) || !isNum(b)) throw new PyError('TypeError', `unsupported operand type(s) for %: '${tname(a)}' and '${tname(b)}'`, line);
      const bv = num(b);
      if (bv === 0) throw new PyError('ZeroDivisionError', 'integer division or modulo by zero', line);
      const av = num(a);
      let r = av - Math.floor(av / bv) * bv;
      if (Number.isInteger(av) && Number.isInteger(bv)) r = Math.round(r);
      return (a.t === 'float' || b.t === 'float') ? FLOAT(r) : INT(r);
    }
    case '**': {
      if (!isNum(a) || !isNum(b)) throw new PyError('TypeError', `unsupported operand type(s) for **: '${tname(a)}' and '${tname(b)}'`, line);
      const res = Math.pow(num(a), num(b));
      if (a.t !== 'float' && b.t !== 'float' && num(b) >= 0) return INT(res);
      return FLOAT(res);
    }
  }
  throw new PyError('SyntaxError', 'invalid syntax', line);
}
function looseEq(a,b){
  if (isNum(a) && isNum(b)) return num(a) === num(b);
  if (a.t !== b.t) return false;
  if (a.t === 'NoneType') return true;
  if (a.t === 'range') return a.v.start===b.v.start && a.v.stop===b.v.stop && a.v.step===b.v.step;
  return a.v === b.v;
}
function compareValues(op,a,b,line){
  switch(op){
    case '==': return looseEq(a,b);
    case '!=': return !looseEq(a,b);
  }
  let cmp;
  if (isNum(a) && isNum(b)){
    const x = num(a), y = num(b);
    cmp = x < y ? -1 : (x > y ? 1 : 0);
  } else if (a.t === 'str' && b.t === 'str'){
    cmp = a.v < b.v ? -1 : (a.v > b.v ? 1 : 0);
  } else {
    throw new PyError('TypeError', `'${op}' not supported between instances of '${tname(a)}' and '${tname(b)}'`, line);
  }
  switch(op){
    case '<':  return cmp <  0;
    case '>':  return cmp >  0;
    case '<=': return cmp <= 0;
    case '>=': return cmp >= 0;
  }
  return false;
}

/* =========================================================
   四、表达式词法分析
   ========================================================= */
function tokenizeExpr(src, lineNo){
  const toks = [];
  let i = 0;
  const n = src.length;
  while (i < n){
    const c = src[i];
    if (c === ' ' || c === '\t'){ i++; continue; }
    if (c === '#') break;

    // 数字
    if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(src[i+1] || ''))){
      let j = i, isFloat = false;
      while (j < n && /[0-9]/.test(src[j])) j++;
      if (src[j] === '.'){
        isFloat = true; j++;
        while (j < n && /[0-9]/.test(src[j])) j++;
      }
      if (src[j] === 'e' || src[j] === 'E'){
        let k = j + 1;
        if (src[k] === '+' || src[k] === '-') k++;
        if (/[0-9]/.test(src[k] || '')){
          isFloat = true; j = k;
          while (j < n && /[0-9]/.test(src[j])) j++;
        }
      }
      toks.push({k:'num', text: src.slice(i,j), isFloat});
      i = j; continue;
    }

    // 字符串
    if (c === '"' || c === "'"){
      const quote = c;
      let j = i + 1, val = '', closed = false;
      while (j < n){
        if (src[j] === '\\'){
          const nx = src[j+1];
          const map = {n:'\n', t:'\t', r:'\r', '\\':'\\', "'":"'", '"':'"', '0':'\0'};
          if (nx in map){ val += map[nx]; j += 2; continue; }
          val += nx; j += 2; continue;
        }
        if (src[j] === quote){ closed = true; j++; break; }
        val += src[j]; j++;
      }
      if (!closed) throw new PyError('SyntaxError', 'unterminated string literal', lineNo);
      toks.push({k:'str', text: val});
      i = j; continue;
    }

    // 标识符 / 关键字
    if (/[A-Za-z_]/.test(c)){
      let j = i;
      while (j < n && /[A-Za-z0-9_]/.test(src[j])) j++;
      toks.push({k:'name', text: src.slice(i,j)});
      i = j; continue;
    }

    // 运算符
    const three = src.substr(i,3);
    const two = src.substr(i,2);
    if (three === '**=' || three === '//='){ toks.push({k:'op', text:three}); i += 3; continue; }
    if (['**','//','==','!=','<=','>=','+=','-=','*=','/='].indexOf(two) >= 0){
      toks.push({k:'op', text:two}); i += 2; continue;
    }
    if ('+-*/%()<>,[]=:'.indexOf(c) >= 0){ toks.push({k:'op', text:c}); i++; continue; }

    throw new PyError('SyntaxError', 'invalid syntax', lineNo);
  }
  return toks;
}

/* =========================================================
   五、表达式语法分析(递归下降)
   ========================================================= */
class ExprParser{
  constructor(toks, line){ this.t = toks; this.i = 0; this.line = line; }
  peek(o){ return this.t[this.i + (o||0)]; }
  atOp(v){ const t = this.t[this.i]; return t && t.k === 'op' && t.text === v; }
  atName(v){ const t = this.t[this.i]; return t && t.k === 'name' && t.text === v; }
  err(msg){ throw new PyError('SyntaxError', msg || 'invalid syntax', this.line); }

  parse(){
    if (!this.t.length) this.err('invalid syntax');
    const e = this.parseOr();
    if (this.i < this.t.length) this.err();
    return e;
  }
  parseOr(){
    let l = this.parseAnd();
    while (this.atName('or')){ this.i++; const r = this.parseAnd(); l = {k:'logic', op:'or', l, r}; }
    return l;
  }
  parseAnd(){
    let l = this.parseNot();
    while (this.atName('and')){ this.i++; const r = this.parseNot(); l = {k:'logic', op:'and', l, r}; }
    return l;
  }
  parseNot(){
    if (this.atName('not')){ this.i++; return {k:'not', e: this.parseNot()}; }
    return this.parseCompare();
  }
  parseCompare(){
    let left = this.parseArith();
    const ops = [], operands = [left];
    while (this.peek() && this.peek().k === 'op' && ['==','!=','<','>','<=','>='].indexOf(this.peek().text) >= 0){
      ops.push(this.t[this.i++].text);
      operands.push(this.parseArith());
    }
    if (!ops.length) return left;
    return {k:'cmp', ops, operands};
  }
  parseArith(){
    let l = this.parseTerm();
    while (this.peek() && this.peek().k === 'op' && (this.peek().text === '+' || this.peek().text === '-')){
      const op = this.t[this.i++].text;
      l = {k:'bin', op, l, r: this.parseTerm()};
    }
    return l;
  }
  parseTerm(){
    let l = this.parseUnary();
    while (this.peek() && this.peek().k === 'op' && ['*','/','//','%'].indexOf(this.peek().text) >= 0){
      const op = this.t[this.i++].text;
      l = {k:'bin', op, l, r: this.parseUnary()};
    }
    return l;
  }
  parseUnary(){
    if (this.atOp('-')){ this.i++; return {k:'neg', e: this.parseUnary()}; }
    if (this.atOp('+')){ this.i++; return this.parseUnary(); }
    return this.parsePower();
  }
  parsePower(){
    const base = this.parseAtom();
    if (this.atOp('**')){
      this.i++;
      return {k:'bin', op:'**', l: base, r: this.parseUnary()};
    }
    return base;
  }
  parseAtom(){
    const t = this.peek();
    if (!t) this.err('unexpected end of expression');
    if (t.k === 'num'){ this.i++; return {k:'num', v: Number(t.text), isFloat: t.isFloat}; }
    if (t.k === 'str'){ this.i++; return {k:'str', v: t.text}; }
    if (t.k === 'name'){
      this.i++;
      const nm = t.text;
      if (nm === 'True')  return {k:'bool', v:true};
      if (nm === 'False') return {k:'bool', v:false};
      if (nm === 'None')  return {k:'none'};
      if (this.atOp('(')){
        this.i++;
        const args = [];
        if (!this.atOp(')')){
          args.push(this.parseOr());
          while (this.atOp(',')){ this.i++; args.push(this.parseOr()); }
        }
        if (!this.atOp(')')) this.err('missing closing parenthesis');
        this.i++;
        return {k:'call', name: nm, args};
      }
      return {k:'name', name: nm};
    }
    if (t.k === 'op' && t.text === '('){
      this.i++;
      const e = this.parseOr();
      if (!this.atOp(')')) this.err('missing closing parenthesis');
      this.i++;
      return e;
    }
    this.err('invalid syntax');
  }
}
function parseExprString(src, lineNo){
  return new ExprParser(tokenizeExpr(src, lineNo), lineNo).parse();
}

/* =========================================================
   六、程序结构解析(按缩进分块)
   ========================================================= */
function stripComment(s){
  let quote = null;
  for (let i = 0; i < s.length; i++){
    const c = s[i];
    if (quote){
      if (c === '\\'){ i++; continue; }
      if (c === quote) quote = null;
    } else {
      if (c === '"' || c === "'") quote = c;
      else if (c === '#') return s.slice(0,i);
    }
  }
  return s;
}
function preprocess(src){
  const raw = String(src).replace(/\r\n?/g,'\n').replace(/\t/g,'    ').split('\n');
  const lines = [];
  for (let i = 0; i < raw.length; i++){
    let text = raw[i];
    let indent = 0;
    while (indent < text.length && text[indent] === ' ') indent++;
    let content = stripComment(text.slice(indent)).replace(/\s+$/,'');
    if (content.trim() === '') continue;
    lines.push({indent, text: content, lineNo: i+1});
  }
  return lines;
}
function parseProgram(src){
  const lines = preprocess(src);
  if (!lines.length) return [];
  if (lines[0].indent !== 0) throw new PyError('IndentationError', 'unexpected indent', lines[0].lineNo);
  let pos = 0;

  function parseBlock(indent){
    const stmts = [];
    while (pos < lines.length){
      const L = lines[pos];
      if (L.indent < indent) break;
      if (L.indent > indent) throw new PyError('IndentationError', 'unexpected indent', L.lineNo);
      stmts.push(parseStatement(indent));
    }
    return stmts;
  }

  function parseBody(parentIndent, headerLine){
    if (pos >= lines.length || lines[pos].indent <= parentIndent){
      throw new PyError('IndentationError', 'expected an indented block', headerLine);
    }
    return parseBlock(lines[pos].indent);
  }

  function parseStatement(ind){
    const L = lines[pos];
    const text = L.text;
    const lineNo = L.lineNo;
    let m;

    if ((m = /^if\s+(.+):$/.exec(text))){
      pos++;
      const condNode = parseExprString(m[1], lineNo);
      const branches = [{condNode, line: lineNo, body: parseBody(ind, lineNo)}];
      let elseBody = null;
      while (pos < lines.length){
        const N = lines[pos];
        if (N.indent !== ind) break;
        const t = N.text;
        const em = /^elif\s+(.+):$/.exec(t);
        if (em){
          pos++;
          branches.push({condNode: parseExprString(em[1], N.lineNo), line: N.lineNo, body: parseBody(ind, N.lineNo)});
        } else if (t === 'else:'){
          pos++;
          elseBody = parseBody(ind, N.lineNo);
          break;
        } else break;
      }
      return {kind:'if', branches, elseBody, line: lineNo};
    }

    if ((m = /^for\s+([A-Za-z_]\w*)\s+in\s+(.+):$/.exec(text))){
      pos++;
      const iterNode = parseExprString(m[2], lineNo);
      const body = parseBody(ind, lineNo);
      return {kind:'for', var: m[1], iterNode, body, line: lineNo};
    }

    if ((m = /^while\s+(.+):$/.exec(text))){
      pos++;
      const condNode = parseExprString(m[1], lineNo);
      const body = parseBody(ind, lineNo);
      return {kind:'while', condNode, body, line: lineNo};
    }

    if (/^(elif|else)\b/.test(text)) throw new PyError('SyntaxError', 'invalid syntax', lineNo);

    pos++;
    const am = /^([A-Za-z_]\w*)\s*(\+|-|\*|\/|\/\/|\*\*|%)?=(?!=)([\s\S]*)$/.exec(text);
    if (am){
      const name = am[1], op = am[2] || null, exprSrc = (am[3] || '').trim();
      if (!exprSrc) throw new PyError('SyntaxError', 'invalid syntax', lineNo);
      return {kind:'assign', name, op, expr: parseExprString(exprSrc, lineNo), line: lineNo};
    }
    return {kind:'expr', expr: parseExprString(text, lineNo), line: lineNo};
  }

  return parseBlock(0);
}

/* =========================================================
   七、求值
   ========================================================= */
function* rangeGen(start, stop, step){
  if (step > 0){ for (let i = start; i < stop; i += step) yield INT(i); }
  else if (step < 0){ for (let i = start; i > stop; i += step) yield INT(i); }
}
function makeIterator(v, line){
  if (v.t === 'range') return rangeGen(v.v.start, v.v.stop, v.v.step);
  if (v.t === 'str'){
    return (function*(){ for (const ch of v.v) yield STR(ch); })();
  }
  throw new PyError('TypeError', `'${tname(v)}' object is not iterable`, line);
}

function callFunction(node, ctx, line){
  const name = node.name;
  const args = node.args.map(a => evalExpr(a, ctx, line));
  switch(name){
    case 'print':
      ctx.out.push(args.map(strOf).join(' '));
      return NONE();
    case 'len': {
      if (args.length !== 1) throw new PyError('TypeError', 'len() takes exactly one argument', line);
      const v = args[0];
      if (v.t === 'str') return INT(v.v.length);
      throw new PyError('TypeError', `object of type '${tname(v)}' has no len()`, line);
    }
    case 'range': {
      const nums = args.map(v => {
        if (!isNum(v)) throw new PyError('TypeError', `'${tname(v)}' object cannot be interpreted as an integer`, line);
        return Math.trunc(num(v));
      });
      let start = 0, stop = 0, step = 1;
      if (nums.length === 1) stop = nums[0];
      else if (nums.length === 2){ start = nums[0]; stop = nums[1]; }
      else if (nums.length === 3){ start = nums[0]; stop = nums[1]; step = nums[2]; }
      else throw new PyError('TypeError', `range expected at most 3 arguments, got ${nums.length}`, line);
      if (step === 0) throw new PyError('ValueError', 'range() arg 3 must not be zero', line);
      return RANGE(start, stop, step);
    }
    case 'str':
      return args.length === 1 ? STR(strOf(args[0])) : STR('');
    case 'int': {
      const v = args[0];
      if (!v) throw new PyError('TypeError', 'int() takes exactly one argument', line);
      if (isNum(v)) return INT(Math.trunc(num(v)));
      if (v.t === 'str'){
        const s = v.v.trim();
        const n = Number(s);
        if (s === '' || Number.isNaN(n)) throw new PyError('ValueError', `invalid literal for int() with base 10: '${v.v}'`, line);
        return INT(Math.trunc(n));
      }
      throw new PyError('TypeError', 'int() argument must be a string or a number', line);
    }
    case 'float': {
      const v = args[0];
      if (isNum(v)) return FLOAT(num(v));
      if (v.t === 'str'){
        const n = Number(v.v.trim());
        if (Number.isNaN(n)) throw new PyError('ValueError', `could not convert string to float: '${v.v}'`, line);
        return FLOAT(n);
      }
      throw new PyError('TypeError', 'float() argument must be a string or a number', line);
    }
    case 'abs': {
      const v = args[0];
      if (isNum(v)) return (v.t === 'float') ? FLOAT(Math.abs(v.v)) : INT(Math.abs(num(v)));
      throw new PyError('TypeError', `bad operand type for abs(): '${tname(v)}'`, line);
    }
    case 'input':
      throw new PyError('NotSupportedError', '本实验室暂不支持 input(),请直接给变量赋值来模拟输入', line);
    default:
      throw new PyError('NameError', `name '${name}' is not defined`, line);
  }
}

function evalExpr(node, ctx, line){
  switch(node.k){
    case 'num':  return node.isFloat ? FLOAT(node.v) : INT(node.v);
    case 'str':  return STR(node.v);
    case 'bool': return BOOL(node.v);
    case 'none': return NONE();
    case 'name': {
      if (Object.prototype.hasOwnProperty.call(ctx.env, node.name)) return ctx.env[node.name];
      throw new PyError('NameError', `name '${node.name}' is not defined`, line);
    }
    case 'neg': {
      const v = evalExpr(node.e, ctx, line);
      if (!isNum(v)) throw new PyError('TypeError', `bad operand type for unary -: '${tname(v)}'`, line);
      return v.t === 'float' ? FLOAT(-v.v) : INT(-num(v));
    }
    case 'not': return BOOL(!truthy(evalExpr(node.e, ctx, line)));
    case 'logic': {
      const l = evalExpr(node.l, ctx, line);
      if (node.op === 'and') return truthy(l) ? evalExpr(node.r, ctx, line) : l;
      return truthy(l) ? l : evalExpr(node.r, ctx, line);
    }
    case 'cmp': {
      let left = evalExpr(node.operands[0], ctx, line);
      for (let i = 0; i < node.ops.length; i++){
        const right = evalExpr(node.operands[i+1], ctx, line);
        if (!compareValues(node.ops[i], left, right, line)) return BOOL(false);
        left = right;
      }
      return BOOL(true);
    }
    case 'bin': {
      const l = evalExpr(node.l, ctx, line);
      const r = evalExpr(node.r, ctx, line);
      return applyBinOp(node.op, l, r, line);
    }
    case 'call': return callFunction(node, ctx, line);
  }
  throw new PyError('SyntaxError', 'invalid syntax', line);
}

/* =========================================================
   八、生成器解释器:每执行一个基本动作就 yield 一次快照
   ========================================================= */
const MAX_STEPS = 2000;

function snapshotEnv(env){
  return Object.keys(env).map(k => ({name:k, repr: repr(env[k]), type: env[k].t}));
}
function* tick(ctx, line, note){
  ctx.steps++;
  if (ctx.steps > MAX_STEPS){
    throw new PyError('RuntimeError', '循环次数过多:已执行超过 2000 步,程序被强制停止(检查是否写出了停不下来的循环)', line);
  }
  yield {
    line,
    vars: snapshotEnv(ctx.env),
    out: ctx.out.slice(),
    steps: ctx.steps,
    note: note || ''
  };
}
function* execBlock(stmts, ctx){
  for (let i = 0; i < stmts.length; i++) yield* execStmt(stmts[i], ctx);
}
function* execStmt(st, ctx){
  switch(st.kind){
    case 'assign': {
      const val = evalExpr(st.expr, ctx, st.line);
      if (!st.op){
        ctx.env[st.name] = val;
        yield* tick(ctx, st.line, `${st.name} 被赋值为 ${repr(val)}`);
      } else {
        if (!Object.prototype.hasOwnProperty.call(ctx.env, st.name)){
          throw new PyError('NameError', `name '${st.name}' is not defined`, st.line);
        }
        const cur = ctx.env[st.name];
        const nv = applyBinOp(st.op, cur, val, st.line);
        ctx.env[st.name] = nv;
        yield* tick(ctx, st.line, `${st.name} ${st.op}= ${repr(val)}  →  ${repr(nv)}`);
      }
      return;
    }
    case 'expr': {
      evalExpr(st.expr, ctx, st.line);
      yield* tick(ctx, st.line, '');
      return;
    }
    case 'if': {
      for (let i = 0; i < st.branches.length; i++){
        const br = st.branches[i];
        const c = evalExpr(br.condNode, ctx, br.line);
        yield* tick(ctx, br.line, `条件判断 → ${truthy(c) ? 'True(进入这个分支)' : 'False(跳过)'}`);
        if (truthy(c)){
          yield* execBlock(br.body, ctx);
          return;
        }
      }
      if (st.elseBody) yield* execBlock(st.elseBody, ctx);
      return;
    }
    case 'while': {
      while (true){
        const c = evalExpr(st.condNode, ctx, st.line);
        yield* tick(ctx, st.line, `while 条件 → ${truthy(c) ? 'True(继续循环)' : 'False(退出循环)'}`);
        if (!truthy(c)) break;
        yield* execBlock(st.body, ctx);
      }
      return;
    }
    case 'for': {
      const iterVal = evalExpr(st.iterNode, ctx, st.line);
      const it = makeIterator(iterVal, st.line);
      let r = it.next();
      while (!r.done){
        ctx.env[st.var] = r.value;
        yield* tick(ctx, st.line, `${st.var} = ${repr(r.value)}(开始新一轮循环)`);
        yield* execBlock(st.body, ctx);
        r = it.next();
      }
      return;
    }
  }
}
function* execProgram(stmts, ctx){
  yield* execBlock(stmts, ctx);
}

/* =========================================================
   九、界面
   ========================================================= */
const codeEl   = document.getElementById('code');
const hlEl     = document.getElementById('hl');
const gutterEl = document.getElementById('gutter');
const lineHl   = document.getElementById('lineHl');
const varsPanel= document.getElementById('varsPanel');
const consoleEl= document.getElementById('consoleOut');
const execNote = document.getElementById('execNote');
const btnRun   = document.getElementById('btnRun');
const btnStep  = document.getElementById('btnStep');
const btnReset = document.getElementById('btnReset');
const speedEl  = document.getElementById('speed');
const speedLabel = document.getElementById('speedLabel');
const stepNumEl = document.getElementById('stepNum');
const examplesEl = document.getElementById('examples');
const hintCardEl = document.getElementById('hintCard');

const LINE_H = 24, PAD = 12;

/* ---------- 语法着色 ---------- */
const KEYWORDS = new Set(['if','elif','else','for','while','in','and','or','not','def','return','break','continue','import','from','pass','class','with','as','try','except','finally','lambda','is','global','None']);
const CONSTS   = new Set(['True','False','None']);
const BUILTINS = new Set(['print','range','len','int','float','str','abs','input','list','dict','set','tuple','sum','max','min','type']);

function escapeHtml(s){
  return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
function highlightLine(line){
  let out = '';
  let i = 0;
  while (i < line.length){
    const c = line[i];
    if (c === '#'){
      out += '<span class="c-com">' + escapeHtml(line.slice(i)) + '</span>';
      break;
    }
    if (c === '"' || c === "'"){
      let j = i + 1;
      while (j < line.length){
        if (line[j] === '\\'){ j += 2; continue; }
        if (line[j] === c){ j++; break; }
        j++;
      }
      out += '<span class="c-str">' + escapeHtml(line.slice(i,j)) + '</span>';
      i = j; continue;
    }
    if (/[0-9]/.test(c)){
      let j = i;
      while (j < line.length && /[0-9._]/.test(line[j])) j++;
      out += '<span class="c-num">' + escapeHtml(line.slice(i,j)) + '</span>';
      i = j; continue;
    }
    if (/[A-Za-z_]/.test(c)){
      let j = i;
      while (j < line.length && /[A-Za-z0-9_]/.test(line[j])) j++;
      const w = line.slice(i,j);
      if (KEYWORDS.has(w)) out += '<span class="c-kw">' + w + '</span>';
      else if (CONSTS.has(w)) out += '<span class="c-const">' + w + '</span>';
      else if (BUILTINS.has(w)) out += '<span class="c-bi">' + w + '</span>';
      else out += escapeHtml(w);
      i = j; continue;
    }
    out += escapeHtml(c);
    i++;
  }
  return out;
}
function updateHighlight(){
  const src = codeEl.value;
  const lines = src.split('\n');
  hlEl.innerHTML = lines.map(highlightLine).join('\n') + '\n';
}
function updateGutter(){
  const n = codeEl.value.split('\n').length;
  let html = '';
  for (let i = 1; i <= n; i++) html += '<div class="ln">' + i + '</div>';
  gutterEl.innerHTML = html;
}
let currentLine = 0;
function updateLineBar(){
  if (!currentLine){ lineHl.style.display = 'none'; return; }
  lineHl.style.display = 'block';
  lineHl.style.top = (PAD + (currentLine - 1) * LINE_H - codeEl.scrollTop) + 'px';
}
function setCurrentLine(n){
  currentLine = n || 0;
  updateLineBar();
}
function syncScroll(){
  hlEl.style.transform = 'translate(' + (-codeEl.scrollLeft) + 'px,' + (-codeEl.scrollTop) + 'px)';
  gutterEl.style.transform = 'translateY(' + (-codeEl.scrollTop) + 'px)';
  updateLineBar();
}
codeEl.addEventListener('input', function(){ updateHighlight(); updateGutter(); });
codeEl.addEventListener('scroll', syncScroll);
codeEl.addEventListener('keydown', function(e){
  if (e.key === 'Tab'){
    e.preventDefault();
    const s = codeEl.selectionStart, en = codeEl.selectionEnd;
    codeEl.value = codeEl.value.slice(0,s) + '    ' + codeEl.value.slice(en);
    codeEl.selectionStart = codeEl.selectionEnd = s + 4;
    updateHighlight(); updateGutter();
  }
});

/* ---------- 变量面板 ---------- */
let prevVars = {};
function renderVars(vars){
  if (!vars.length){
    varsPanel.innerHTML = '<div class="empty">变量面板现在是空的 —— 点击「▶ 连续运行」或「⏭ 单步执行」,看变量一个个出现。</div>';
    prevVars = {};
    return;
  }
  const newMap = {};
  for (const v of vars) newMap[v.name] = v.repr;
  let html = '';
  for (const v of vars){
    const changed = prevVars[v.name] !== v.repr;
    html += '<div class="var-card t-' + v.type + (changed ? ' flash' : '') + '">' +
              '<div class="var-name">' + escapeHtml(v.name) + '</div>' +
              '<div class="var-value">' + escapeHtml(v.repr) + '</div>' +
              '<span class="badge b-' + v.type + '">' + v.type + '</span>' +
            '</div>';
  }
  varsPanel.innerHTML = html;
  prevVars = newMap;
}

/* ---------- 控制台 ---------- */
let renderedOut = 0;
function clearConsole(){ consoleEl.innerHTML = ''; renderedOut = 0; }
function renderOut(out){
  while (renderedOut < out.length){
    const d = document.createElement('div');
    d.className = 'out-line';
    d.textContent = out[renderedOut];
    consoleEl.appendChild(d);
    renderedOut++;
  }
  consoleEl.scrollTop = consoleEl.scrollHeight;
}
function appendErr(text){
  String(text).split('\n').forEach(function(t){
    const d = document.createElement('div');
    d.className = 'err-line';
    d.textContent = t;
    consoleEl.appendChild(d);
  });
  consoleEl.scrollTop = consoleEl.scrollHeight;
}
function appendInfo(text){
  const d = document.createElement('div');
  d.className = 'info-line';
  d.textContent = text;
  consoleEl.appendChild(d);
  consoleEl.scrollTop = consoleEl.scrollHeight;
}

/* ---------- 状态 ---------- */
let gen = null;
let ctx = null;
let running = false;
let timer = null;
let dead = false;

function setNote(text, isErr){
  execNote.textContent = text;
  execNote.className = 'exec-note' + (isErr ? ' err' : '');
}
function setStep(n){ stepNumEl.textContent = n; }
function updateRunBtn(){
  btnRun.textContent = running ? '⏸ 暂停' : '▶ 连续运行';
  btnStep.disabled = running;
}
function speedValue(){ return parseInt(speedEl.value, 10); }

/* ---------- 重置 / 准备 ---------- */
function reset(){
  stopRun();
  dead = false;
  clearConsole();
  prevVars = {};
  renderVars([]);
  setStep(0);
  currentLine = 0;
  lineHl.classList.remove('error');
  updateLineBar();
  gen = null; ctx = null;
  try {
    const stmts = parseProgram(codeEl.value);
    ctx = {env:{}, out:[], steps:0};
    gen = execProgram(stmts, ctx);
    setNote('代码已编译完成 —— 点击「▶ 连续运行」或「⏭ 单步执行」开始');
    return true;
  } catch(e){
    if (e instanceof PyError){
      currentLine = e.line || 0;
      lineHl.classList.add('error');
      updateLineBar();
      appendErr('  File "<lab>", line ' + (e.line || '?'));
      appendErr(e.pyType + ': ' + e.message);
      setNote(e.pyType + ': ' + e.message + '(第 ' + (e.line || '?') + ' 行)', true);
    } else {
      appendErr('内部错误:' + (e && e.message));
      setNote('内部错误:' + (e && e.message), true);
    }
    dead = true;
    gen = null;
    return false;
  }
}

function applySnapshot(s){
  setCurrentLine(s.line);
  renderVars(s.vars);
  renderOut(s.out);
  setStep(s.steps);
  if (s.note) setNote('第 ' + s.steps + ' 步 · 第 ' + s.line + ' 行 —— ' + s.note);
  else setNote('第 ' + s.steps + ' 步 · 第 ' + s.line + ' 行');
}

function handleRuntimeError(e){
  if (e instanceof PyError){
    if (e.line){ setCurrentLine(e.line); lineHl.classList.add('error'); updateLineBar(); }
    appendErr('Traceback (most recent call last):');
    appendErr('  File "<lab>", line ' + (e.line || '?'));
    appendErr(e.pyType + ': ' + e.message);
    setNote(e.pyType + ': ' + e.message + '(第 ' + (e.line || '?') + ' 行,执行已停止)', true);
  } else {
    appendErr('内部错误:' + (e && e.message));
    setNote('内部错误:' + (e && e.message), true);
  }
}

function doStep(){
  if (dead) return false;
  if (!gen){
    if (!reset()) return false;
    if (!gen) return false;
  }
  try {
    const r = gen.next();
    if (r.done){
      dead = true;
      setNote('程序执行完毕,共 ' + ctx.steps + ' 步');
      appendInfo('—— 程序执行完毕,共 ' + ctx.steps + ' 步 ——');
      return false;
    }
    applySnapshot(r.value);
    return true;
  } catch(e){
    dead = true;
    handleRuntimeError(e);
    return false;
  }
}

function stopRun(){
  running = false;
  if (timer){ clearTimeout(timer); timer = null; }
  updateRunBtn();
}
function scheduleNext(){
  if (!running) return;
  timer = setTimeout(function(){
    if (!running) return;
    const ok = doStep();
    if (ok && running) scheduleNext();
    else stopRun();
  }, speedValue());
}
function onRun(){
  if (running){ stopRun(); return; }
  if (dead){ if (!reset()) return; }
  running = true;
  updateRunBtn();
  scheduleNext();
}
function onStep(){
  if (running) return;
  doStep();
}

btnRun.addEventListener('click', onRun);
btnStep.addEventListener('click', onStep);
btnReset.addEventListener('click', function(){ reset(); });

speedEl.addEventListener('input', function(){
  speedLabel.textContent = speedEl.value + ' ms / 步';
  if (running){ clearTimeout(timer); scheduleNext(); }
});
speedLabel.textContent = speedEl.value + ' ms / 步';

/* =========================================================
   十、示例
   ========================================================= */
const EXAMPLES = [
  {
    title: '1 · 三种除法',
    code: 'x = 7\ny = 2\nprint(x / y)\nprint(x // y)\nprint(x % y)\na = x / y\nb = x // y\n',
    hint:
      '<div class="hint-block"><b>🔍 观察提示</b>' +
      '<p>单步执行(或把速度调慢),注意左侧变量面板里 <code>a</code> 和 <code>b</code> 的类型徽章颜色不一样——一个是蓝色 <b>int</b>,一个是绿色 <b>float</b>。同时控制台里也逐行出现了三个结果。</p></div>' +
      '<div class="hint-block"><b>💡 动手试试</b>' +
      '<p>同样是 7 和 2,为什么 <code>/</code> 得到 3.5 而 <code>//</code> 得到 3?<code>%</code> 那个结果代表什么?<br>' +
      '把 <code>x = 7</code> 改成 <code>x = 8</code> 再跑一遍,三个结果分别变成多少?<code>%</code> 的结果变了吗?</p></div>'
  },
  {
    title: '2 · 循环累加',
    code: 'total = 0\nfor i in range(1, 6):\n    total = total + i\n    print(i, total)\n',
    hint:
      '<div class="hint-block"><b>🔍 观察提示</b>' +
      '<p>把速度滑块拉到最慢,盯着变量面板——每走一步,<code>i</code> 增加 1,而 <code>total</code> 是 <strong>上一次的 total 加上这一次的 i</strong>。每次变量改变时,那张卡片会闪一下。</p></div>' +
      '<div class="hint-block"><b>💡 动手试试</b>' +
      '<p>第 1 次循环结束后 total 是多少?第 3 次呢?<br>' +
      '为什么最后一次 print 出来的 total 等于 1+2+3+4+5?<br>' +
      '把 <code>range(1, 6)</code> 改成 <code>range(1, 101)</code>,先猜 total 会变成多少,再运行验证。</p></div>'
  },
  {
    title: '3 · 缩进决定归属',
    code: 'score = 85\nif score >= 60:\n    print("及格")\n    print("恭喜")\nprint("检查完毕")\n',
    hint:
      '<div class="hint-block"><b>🔍 观察提示</b>' +
      '<p>运行完,看控制台里三行输出的顺序。前两行 print 前面有 4 个空格,它们是 <code>if</code> 的“下属”;第三行没有缩进,它不属于 if,无论如何都会执行。</p></div>' +
      '<div class="hint-block"><b>💡 动手试试</b>' +
      '<p>把第二个 <code>print("恭喜")</code> 前面的 4 个空格删掉(光标放到行首按退格),再运行——输出变成什么?为什么?<br>' +
      '再把 <code>score = 85</code> 改成 <code>score = 45</code>,看输出怎么变。</p></div>'
  },
  {
    title: '4 · 同样的加号',
    code: 'print(3 + 5)\nprint("3" + "5")\nprint("3" * 3)\n',
    hint:
      '<div class="hint-block"><b>🔍 观察提示</b>' +
      '<p>同一个 <code>+</code>,第一行做的是数学加法,第二行做的是字符串拼接(把两段文字接在一起)。第三行的 <code>*</code> 把字符串重复了 3 遍。</p></div>' +
      '<div class="hint-block"><b>💡 动手试试</b>' +
      '<p>把代码改成 <code>print(3 + "5")</code>,点运行,读一读控制台里红色的报错信息——它说清楚问题出在哪了吗?<br>' +
      '再试试 <code>print(int("3") + 5)</code> 会得到什么?</p></div>'
  },
  {
    title: '5 · while 循环',
    code: 'n = 5\nwhile n > 0:\n    print("倒数", n)\n    n = n - 1\nprint("发射")\n',
    hint:
      '<div class="hint-block"><b>🔍 观察提示</b>' +
      '<p>每次循环先打印「倒数 n」,再把 n 减 1。变量面板里 <code>n</code> 的数值一步一步往下掉:5 → 4 → 3 → 2 → 1,然后条件变成 False,循环退出。</p></div>' +
      '<div class="hint-block"><b>💡 动手试试</b>' +
      '<p>如果忘记写 <code>n = n - 1</code>,会发生什么?先把它删掉运行一次看看(会有保护机制提示),再想想为什么这一行不能少。</p></div>'
  },
  {
    title: '6 · 自由探索',
    code: '# 在这里写你的代码\n',
    hint:
      '<div class="hint-block"><b>🔍 自由探索</b>' +
      '<p>编辑器里的代码可以随便改。推荐试试:</p>' +
      '<ul>' +
      '<li>用 <code>for i in range(1, 6):</code> 打印一行乘法表</li>' +
      '<li>用 <code>while</code> 算 1 到 100 的和</li>' +
      '<li>故意写一个没定义的变量名,看报错信息长什么样</li>' +
      '<li>写 <code>print(len("hello"))</code> 看看 len 是做什么的</li>' +
      '</ul></div>'
  }
];

let activeExample = 0;
function loadExample(i){
  activeExample = i;
  const ex = EXAMPLES[i];
  codeEl.value = ex.code;
  updateHighlight();
  updateGutter();
  codeEl.scrollTop = 0;
  codeEl.scrollLeft = 0;
  syncScroll();
  document.querySelectorAll('.ex-btn').forEach(function(b, idx){
    b.classList.toggle('active', idx === i);
  });
  hintCardEl.innerHTML = ex.hint;
  reset();
}
EXAMPLES.forEach(function(ex, i){
  const b = document.createElement('button');
  b.className = 'ex-btn';
  b.textContent = ex.title;
  b.addEventListener('click', function(){ loadExample(i); });
  examplesEl.appendChild(b);
});

/* ---------- 初始化 ---------- */
loadExample(0);
updateHighlight();
updateGutter();
syncScroll();
renderVars([]);
setNote('准备就绪 —— 点击「▶ 连续运行」或「⏭ 单步执行」开始');

})();
</script>
</body>
</html>
```
### 交互机制与动手探究

通过单步执行和实时反馈,让抽象语法变成看得见的变化。

- **代码编辑与执行**:左侧代码区用 textarea 叠加语法高亮层,支持缩进与注释;点击“连续运行”或“单步执行”会生成器逐步解释代码,当前行以琥珀色高亮横条标记。
- **变量与输出的实时反馈**:右侧变量面板为每个变量显示名称、值与类型徽章,值变化时卡片会闪烁提示;控制台逐行追加 print 输出,让每一步的结果都有迹可循。
- **内置引导示例**:六个示例按钮载入不同代码片段,配有观察提示卡,引导您对比除法结果、追踪循环累加、发现缩进与运算符的差异。
---

**优化建议:** 您可以调整速度滑块的 min/max 或初始 value 修改步进间隔;将 MAX_STEPS 从 2000 改小可更早提示死循环。