calc-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +152 -0
- package/.claude/commands/opsx/archive.md +157 -0
- package/.claude/commands/opsx/bulk-archive.md +242 -0
- package/.claude/commands/opsx/continue.md +114 -0
- package/.claude/commands/opsx/explore.md +174 -0
- package/.claude/commands/opsx/ff.md +94 -0
- package/.claude/commands/opsx/new.md +69 -0
- package/.claude/commands/opsx/onboard.md +534 -0
- package/.claude/commands/opsx/sync.md +134 -0
- package/.claude/commands/opsx/verify.md +164 -0
- package/.claude/settings.local.json +8 -0
- package/.claude/skills/npm-publish/SKILL.md +164 -0
- package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +161 -0
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
- package/.claude/skills/openspec-explore/SKILL.md +289 -0
- package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
- package/.claude/skills/openspec-new-change/SKILL.md +74 -0
- package/.claude/skills/openspec-onboard/SKILL.md +538 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
- package/CLAUDE.md +92 -0
- package/README.md +319 -0
- package/build/engines/decimal.d.ts +10 -0
- package/build/engines/decimal.d.ts.map +1 -0
- package/build/engines/decimal.js +61 -0
- package/build/engines/decimal.js.map +1 -0
- package/build/engines/programmer.d.ts +18 -0
- package/build/engines/programmer.d.ts.map +1 -0
- package/build/engines/programmer.js +103 -0
- package/build/engines/programmer.js.map +1 -0
- package/build/errors/handler.d.ts +10 -0
- package/build/errors/handler.d.ts.map +1 -0
- package/build/errors/handler.js +37 -0
- package/build/errors/handler.js.map +1 -0
- package/build/errors/types.d.ts +25 -0
- package/build/errors/types.d.ts.map +1 -0
- package/build/errors/types.js +2 -0
- package/build/errors/types.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +16 -0
- package/build/index.js.map +1 -0
- package/build/mcp/server.d.ts +3 -0
- package/build/mcp/server.d.ts.map +1 -0
- package/build/mcp/server.js +270 -0
- package/build/mcp/server.js.map +1 -0
- package/build/mcp/tools/ascii.d.ts +11 -0
- package/build/mcp/tools/ascii.d.ts.map +1 -0
- package/build/mcp/tools/ascii.js +93 -0
- package/build/mcp/tools/ascii.js.map +1 -0
- package/build/mcp/tools/basic.d.ts +6 -0
- package/build/mcp/tools/basic.d.ts.map +1 -0
- package/build/mcp/tools/basic.js +34 -0
- package/build/mcp/tools/basic.js.map +1 -0
- package/build/mcp/tools/conversion.d.ts +8 -0
- package/build/mcp/tools/conversion.d.ts.map +1 -0
- package/build/mcp/tools/conversion.js +81 -0
- package/build/mcp/tools/conversion.js.map +1 -0
- package/build/mcp/tools/programmer.d.ts +6 -0
- package/build/mcp/tools/programmer.d.ts.map +1 -0
- package/build/mcp/tools/programmer.js +29 -0
- package/build/mcp/tools/programmer.js.map +1 -0
- package/build/parser/ast.d.ts +47 -0
- package/build/parser/ast.d.ts.map +1 -0
- package/build/parser/ast.js +2 -0
- package/build/parser/ast.js.map +1 -0
- package/build/parser/lexer.d.ts +24 -0
- package/build/parser/lexer.d.ts.map +1 -0
- package/build/parser/lexer.js +168 -0
- package/build/parser/lexer.js.map +1 -0
- package/build/parser/parser.d.ts +14 -0
- package/build/parser/parser.d.ts.map +1 -0
- package/build/parser/parser.js +115 -0
- package/build/parser/parser.js.map +1 -0
- package/docs/plans/2025-02-24-mcp-calculator-design.md +344 -0
- package/docs/plans/2025-02-24-mcp-calculator-implementation.md +2626 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/design.md +46 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/proposal.md +21 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/specs/ascii-conversion/spec.md +22 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/tasks.md +24 -0
- package/openspec/config.yaml +20 -0
- package/openspec/specs/ascii-conversion/spec.md +43 -0
- package/package.json +40 -0
- package/src/engines/decimal.ts +69 -0
- package/src/engines/programmer.ts +112 -0
- package/src/errors/handler.ts +55 -0
- package/src/errors/types.ts +37 -0
- package/src/index.ts +20 -0
- package/src/mcp/server.ts +287 -0
- package/src/mcp/tools/ascii.ts +116 -0
- package/src/mcp/tools/basic.ts +44 -0
- package/src/mcp/tools/conversion.ts +95 -0
- package/src/mcp/tools/programmer.ts +36 -0
- package/src/parser/ast.ts +51 -0
- package/src/parser/lexer.ts +216 -0
- package/src/parser/parser.ts +154 -0
- package/test/integration/ascii.test.ts +450 -0
- package/test/integration/basic-calculate.test.ts +272 -0
- package/test/integration/conversion.test.ts +357 -0
- package/test/integration/programmer-calculate.test.ts +363 -0
- package/test/unit/decimal-engine.test.ts +134 -0
- package/test/unit/error-handler.test.ts +173 -0
- package/test/unit/lexer.test.ts +176 -0
- package/test/unit/parser.test.ts +197 -0
- package/test/unit/programmer-engine.test.ts +234 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,2626 @@
|
|
|
1
|
+
# MCP Server 计算器实施计划
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**目标:** 构建一个为 AI 助手提供计算能力的 MCP Server,支持四则运算(使用 Decimal 精度)和程序员计算(使用 BigInt)功能。
|
|
6
|
+
|
|
7
|
+
**架构:** 自研递归下降解析器 + 双引擎架构。Decimal 引擎处理四则运算,BigInt 引擎处理程序员计算。共享 Lexer 和 Parser,不同引擎处理 AST。
|
|
8
|
+
|
|
9
|
+
**技术栈:** TypeScript, decimal.js, @modelcontextprotocol/sdk, vitest
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 阶段 1: 项目初始化
|
|
14
|
+
|
|
15
|
+
### 任务 1: 初始化项目结构和配置
|
|
16
|
+
|
|
17
|
+
**文件:**
|
|
18
|
+
- 创建: `package.json`
|
|
19
|
+
- 创建: `tsconfig.json`
|
|
20
|
+
- 创建: `vite.config.ts`
|
|
21
|
+
- 创建: `vitest.config.ts`
|
|
22
|
+
- 创建: `.gitignore`
|
|
23
|
+
|
|
24
|
+
**Step 1: 创建 package.json**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cat > package.json << 'EOF'
|
|
28
|
+
{
|
|
29
|
+
"name": "mcp-calculator",
|
|
30
|
+
"version": "0.1.0",
|
|
31
|
+
"description": "MCP Server for calculator with basic and programmer operations",
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "dist/index.js",
|
|
34
|
+
"bin": {
|
|
35
|
+
"mcp-calculator": "dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"test": "vitest",
|
|
41
|
+
"test:ui": "vitest --ui",
|
|
42
|
+
"test:coverage": "vitest --coverage",
|
|
43
|
+
"lint": "eslint src --ext .ts",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"keywords": ["mcp", "calculator", "typescript"],
|
|
47
|
+
"author": "",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
51
|
+
"decimal.js": "^10.4.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.11.0",
|
|
55
|
+
"@vitest/ui": "^1.2.0",
|
|
56
|
+
"@vitest/coverage-v8": "^1.2.0",
|
|
57
|
+
"typescript": "^5.3.3",
|
|
58
|
+
"vitest": "^1.2.0",
|
|
59
|
+
"eslint": "^8.56.0",
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
|
61
|
+
"@typescript-eslint/parser": "^6.19.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
EOF
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Step 2: 安装依赖**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Step 3: 创建 tsconfig.json**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cat > tsconfig.json << 'EOF'
|
|
77
|
+
{
|
|
78
|
+
"compilerOptions": {
|
|
79
|
+
"target": "ES2022",
|
|
80
|
+
"module": "Node16",
|
|
81
|
+
"moduleResolution": "Node16",
|
|
82
|
+
"lib": ["ES2022"],
|
|
83
|
+
"outDir": "./dist",
|
|
84
|
+
"rootDir": "./src",
|
|
85
|
+
"strict": true,
|
|
86
|
+
"esModuleInterop": true,
|
|
87
|
+
"skipLibCheck": true,
|
|
88
|
+
"forceConsistentCasingInFileNames": true,
|
|
89
|
+
"resolveJsonModule": true,
|
|
90
|
+
"declaration": true,
|
|
91
|
+
"declarationMap": true,
|
|
92
|
+
"sourceMap": true
|
|
93
|
+
},
|
|
94
|
+
"include": ["src/**/*"],
|
|
95
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
96
|
+
}
|
|
97
|
+
EOF
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Step 4: 创建 vitest.config.ts**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cat > vitest.config.ts << 'EOF'
|
|
104
|
+
import { defineConfig } from 'vitest/config';
|
|
105
|
+
|
|
106
|
+
export default defineConfig({
|
|
107
|
+
test: {
|
|
108
|
+
globals: true,
|
|
109
|
+
environment: 'node',
|
|
110
|
+
coverage: {
|
|
111
|
+
provider: 'v8',
|
|
112
|
+
reporter: ['text', 'json', 'html'],
|
|
113
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/test/**'],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
EOF
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Step 5: 创建 .gitignore**
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
cat > .gitignore << 'EOF'
|
|
124
|
+
node_modules/
|
|
125
|
+
dist/
|
|
126
|
+
*.log
|
|
127
|
+
.DS_Store
|
|
128
|
+
coverage/
|
|
129
|
+
.nyc_output/
|
|
130
|
+
EOF
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Step 6: 验证配置**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm run typecheck
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Step 7: 初始化 Git 仓库并提交**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git init
|
|
143
|
+
git add .
|
|
144
|
+
git commit -m "feat: initialize project with TypeScript and Vitest"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 阶段 2: 错误处理系统
|
|
150
|
+
|
|
151
|
+
### 任务 2: 定义错误类型和处理器
|
|
152
|
+
|
|
153
|
+
**文件:**
|
|
154
|
+
- 创建: `src/errors/types.ts`
|
|
155
|
+
- 创建: `src/errors/handler.ts`
|
|
156
|
+
- 测试: `test/unit/error-handler.test.ts`
|
|
157
|
+
|
|
158
|
+
**Step 1: 编写错误类型定义的测试**
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
mkdir -p src/errors test/unit
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// test/unit/error-handler.test.ts
|
|
166
|
+
import { describe, it, expect } from 'vitest';
|
|
167
|
+
import { ErrorHandler, ErrorType } from '../../src/errors/handler.js';
|
|
168
|
+
|
|
169
|
+
describe('ErrorHandler', () => {
|
|
170
|
+
it('should create syntax error with position', () => {
|
|
171
|
+
const handler = new ErrorHandler();
|
|
172
|
+
const error = handler.syntaxError('Unexpected token', 5, 8);
|
|
173
|
+
|
|
174
|
+
expect(error.success).toBe(false);
|
|
175
|
+
expect(error.error.type).toBe('SyntaxError');
|
|
176
|
+
expect(error.error.message).toBe('Unexpected token');
|
|
177
|
+
expect(error.error.position).toEqual({ start: 5, end: 8 });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should create runtime error', () => {
|
|
181
|
+
const handler = new ErrorHandler();
|
|
182
|
+
const error = handler.runtimeError('DivisionByZero', 'Cannot divide by zero');
|
|
183
|
+
|
|
184
|
+
expect(error.success).toBe(false);
|
|
185
|
+
expect(error.error.type).toBe('DivisionByZero');
|
|
186
|
+
expect(error.error.message).toBe('Cannot divide by zero');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should include suggestion for known error types', () => {
|
|
190
|
+
const handler = new ErrorHandler();
|
|
191
|
+
const error = handler.syntaxError('Unexpected token', 5, 8);
|
|
192
|
+
|
|
193
|
+
expect(error.error.suggestion).toBeDefined();
|
|
194
|
+
expect(typeof error.error.suggestion).toBe('string');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Step 2: 运行测试验证失败**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm test -- test/unit/error-handler.test.ts
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Step 3: 实现错误类型**
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// src/errors/types.ts
|
|
209
|
+
export type ErrorType =
|
|
210
|
+
| 'SyntaxError'
|
|
211
|
+
| 'DivisionByZero'
|
|
212
|
+
| 'InvalidNumber'
|
|
213
|
+
| 'OverflowError'
|
|
214
|
+
| 'InvalidBase'
|
|
215
|
+
| 'UnmatchedParen'
|
|
216
|
+
| 'InvalidOperator'
|
|
217
|
+
| 'EmptyInput';
|
|
218
|
+
|
|
219
|
+
export interface Position {
|
|
220
|
+
start: number;
|
|
221
|
+
end: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface ErrorDetail {
|
|
225
|
+
type: ErrorType;
|
|
226
|
+
message: string;
|
|
227
|
+
position?: Position;
|
|
228
|
+
suggestion: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface SuccessResponse {
|
|
232
|
+
success: true;
|
|
233
|
+
result: string;
|
|
234
|
+
details?: {
|
|
235
|
+
expression?: string;
|
|
236
|
+
steps?: string[];
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface ErrorResponse {
|
|
241
|
+
success: false;
|
|
242
|
+
error: ErrorDetail;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export type CalculationResponse = SuccessResponse | ErrorResponse;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Step 4: 实现错误处理器**
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// src/errors/handler.ts
|
|
252
|
+
import {
|
|
253
|
+
ErrorType,
|
|
254
|
+
Position,
|
|
255
|
+
ErrorDetail,
|
|
256
|
+
ErrorResponse,
|
|
257
|
+
CalculationResponse,
|
|
258
|
+
SuccessResponse,
|
|
259
|
+
} from './types.js';
|
|
260
|
+
|
|
261
|
+
export class ErrorHandler {
|
|
262
|
+
private suggestions: Record<ErrorType, string> = {
|
|
263
|
+
SyntaxError: 'Check your expression syntax. Ensure operators and operands are correctly placed.',
|
|
264
|
+
DivisionByZero: 'Division by zero is not allowed. Check your denominators.',
|
|
265
|
+
InvalidNumber: 'The number format is invalid. Check for typos or unsupported formats.',
|
|
266
|
+
OverflowError: 'The number is too large or too small to process.',
|
|
267
|
+
InvalidBase: 'Invalid base specified. Supported bases are 2, 8, 10, and 16.',
|
|
268
|
+
UnmatchedParen: 'Unmatched parenthesis. Ensure every opening parenthesis has a closing one.',
|
|
269
|
+
InvalidOperator: 'Invalid operator for this operation. Check supported operators.',
|
|
270
|
+
EmptyInput: 'Expression cannot be empty. Please provide a valid expression.',
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
syntaxError(message: string, start: number, end: number): ErrorResponse {
|
|
274
|
+
return this.createError('SyntaxError', message, { start, end });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
runtimeError(type: ErrorType, message: string): ErrorResponse {
|
|
278
|
+
return this.createError(type, message);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
createError(
|
|
282
|
+
type: ErrorType,
|
|
283
|
+
message: string,
|
|
284
|
+
position?: Position
|
|
285
|
+
): ErrorResponse {
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
error: {
|
|
289
|
+
type,
|
|
290
|
+
message,
|
|
291
|
+
position,
|
|
292
|
+
suggestion: this.suggestions[type],
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
success(result: string, details?: SuccessResponse['details']): SuccessResponse {
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
result,
|
|
301
|
+
details,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export type { ErrorType, Position, ErrorDetail, ErrorResponse, CalculationResponse, SuccessResponse };
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Step 5: 运行测试验证通过**
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm test -- test/unit/error-handler.test.ts
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Step 6: 提交**
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git add src/errors test/unit/error-handler.test.ts
|
|
319
|
+
git commit -m "feat: implement error handling system with structured errors"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 阶段 3: 词法分析器 (Lexer)
|
|
325
|
+
|
|
326
|
+
### 任务 3: 实现 Token 类型和 Lexer
|
|
327
|
+
|
|
328
|
+
**文件:**
|
|
329
|
+
- 创建: `src/parser/ast.ts`
|
|
330
|
+
- 创建: `src/parser/lexer.ts`
|
|
331
|
+
- 测试: `test/unit/lexer.test.ts`
|
|
332
|
+
|
|
333
|
+
**Step 1: 编写 Lexer 测试 - 数字字面量**
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
mkdir -p src/parser
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
// test/unit/lexer.test.ts
|
|
341
|
+
import { describe, it, expect } from 'vitest';
|
|
342
|
+
import { Lexer } from '../../src/parser/lexer.js';
|
|
343
|
+
import { TokenType } from '../../src/parser/ast.js';
|
|
344
|
+
|
|
345
|
+
describe('Lexer', () => {
|
|
346
|
+
describe('Number literals', () => {
|
|
347
|
+
it('should tokenize decimal integers', () => {
|
|
348
|
+
const lexer = new Lexer();
|
|
349
|
+
const tokens = lexer.tokenize('42');
|
|
350
|
+
|
|
351
|
+
expect(tokens).toHaveLength(2); // NUMBER + EOF
|
|
352
|
+
expect(tokens[0].type).toBe('NUMBER');
|
|
353
|
+
expect(tokens[0].value).toBe('42');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should tokenize decimal floats', () => {
|
|
357
|
+
const lexer = new Lexer();
|
|
358
|
+
const tokens = lexer.tokenize('3.14');
|
|
359
|
+
|
|
360
|
+
expect(tokens[0].type).toBe('NUMBER');
|
|
361
|
+
expect(tokens[0].value).toBe('3.14');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should tokenize hex literals', () => {
|
|
365
|
+
const lexer = new Lexer();
|
|
366
|
+
const tokens = lexer.tokenize('0xFF');
|
|
367
|
+
|
|
368
|
+
expect(tokens[0].type).toBe('HEX');
|
|
369
|
+
expect(tokens[0].value).toBe('0xFF');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should tokenize binary literals', () => {
|
|
373
|
+
const lexer = new Lexer();
|
|
374
|
+
const tokens = lexer.tokenize('0b1011');
|
|
375
|
+
|
|
376
|
+
expect(tokens[0].type).toBe('BINARY');
|
|
377
|
+
expect(tokens[0].value).toBe('0b1011');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should tokenize octal literals', () => {
|
|
381
|
+
const lexer = new Lexer();
|
|
382
|
+
const tokens = lexer.tokenize('0o77');
|
|
383
|
+
|
|
384
|
+
expect(tokens[0].type).toBe('OCTAL');
|
|
385
|
+
expect(tokens[0].value).toBe('0o77');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Step 2: 运行测试验证失败**
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
npm test -- test/unit/lexer.test.ts
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Step 3: 实现 AST 类型定义**
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// src/parser/ast.ts
|
|
401
|
+
export type TokenType =
|
|
402
|
+
| 'NUMBER'
|
|
403
|
+
| 'HEX'
|
|
404
|
+
| 'BINARY'
|
|
405
|
+
| 'OCTAL'
|
|
406
|
+
| 'OPERATOR'
|
|
407
|
+
| 'LPAREN'
|
|
408
|
+
| 'RPAREN'
|
|
409
|
+
| 'CHAR_LITERAL'
|
|
410
|
+
| 'WHITESPACE'
|
|
411
|
+
| 'EOF';
|
|
412
|
+
|
|
413
|
+
export interface Token {
|
|
414
|
+
type: TokenType;
|
|
415
|
+
value: string;
|
|
416
|
+
position: { start: number; end: number };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export type ASTNode =
|
|
420
|
+
| BinaryOpNode
|
|
421
|
+
| UnaryOpNode
|
|
422
|
+
| LiteralNode
|
|
423
|
+
| GroupNode;
|
|
424
|
+
|
|
425
|
+
export interface BinaryOpNode {
|
|
426
|
+
type: 'BinaryOp';
|
|
427
|
+
operator: string;
|
|
428
|
+
left: ASTNode;
|
|
429
|
+
right: ASTNode;
|
|
430
|
+
position?: { start: number; end: number };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface UnaryOpNode {
|
|
434
|
+
type: 'UnaryOp';
|
|
435
|
+
operator: string;
|
|
436
|
+
operand: ASTNode;
|
|
437
|
+
position?: { start: number; end: number };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export interface LiteralNode {
|
|
441
|
+
type: 'Literal';
|
|
442
|
+
value: string;
|
|
443
|
+
tokenType: TokenType;
|
|
444
|
+
position?: { start: number; end: number };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export interface GroupNode {
|
|
448
|
+
type: 'Group';
|
|
449
|
+
expression: ASTNode;
|
|
450
|
+
position?: { start: number; end: number };
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Step 4: 实现 Lexer**
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// src/parser/lexer.ts
|
|
458
|
+
import { Token, TokenType } from './ast.js';
|
|
459
|
+
|
|
460
|
+
export class Lexer {
|
|
461
|
+
private pos = 0;
|
|
462
|
+
private input = '';
|
|
463
|
+
private length = 0;
|
|
464
|
+
|
|
465
|
+
tokenize(input: string): Token[] {
|
|
466
|
+
this.input = input;
|
|
467
|
+
this.pos = 0;
|
|
468
|
+
this.length = input.length;
|
|
469
|
+
const tokens: Token[] = [];
|
|
470
|
+
|
|
471
|
+
while (this.pos < this.length) {
|
|
472
|
+
const char = this.input[this.pos];
|
|
473
|
+
|
|
474
|
+
if (this.isWhitespace(char)) {
|
|
475
|
+
this.skipWhitespace();
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const token = this.readToken();
|
|
480
|
+
if (token && token.type !== 'WHITESPACE') {
|
|
481
|
+
tokens.push(token);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
tokens.push({
|
|
486
|
+
type: 'EOF',
|
|
487
|
+
value: '',
|
|
488
|
+
position: { start: this.pos, end: this.pos },
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return tokens;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private readToken(): Token | null {
|
|
495
|
+
const char = this.input[this.pos];
|
|
496
|
+
const start = this.pos;
|
|
497
|
+
|
|
498
|
+
// Hex literal: 0x or 0X
|
|
499
|
+
if (char === '0' && this.peek() === 'x') {
|
|
500
|
+
return this.readHexLiteral(start);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Binary literal: 0b or 0B
|
|
504
|
+
if (char === '0' && this.peek() === 'b') {
|
|
505
|
+
return this.readBinaryLiteral(start);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Octal literal: 0o or 0O
|
|
509
|
+
if (char === '0' && this.peek() === 'o') {
|
|
510
|
+
return this.readOctalLiteral(start);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Number (decimal or float)
|
|
514
|
+
if (this.isDigit(char) || (char === '.' && this.isDigit(this.peek()))) {
|
|
515
|
+
return this.readNumber(start);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Operators
|
|
519
|
+
if (this.isOperatorChar(char)) {
|
|
520
|
+
return this.readOperator(start);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Parentheses
|
|
524
|
+
if (char === '(') {
|
|
525
|
+
this.advance();
|
|
526
|
+
return { type: 'LPAREN', value: '(', position: { start, end: this.pos } };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (char === ')') {
|
|
530
|
+
this.advance();
|
|
531
|
+
return { type: 'RPAREN', value: ')', position: { start, end: this.pos } };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Char literal
|
|
535
|
+
if (char === "'") {
|
|
536
|
+
return this.readCharLiteral(start);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Unknown character
|
|
540
|
+
this.advance();
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private readNumber(start: number): Token {
|
|
545
|
+
let value = '';
|
|
546
|
+
|
|
547
|
+
while (this.pos < this.length && (this.isDigit(this.input[this.pos]) || this.input[this.pos] === '.')) {
|
|
548
|
+
value += this.input[this.pos];
|
|
549
|
+
this.advance();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return { type: 'NUMBER', value, position: { start, end: this.pos } };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private readHexLiteral(start: number): Token {
|
|
556
|
+
this.advance(); // '0'
|
|
557
|
+
this.advance(); // 'x'
|
|
558
|
+
|
|
559
|
+
let value = '0x';
|
|
560
|
+
while (this.pos < this.length && this.isHexDigit(this.input[this.pos])) {
|
|
561
|
+
value += this.input[this.pos];
|
|
562
|
+
this.advance();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return { type: 'HEX', value, position: { start, end: this.pos } };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private readBinaryLiteral(start: number): Token {
|
|
569
|
+
this.advance(); // '0'
|
|
570
|
+
this.advance(); // 'b'
|
|
571
|
+
|
|
572
|
+
let value = '0b';
|
|
573
|
+
while (this.pos < this.length && (this.input[this.pos] === '0' || this.input[this.pos] === '1')) {
|
|
574
|
+
value += this.input[this.pos];
|
|
575
|
+
this.advance();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { type: 'BINARY', value, position: { start, end: this.pos } };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private readOctalLiteral(start: number): Token {
|
|
582
|
+
this.advance(); // '0'
|
|
583
|
+
this.advance(); // 'o'
|
|
584
|
+
|
|
585
|
+
let value = '0o';
|
|
586
|
+
while (this.pos < this.length && this.isOctalDigit(this.input[this.pos])) {
|
|
587
|
+
value += this.input[this.pos];
|
|
588
|
+
this.advance();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { type: 'OCTAL', value, position: { start, end: this.pos } };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private readOperator(start: number): Token {
|
|
595
|
+
// Check for multi-char operators: <<, >>, >>>
|
|
596
|
+
const twoChar = this.input.substring(this.pos, this.pos + 2);
|
|
597
|
+
|
|
598
|
+
if (twoChar === '<<' || twoChar === '>>') {
|
|
599
|
+
// Check for >>>
|
|
600
|
+
if (twoChar === '>>' && this.peek(1) === '>') {
|
|
601
|
+
this.pos += 3;
|
|
602
|
+
return { type: 'OPERATOR', value: '>>>', position: { start, end: this.pos } };
|
|
603
|
+
}
|
|
604
|
+
this.pos += 2;
|
|
605
|
+
return { type: 'OPERATOR', value: twoChar, position: { start, end: this.pos } };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Single char operators
|
|
609
|
+
const op = this.input[this.pos];
|
|
610
|
+
this.advance();
|
|
611
|
+
return { type: 'OPERATOR', value: op, position: { start, end: this.pos } };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private readCharLiteral(start: number): Token {
|
|
615
|
+
this.advance(); // opening '
|
|
616
|
+
|
|
617
|
+
let value = "'";
|
|
618
|
+
|
|
619
|
+
// Handle escape sequences
|
|
620
|
+
if (this.input[this.pos] === '\\') {
|
|
621
|
+
value += this.input[this.pos];
|
|
622
|
+
this.advance();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (this.pos < this.length) {
|
|
626
|
+
value += this.input[this.pos];
|
|
627
|
+
this.advance();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (this.pos < this.length && this.input[this.pos] === "'") {
|
|
631
|
+
value += this.input[this.pos];
|
|
632
|
+
this.advance();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { type: 'CHAR_LITERAL', value, position: { start, end: this.pos } };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private skipWhitespace(): void {
|
|
639
|
+
while (this.pos < this.length && this.isWhitespace(this.input[this.pos])) {
|
|
640
|
+
this.pos++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private advance(): void {
|
|
645
|
+
this.pos++;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private peek(offset = 1): string {
|
|
649
|
+
return this.pos + offset < this.length ? this.input[this.pos + offset] : '';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private isWhitespace(char: string): boolean {
|
|
653
|
+
return /\s/.test(char);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private isDigit(char: string): boolean {
|
|
657
|
+
return /[0-9]/.test(char);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private isHexDigit(char: string): boolean {
|
|
661
|
+
return /[0-9a-fA-F]/.test(char);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private isOctalDigit(char: string): boolean {
|
|
665
|
+
return /[0-7]/.test(char);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private isOperatorChar(char: string): boolean {
|
|
669
|
+
return ['+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '<', '>'].includes(char);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export type { Token, TokenType };
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Step 5: 运行测试验证通过**
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
npm test -- test/unit/lexer.test.ts
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Step 6: 添加更多 Lexer 测试**
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
// 添加到 test/unit/lexer.test.ts
|
|
686
|
+
|
|
687
|
+
describe('Operators', () => {
|
|
688
|
+
it('should tokenize single-char operators', () => {
|
|
689
|
+
const lexer = new Lexer();
|
|
690
|
+
const tokens = lexer.tokenize('2+3*4');
|
|
691
|
+
|
|
692
|
+
expect(tokens[1].type).toBe('OPERATOR');
|
|
693
|
+
expect(tokens[1].value).toBe('+');
|
|
694
|
+
expect(tokens[3].type).toBe('OPERATOR');
|
|
695
|
+
expect(tokens[3].value).toBe('*');
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('should tokenize shift operators', () => {
|
|
699
|
+
const lexer = new Lexer();
|
|
700
|
+
const tokens = lexer.tokenize('1 << 2 >> 1 >>> 3');
|
|
701
|
+
|
|
702
|
+
expect(tokens[1].value).toBe('<<');
|
|
703
|
+
expect(tokens[4].value).toBe('>>');
|
|
704
|
+
expect(tokens[7].value).toBe('>>>');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('should tokenize bitwise operators', () => {
|
|
708
|
+
const lexer = new Lexer();
|
|
709
|
+
const tokens = lexer.tokenize('0xFF & 0x0F | 0x10 ^ 0x20');
|
|
710
|
+
|
|
711
|
+
expect(tokens[1].value).toBe('&');
|
|
712
|
+
expect(tokens[3].value).toBe('|');
|
|
713
|
+
expect(tokens[5].value).toBe('^');
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
describe('Parentheses', () => {
|
|
718
|
+
it('should tokenize parentheses', () => {
|
|
719
|
+
const lexer = new Lexer();
|
|
720
|
+
const tokens = lexer.tokenize('(2 + 3)');
|
|
721
|
+
|
|
722
|
+
expect(tokens[0].type).toBe('LPAREN');
|
|
723
|
+
expect(tokens[4].type).toBe('RPAREN');
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe('Positions', () => {
|
|
728
|
+
it('should track token positions correctly', () => {
|
|
729
|
+
const lexer = new Lexer();
|
|
730
|
+
const tokens = lexer.tokenize('42 + 3');
|
|
731
|
+
|
|
732
|
+
expect(tokens[0].position).toEqual({ start: 0, end: 2 });
|
|
733
|
+
expect(tokens[1].position).toEqual({ start: 3, end: 4 });
|
|
734
|
+
expect(tokens[2].position).toEqual({ start: 5, end: 6 });
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Step 7: 运行测试验证通过**
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
npm test -- test/unit/lexer.test.ts
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**Step 8: 提交**
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
git add src/parser test/unit/lexer.test.ts
|
|
749
|
+
git commit -m "feat: implement lexer with support for all numeric literals and operators"
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## 阶段 4: 语法分析器 (Parser)
|
|
755
|
+
|
|
756
|
+
### 任务 4: 实现 Parser
|
|
757
|
+
|
|
758
|
+
**文件:**
|
|
759
|
+
- 创建: `src/parser/parser.ts`
|
|
760
|
+
- 测试: `test/unit/parser.test.ts`
|
|
761
|
+
|
|
762
|
+
**Step 1: 编写 Parser 测试 - 运算符优先级**
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
// test/unit/parser.test.ts
|
|
766
|
+
import { describe, it, expect } from 'vitest';
|
|
767
|
+
import { Parser } from '../../src/parser/parser.js';
|
|
768
|
+
import { Lexer } from '../../src/parser/lexer.js';
|
|
769
|
+
|
|
770
|
+
describe('Parser', () => {
|
|
771
|
+
describe('Operator precedence', () => {
|
|
772
|
+
it('should respect multiplication over addition', () => {
|
|
773
|
+
const lexer = new Lexer();
|
|
774
|
+
const parser = new Parser();
|
|
775
|
+
const tokens = lexer.tokenize('2 + 3 * 4');
|
|
776
|
+
const ast = parser.parse(tokens);
|
|
777
|
+
|
|
778
|
+
expect(ast.type).toBe('BinaryOp');
|
|
779
|
+
expect(ast.operator).toBe('+');
|
|
780
|
+
expect((ast as any).left.type).toBe('Literal');
|
|
781
|
+
expect((ast as any).left.value).toBe('2');
|
|
782
|
+
expect((ast as any).right.type).toBe('BinaryOp');
|
|
783
|
+
expect((ast as any).right.operator).toBe('*');
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('should respect parentheses', () => {
|
|
787
|
+
const lexer = new Lexer();
|
|
788
|
+
const parser = new Parser();
|
|
789
|
+
const tokens = lexer.tokenize('(2 + 3) * 4');
|
|
790
|
+
const ast = parser.parse(tokens);
|
|
791
|
+
|
|
792
|
+
expect(ast.type).toBe('BinaryOp');
|
|
793
|
+
expect(ast.operator).toBe('*');
|
|
794
|
+
expect((ast as any).left.type).toBe('Group');
|
|
795
|
+
expect((ast as any).right.type).toBe('Literal');
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Step 2: 运行测试验证失败**
|
|
802
|
+
|
|
803
|
+
```bash
|
|
804
|
+
npm test -- test/unit/parser.test.ts
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**Step 3: 实现 Parser**
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
// src/parser/parser.ts
|
|
811
|
+
import { Token, ASTNode, BinaryOpNode, UnaryOpNode, LiteralNode, GroupNode, TokenType } from './ast.js';
|
|
812
|
+
import { ErrorHandler } from '../errors/handler.js';
|
|
813
|
+
|
|
814
|
+
// Operator precedence (higher number = higher precedence)
|
|
815
|
+
const PRECEDENCE: Record<string, number> = {
|
|
816
|
+
'(': 10,
|
|
817
|
+
')': 10,
|
|
818
|
+
'~': 9,
|
|
819
|
+
'!': 9,
|
|
820
|
+
'*': 8,
|
|
821
|
+
'/': 8,
|
|
822
|
+
'%': 8,
|
|
823
|
+
'+': 7,
|
|
824
|
+
'-': 7,
|
|
825
|
+
'<<': 6,
|
|
826
|
+
'>>': 6,
|
|
827
|
+
'>>>': 6,
|
|
828
|
+
'&': 5,
|
|
829
|
+
'^': 4,
|
|
830
|
+
'|': 3,
|
|
831
|
+
'&&': 2,
|
|
832
|
+
'||': 1,
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
export class Parser {
|
|
836
|
+
private pos = 0;
|
|
837
|
+
private tokens: Token[] = [];
|
|
838
|
+
private errorHandler = new ErrorHandler();
|
|
839
|
+
|
|
840
|
+
parse(tokens: Token[]): ASTNode {
|
|
841
|
+
this.tokens = tokens;
|
|
842
|
+
this.pos = 0;
|
|
843
|
+
|
|
844
|
+
if (this.tokens.length === 0 || (this.tokens.length === 1 && this.tokens[0].type === 'EOF')) {
|
|
845
|
+
throw new Error('Empty input');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const ast = this.parseExpression();
|
|
849
|
+
|
|
850
|
+
if (this.current()?.type !== 'EOF') {
|
|
851
|
+
const token = this.current()!;
|
|
852
|
+
throw this.errorHandler.syntaxError(
|
|
853
|
+
`Unexpected token: ${token.value}`,
|
|
854
|
+
token.position.start,
|
|
855
|
+
token.position.end
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return ast;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private parseExpression(minPrecedence = 0): ASTNode {
|
|
863
|
+
let left = this.parseUnary();
|
|
864
|
+
|
|
865
|
+
while (true) {
|
|
866
|
+
const op = this.current();
|
|
867
|
+
if (!op || op.type !== 'OPERATOR') {
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const precedence = PRECEDENCE[op.value] ?? 0;
|
|
872
|
+
if (precedence < minPrecedence) {
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
this.advance();
|
|
877
|
+
const right = this.parseExpression(precedence + 1);
|
|
878
|
+
|
|
879
|
+
left = {
|
|
880
|
+
type: 'BinaryOp',
|
|
881
|
+
operator: op.value,
|
|
882
|
+
left,
|
|
883
|
+
right,
|
|
884
|
+
position: { start: left.position?.start ?? 0, end: right.position?.end ?? 0 },
|
|
885
|
+
} as BinaryOpNode;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return left;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private parseUnary(): ASTNode {
|
|
892
|
+
const token = this.current();
|
|
893
|
+
|
|
894
|
+
if (token && token.type === 'OPERATOR' && (token.value === '~' || token.value === '!' || token.value === '-')) {
|
|
895
|
+
this.advance();
|
|
896
|
+
const operand = this.parseUnary();
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
type: 'UnaryOp',
|
|
900
|
+
operator: token.value,
|
|
901
|
+
operand,
|
|
902
|
+
position: { start: token.position.start, end: operand.position?.end ?? token.position.end },
|
|
903
|
+
} as UnaryOpNode;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return this.parsePrimary();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private parsePrimary(): ASTNode {
|
|
910
|
+
const token = this.current();
|
|
911
|
+
|
|
912
|
+
if (!token || token.type === 'EOF') {
|
|
913
|
+
throw new Error('Unexpected end of input');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Parenthesized expression
|
|
917
|
+
if (token.type === 'LPAREN') {
|
|
918
|
+
this.advance();
|
|
919
|
+
const expr = this.parseExpression();
|
|
920
|
+
|
|
921
|
+
const closing = this.current();
|
|
922
|
+
if (!closing || closing.type !== 'RPAREN') {
|
|
923
|
+
throw this.errorHandler.syntaxError(
|
|
924
|
+
'Unmatched parenthesis: expected )',
|
|
925
|
+
token.position.start,
|
|
926
|
+
token.position.end
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
this.advance();
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
type: 'Group',
|
|
933
|
+
expression: expr,
|
|
934
|
+
position: { start: token.position.start, end: closing.position.end },
|
|
935
|
+
} as GroupNode;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Literal
|
|
939
|
+
if (this.isLiteral(token)) {
|
|
940
|
+
this.advance();
|
|
941
|
+
return {
|
|
942
|
+
type: 'Literal',
|
|
943
|
+
value: token.value,
|
|
944
|
+
tokenType: token.type,
|
|
945
|
+
position: token.position,
|
|
946
|
+
} as LiteralNode;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
throw this.errorHandler.syntaxError(
|
|
950
|
+
`Unexpected token: ${token.value}`,
|
|
951
|
+
token.position.start,
|
|
952
|
+
token.position.end
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private isLiteral(token: Token): boolean {
|
|
957
|
+
return ['NUMBER', 'HEX', 'BINARY', 'OCTAL', 'CHAR_LITERAL'].includes(token.type);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private current(): Token | undefined {
|
|
961
|
+
return this.tokens[this.pos];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
private advance(): void {
|
|
965
|
+
this.pos++;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
**Step 4: 运行测试验证通过**
|
|
971
|
+
|
|
972
|
+
```bash
|
|
973
|
+
npm test -- test/unit/parser.test.ts
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
**Step 5: 添加更多 Parser 测试**
|
|
977
|
+
|
|
978
|
+
```typescript
|
|
979
|
+
// 添加到 test/unit/parser.test.ts
|
|
980
|
+
|
|
981
|
+
describe('Unary operators', () => {
|
|
982
|
+
it('should parse bitwise NOT', () => {
|
|
983
|
+
const lexer = new Lexer();
|
|
984
|
+
const parser = new Parser();
|
|
985
|
+
const tokens = lexer.tokenize('~0xFF');
|
|
986
|
+
const ast = parser.parse(tokens);
|
|
987
|
+
|
|
988
|
+
expect(ast.type).toBe('UnaryOp');
|
|
989
|
+
expect((ast as any).operator).toBe('~');
|
|
990
|
+
expect((ast as any).operand.type).toBe('Literal');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('should parse negation', () => {
|
|
994
|
+
const lexer = new Lexer();
|
|
995
|
+
const parser = new Parser();
|
|
996
|
+
const tokens = lexer.tokenize('-42');
|
|
997
|
+
const ast = parser.parse(tokens);
|
|
998
|
+
|
|
999
|
+
expect(ast.type).toBe('UnaryOp');
|
|
1000
|
+
expect((ast as any).operator).toBe('-');
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
describe('Bitwise operators', () => {
|
|
1005
|
+
it('should parse AND', () => {
|
|
1006
|
+
const lexer = new Lexer();
|
|
1007
|
+
const parser = new Parser();
|
|
1008
|
+
const tokens = lexer.tokenize('0xFF & 0x0F');
|
|
1009
|
+
const ast = parser.parse(tokens);
|
|
1010
|
+
|
|
1011
|
+
expect(ast.type).toBe('BinaryOp');
|
|
1012
|
+
expect((ast as any).operator).toBe('&');
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('should parse OR', () => {
|
|
1016
|
+
const lexer = new Lexer();
|
|
1017
|
+
const parser = new Parser();
|
|
1018
|
+
const tokens = lexer.tokenize('0xFF | 0x0F');
|
|
1019
|
+
const ast = parser.parse(tokens);
|
|
1020
|
+
|
|
1021
|
+
expect(ast.type).toBe('BinaryOp');
|
|
1022
|
+
expect((ast as any).operator).toBe('|');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('should parse XOR', () => {
|
|
1026
|
+
const lexer = new Lexer();
|
|
1027
|
+
const parser = new Parser();
|
|
1028
|
+
const tokens = lexer.tokenize('0xFF ^ 0x0F');
|
|
1029
|
+
const ast = parser.parse(tokens);
|
|
1030
|
+
|
|
1031
|
+
expect(ast.type).toBe('BinaryOp');
|
|
1032
|
+
expect((ast as any).operator).toBe('^');
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
describe('Shift operators', () => {
|
|
1037
|
+
it('should parse left shift', () => {
|
|
1038
|
+
const lexer = new Lexer();
|
|
1039
|
+
const parser = new Parser();
|
|
1040
|
+
const tokens = lexer.tokenize('1 << 4');
|
|
1041
|
+
const ast = parser.parse(tokens);
|
|
1042
|
+
|
|
1043
|
+
expect(ast.type).toBe('BinaryOp');
|
|
1044
|
+
expect((ast as any).operator).toBe('<<');
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it('should parse right shift', () => {
|
|
1048
|
+
const lexer = new Lexer();
|
|
1049
|
+
const parser = new Parser();
|
|
1050
|
+
const tokens = lexer.tokenize('16 >> 2');
|
|
1051
|
+
const ast = parser.parse(tokens);
|
|
1052
|
+
|
|
1053
|
+
expect(ast.type).toBe('BinaryOp');
|
|
1054
|
+
expect((ast as any).operator).toBe('>>');
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
**Step 6: 运行测试验证通过**
|
|
1060
|
+
|
|
1061
|
+
```bash
|
|
1062
|
+
npm test -- test/unit/parser.test.ts
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Step 7: 提交**
|
|
1066
|
+
|
|
1067
|
+
```bash
|
|
1068
|
+
git add src/parser/parser.ts test/unit/parser.test.ts
|
|
1069
|
+
git commit -m "feat: implement recursive descent parser with operator precedence"
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
---
|
|
1073
|
+
|
|
1074
|
+
## 阶段 5: 四则运算引擎
|
|
1075
|
+
|
|
1076
|
+
### 任务 5: 实现 DecimalEngine
|
|
1077
|
+
|
|
1078
|
+
**文件:**
|
|
1079
|
+
- 创建: `src/engines/decimal.ts`
|
|
1080
|
+
- 测试: `test/unit/decimal-engine.test.ts`
|
|
1081
|
+
|
|
1082
|
+
**Step 1: 编写 DecimalEngine 测试**
|
|
1083
|
+
|
|
1084
|
+
```bash
|
|
1085
|
+
mkdir -p src/engines
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
```typescript
|
|
1089
|
+
// test/unit/decimal-engine.test.ts
|
|
1090
|
+
import { describe, it, expect } from 'vitest';
|
|
1091
|
+
import { DecimalEngine } from '../../src/engines/decimal.js';
|
|
1092
|
+
import { Lexer } from '../../src/parser/lexer.js';
|
|
1093
|
+
import { Parser } from '../../src/parser/parser.js';
|
|
1094
|
+
|
|
1095
|
+
describe('DecimalEngine', () => {
|
|
1096
|
+
const lexer = new Lexer();
|
|
1097
|
+
const parser = new Parser();
|
|
1098
|
+
const engine = new DecimalEngine();
|
|
1099
|
+
|
|
1100
|
+
describe('Basic operations', () => {
|
|
1101
|
+
it('should add two numbers', () => {
|
|
1102
|
+
const tokens = lexer.tokenize('2 + 3');
|
|
1103
|
+
const ast = parser.parse(tokens);
|
|
1104
|
+
const result = engine.evaluate(ast);
|
|
1105
|
+
|
|
1106
|
+
expect(result.toString()).toBe('5');
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it('should subtract two numbers', () => {
|
|
1110
|
+
const tokens = lexer.tokenize('5 - 3');
|
|
1111
|
+
const ast = parser.parse(tokens);
|
|
1112
|
+
const result = engine.evaluate(ast);
|
|
1113
|
+
|
|
1114
|
+
expect(result.toString()).toBe('2');
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
it('should multiply two numbers', () => {
|
|
1118
|
+
const tokens = lexer.tokenize('4 * 3');
|
|
1119
|
+
const ast = parser.parse(tokens);
|
|
1120
|
+
const result = engine.evaluate(ast);
|
|
1121
|
+
|
|
1122
|
+
expect(result.toString()).toBe('12');
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it('should divide two numbers', () => {
|
|
1126
|
+
const tokens = lexer.tokenize('10 / 2');
|
|
1127
|
+
const ast = parser.parse(tokens);
|
|
1128
|
+
const result = engine.evaluate(ast);
|
|
1129
|
+
|
|
1130
|
+
expect(result.toString()).toBe('5');
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
describe('Decimal precision', () => {
|
|
1135
|
+
it('should handle 0.1 + 0.2 correctly', () => {
|
|
1136
|
+
const tokens = lexer.tokenize('0.1 + 0.2');
|
|
1137
|
+
const ast = parser.parse(tokens);
|
|
1138
|
+
const result = engine.evaluate(ast);
|
|
1139
|
+
|
|
1140
|
+
expect(result.toString()).toBe('0.3');
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it('should handle floating point division', () => {
|
|
1144
|
+
const tokens = lexer.tokenize('1 / 3');
|
|
1145
|
+
const ast = parser.parse(tokens);
|
|
1146
|
+
const result = engine.evaluate(ast);
|
|
1147
|
+
|
|
1148
|
+
expect(parseFloat(result.toString())).toBeCloseTo(0.333, 2);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
describe('Operator precedence', () => {
|
|
1153
|
+
it('should respect precedence', () => {
|
|
1154
|
+
const tokens = lexer.tokenize('2 + 3 * 4');
|
|
1155
|
+
const ast = parser.parse(tokens);
|
|
1156
|
+
const result = engine.evaluate(ast);
|
|
1157
|
+
|
|
1158
|
+
expect(result.toString()).toBe('14');
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it('should respect parentheses', () => {
|
|
1162
|
+
const tokens = lexer.tokenize('(2 + 3) * 4');
|
|
1163
|
+
const ast = parser.parse(tokens);
|
|
1164
|
+
const result = engine.evaluate(ast);
|
|
1165
|
+
|
|
1166
|
+
expect(result.toString()).toBe('20');
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
describe('Error handling', () => {
|
|
1171
|
+
it('should throw on division by zero', () => {
|
|
1172
|
+
const tokens = lexer.tokenize('1 / 0');
|
|
1173
|
+
const ast = parser.parse(tokens);
|
|
1174
|
+
|
|
1175
|
+
expect(() => engine.evaluate(ast)).toThrow('DivisionByZero');
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
**Step 2: 运行测试验证失败**
|
|
1182
|
+
|
|
1183
|
+
```bash
|
|
1184
|
+
npm test -- test/unit/decimal-engine.test.ts
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
**Step 3: 实现 DecimalEngine**
|
|
1188
|
+
|
|
1189
|
+
```typescript
|
|
1190
|
+
// src/engines/decimal.ts
|
|
1191
|
+
import { Decimal } from 'decimal.js';
|
|
1192
|
+
import { ASTNode, BinaryOpNode, UnaryOpNode, LiteralNode, GroupNode } from '../parser/ast.js';
|
|
1193
|
+
import { ErrorHandler } from '../errors/handler.js';
|
|
1194
|
+
|
|
1195
|
+
export class DecimalEngine {
|
|
1196
|
+
private errorHandler = new ErrorHandler();
|
|
1197
|
+
|
|
1198
|
+
evaluate(ast: ASTNode): Decimal {
|
|
1199
|
+
return this.visit(ast);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
private visit(node: ASTNode): Decimal {
|
|
1203
|
+
switch (node.type) {
|
|
1204
|
+
case 'Literal':
|
|
1205
|
+
return this.visitLiteral(node as LiteralNode);
|
|
1206
|
+
case 'BinaryOp':
|
|
1207
|
+
return this.visitBinaryOp(node as BinaryOpNode);
|
|
1208
|
+
case 'UnaryOp':
|
|
1209
|
+
return this.visitUnaryOp(node as UnaryOpNode);
|
|
1210
|
+
case 'Group':
|
|
1211
|
+
return this.visit((node as GroupNode).expression);
|
|
1212
|
+
default:
|
|
1213
|
+
throw new Error(`Unknown node type: ${(node as any).type}`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private visitLiteral(node: LiteralNode): Decimal {
|
|
1218
|
+
// Strip prefixes for non-decimal literals
|
|
1219
|
+
const value = node.value.replace(/^(0x|0b|0o)/, '');
|
|
1220
|
+
|
|
1221
|
+
// For literals with non-standard prefixes, parse as if they were decimal
|
|
1222
|
+
// This is a simplification - proper implementation would handle these differently
|
|
1223
|
+
return new Decimal(value);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private visitBinaryOp(node: BinaryOpNode): Decimal {
|
|
1227
|
+
const left = this.visit(node.left);
|
|
1228
|
+
const right = this.visit(node.right);
|
|
1229
|
+
|
|
1230
|
+
switch (node.operator) {
|
|
1231
|
+
case '+':
|
|
1232
|
+
return left.plus(right);
|
|
1233
|
+
case '-':
|
|
1234
|
+
return left.minus(right);
|
|
1235
|
+
case '*':
|
|
1236
|
+
return left.times(right);
|
|
1237
|
+
case '/':
|
|
1238
|
+
if (right.isZero()) {
|
|
1239
|
+
throw this.errorHandler.runtimeError('DivisionByZero', 'Cannot divide by zero');
|
|
1240
|
+
}
|
|
1241
|
+
return left.dividedBy(right);
|
|
1242
|
+
case '%':
|
|
1243
|
+
if (right.isZero()) {
|
|
1244
|
+
throw this.errorHandler.runtimeError('DivisionByZero', 'Cannot divide by zero');
|
|
1245
|
+
}
|
|
1246
|
+
return left.modulo(right);
|
|
1247
|
+
default:
|
|
1248
|
+
throw new Error(`Unsupported operator for decimal engine: ${node.operator}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private visitUnaryOp(node: UnaryOpNode): Decimal {
|
|
1253
|
+
const operand = this.visit(node.operand);
|
|
1254
|
+
|
|
1255
|
+
switch (node.operator) {
|
|
1256
|
+
case '-':
|
|
1257
|
+
return operand.negated();
|
|
1258
|
+
case '+':
|
|
1259
|
+
return operand;
|
|
1260
|
+
default:
|
|
1261
|
+
throw new Error(`Unsupported unary operator for decimal engine: ${node.operator}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
**Step 4: 运行测试验证通过**
|
|
1268
|
+
|
|
1269
|
+
```bash
|
|
1270
|
+
npm test -- test/unit/decimal-engine.test.ts
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
**Step 5: 添加更多测试**
|
|
1274
|
+
|
|
1275
|
+
```typescript
|
|
1276
|
+
// 添加到 test/unit/decimal-engine.test.ts
|
|
1277
|
+
|
|
1278
|
+
describe('Complex expressions', () => {
|
|
1279
|
+
it('should handle multiple operations', () => {
|
|
1280
|
+
const tokens = lexer.tokenize('2.5 * 3 + (10 - 4) / 2');
|
|
1281
|
+
const ast = parser.parse(tokens);
|
|
1282
|
+
const result = engine.evaluate(ast);
|
|
1283
|
+
|
|
1284
|
+
expect(result.toString()).toBe('10.5');
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it('should handle modulo', () => {
|
|
1288
|
+
const tokens = lexer.tokenize('10 % 3');
|
|
1289
|
+
const ast = parser.parse(tokens);
|
|
1290
|
+
const result = engine.evaluate(ast);
|
|
1291
|
+
|
|
1292
|
+
expect(result.toString()).toBe('1');
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
**Step 6: 运行测试验证通过**
|
|
1298
|
+
|
|
1299
|
+
```bash
|
|
1300
|
+
npm test -- test/unit/decimal-engine.test.ts
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
**Step 7: 提交**
|
|
1304
|
+
|
|
1305
|
+
```bash
|
|
1306
|
+
git add src/engines/decimal.ts test/unit/decimal-engine.test.ts
|
|
1307
|
+
git commit -m "feat: implement decimal engine with high precision arithmetic"
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
## 阶段 6: 程序员计算引擎
|
|
1313
|
+
|
|
1314
|
+
### 任务 6: 实现 ProgrammerEngine
|
|
1315
|
+
|
|
1316
|
+
**文件:**
|
|
1317
|
+
- 创建: `src/engines/programmer.ts`
|
|
1318
|
+
- 测试: `test/unit/programmer-engine.test.ts`
|
|
1319
|
+
|
|
1320
|
+
**Step 1: 编写 ProgrammerEngine 测试**
|
|
1321
|
+
|
|
1322
|
+
```typescript
|
|
1323
|
+
// test/unit/programmer-engine.test.ts
|
|
1324
|
+
import { describe, it, expect } from 'vitest';
|
|
1325
|
+
import { ProgrammerEngine } from '../../src/engines/programmer.js';
|
|
1326
|
+
import { Lexer } from '../../src/parser/lexer.js';
|
|
1327
|
+
import { Parser } from '../../src/parser/parser.js';
|
|
1328
|
+
|
|
1329
|
+
describe('ProgrammerEngine', () => {
|
|
1330
|
+
const lexer = new Lexer();
|
|
1331
|
+
const parser = new Parser();
|
|
1332
|
+
const engine = new ProgrammerEngine();
|
|
1333
|
+
|
|
1334
|
+
describe('Bitwise AND', () => {
|
|
1335
|
+
it('should perform bitwise AND', () => {
|
|
1336
|
+
const tokens = lexer.tokenize('0xFF & 0x0F');
|
|
1337
|
+
const ast = parser.parse(tokens);
|
|
1338
|
+
const result = engine.evaluate(ast);
|
|
1339
|
+
|
|
1340
|
+
expect(result.toString()).toBe('15');
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
describe('Bitwise OR', () => {
|
|
1345
|
+
it('should perform bitwise OR', () => {
|
|
1346
|
+
const tokens = lexer.tokenize('0xF0 | 0x0F');
|
|
1347
|
+
const ast = parser.parse(tokens);
|
|
1348
|
+
const result = engine.evaluate(ast);
|
|
1349
|
+
|
|
1350
|
+
expect(result.toString()).toBe('255');
|
|
1351
|
+
});
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
describe('Bitwise XOR', () => {
|
|
1355
|
+
it('should perform bitwise XOR', () => {
|
|
1356
|
+
const tokens = lexer.tokenize('0xFF ^ 0xFF');
|
|
1357
|
+
const ast = parser.parse(tokens);
|
|
1358
|
+
const result = engine.evaluate(ast);
|
|
1359
|
+
|
|
1360
|
+
expect(result.toString()).toBe('0');
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
describe('Bitwise NOT', () => {
|
|
1365
|
+
it('should perform bitwise NOT', () => {
|
|
1366
|
+
const tokens = lexer.tokenize('~0x00');
|
|
1367
|
+
const ast = parser.parse(tokens);
|
|
1368
|
+
const result = engine.evaluate(ast);
|
|
1369
|
+
|
|
1370
|
+
// ~0 = -1 in two's complement
|
|
1371
|
+
expect(result.toString()).toBe('-1');
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
describe('Left shift', () => {
|
|
1376
|
+
it('should perform left shift', () => {
|
|
1377
|
+
const tokens = lexer.tokenize('1 << 4');
|
|
1378
|
+
const ast = parser.parse(tokens);
|
|
1379
|
+
const result = engine.evaluate(ast);
|
|
1380
|
+
|
|
1381
|
+
expect(result.toString()).toBe('16');
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
describe('Right shift', () => {
|
|
1386
|
+
it('should perform right shift', () => {
|
|
1387
|
+
const tokens = lexer.tokenize('16 >> 2');
|
|
1388
|
+
const ast = parser.parse(tokens);
|
|
1389
|
+
const result = engine.evaluate(ast);
|
|
1390
|
+
|
|
1391
|
+
expect(result.toString()).toBe('4');
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
describe('BigInt support', () => {
|
|
1396
|
+
it('should handle very large numbers', () => {
|
|
1397
|
+
const tokens = lexer.tokenize('0xFFFFFFFFFFFFFFFF + 1');
|
|
1398
|
+
const ast = parser.parse(tokens);
|
|
1399
|
+
const result = engine.evaluate(ast);
|
|
1400
|
+
|
|
1401
|
+
expect(result.toString()).toBe('18446744073709551616');
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
**Step 2: 运行测试验证失败**
|
|
1408
|
+
|
|
1409
|
+
```bash
|
|
1410
|
+
npm test -- test/unit/programmer-engine.test.ts
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
**Step 3: 实现 ProgrammerEngine**
|
|
1414
|
+
|
|
1415
|
+
```typescript
|
|
1416
|
+
// src/engines/programmer.ts
|
|
1417
|
+
import { ASTNode, BinaryOpNode, UnaryOpNode, LiteralNode, GroupNode, TokenType } from '../parser/ast.js';
|
|
1418
|
+
import { ErrorHandler } from '../errors/handler.js';
|
|
1419
|
+
|
|
1420
|
+
export class ProgrammerEngine {
|
|
1421
|
+
private errorHandler = new ErrorHandler();
|
|
1422
|
+
|
|
1423
|
+
evaluate(ast: ASTNode): bigint {
|
|
1424
|
+
return this.visit(ast);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private visit(node: ASTNode): bigint {
|
|
1428
|
+
switch (node.type) {
|
|
1429
|
+
case 'Literal':
|
|
1430
|
+
return this.parseLiteral(node as LiteralNode);
|
|
1431
|
+
case 'BinaryOp':
|
|
1432
|
+
return this.visitBinaryOp(node as BinaryOpNode);
|
|
1433
|
+
case 'UnaryOp':
|
|
1434
|
+
return this.visitUnaryOp(node as UnaryOpNode);
|
|
1435
|
+
case 'Group':
|
|
1436
|
+
return this.visit((node as GroupNode).expression);
|
|
1437
|
+
default:
|
|
1438
|
+
throw new Error(`Unknown node type: ${(node as any).type}`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private parseLiteral(node: LiteralNode): bigint {
|
|
1443
|
+
const { value, tokenType } = node;
|
|
1444
|
+
|
|
1445
|
+
switch (tokenType) {
|
|
1446
|
+
case 'NUMBER':
|
|
1447
|
+
// Decimal integer
|
|
1448
|
+
if (value.includes('.')) {
|
|
1449
|
+
throw this.errorHandler.runtimeError('InvalidNumber', 'Floating point numbers not supported in programmer mode');
|
|
1450
|
+
}
|
|
1451
|
+
return BigInt(value);
|
|
1452
|
+
case 'HEX':
|
|
1453
|
+
return BigInt(value);
|
|
1454
|
+
case 'BINARY':
|
|
1455
|
+
return BigInt(value);
|
|
1456
|
+
case 'OCTAL':
|
|
1457
|
+
return BigInt(value);
|
|
1458
|
+
case 'CHAR_LITERAL':
|
|
1459
|
+
// Parse char literal like 'A' or '\n'
|
|
1460
|
+
const char = value.substring(1, value.length - 1);
|
|
1461
|
+
return BigInt(char.charCodeAt(0));
|
|
1462
|
+
default:
|
|
1463
|
+
throw new Error(`Unsupported literal type: ${tokenType}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
private visitBinaryOp(node: BinaryOpNode): bigint {
|
|
1468
|
+
const left = this.visit(node.left);
|
|
1469
|
+
const right = this.visit(node.right);
|
|
1470
|
+
|
|
1471
|
+
switch (node.operator) {
|
|
1472
|
+
case '+':
|
|
1473
|
+
return left + right;
|
|
1474
|
+
case '-':
|
|
1475
|
+
return left - right;
|
|
1476
|
+
case '*':
|
|
1477
|
+
return left * right;
|
|
1478
|
+
case '/':
|
|
1479
|
+
if (right === 0n) {
|
|
1480
|
+
throw this.errorHandler.runtimeError('DivisionByZero', 'Cannot divide by zero');
|
|
1481
|
+
}
|
|
1482
|
+
return left / right;
|
|
1483
|
+
case '%':
|
|
1484
|
+
if (right === 0n) {
|
|
1485
|
+
throw this.errorHandler.runtimeError('DivisionByZero', 'Cannot divide by zero');
|
|
1486
|
+
}
|
|
1487
|
+
return left % right;
|
|
1488
|
+
case '&':
|
|
1489
|
+
return left & right;
|
|
1490
|
+
case '|':
|
|
1491
|
+
return left | right;
|
|
1492
|
+
case '^':
|
|
1493
|
+
return left ^ right;
|
|
1494
|
+
case '<<':
|
|
1495
|
+
return left << right;
|
|
1496
|
+
case '>>':
|
|
1497
|
+
return left >> right;
|
|
1498
|
+
case '>>>':
|
|
1499
|
+
// Unsigned right shift - convert to number, shift, back to bigint
|
|
1500
|
+
return BigInt(Number(left) >>> Number(right));
|
|
1501
|
+
default:
|
|
1502
|
+
throw new Error(`Unsupported operator for programmer engine: ${node.operator}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
private visitUnaryOp(node: UnaryOpNode): bigint {
|
|
1507
|
+
const operand = this.visit(node.operand);
|
|
1508
|
+
|
|
1509
|
+
switch (node.operator) {
|
|
1510
|
+
case '~':
|
|
1511
|
+
return ~operand;
|
|
1512
|
+
case '-':
|
|
1513
|
+
return -operand;
|
|
1514
|
+
case '+':
|
|
1515
|
+
return operand;
|
|
1516
|
+
default:
|
|
1517
|
+
throw new Error(`Unsupported unary operator for programmer engine: ${node.operator}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
**Step 4: 运行测试验证通过**
|
|
1524
|
+
|
|
1525
|
+
```bash
|
|
1526
|
+
npm test -- test/unit/programmer-engine.test.ts
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
**Step 5: 添加更多测试**
|
|
1530
|
+
|
|
1531
|
+
```typescript
|
|
1532
|
+
// 添加到 test/unit/programmer-engine.test.ts
|
|
1533
|
+
|
|
1534
|
+
describe('Complex expressions', () => {
|
|
1535
|
+
it('should handle multiple bitwise operations', () => {
|
|
1536
|
+
const tokens = lexer.tokenize('0xFF & 0x0F | 0x10');
|
|
1537
|
+
const ast = parser.parse(tokens);
|
|
1538
|
+
const result = engine.evaluate(ast);
|
|
1539
|
+
|
|
1540
|
+
// (0xFF & 0x0F) | 0x10 = 15 | 16 = 31
|
|
1541
|
+
expect(result.toString()).toBe('31');
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it('should handle shift with bitwise ops', () => {
|
|
1545
|
+
const tokens = lexer.tokenize('(1 << 4) & 0xFF');
|
|
1546
|
+
const ast = parser.parse(tokens);
|
|
1547
|
+
const result = engine.evaluate(ast);
|
|
1548
|
+
|
|
1549
|
+
// (16) & 255 = 16
|
|
1550
|
+
expect(result.toString()).toBe('16');
|
|
1551
|
+
});
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
describe('Binary literals', () => {
|
|
1555
|
+
it('should parse binary literals', () => {
|
|
1556
|
+
const tokens = lexer.tokenize('0b1010');
|
|
1557
|
+
const ast = parser.parse(tokens);
|
|
1558
|
+
const result = engine.evaluate(ast);
|
|
1559
|
+
|
|
1560
|
+
expect(result.toString()).toBe('10');
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
it('should perform operations on binary literals', () => {
|
|
1564
|
+
const tokens = lexer.tokenize('0b1010 << 2');
|
|
1565
|
+
const ast = parser.parse(tokens);
|
|
1566
|
+
const result = engine.evaluate(ast);
|
|
1567
|
+
|
|
1568
|
+
expect(result.toString()).toBe('40');
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
describe('Octal literals', () => {
|
|
1573
|
+
it('should parse octal literals', () => {
|
|
1574
|
+
const tokens = lexer.tokenize('0o77');
|
|
1575
|
+
const ast = parser.parse(tokens);
|
|
1576
|
+
const result = engine.evaluate(ast);
|
|
1577
|
+
|
|
1578
|
+
expect(result.toString()).toBe('63');
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
**Step 6: 运行测试验证通过**
|
|
1584
|
+
|
|
1585
|
+
```bash
|
|
1586
|
+
npm test -- test/unit/programmer-engine.test.ts
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
**Step 7: 提交**
|
|
1590
|
+
|
|
1591
|
+
```bash
|
|
1592
|
+
git add src/engines/programmer.ts test/unit/programmer-engine.test.ts
|
|
1593
|
+
git commit -m "feat: implement programmer engine with BigInt and bitwise operations"
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
---
|
|
1597
|
+
|
|
1598
|
+
## 阶段 7: MCP 工具实现
|
|
1599
|
+
|
|
1600
|
+
### 任务 7: 实现 basic_calculate 工具
|
|
1601
|
+
|
|
1602
|
+
**文件:**
|
|
1603
|
+
- 创建: `src/mcp/tools/basic.ts`
|
|
1604
|
+
- 创建: `src/mcp/server.ts`
|
|
1605
|
+
- 测试: `test/integration/basic-calculate.test.ts`
|
|
1606
|
+
|
|
1607
|
+
**Step 1: 编写 basic_calculate 集成测试**
|
|
1608
|
+
|
|
1609
|
+
```bash
|
|
1610
|
+
mkdir -p src/mcp/tools test/integration
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
```typescript
|
|
1614
|
+
// test/integration/basic-calculate.test.ts
|
|
1615
|
+
import { describe, it, expect } from 'vitest';
|
|
1616
|
+
import { basicCalculate } from '../../src/mcp/tools/basic.js';
|
|
1617
|
+
|
|
1618
|
+
describe('basic_calculate MCP tool', () => {
|
|
1619
|
+
it('should calculate simple addition', () => {
|
|
1620
|
+
const result = basicCalculate({ expression: '2 + 3' });
|
|
1621
|
+
|
|
1622
|
+
expect(result.success).toBe(true);
|
|
1623
|
+
expect(result.result).toBe('5');
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
it('should calculate with operator precedence', () => {
|
|
1627
|
+
const result = basicCalculate({ expression: '2 + 3 * 4' });
|
|
1628
|
+
|
|
1629
|
+
expect(result.success).toBe(true);
|
|
1630
|
+
expect(result.result).toBe('14');
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it('should handle decimal precision', () => {
|
|
1634
|
+
const result = basicCalculate({ expression: '0.1 + 0.2' });
|
|
1635
|
+
|
|
1636
|
+
expect(result.success).toBe(true);
|
|
1637
|
+
expect(result.result).toBe('0.3');
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it('should return structured error for invalid syntax', () => {
|
|
1641
|
+
const result = basicCalculate({ expression: '2 + * 3' });
|
|
1642
|
+
|
|
1643
|
+
expect(result.success).toBe(false);
|
|
1644
|
+
expect(result.error?.type).toBe('SyntaxError');
|
|
1645
|
+
expect(result.error?.position).toBeDefined();
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
it('should return structured error for division by zero', () => {
|
|
1649
|
+
const result = basicCalculate({ expression: '1 / 0' });
|
|
1650
|
+
|
|
1651
|
+
expect(result.success).toBe(false);
|
|
1652
|
+
expect(result.error?.type).toBe('DivisionByZero');
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
it('should handle empty input', () => {
|
|
1656
|
+
const result = basicCalculate({ expression: '' });
|
|
1657
|
+
|
|
1658
|
+
expect(result.success).toBe(false);
|
|
1659
|
+
expect(result.error?.type).toBe('EmptyInput');
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
**Step 2: 运行测试验证失败**
|
|
1665
|
+
|
|
1666
|
+
```bash
|
|
1667
|
+
npm test -- test/integration/basic-calculate.test.ts
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
**Step 3: 实现 basic_calculate 工具**
|
|
1671
|
+
|
|
1672
|
+
```typescript
|
|
1673
|
+
// src/mcp/tools/basic.ts
|
|
1674
|
+
import { Lexer } from '../../parser/lexer.js';
|
|
1675
|
+
import { Parser } from '../../parser/parser.js';
|
|
1676
|
+
import { DecimalEngine } from '../../engines/decimal.js';
|
|
1677
|
+
import { ErrorHandler, CalculationResponse } from '../../errors/handler.js';
|
|
1678
|
+
|
|
1679
|
+
export interface BasicCalculateInput {
|
|
1680
|
+
expression: string;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
export function basicCalculate(input: BasicCalculateInput): CalculationResponse {
|
|
1684
|
+
const errorHandler = new ErrorHandler();
|
|
1685
|
+
const lexer = new Lexer();
|
|
1686
|
+
const parser = new Parser();
|
|
1687
|
+
const engine = new DecimalEngine();
|
|
1688
|
+
|
|
1689
|
+
const { expression } = input;
|
|
1690
|
+
|
|
1691
|
+
// Validate input
|
|
1692
|
+
if (!expression || expression.trim().length === 0) {
|
|
1693
|
+
return errorHandler.runtimeError('EmptyInput', 'Expression cannot be empty');
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
try {
|
|
1697
|
+
// Tokenize
|
|
1698
|
+
const tokens = lexer.tokenize(expression);
|
|
1699
|
+
|
|
1700
|
+
// Parse
|
|
1701
|
+
const ast = parser.parse(tokens);
|
|
1702
|
+
|
|
1703
|
+
// Evaluate
|
|
1704
|
+
const result = engine.evaluate(ast);
|
|
1705
|
+
|
|
1706
|
+
return errorHandler.success(result.toString(), {
|
|
1707
|
+
expression,
|
|
1708
|
+
});
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
// Handle structured errors
|
|
1711
|
+
if (typeof error === 'object' && error !== null && 'success' in error) {
|
|
1712
|
+
return error as CalculationResponse;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Handle unexpected errors
|
|
1716
|
+
return errorHandler.runtimeError('InvalidNumber', error instanceof Error ? error.message : 'Unknown error');
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
**Step 4: 运行测试验证通过**
|
|
1722
|
+
|
|
1723
|
+
```bash
|
|
1724
|
+
npm test -- test/integration/basic-calculate.test.ts
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
**Step 5: 提交**
|
|
1728
|
+
|
|
1729
|
+
```bash
|
|
1730
|
+
git add src/mcp/tools/basic.ts test/integration/basic-calculate.test.ts
|
|
1731
|
+
git commit -m "feat: implement basic_calculate MCP tool with decimal precision"
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
---
|
|
1735
|
+
|
|
1736
|
+
### 任务 8: 实现 programmer_calculate 工具
|
|
1737
|
+
|
|
1738
|
+
**文件:**
|
|
1739
|
+
- 创建: `src/mcp/tools/programmer.ts`
|
|
1740
|
+
- 测试: `test/integration/programmer-calculate.test.ts`
|
|
1741
|
+
|
|
1742
|
+
**Step 1: 编写 programmer_calculate 集成测试**
|
|
1743
|
+
|
|
1744
|
+
```typescript
|
|
1745
|
+
// test/integration/programmer-calculate.test.ts
|
|
1746
|
+
import { describe, it, expect } from 'vitest';
|
|
1747
|
+
import { programmerCalculate } from '../../src/mcp/tools/programmer.js';
|
|
1748
|
+
|
|
1749
|
+
describe('programmer_calculate MCP tool', () => {
|
|
1750
|
+
it('should calculate bitwise AND', () => {
|
|
1751
|
+
const result = programmerCalculate({ expression: '0xFF & 0x0F' });
|
|
1752
|
+
|
|
1753
|
+
expect(result.success).toBe(true);
|
|
1754
|
+
expect(result.result).toBe('15');
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
it('should calculate bitwise OR', () => {
|
|
1758
|
+
const result = programmerCalculate({ expression: '0xFF | 0x0F' });
|
|
1759
|
+
|
|
1760
|
+
expect(result.success).toBe(true);
|
|
1761
|
+
expect(result.result).toBe('255');
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
it('should calculate left shift', () => {
|
|
1765
|
+
const result = programmerCalculate({ expression: '1 << 4' });
|
|
1766
|
+
|
|
1767
|
+
expect(result.success).toBe(true);
|
|
1768
|
+
expect(result.result).toBe('16');
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
it('should handle binary literals', () => {
|
|
1772
|
+
const result = programmerCalculate({ expression: '0b1010 << 2' });
|
|
1773
|
+
|
|
1774
|
+
expect(result.success).toBe(true);
|
|
1775
|
+
expect(result.result).toBe('40');
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
it('should handle very large numbers', () => {
|
|
1779
|
+
const result = programmerCalculate({ expression: '0xFFFFFFFFFFFFFFFF + 1' });
|
|
1780
|
+
|
|
1781
|
+
expect(result.success).toBe(true);
|
|
1782
|
+
expect(result.result).toBe('18446744073709551616');
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
it('should return structured error for invalid syntax', () => {
|
|
1786
|
+
const result = programmerCalculate({ expression: '0xFF && 0x0F' });
|
|
1787
|
+
|
|
1788
|
+
expect(result.success).toBe(false);
|
|
1789
|
+
expect(result.error?.type).toBe('SyntaxError');
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
**Step 2: 运行测试验证失败**
|
|
1795
|
+
|
|
1796
|
+
```bash
|
|
1797
|
+
npm test -- test/integration/programmer-calculate.test.ts
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
**Step 3: 实现 programmer_calculate 工具**
|
|
1801
|
+
|
|
1802
|
+
```typescript
|
|
1803
|
+
// src/mcp/tools/programmer.ts
|
|
1804
|
+
import { Lexer } from '../../parser/lexer.js';
|
|
1805
|
+
import { Parser } from '../../parser/parser.js';
|
|
1806
|
+
import { ProgrammerEngine } from '../../engines/programmer.js';
|
|
1807
|
+
import { ErrorHandler, CalculationResponse } from '../../errors/handler.js';
|
|
1808
|
+
|
|
1809
|
+
export interface ProgrammerCalculateInput {
|
|
1810
|
+
expression: string;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
export function programmerCalculate(input: ProgrammerCalculateInput): CalculationResponse {
|
|
1814
|
+
const errorHandler = new ErrorHandler();
|
|
1815
|
+
const lexer = new Lexer();
|
|
1816
|
+
const parser = new Parser();
|
|
1817
|
+
const engine = new ProgrammerEngine();
|
|
1818
|
+
|
|
1819
|
+
const { expression } = input;
|
|
1820
|
+
|
|
1821
|
+
// Validate input
|
|
1822
|
+
if (!expression || expression.trim().length === 0) {
|
|
1823
|
+
return errorHandler.runtimeError('EmptyInput', 'Expression cannot be empty');
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
try {
|
|
1827
|
+
// Tokenize
|
|
1828
|
+
const tokens = lexer.tokenize(expression);
|
|
1829
|
+
|
|
1830
|
+
// Parse
|
|
1831
|
+
const ast = parser.parse(tokens);
|
|
1832
|
+
|
|
1833
|
+
// Evaluate
|
|
1834
|
+
const result = engine.evaluate(ast);
|
|
1835
|
+
|
|
1836
|
+
return errorHandler.success(result.toString(), {
|
|
1837
|
+
expression,
|
|
1838
|
+
});
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
// Handle structured errors
|
|
1841
|
+
if (typeof error === 'object' && error !== null && 'success' in error) {
|
|
1842
|
+
return error as CalculationResponse;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Handle unexpected errors
|
|
1846
|
+
return errorHandler.runtimeError('InvalidNumber', error instanceof Error ? error.message : 'Unknown error');
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
```
|
|
1850
|
+
|
|
1851
|
+
**Step 4: 运行测试验证通过**
|
|
1852
|
+
|
|
1853
|
+
```bash
|
|
1854
|
+
npm test -- test/integration/programmer-calculate.test.ts
|
|
1855
|
+
```
|
|
1856
|
+
|
|
1857
|
+
**Step 5: 提交**
|
|
1858
|
+
|
|
1859
|
+
```bash
|
|
1860
|
+
git add src/mcp/tools/programmer.ts test/integration/programmer-calculate.test.ts
|
|
1861
|
+
git commit -m "feat: implement programmer_calculate MCP tool with BigInt"
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
---
|
|
1865
|
+
|
|
1866
|
+
### 任务 9: 实现 convert_base 工具
|
|
1867
|
+
|
|
1868
|
+
**文件:**
|
|
1869
|
+
- 创建: `src/mcp/tools/conversion.ts`
|
|
1870
|
+
- 测试: `test/integration/conversion.test.ts`
|
|
1871
|
+
|
|
1872
|
+
**Step 1: 编写 convert_base 集成测试**
|
|
1873
|
+
|
|
1874
|
+
```typescript
|
|
1875
|
+
// test/integration/conversion.test.ts
|
|
1876
|
+
import { describe, it, expect } from 'vitest';
|
|
1877
|
+
import { convertBase } from '../../src/mcp/tools/conversion.js';
|
|
1878
|
+
|
|
1879
|
+
describe('convert_base MCP tool', () => {
|
|
1880
|
+
it('should convert decimal to hex', () => {
|
|
1881
|
+
const result = convertBase({ value: '255', from: 10, to: 16 });
|
|
1882
|
+
|
|
1883
|
+
expect(result.success).toBe(true);
|
|
1884
|
+
expect(result.result).toBe('ff');
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
it('should convert hex to decimal', () => {
|
|
1888
|
+
const result = convertBase({ value: 'FF', from: 16, to: 10 });
|
|
1889
|
+
|
|
1890
|
+
expect(result.success).toBe(true);
|
|
1891
|
+
expect(result.result).toBe('255');
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
it('should convert decimal to binary', () => {
|
|
1895
|
+
const result = convertBase({ value: '10', from: 10, to: 2 });
|
|
1896
|
+
|
|
1897
|
+
expect(result.success).toBe(true);
|
|
1898
|
+
expect(result.result).toBe('1010');
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
it('should convert binary to decimal', () => {
|
|
1902
|
+
const result = convertBase({ value: '1010', from: 2, to: 10 });
|
|
1903
|
+
|
|
1904
|
+
expect(result.success).toBe(true);
|
|
1905
|
+
expect(result.result).toBe('10');
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
it('should convert hex to binary', () => {
|
|
1909
|
+
const result = convertBase({ value: 'F', from: 16, to: 2 });
|
|
1910
|
+
|
|
1911
|
+
expect(result.success).toBe(true);
|
|
1912
|
+
expect(result.result).toBe('1111');
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
it('should return error for invalid base', () => {
|
|
1916
|
+
const result = convertBase({ value: '10', from: 10, to: 3 });
|
|
1917
|
+
|
|
1918
|
+
expect(result.success).toBe(false);
|
|
1919
|
+
expect(result.error?.type).toBe('InvalidBase');
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
it('should return error for invalid value', () => {
|
|
1923
|
+
const result = convertBase({ value: 'ZZ', from: 16, to: 10 });
|
|
1924
|
+
|
|
1925
|
+
expect(result.success).toBe(false);
|
|
1926
|
+
expect(result.error?.type).toBe('InvalidNumber');
|
|
1927
|
+
});
|
|
1928
|
+
});
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
**Step 2: 运行测试验证失败**
|
|
1932
|
+
|
|
1933
|
+
```bash
|
|
1934
|
+
npm test -- test/integration/conversion.test.ts
|
|
1935
|
+
```
|
|
1936
|
+
|
|
1937
|
+
**Step 3: 实现 convert_base 工具**
|
|
1938
|
+
|
|
1939
|
+
```typescript
|
|
1940
|
+
// src/mcp/tools/conversion.ts
|
|
1941
|
+
import { ErrorHandler, CalculationResponse } from '../../errors/handler.js';
|
|
1942
|
+
|
|
1943
|
+
export interface ConvertBaseInput {
|
|
1944
|
+
value: string;
|
|
1945
|
+
from: number;
|
|
1946
|
+
to: number;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
export function convertBase(input: ConvertBaseInput): CalculationResponse {
|
|
1950
|
+
const errorHandler = new ErrorHandler();
|
|
1951
|
+
const { value, from, to } = input;
|
|
1952
|
+
|
|
1953
|
+
// Validate bases
|
|
1954
|
+
const validBases = [2, 8, 10, 16];
|
|
1955
|
+
if (!validBases.includes(from)) {
|
|
1956
|
+
return errorHandler.runtimeError('InvalidBase', `Source base ${from} is not supported. Supported bases: ${validBases.join(', ')}`);
|
|
1957
|
+
}
|
|
1958
|
+
if (!validBases.includes(to)) {
|
|
1959
|
+
return errorHandler.runtimeError('InvalidBase', `Target base ${to} is not supported. Supported bases: ${validBases.join(', ')}`);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Validate value
|
|
1963
|
+
if (!value || value.trim().length === 0) {
|
|
1964
|
+
return errorHandler.runtimeError('InvalidNumber', 'Value cannot be empty');
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
try {
|
|
1968
|
+
// Parse from source base
|
|
1969
|
+
const bigintValue = parseValueToBigInt(value, from);
|
|
1970
|
+
|
|
1971
|
+
// Convert to target base
|
|
1972
|
+
const result = bigintValue.toString(to);
|
|
1973
|
+
|
|
1974
|
+
return errorHandler.success(result, {
|
|
1975
|
+
expression: `${value} (base ${from}) → base ${to}`,
|
|
1976
|
+
});
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
return errorHandler.runtimeError('InvalidNumber', error instanceof Error ? error.message : 'Failed to parse value');
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function parseValueToBigInt(value: string, base: number): bigint {
|
|
1983
|
+
const cleanValue = value.trim().toLowerCase();
|
|
1984
|
+
|
|
1985
|
+
// Remove common prefixes if present
|
|
1986
|
+
let numValue = cleanValue;
|
|
1987
|
+
if (cleanValue.startsWith('0x')) {
|
|
1988
|
+
numValue = cleanValue.substring(2);
|
|
1989
|
+
} else if (cleanValue.startsWith('0b')) {
|
|
1990
|
+
numValue = cleanValue.substring(2);
|
|
1991
|
+
} else if (cleanValue.startsWith('0o')) {
|
|
1992
|
+
numValue = cleanValue.substring(2);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// Validate characters for the base
|
|
1996
|
+
const validDigits: Record<number, RegExp> = {
|
|
1997
|
+
2: /^[01]+$/,
|
|
1998
|
+
8: /^[0-7]+$/,
|
|
1999
|
+
10: /^[0-9]+$/,
|
|
2000
|
+
16: /^[0-9a-f]+$/,
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
if (!validDigits[base].test(numValue)) {
|
|
2004
|
+
throw new Error(`Invalid value for base ${base}: ${value}`);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
return BigInt(`0${base === 16 ? 'x' : ''}${numValue}`);
|
|
2008
|
+
}
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
**Step 4: 运行测试验证通过**
|
|
2012
|
+
|
|
2013
|
+
```bash
|
|
2014
|
+
npm test -- test/integration/conversion.test.ts
|
|
2015
|
+
```
|
|
2016
|
+
|
|
2017
|
+
**Step 5: 提交**
|
|
2018
|
+
|
|
2019
|
+
```bash
|
|
2020
|
+
git add src/mcp/tools/conversion.ts test/integration/conversion.test.ts
|
|
2021
|
+
git commit -m "feat: implement convert_base MCP tool"
|
|
2022
|
+
```
|
|
2023
|
+
|
|
2024
|
+
---
|
|
2025
|
+
|
|
2026
|
+
### 任务 10: 实现 ASCII 工具
|
|
2027
|
+
|
|
2028
|
+
**文件:**
|
|
2029
|
+
- 创建: `src/mcp/tools/ascii.ts`
|
|
2030
|
+
- 测试: `test/integration/ascii.test.ts`
|
|
2031
|
+
|
|
2032
|
+
**Step 1: 编写 ASCII 工具集成测试**
|
|
2033
|
+
|
|
2034
|
+
```typescript
|
|
2035
|
+
// test/integration/ascii.test.ts
|
|
2036
|
+
import { describe, it, expect } from 'vitest';
|
|
2037
|
+
import { asciiEncode, asciiDecode } from '../../src/mcp/tools/ascii.js';
|
|
2038
|
+
|
|
2039
|
+
describe('ASCII MCP tools', () => {
|
|
2040
|
+
describe('ascii_encode', () => {
|
|
2041
|
+
it('should encode text to ASCII codes', () => {
|
|
2042
|
+
const result = asciiEncode({ text: 'Hello' });
|
|
2043
|
+
|
|
2044
|
+
expect(result.success).toBe(true);
|
|
2045
|
+
expect(result.result).toBe('72 101 108 108 111');
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
it('should encode single character', () => {
|
|
2049
|
+
const result = asciiEncode({ text: 'A' });
|
|
2050
|
+
|
|
2051
|
+
expect(result.success).toBe(true);
|
|
2052
|
+
expect(result.result).toBe('65');
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
it('should handle empty string', () => {
|
|
2056
|
+
const result = asciiEncode({ text: '' });
|
|
2057
|
+
|
|
2058
|
+
expect(result.success).toBe(true);
|
|
2059
|
+
expect(result.result).toBe('');
|
|
2060
|
+
});
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
describe('ascii_decode', () => {
|
|
2064
|
+
it('should decode ASCII codes to text', () => {
|
|
2065
|
+
const result = asciiDecode({ codes: '72 101 108 108 111' });
|
|
2066
|
+
|
|
2067
|
+
expect(result.success).toBe(true);
|
|
2068
|
+
expect(result.result).toBe('Hello');
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
it('should decode single code', () => {
|
|
2072
|
+
const result = asciiDecode({ codes: '65' });
|
|
2073
|
+
|
|
2074
|
+
expect(result.success).toBe(true);
|
|
2075
|
+
expect(result.result).toBe('A');
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
it('should handle comma-separated codes', () => {
|
|
2079
|
+
const result = asciiDecode({ codes: '72,101,108,108,111' });
|
|
2080
|
+
|
|
2081
|
+
expect(result.success).toBe(true);
|
|
2082
|
+
expect(result.result).toBe('Hello');
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
it('should return error for invalid code', () => {
|
|
2086
|
+
const result = asciiDecode({ codes: '999' });
|
|
2087
|
+
|
|
2088
|
+
expect(result.success).toBe(false);
|
|
2089
|
+
expect(result.error?.type).toBe('InvalidNumber');
|
|
2090
|
+
});
|
|
2091
|
+
});
|
|
2092
|
+
});
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
**Step 2: 运行测试验证失败**
|
|
2096
|
+
|
|
2097
|
+
```bash
|
|
2098
|
+
npm test -- test/integration/ascii.test.ts
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
**Step 3: 实现 ASCII 工具**
|
|
2102
|
+
|
|
2103
|
+
```typescript
|
|
2104
|
+
// src/mcp/tools/ascii.ts
|
|
2105
|
+
import { ErrorHandler, CalculationResponse } from '../../errors/handler.js';
|
|
2106
|
+
|
|
2107
|
+
export interface AsciiEncodeInput {
|
|
2108
|
+
text: string;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
export interface AsciiDecodeInput {
|
|
2112
|
+
codes: string;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
export function asciiEncode(input: AsciiEncodeInput): CalculationResponse {
|
|
2116
|
+
const errorHandler = new ErrorHandler();
|
|
2117
|
+
const { text } = input;
|
|
2118
|
+
|
|
2119
|
+
try {
|
|
2120
|
+
const codes = text
|
|
2121
|
+
.split('')
|
|
2122
|
+
.map(char => char.charCodeAt(0))
|
|
2123
|
+
.join(' ');
|
|
2124
|
+
|
|
2125
|
+
return errorHandler.success(codes, {
|
|
2126
|
+
expression: `"${text}" → ASCII codes`,
|
|
2127
|
+
});
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
return errorHandler.runtimeError('InvalidNumber', error instanceof Error ? error.message : 'Failed to encode text');
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
export function asciiDecode(input: AsciiDecodeInput): CalculationResponse {
|
|
2134
|
+
const errorHandler = new ErrorHandler();
|
|
2135
|
+
const { codes } = input;
|
|
2136
|
+
|
|
2137
|
+
if (!codes || codes.trim().length === 0) {
|
|
2138
|
+
return errorHandler.success('', {
|
|
2139
|
+
expression: 'Empty codes → empty text',
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
try {
|
|
2144
|
+
// Parse codes (support space and comma separated)
|
|
2145
|
+
const codeList = codes
|
|
2146
|
+
.split(/[\s,]+/)
|
|
2147
|
+
.filter(s => s.length > 0)
|
|
2148
|
+
.map(s => parseInt(s, 10));
|
|
2149
|
+
|
|
2150
|
+
// Validate codes
|
|
2151
|
+
for (const code of codeList) {
|
|
2152
|
+
if (isNaN(code) || code < 0 || code > 127) {
|
|
2153
|
+
throw new Error(`Invalid ASCII code: ${code}`);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const text = codeList.map(code => String.fromCharCode(code)).join('');
|
|
2158
|
+
|
|
2159
|
+
return errorHandler.success(text, {
|
|
2160
|
+
expression: `ASCII codes → "${text}"`,
|
|
2161
|
+
});
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
return errorHandler.runtimeError('InvalidNumber', error instanceof Error ? error.message : 'Failed to decode ASCII codes');
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
```
|
|
2167
|
+
|
|
2168
|
+
**Step 4: 运行测试验证通过**
|
|
2169
|
+
|
|
2170
|
+
```bash
|
|
2171
|
+
npm test -- test/integration/ascii.test.ts
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
**Step 5: 提交**
|
|
2175
|
+
|
|
2176
|
+
```bash
|
|
2177
|
+
git add src/mcp/tools/ascii.ts test/integration/ascii.test.ts
|
|
2178
|
+
git commit -m "feat: implement ASCII encode/decode MCP tools"
|
|
2179
|
+
```
|
|
2180
|
+
|
|
2181
|
+
---
|
|
2182
|
+
|
|
2183
|
+
### 任务 11: 实现 MCP Server
|
|
2184
|
+
|
|
2185
|
+
**文件:**
|
|
2186
|
+
- 修改: `src/mcp/server.ts`
|
|
2187
|
+
- 创建: `src/index.ts`
|
|
2188
|
+
|
|
2189
|
+
**Step 1: 实现 MCP Server**
|
|
2190
|
+
|
|
2191
|
+
```typescript
|
|
2192
|
+
// src/mcp/server.ts
|
|
2193
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2194
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
2195
|
+
import {
|
|
2196
|
+
CallToolRequestSchema,
|
|
2197
|
+
ListToolsRequestSchema,
|
|
2198
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
2199
|
+
import { basicCalculate } from './tools/basic.js';
|
|
2200
|
+
import { programmerCalculate } from './tools/programmer.js';
|
|
2201
|
+
import { convertBase } from './tools/conversion.js';
|
|
2202
|
+
import { asciiEncode, asciiDecode } from './tools/ascii.js';
|
|
2203
|
+
|
|
2204
|
+
const server = new Server(
|
|
2205
|
+
{
|
|
2206
|
+
name: 'mcp-calculator',
|
|
2207
|
+
version: '0.1.0',
|
|
2208
|
+
},
|
|
2209
|
+
{
|
|
2210
|
+
capabilities: {
|
|
2211
|
+
tools: {},
|
|
2212
|
+
},
|
|
2213
|
+
}
|
|
2214
|
+
);
|
|
2215
|
+
|
|
2216
|
+
// List available tools
|
|
2217
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2218
|
+
return {
|
|
2219
|
+
tools: [
|
|
2220
|
+
{
|
|
2221
|
+
name: 'basic_calculate',
|
|
2222
|
+
description: 'Calculate basic arithmetic expressions with decimal precision. Supports: +, -, *, /, %, parentheses. Example: "2 + 3 * 4" or "0.1 + 0.2"',
|
|
2223
|
+
inputSchema: {
|
|
2224
|
+
type: 'object',
|
|
2225
|
+
properties: {
|
|
2226
|
+
expression: {
|
|
2227
|
+
type: 'string',
|
|
2228
|
+
description: 'The arithmetic expression to evaluate',
|
|
2229
|
+
},
|
|
2230
|
+
},
|
|
2231
|
+
required: ['expression'],
|
|
2232
|
+
},
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
name: 'programmer_calculate',
|
|
2236
|
+
description: 'Calculate programmer-style expressions with BigInt and bitwise operations. Supports: hex (0xFF), binary (0b1010), octal (0o77), &, |, ^, ~, <<, >>, >>>. Example: "0xFF & 0x0F" or "1 << 4"',
|
|
2237
|
+
inputSchema: {
|
|
2238
|
+
type: 'object',
|
|
2239
|
+
properties: {
|
|
2240
|
+
expression: {
|
|
2241
|
+
type: 'string',
|
|
2242
|
+
description: 'The programmer expression to evaluate',
|
|
2243
|
+
},
|
|
2244
|
+
},
|
|
2245
|
+
required: ['expression'],
|
|
2246
|
+
},
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
name: 'convert_base',
|
|
2250
|
+
description: 'Convert numbers between different bases (2, 8, 10, 16). Example: { value: "255", from: 10, to: 16 }',
|
|
2251
|
+
inputSchema: {
|
|
2252
|
+
type: 'object',
|
|
2253
|
+
properties: {
|
|
2254
|
+
value: {
|
|
2255
|
+
type: 'string',
|
|
2256
|
+
description: 'The number value to convert',
|
|
2257
|
+
},
|
|
2258
|
+
from: {
|
|
2259
|
+
type: 'number',
|
|
2260
|
+
description: 'Source base (2, 8, 10, or 16)',
|
|
2261
|
+
},
|
|
2262
|
+
to: {
|
|
2263
|
+
type: 'number',
|
|
2264
|
+
description: 'Target base (2, 8, 10, or 16)',
|
|
2265
|
+
},
|
|
2266
|
+
},
|
|
2267
|
+
required: ['value', 'from', 'to'],
|
|
2268
|
+
},
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
name: 'ascii_encode',
|
|
2272
|
+
description: 'Encode text to ASCII codes. Example: "Hello" → "72 101 108 108 111"',
|
|
2273
|
+
inputSchema: {
|
|
2274
|
+
type: 'object',
|
|
2275
|
+
properties: {
|
|
2276
|
+
text: {
|
|
2277
|
+
type: 'string',
|
|
2278
|
+
description: 'The text to encode to ASCII codes',
|
|
2279
|
+
},
|
|
2280
|
+
},
|
|
2281
|
+
required: ['text'],
|
|
2282
|
+
},
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
name: 'ascii_decode',
|
|
2286
|
+
description: 'Decode ASCII codes to text. Example: "72 101 108 108 111" → "Hello"',
|
|
2287
|
+
inputSchema: {
|
|
2288
|
+
type: 'object',
|
|
2289
|
+
properties: {
|
|
2290
|
+
codes: {
|
|
2291
|
+
type: 'string',
|
|
2292
|
+
description: 'ASCII codes separated by spaces or commas',
|
|
2293
|
+
},
|
|
2294
|
+
},
|
|
2295
|
+
required: ['codes'],
|
|
2296
|
+
},
|
|
2297
|
+
},
|
|
2298
|
+
],
|
|
2299
|
+
};
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
// Handle tool calls
|
|
2303
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2304
|
+
const { name, arguments: args } = request.params;
|
|
2305
|
+
|
|
2306
|
+
switch (name) {
|
|
2307
|
+
case 'basic_calculate':
|
|
2308
|
+
return {
|
|
2309
|
+
content: [
|
|
2310
|
+
{
|
|
2311
|
+
type: 'text',
|
|
2312
|
+
text: JSON.stringify(basicCalculate(args as { expression: string })),
|
|
2313
|
+
},
|
|
2314
|
+
],
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
case 'programmer_calculate':
|
|
2318
|
+
return {
|
|
2319
|
+
content: [
|
|
2320
|
+
{
|
|
2321
|
+
type: 'text',
|
|
2322
|
+
text: JSON.stringify(programmerCalculate(args as { expression: string })),
|
|
2323
|
+
},
|
|
2324
|
+
],
|
|
2325
|
+
};
|
|
2326
|
+
|
|
2327
|
+
case 'convert_base':
|
|
2328
|
+
return {
|
|
2329
|
+
content: [
|
|
2330
|
+
{
|
|
2331
|
+
type: 'text',
|
|
2332
|
+
text: JSON.stringify(convertBase(args as { value: string; from: number; to: number })),
|
|
2333
|
+
},
|
|
2334
|
+
],
|
|
2335
|
+
};
|
|
2336
|
+
|
|
2337
|
+
case 'ascii_encode':
|
|
2338
|
+
return {
|
|
2339
|
+
content: [
|
|
2340
|
+
{
|
|
2341
|
+
type: 'text',
|
|
2342
|
+
text: JSON.stringify(asciiEncode(args as { text: string })),
|
|
2343
|
+
},
|
|
2344
|
+
],
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
case 'ascii_decode':
|
|
2348
|
+
return {
|
|
2349
|
+
content: [
|
|
2350
|
+
{
|
|
2351
|
+
type: 'text',
|
|
2352
|
+
text: JSON.stringify(asciiDecode(args as { codes: string })),
|
|
2353
|
+
},
|
|
2354
|
+
],
|
|
2355
|
+
};
|
|
2356
|
+
|
|
2357
|
+
default:
|
|
2358
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
// Start server
|
|
2363
|
+
async function main() {
|
|
2364
|
+
const transport = new StdioServerTransport();
|
|
2365
|
+
await server.connect(transport);
|
|
2366
|
+
console.error('MCP Calculator server running on stdio');
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
main().catch((error) => {
|
|
2370
|
+
console.error('Server error:', error);
|
|
2371
|
+
process.exit(1);
|
|
2372
|
+
});
|
|
2373
|
+
```
|
|
2374
|
+
|
|
2375
|
+
**Step 2: 创建入口文件**
|
|
2376
|
+
|
|
2377
|
+
```typescript
|
|
2378
|
+
// src/index.ts
|
|
2379
|
+
export * from './parser/ast.js';
|
|
2380
|
+
export * from './parser/lexer.js';
|
|
2381
|
+
export * from './parser/parser.js';
|
|
2382
|
+
export * from './engines/decimal.js';
|
|
2383
|
+
export * from './engines/programmer.js';
|
|
2384
|
+
export * from './errors/handler.js';
|
|
2385
|
+
export * from './mcp/tools/basic.js';
|
|
2386
|
+
export * from './mcp/tools/programmer.js';
|
|
2387
|
+
export * from './mcp/tools/conversion.js';
|
|
2388
|
+
export * from './mcp/tools/ascii.js';
|
|
2389
|
+
|
|
2390
|
+
// Run server if this is the main module
|
|
2391
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
2392
|
+
await import('./mcp/server.js');
|
|
2393
|
+
}
|
|
2394
|
+
```
|
|
2395
|
+
|
|
2396
|
+
**Step 3: 构建并测试**
|
|
2397
|
+
|
|
2398
|
+
```bash
|
|
2399
|
+
npm run build
|
|
2400
|
+
```
|
|
2401
|
+
|
|
2402
|
+
**Step 4: 提交**
|
|
2403
|
+
|
|
2404
|
+
```bash
|
|
2405
|
+
git add src/mcp/server.ts src/index.ts
|
|
2406
|
+
git commit -m "feat: implement MCP Server with all tools"
|
|
2407
|
+
```
|
|
2408
|
+
|
|
2409
|
+
---
|
|
2410
|
+
|
|
2411
|
+
## 阶段 8: 最终验证和文档
|
|
2412
|
+
|
|
2413
|
+
### 任务 12: 运行完整测试套件
|
|
2414
|
+
|
|
2415
|
+
**Step 1: 运行所有测试**
|
|
2416
|
+
|
|
2417
|
+
```bash
|
|
2418
|
+
npm run test:coverage
|
|
2419
|
+
```
|
|
2420
|
+
|
|
2421
|
+
**Step 2: 验证覆盖率**
|
|
2422
|
+
|
|
2423
|
+
检查每个组件的覆盖率是否达到目标:
|
|
2424
|
+
- Lexer: 100%
|
|
2425
|
+
- Parser: 95%+
|
|
2426
|
+
- DecimalEngine: 90%+
|
|
2427
|
+
- ProgrammerEngine: 95%+
|
|
2428
|
+
- ErrorHandler: 90%+
|
|
2429
|
+
|
|
2430
|
+
**Step 3: 类型检查**
|
|
2431
|
+
|
|
2432
|
+
```bash
|
|
2433
|
+
npm run typecheck
|
|
2434
|
+
```
|
|
2435
|
+
|
|
2436
|
+
**Step 4: 构建验证**
|
|
2437
|
+
|
|
2438
|
+
```bash
|
|
2439
|
+
npm run build
|
|
2440
|
+
```
|
|
2441
|
+
|
|
2442
|
+
**Step 5: 提交**
|
|
2443
|
+
|
|
2444
|
+
```bash
|
|
2445
|
+
git add .
|
|
2446
|
+
git commit -m "test: ensure full test coverage and type safety"
|
|
2447
|
+
```
|
|
2448
|
+
|
|
2449
|
+
---
|
|
2450
|
+
|
|
2451
|
+
### 任务 13: 创建 README
|
|
2452
|
+
|
|
2453
|
+
**文件:**
|
|
2454
|
+
- 创建: `README.md`
|
|
2455
|
+
|
|
2456
|
+
```bash
|
|
2457
|
+
cat > README.md << 'EOF'
|
|
2458
|
+
# MCP Calculator
|
|
2459
|
+
|
|
2460
|
+
一个为 AI 助手提供计算能力的 MCP Server,支持四则运算和程序员计算功能。
|
|
2461
|
+
|
|
2462
|
+
## 功能特性
|
|
2463
|
+
|
|
2464
|
+
- **四则运算**: 高精度十进制计算(使用 decimal.js)
|
|
2465
|
+
- **程序员计算**: 大整数和位运算(使用 BigInt)
|
|
2466
|
+
- **进制转换**: 支持 2/8/10/16 进制互转
|
|
2467
|
+
- **ASCII 编码**: 文本与 ASCII 码互转
|
|
2468
|
+
|
|
2469
|
+
## 安装
|
|
2470
|
+
|
|
2471
|
+
\`\`\`bash
|
|
2472
|
+
npm install
|
|
2473
|
+
npm run build
|
|
2474
|
+
\`\`\`
|
|
2475
|
+
|
|
2476
|
+
## 使用方法
|
|
2477
|
+
|
|
2478
|
+
### 作为 MCP Server
|
|
2479
|
+
|
|
2480
|
+
在 Claude Desktop 的配置文件中添加:
|
|
2481
|
+
|
|
2482
|
+
\`\`\`json
|
|
2483
|
+
{
|
|
2484
|
+
"mcpServers": {
|
|
2485
|
+
"calculator": {
|
|
2486
|
+
"command": "node",
|
|
2487
|
+
"args": ["/path/to/calculator/dist/index.js"]
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
\`\`\`
|
|
2492
|
+
|
|
2493
|
+
### 可用工具
|
|
2494
|
+
|
|
2495
|
+
#### basic_calculate
|
|
2496
|
+
|
|
2497
|
+
基础四则运算,支持高精度小数。
|
|
2498
|
+
|
|
2499
|
+
\`\`\`typescript
|
|
2500
|
+
basic_calculate({ expression: "2 + 3 * 4" })
|
|
2501
|
+
// => { success: true, result: "14" }
|
|
2502
|
+
|
|
2503
|
+
basic_calculate({ expression: "0.1 + 0.2" })
|
|
2504
|
+
// => { success: true, result: "0.3" }
|
|
2505
|
+
\`\`\`
|
|
2506
|
+
|
|
2507
|
+
#### programmer_calculate
|
|
2508
|
+
|
|
2509
|
+
程序员计算,支持位运算和大整数。
|
|
2510
|
+
|
|
2511
|
+
\`\`\`typescript
|
|
2512
|
+
programmer_calculate({ expression: "0xFF & 0x0F" })
|
|
2513
|
+
// => { success: true, result: "15" }
|
|
2514
|
+
|
|
2515
|
+
programmer_calculate({ expression: "1 << 4" })
|
|
2516
|
+
// => { success: true, result: "16" }
|
|
2517
|
+
|
|
2518
|
+
programmer_calculate({ expression: "0xFFFFFFFFFFFFFFFF + 1" })
|
|
2519
|
+
// => { success: true, result: "18446744073709551616" }
|
|
2520
|
+
\`\`\`
|
|
2521
|
+
|
|
2522
|
+
#### convert_base
|
|
2523
|
+
|
|
2524
|
+
进制转换。
|
|
2525
|
+
|
|
2526
|
+
\`\`\`typescript
|
|
2527
|
+
convert_base({ value: "255", from: 10, to: 16 })
|
|
2528
|
+
// => { success: true, result: "ff" }
|
|
2529
|
+
|
|
2530
|
+
convert_base({ value: "1010", from: 2, to: 10 })
|
|
2531
|
+
// => { success: true, result: "10" }
|
|
2532
|
+
\`\`\`
|
|
2533
|
+
|
|
2534
|
+
#### ascii_encode / ascii_decode
|
|
2535
|
+
|
|
2536
|
+
ASCII 编码解码。
|
|
2537
|
+
|
|
2538
|
+
\`\`\`typescript
|
|
2539
|
+
ascii_encode({ text: "Hello" })
|
|
2540
|
+
// => { success: true, result: "72 101 108 108 111" }
|
|
2541
|
+
|
|
2542
|
+
ascii_decode({ codes: "72 101 108 108 111" })
|
|
2543
|
+
// => { success: true, result: "Hello" }
|
|
2544
|
+
\`\`\`
|
|
2545
|
+
|
|
2546
|
+
## 开发
|
|
2547
|
+
|
|
2548
|
+
\`\`\`bash
|
|
2549
|
+
# 开发模式
|
|
2550
|
+
npm run dev
|
|
2551
|
+
|
|
2552
|
+
# 运行测试
|
|
2553
|
+
npm test
|
|
2554
|
+
|
|
2555
|
+
# 测试覆盖率
|
|
2556
|
+
npm run test:coverage
|
|
2557
|
+
|
|
2558
|
+
# 类型检查
|
|
2559
|
+
npm run typecheck
|
|
2560
|
+
\`\`\`
|
|
2561
|
+
|
|
2562
|
+
## 返回格式
|
|
2563
|
+
|
|
2564
|
+
### 成功
|
|
2565
|
+
|
|
2566
|
+
\`\`\`json
|
|
2567
|
+
{
|
|
2568
|
+
"success": true,
|
|
2569
|
+
"result": "结果值(字符串形式)",
|
|
2570
|
+
"details": {
|
|
2571
|
+
"expression": "原始表达式"
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
\`\`\`
|
|
2575
|
+
|
|
2576
|
+
### 错误
|
|
2577
|
+
|
|
2578
|
+
\`\`\`json
|
|
2579
|
+
{
|
|
2580
|
+
"success": false,
|
|
2581
|
+
"error": {
|
|
2582
|
+
"type": "SyntaxError | DivisionByZero | InvalidNumber | ...",
|
|
2583
|
+
"message": "人类可读错误信息",
|
|
2584
|
+
"position": { "start": 5, "end": 8 },
|
|
2585
|
+
"suggestion": "修复建议"
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
\`\`\`
|
|
2589
|
+
|
|
2590
|
+
## 许可证
|
|
2591
|
+
|
|
2592
|
+
MIT
|
|
2593
|
+
EOF
|
|
2594
|
+
```
|
|
2595
|
+
|
|
2596
|
+
**Step 6: 提交最终版本**
|
|
2597
|
+
|
|
2598
|
+
```bash
|
|
2599
|
+
git add README.md
|
|
2600
|
+
git commit -m "docs: add comprehensive README"
|
|
2601
|
+
```
|
|
2602
|
+
|
|
2603
|
+
---
|
|
2604
|
+
|
|
2605
|
+
## 实施完成检查清单
|
|
2606
|
+
|
|
2607
|
+
- [ ] 项目初始化完成(package.json, tsconfig.json, vitest.config.ts)
|
|
2608
|
+
- [ ] 错误处理系统实现(ErrorHandler, 结构化错误)
|
|
2609
|
+
- [ ] Lexer 实现(所有字面量和运算符)
|
|
2610
|
+
- [ ] Parser 实现(递归下降,运算符优先级)
|
|
2611
|
+
- [ ] DecimalEngine 实现(高精度四则运算)
|
|
2612
|
+
- [ ] ProgrammerEngine 实现(BigInt 和位运算)
|
|
2613
|
+
- [ ] basic_calculate 工具实现
|
|
2614
|
+
- [ ] programmer_calculate 工具实现
|
|
2615
|
+
- [ ] convert_base 工具实现
|
|
2616
|
+
- [ ] ASCII 工具实现
|
|
2617
|
+
- [ ] MCP Server 实现
|
|
2618
|
+
- [ ] 所有测试通过
|
|
2619
|
+
- [ ] 测试覆盖率达标
|
|
2620
|
+
- [ ] README 文档完成
|
|
2621
|
+
|
|
2622
|
+
---
|
|
2623
|
+
|
|
2624
|
+
**计划总任务数:** 13 个任务
|
|
2625
|
+
**估计时间:** 每个任务 20-40 分钟
|
|
2626
|
+
**总计时间:** 约 4-8 小时(取决于经验)
|