










请用**单个 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 改小可更早提示死循环。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。