Trae Assistant commited on
Commit
602c51e
·
0 Parent(s):

feat: complete hf spaces configuration with interactive demo

Browse files
Files changed (5) hide show
  1. README.md +8 -0
  2. example.mini +5 -0
  3. index.html +284 -0
  4. main.js +12 -0
  5. mini_lang.js +389 -0
README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Mini Lang
3
+ emoji: 💻
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: static
7
+ pinned: false
8
+ ---
example.mini ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ let x = 1 + 2 * 3;
2
+ let y = (x - 2) / 2;
3
+ print x;
4
+ print y;
5
+ print x + y * 10;
index.html ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Mini Lang - 功能展示</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
15
+ background-color: #0f172a;
16
+ color: #e5e7eb;
17
+ line-height: 1.6;
18
+ }
19
+ main {
20
+ max-width: 960px;
21
+ margin: 0 auto;
22
+ padding: 32px 20px 48px;
23
+ }
24
+ header {
25
+ margin-bottom: 32px;
26
+ text-align: left;
27
+ }
28
+ h1 {
29
+ margin: 0 0 8px;
30
+ font-size: 32px;
31
+ color: #f9fafb;
32
+ }
33
+ h2 {
34
+ margin-top: 32px;
35
+ margin-bottom: 12px;
36
+ font-size: 22px;
37
+ color: #e5e7eb;
38
+ }
39
+ p {
40
+ margin: 4px 0 12px;
41
+ color: #cbd5f5;
42
+ }
43
+ .tagline {
44
+ color: #9ca3af;
45
+ font-size: 14px;
46
+ }
47
+ .section-card {
48
+ border-radius: 12px;
49
+ border: 1px solid rgba(148, 163, 184, 0.35);
50
+ background: radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 55%),
51
+ radial-gradient(circle at bottom right, rgba(236, 72, 153, 0.18), transparent 55%),
52
+ rgba(15, 23, 42, 0.9);
53
+ padding: 18px 16px 16px;
54
+ margin-bottom: 18px;
55
+ }
56
+ .badge {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 8px;
60
+ padding: 4px 10px;
61
+ border-radius: 999px;
62
+ border: 1px solid rgba(148, 163, 184, 0.4);
63
+ background-color: rgba(15, 23, 42, 0.8);
64
+ color: #e5e7eb;
65
+ font-size: 12px;
66
+ margin-top: 8px;
67
+ }
68
+ .badge span {
69
+ display: inline-block;
70
+ width: 6px;
71
+ height: 6px;
72
+ border-radius: 999px;
73
+ background: radial-gradient(circle at 30% 30%, #22c55e, #16a34a);
74
+ }
75
+ ul {
76
+ padding-left: 20px;
77
+ margin: 6px 0 0;
78
+ color: #d1d5db;
79
+ }
80
+ code {
81
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
82
+ background-color: rgba(15, 23, 42, 0.9);
83
+ padding: 1px 4px;
84
+ border-radius: 4px;
85
+ font-size: 13px;
86
+ }
87
+ pre {
88
+ margin: 8px 0 0;
89
+ padding: 10px 12px;
90
+ border-radius: 10px;
91
+ background-color: #020617;
92
+ color: #e5e7eb;
93
+ overflow-x: auto;
94
+ font-size: 13px;
95
+ border: 1px solid rgba(30, 64, 175, 0.7);
96
+ }
97
+ pre code {
98
+ background: none;
99
+ padding: 0;
100
+ }
101
+ .pill-label {
102
+ display: inline-block;
103
+ font-size: 12px;
104
+ color: #a5b4fc;
105
+ margin-bottom: 4px;
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.08em;
108
+ }
109
+ .grid {
110
+ display: grid;
111
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
112
+ gap: 16px;
113
+ align-items: flex-start;
114
+ }
115
+ .grid-item {
116
+ min-width: 0;
117
+ }
118
+ .output-line {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ gap: 8px;
122
+ padding: 3px 8px;
123
+ border-radius: 999px;
124
+ background: rgba(15, 23, 42, 0.85);
125
+ font-size: 12px;
126
+ color: #e5e7eb;
127
+ }
128
+ .output-line span {
129
+ display: inline-block;
130
+ width: 6px;
131
+ height: 6px;
132
+ border-radius: 999px;
133
+ background: radial-gradient(circle at 30% 30%, #22c55e, #16a34a);
134
+ }
135
+ .footer {
136
+ margin-top: 28px;
137
+ font-size: 12px;
138
+ color: #6b7280;
139
+ text-align: right;
140
+ }
141
+ @media (max-width: 720px) {
142
+ main {
143
+ padding: 20px 16px 32px;
144
+ }
145
+ h1 {
146
+ font-size: 26px;
147
+ }
148
+ .grid {
149
+ grid-template-columns: minmax(0, 1fr);
150
+ }
151
+ }
152
+ </style>
153
+ </head>
154
+ <body>
155
+ <main>
156
+ <header>
157
+ <h1>Mini Lang 自定义语言</h1>
158
+ <p class="tagline">一个用 JavaScript 实现的微型脚本语言,支持变量、表达式与打印输出。</p>
159
+ <div class="badge">
160
+ <span></span>
161
+ <div>命令行运行 · Hugging Face Spaces 静态展示</div>
162
+ </div>
163
+ </header>
164
+
165
+ <section class="section-card">
166
+ <div class="pill-label">语言特性概览</div>
167
+ <h2>支持的语法</h2>
168
+ <p>Mini Lang 目前专注于表达式计算与简单的脚本结构:</p>
169
+ <ul>
170
+ <li>数字字面量,例如 <code>1</code>、<code>3.14</code></li>
171
+ <li>变量声明:<code>let x = 表达式;</code></li>
172
+ <li>四则运算:<code>+</code>、<code>-</code>、<code>*</code>、<code>/</code>,支持括号改变优先级</li>
173
+ <li>打印语句:<code>print 表达式;</code> 会在控制台输出值</li>
174
+ <li>语句之间用分号结尾:<code>;</code></li>
175
+ </ul>
176
+ </section>
177
+
178
+ <section class="section-card">
179
+ <div class="pill-label">在线体验</div>
180
+ <h2>在线运行 Mini Lang</h2>
181
+ <p>在下方输入框编写 Mini Lang 代码,点击运行查看结果:</p>
182
+ <div class="grid">
183
+ <div class="grid-item">
184
+ <textarea id="code-input" style="width: 100%; height: 200px; background: #020617; color: #e5e7eb; border: 1px solid rgba(30, 64, 175, 0.7); border-radius: 8px; padding: 12px; font-family: monospace;">let x = 10;
185
+ let y = 20;
186
+ print x + y;
187
+ print x * y;
188
+ </textarea>
189
+ <button onclick="runCode()" style="margin-top: 10px; padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer;">运行代码</button>
190
+ </div>
191
+ <div class="grid-item">
192
+ <pre id="output-area" style="height: 200px; overflow-y: auto;">Waiting for output...</pre>
193
+ </div>
194
+ </div>
195
+ <script src="mini_lang.js"></script>
196
+ <script>
197
+ function runCode() {
198
+ const source = document.getElementById('code-input').value;
199
+ const outputArea = document.getElementById('output-area');
200
+ outputArea.innerText = ''; // 清空输出
201
+
202
+ try {
203
+ // 重定向输出到页面
204
+ if (window.MiniLang) {
205
+ window.MiniLang.run(source, (val) => {
206
+ outputArea.innerText += val + '\n';
207
+ });
208
+ } else {
209
+ outputArea.innerText = 'Error: MiniLang engine not loaded.';
210
+ }
211
+ } catch (e) {
212
+ outputArea.innerText += 'Error: ' + e.message;
213
+ }
214
+ }
215
+ </script>
216
+ </section>
217
+
218
+ <section class="section-card">
219
+ <div class="pill-label">最小可运行示例</div>
220
+ <h2>示例程序与运行效果</h2>
221
+ <div class="grid">
222
+ <div class="grid-item">
223
+ <p>示例源文件 <code>example.mini</code>:</p>
224
+ <pre><code>let x = 1 + 2 * 3;
225
+ let y = (x - 2) / 2;
226
+ print x;
227
+ print y;
228
+ print x + y * 10;</code></pre>
229
+ </div>
230
+ <div class="grid-item">
231
+ <p>在命令行运行后输出:</p>
232
+ <pre><code>7
233
+ 2.5
234
+ 32</code></pre>
235
+ <p>
236
+ 其中:
237
+ </p>
238
+ <ul>
239
+ <li><code>x = 1 + 2 * 3 = 7</code></li>
240
+ <li><code>y = (x - 2) / 2 = 2.5</code></li>
241
+ <li>最后一行输出 <code>7 + 2.5 * 10 = 32</code></li>
242
+ </ul>
243
+ </div>
244
+ </div>
245
+ </section>
246
+
247
+ <section class="section-card">
248
+ <div class="pill-label">本地命令行使用方式</div>
249
+ <h2>如何在本地运行 Mini Lang</h2>
250
+ <p>本项目使用 Node.js 作为运行时,只依赖内置模块 <code>fs</code> 与 <code>path</code>:</p>
251
+ <pre><code># 进入项目目录
252
+ cd mini-lang
253
+
254
+ # 使用默认示例程序运行
255
+ node main.js
256
+
257
+ # 或者指定自己的脚本文件
258
+ node main.js your_program.mini</code></pre>
259
+ <p>在入口文件 <code>main.js</code> 中,会读取传入的脚本文件内容,并调用解释器执行。</p>
260
+ </section>
261
+
262
+ <section class="section-card">
263
+ <div class="pill-label">部署到 Hugging Face Spaces</div>
264
+ <h2>在 Spaces 上展示</h2>
265
+ <p>
266
+ 将本项目作为 <strong>Static</strong> 类型的 Space 使用时,只需要把整个
267
+ <code>mini-lang</code> 文件夹推送到
268
+ <code>hf026:spaces/duqing026/mini-lang</code> 对应的仓库即可。
269
+ </p>
270
+ <p>此页面 <code>index.html</code> 会作为默认展示入口,用于说明 Mini Lang 的语法与示例。</p>
271
+ <pre><code># 在 mini-lang 目录下(本地已经配置好 huggingface 的 ssh 别名 hf026)
272
+ git init
273
+ git add .
274
+ git commit -m "chore: add mini-lang demo page"
275
+ git push hf026:spaces/duqing026/mini-lang main</code></pre>
276
+ <p>推送成功后,在浏览器访问对应的 Hugging Face Space,即可看到本页面。</p>
277
+ </section>
278
+
279
+ <div class="footer">
280
+ Mini Lang · 仅用于教学与功能演示
281
+ </div>
282
+ </main>
283
+ </body>
284
+ </html>
main.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { run } = require("./mini_lang");
4
+
5
+ function main() {
6
+ const filePath = process.argv[2] || path.join(__dirname, "example.mini");
7
+ const source = fs.readFileSync(filePath, "utf8");
8
+ console.log("运行自定义语言程序,源文件:", filePath);
9
+ run(source);
10
+ }
11
+
12
+ main();
mini_lang.js ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const TokenType = {
2
+ NUMBER: "NUMBER",
3
+ IDENT: "IDENT",
4
+ LET: "LET",
5
+ PRINT: "PRINT",
6
+ EQUAL: "EQUAL",
7
+ PLUS: "PLUS",
8
+ MINUS: "MINUS",
9
+ STAR: "STAR",
10
+ SLASH: "SLASH",
11
+ SEMICOLON: "SEMICOLON",
12
+ LPAREN: "LPAREN",
13
+ RPAREN: "RPAREN",
14
+ EOF: "EOF",
15
+ };
16
+
17
+ const keywords = {
18
+ let: TokenType.LET,
19
+ print: TokenType.PRINT,
20
+ };
21
+
22
+ class Token {
23
+ constructor(type, lexeme, literal, position) {
24
+ this.type = type;
25
+ this.lexeme = lexeme;
26
+ this.literal = literal;
27
+ this.position = position;
28
+ }
29
+ }
30
+
31
+ class Lexer {
32
+ constructor(source) {
33
+ this.source = source;
34
+ this.tokens = [];
35
+ this.start = 0;
36
+ this.current = 0;
37
+ this.line = 1;
38
+ this.column = 1;
39
+ }
40
+
41
+ scanTokens() {
42
+ while (!this.isAtEnd()) {
43
+ this.start = this.current;
44
+ this.scanToken();
45
+ }
46
+ this.tokens.push(new Token(TokenType.EOF, "", null, { line: this.line, column: this.column }));
47
+ return this.tokens;
48
+ }
49
+
50
+ isAtEnd() {
51
+ return this.current >= this.source.length;
52
+ }
53
+
54
+ scanToken() {
55
+ const c = this.advance();
56
+ if (this.isDigit(c)) {
57
+ this.number();
58
+ return;
59
+ }
60
+ if (this.isAlpha(c)) {
61
+ this.identifier();
62
+ return;
63
+ }
64
+ switch (c) {
65
+ case "+":
66
+ this.addToken(TokenType.PLUS);
67
+ break;
68
+ case "-":
69
+ this.addToken(TokenType.MINUS);
70
+ break;
71
+ case "*":
72
+ this.addToken(TokenType.STAR);
73
+ break;
74
+ case "/":
75
+ if (this.match("/")) {
76
+ while (!this.isAtEnd() && this.peek() !== "\n") {
77
+ this.advance();
78
+ }
79
+ } else {
80
+ this.addToken(TokenType.SLASH);
81
+ }
82
+ break;
83
+ case "=":
84
+ this.addToken(TokenType.EQUAL);
85
+ break;
86
+ case ";":
87
+ this.addToken(TokenType.SEMICOLON);
88
+ break;
89
+ case "(":
90
+ this.addToken(TokenType.LPAREN);
91
+ break;
92
+ case ")":
93
+ this.addToken(TokenType.RPAREN);
94
+ break;
95
+ case " ":
96
+ case "\r":
97
+ case "\t":
98
+ break;
99
+ case "\n":
100
+ this.line += 1;
101
+ this.column = 1;
102
+ break;
103
+ default:
104
+ throw new Error("无法识别的字符: '" + c + "' 在第 " + this.line + " 行");
105
+ }
106
+ }
107
+
108
+ advance() {
109
+ const ch = this.source[this.current];
110
+ this.current += 1;
111
+ this.column += 1;
112
+ return ch;
113
+ }
114
+
115
+ addToken(type, literal = null) {
116
+ const text = this.source.slice(this.start, this.current);
117
+ this.tokens.push(new Token(type, text, literal, { line: this.line, column: this.column }));
118
+ }
119
+
120
+ match(expected) {
121
+ if (this.isAtEnd()) return false;
122
+ if (this.source[this.current] !== expected) return false;
123
+ this.current += 1;
124
+ this.column += 1;
125
+ return true;
126
+ }
127
+
128
+ peek() {
129
+ if (this.isAtEnd()) return "\0";
130
+ return this.source[this.current];
131
+ }
132
+
133
+ isDigit(c) {
134
+ return c >= "0" && c <= "9";
135
+ }
136
+
137
+ isAlpha(c) {
138
+ return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_";
139
+ }
140
+
141
+ isAlphaNumeric(c) {
142
+ return this.isAlpha(c) || this.isDigit(c);
143
+ }
144
+
145
+ number() {
146
+ while (this.isDigit(this.peek())) {
147
+ this.advance();
148
+ }
149
+ if (this.peek() === "." && this.isDigit(this.peekNext())) {
150
+ this.advance();
151
+ while (this.isDigit(this.peek())) {
152
+ this.advance();
153
+ }
154
+ }
155
+ const text = this.source.slice(this.start, this.current);
156
+ const value = Number(text);
157
+ this.addToken(TokenType.NUMBER, value);
158
+ }
159
+
160
+ peekNext() {
161
+ if (this.current + 1 >= this.source.length) return "\0";
162
+ return this.source[this.current + 1];
163
+ }
164
+
165
+ identifier() {
166
+ while (this.isAlphaNumeric(this.peek())) {
167
+ this.advance();
168
+ }
169
+ const text = this.source.slice(this.start, this.current);
170
+ const type = keywords[text] || TokenType.IDENT;
171
+ this.addToken(type);
172
+ }
173
+ }
174
+
175
+ class Parser {
176
+ constructor(tokens) {
177
+ this.tokens = tokens;
178
+ this.current = 0;
179
+ }
180
+
181
+ parseProgram() {
182
+ const statements = [];
183
+ while (!this.isAtEnd()) {
184
+ statements.push(this.statement());
185
+ }
186
+ return { type: "Program", statements };
187
+ }
188
+
189
+ statement() {
190
+ if (this.match(TokenType.LET)) return this.letStatement();
191
+ if (this.match(TokenType.PRINT)) return this.printStatement();
192
+ const expr = this.expression();
193
+ this.consume(TokenType.SEMICOLON, "期望在表达式后面出现分号");
194
+ return { type: "ExprStmt", expression: expr };
195
+ }
196
+
197
+ letStatement() {
198
+ const nameToken = this.consume(TokenType.IDENT, "let 后面需要变量名");
199
+ this.consume(TokenType.EQUAL, "变量声明需要等号");
200
+ const initializer = this.expression();
201
+ this.consume(TokenType.SEMICOLON, "变量声明后需要分号");
202
+ return { type: "LetStmt", name: nameToken.lexeme, initializer };
203
+ }
204
+
205
+ printStatement() {
206
+ const value = this.expression();
207
+ this.consume(TokenType.SEMICOLON, "print 后需要分号");
208
+ return { type: "PrintStmt", expression: value };
209
+ }
210
+
211
+ expression() {
212
+ return this.term();
213
+ }
214
+
215
+ term() {
216
+ let expr = this.factor();
217
+ while (this.match(TokenType.PLUS) || this.match(TokenType.MINUS)) {
218
+ const operator = this.previous();
219
+ const right = this.factor();
220
+ expr = { type: "Binary", left: expr, operator: operator.type, right };
221
+ }
222
+ return expr;
223
+ }
224
+
225
+ factor() {
226
+ let expr = this.unary();
227
+ while (this.match(TokenType.STAR) || this.match(TokenType.SLASH)) {
228
+ const operator = this.previous();
229
+ const right = this.unary();
230
+ expr = { type: "Binary", left: expr, operator: operator.type, right };
231
+ }
232
+ return expr;
233
+ }
234
+
235
+ unary() {
236
+ if (this.match(TokenType.MINUS)) {
237
+ const operator = this.previous();
238
+ const right = this.unary();
239
+ return { type: "Unary", operator: operator.type, right };
240
+ }
241
+ return this.primary();
242
+ }
243
+
244
+ primary() {
245
+ if (this.match(TokenType.NUMBER)) {
246
+ return { type: "Literal", value: this.previous().literal };
247
+ }
248
+ if (this.match(TokenType.IDENT)) {
249
+ return { type: "Variable", name: this.previous().lexeme };
250
+ }
251
+ if (this.match(TokenType.LPAREN)) {
252
+ const expr = this.expression();
253
+ this.consume(TokenType.RPAREN, "缺少右括号");
254
+ return expr;
255
+ }
256
+ throw new Error("无法解析的表达式起始符号");
257
+ }
258
+
259
+ match(type) {
260
+ if (this.check(type)) {
261
+ this.advance();
262
+ return true;
263
+ }
264
+ return false;
265
+ }
266
+
267
+ consume(type, message) {
268
+ if (this.check(type)) return this.advance();
269
+ throw new Error(message);
270
+ }
271
+
272
+ check(type) {
273
+ if (this.isAtEnd()) return false;
274
+ return this.peek().type === type;
275
+ }
276
+
277
+ advance() {
278
+ if (!this.isAtEnd()) this.current += 1;
279
+ return this.previous();
280
+ }
281
+
282
+ isAtEnd() {
283
+ return this.peek().type === TokenType.EOF;
284
+ }
285
+
286
+ peek() {
287
+ return this.tokens[this.current];
288
+ }
289
+
290
+ previous() {
291
+ return this.tokens[this.current - 1];
292
+ }
293
+ }
294
+
295
+ class Interpreter {
296
+ constructor() {
297
+ this.globals = Object.create(null);
298
+ }
299
+
300
+ execute(program, outputCallback = console.log) {
301
+ for (const stmt of program.statements) {
302
+ this.executeStatement(stmt, outputCallback);
303
+ }
304
+ }
305
+
306
+ executeStatement(stmt, outputCallback) {
307
+ switch (stmt.type) {
308
+ case "LetStmt":
309
+ this.globals[stmt.name] = this.evaluate(stmt.initializer);
310
+ break;
311
+ case "PrintStmt":
312
+ const value = this.evaluate(stmt.expression);
313
+ outputCallback(value);
314
+ break;
315
+ case "ExprStmt":
316
+ this.evaluate(stmt.expression);
317
+ break;
318
+ default:
319
+ throw new Error("未知语句类型: " + stmt.type);
320
+ }
321
+ }
322
+
323
+ evaluate(expr) {
324
+ switch (expr.type) {
325
+ case "Literal":
326
+ return expr.value;
327
+ case "Variable":
328
+ if (Object.prototype.hasOwnProperty.call(this.globals, expr.name)) {
329
+ return this.globals[expr.name];
330
+ }
331
+ throw new Error("未定义的变量: " + expr.name);
332
+ case "Unary":
333
+ const right = this.evaluate(expr.right);
334
+ if (expr.operator === TokenType.MINUS) {
335
+ this.ensureNumber(right);
336
+ return -right;
337
+ }
338
+ throw new Error("未知一元运算符");
339
+ case "Binary":
340
+ const left = this.evaluate(expr.left);
341
+ const r = this.evaluate(expr.right);
342
+ if (expr.operator === TokenType.PLUS) {
343
+ this.ensureNumber(left);
344
+ this.ensureNumber(r);
345
+ return left + r;
346
+ }
347
+ if (expr.operator === TokenType.MINUS) {
348
+ this.ensureNumber(left);
349
+ this.ensureNumber(r);
350
+ return left - r;
351
+ }
352
+ if (expr.operator === TokenType.STAR) {
353
+ this.ensureNumber(left);
354
+ this.ensureNumber(r);
355
+ return left * r;
356
+ }
357
+ if (expr.operator === TokenType.SLASH) {
358
+ this.ensureNumber(left);
359
+ this.ensureNumber(r);
360
+ return left / r;
361
+ }
362
+ throw new Error("未知二元运算符");
363
+ default:
364
+ throw new Error("未知表达式类型: " + expr.type);
365
+ }
366
+ }
367
+
368
+ ensureNumber(value) {
369
+ if (typeof value !== "number" || Number.isNaN(value)) {
370
+ throw new Error("期望数字类型");
371
+ }
372
+ }
373
+ }
374
+
375
+ function run(source, outputCallback = console.log) {
376
+ const lexer = new Lexer(source);
377
+ const tokens = lexer.scanTokens();
378
+ const parser = new Parser(tokens);
379
+ const program = parser.parseProgram();
380
+ const interpreter = new Interpreter();
381
+ interpreter.execute(program, outputCallback);
382
+ }
383
+
384
+ if (typeof module !== "undefined" && module.exports) {
385
+ module.exports = { run };
386
+ } else {
387
+ // 浏览器环境
388
+ window.MiniLang = { run };
389
+ }