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.
Files changed (110) hide show
  1. package/.claude/commands/opsx/apply.md +152 -0
  2. package/.claude/commands/opsx/archive.md +157 -0
  3. package/.claude/commands/opsx/bulk-archive.md +242 -0
  4. package/.claude/commands/opsx/continue.md +114 -0
  5. package/.claude/commands/opsx/explore.md +174 -0
  6. package/.claude/commands/opsx/ff.md +94 -0
  7. package/.claude/commands/opsx/new.md +69 -0
  8. package/.claude/commands/opsx/onboard.md +534 -0
  9. package/.claude/commands/opsx/sync.md +134 -0
  10. package/.claude/commands/opsx/verify.md +164 -0
  11. package/.claude/settings.local.json +8 -0
  12. package/.claude/skills/npm-publish/SKILL.md +164 -0
  13. package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
  14. package/.claude/skills/openspec-archive-change/SKILL.md +161 -0
  15. package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  16. package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
  17. package/.claude/skills/openspec-explore/SKILL.md +289 -0
  18. package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
  19. package/.claude/skills/openspec-new-change/SKILL.md +74 -0
  20. package/.claude/skills/openspec-onboard/SKILL.md +538 -0
  21. package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
  22. package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
  23. package/CLAUDE.md +92 -0
  24. package/README.md +319 -0
  25. package/build/engines/decimal.d.ts +10 -0
  26. package/build/engines/decimal.d.ts.map +1 -0
  27. package/build/engines/decimal.js +61 -0
  28. package/build/engines/decimal.js.map +1 -0
  29. package/build/engines/programmer.d.ts +18 -0
  30. package/build/engines/programmer.d.ts.map +1 -0
  31. package/build/engines/programmer.js +103 -0
  32. package/build/engines/programmer.js.map +1 -0
  33. package/build/errors/handler.d.ts +10 -0
  34. package/build/errors/handler.d.ts.map +1 -0
  35. package/build/errors/handler.js +37 -0
  36. package/build/errors/handler.js.map +1 -0
  37. package/build/errors/types.d.ts +25 -0
  38. package/build/errors/types.d.ts.map +1 -0
  39. package/build/errors/types.js +2 -0
  40. package/build/errors/types.js.map +1 -0
  41. package/build/index.d.ts +3 -0
  42. package/build/index.d.ts.map +1 -0
  43. package/build/index.js +16 -0
  44. package/build/index.js.map +1 -0
  45. package/build/mcp/server.d.ts +3 -0
  46. package/build/mcp/server.d.ts.map +1 -0
  47. package/build/mcp/server.js +270 -0
  48. package/build/mcp/server.js.map +1 -0
  49. package/build/mcp/tools/ascii.d.ts +11 -0
  50. package/build/mcp/tools/ascii.d.ts.map +1 -0
  51. package/build/mcp/tools/ascii.js +93 -0
  52. package/build/mcp/tools/ascii.js.map +1 -0
  53. package/build/mcp/tools/basic.d.ts +6 -0
  54. package/build/mcp/tools/basic.d.ts.map +1 -0
  55. package/build/mcp/tools/basic.js +34 -0
  56. package/build/mcp/tools/basic.js.map +1 -0
  57. package/build/mcp/tools/conversion.d.ts +8 -0
  58. package/build/mcp/tools/conversion.d.ts.map +1 -0
  59. package/build/mcp/tools/conversion.js +81 -0
  60. package/build/mcp/tools/conversion.js.map +1 -0
  61. package/build/mcp/tools/programmer.d.ts +6 -0
  62. package/build/mcp/tools/programmer.d.ts.map +1 -0
  63. package/build/mcp/tools/programmer.js +29 -0
  64. package/build/mcp/tools/programmer.js.map +1 -0
  65. package/build/parser/ast.d.ts +47 -0
  66. package/build/parser/ast.d.ts.map +1 -0
  67. package/build/parser/ast.js +2 -0
  68. package/build/parser/ast.js.map +1 -0
  69. package/build/parser/lexer.d.ts +24 -0
  70. package/build/parser/lexer.d.ts.map +1 -0
  71. package/build/parser/lexer.js +168 -0
  72. package/build/parser/lexer.js.map +1 -0
  73. package/build/parser/parser.d.ts +14 -0
  74. package/build/parser/parser.d.ts.map +1 -0
  75. package/build/parser/parser.js +115 -0
  76. package/build/parser/parser.js.map +1 -0
  77. package/docs/plans/2025-02-24-mcp-calculator-design.md +344 -0
  78. package/docs/plans/2025-02-24-mcp-calculator-implementation.md +2626 -0
  79. package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/.openspec.yaml +2 -0
  80. package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/design.md +46 -0
  81. package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/proposal.md +21 -0
  82. package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/specs/ascii-conversion/spec.md +22 -0
  83. package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/tasks.md +24 -0
  84. package/openspec/config.yaml +20 -0
  85. package/openspec/specs/ascii-conversion/spec.md +43 -0
  86. package/package.json +40 -0
  87. package/src/engines/decimal.ts +69 -0
  88. package/src/engines/programmer.ts +112 -0
  89. package/src/errors/handler.ts +55 -0
  90. package/src/errors/types.ts +37 -0
  91. package/src/index.ts +20 -0
  92. package/src/mcp/server.ts +287 -0
  93. package/src/mcp/tools/ascii.ts +116 -0
  94. package/src/mcp/tools/basic.ts +44 -0
  95. package/src/mcp/tools/conversion.ts +95 -0
  96. package/src/mcp/tools/programmer.ts +36 -0
  97. package/src/parser/ast.ts +51 -0
  98. package/src/parser/lexer.ts +216 -0
  99. package/src/parser/parser.ts +154 -0
  100. package/test/integration/ascii.test.ts +450 -0
  101. package/test/integration/basic-calculate.test.ts +272 -0
  102. package/test/integration/conversion.test.ts +357 -0
  103. package/test/integration/programmer-calculate.test.ts +363 -0
  104. package/test/unit/decimal-engine.test.ts +134 -0
  105. package/test/unit/error-handler.test.ts +173 -0
  106. package/test/unit/lexer.test.ts +176 -0
  107. package/test/unit/parser.test.ts +197 -0
  108. package/test/unit/programmer-engine.test.ts +234 -0
  109. package/tsconfig.json +20 -0
  110. 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 小时(取决于经验)