bfbf 0.1.0 → 0.1.1
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/AGENTS.md +113 -0
- package/README.md +16 -2
- package/hanoi.mp4 +0 -0
- package/index.js +74 -0
- package/package.json +1 -1
- package/{turing.js → sample.js} +3 -69
- package/visualize.html +632 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Agent Guidelines for bfbf Project
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
bfbf is a Brainfuck interpreter implemented in pure JavaScript. The project demonstrates Turing completeness through a Towers of Hanoi implementation in Brainfuck code.
|
|
5
|
+
|
|
6
|
+
- **Language**: JavaScript (Node.js)
|
|
7
|
+
- **Type System**: Plain JavaScript (no TypeScript)
|
|
8
|
+
- **Test Framework**: Mocha
|
|
9
|
+
- **Package Manager**: npm (pnpm lock file present)
|
|
10
|
+
- **Module System**: CommonJS
|
|
11
|
+
- **Commit Convention**: Commitizen with cz-jt adapter
|
|
12
|
+
|
|
13
|
+
## Build, Lint, and Test Commands
|
|
14
|
+
|
|
15
|
+
### Core Commands
|
|
16
|
+
```bash
|
|
17
|
+
npm start # Start development server with nodemon (auto-restarts on changes)
|
|
18
|
+
npm test # Run all tests with mocha
|
|
19
|
+
npm run cz # Run commitizen for interactive conventional commits
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Running Specific Tests
|
|
23
|
+
```bash
|
|
24
|
+
# Run a single test file
|
|
25
|
+
npx mocha tests/filename.js
|
|
26
|
+
|
|
27
|
+
# Run all test files matching a pattern
|
|
28
|
+
npx mocha --grep "pattern" ./tests/*.js
|
|
29
|
+
|
|
30
|
+
# Run a specific test within a file
|
|
31
|
+
npx mocha tests/filename.js --grep "test name"
|
|
32
|
+
|
|
33
|
+
# Run tests with reporter options
|
|
34
|
+
npx mocha tests/*.js --reporter spec
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Code Style Guidelines
|
|
38
|
+
|
|
39
|
+
### Formatting
|
|
40
|
+
- Use **tabs** for indentation (2 spaces per tab) - not spaces
|
|
41
|
+
- **No semicolons** at statement ends
|
|
42
|
+
- Maximum line length: 100 characters
|
|
43
|
+
- Use template literals for string interpolation (backticks)
|
|
44
|
+
- Use consistent spacing around operators and after commas
|
|
45
|
+
- Break long lines at logical points with proper indentation
|
|
46
|
+
|
|
47
|
+
### Naming Conventions
|
|
48
|
+
- **Functions**: camelCase with descriptive names (e.g., `js2BF`, `parseInput`, `executeBF`)
|
|
49
|
+
- **Variables**: camelCase with descriptive names (e.g., `memoryPtr`, `inputPtr`, `outputBuffer`)
|
|
50
|
+
- **Constants**: UPPER_SNAKE_CASE for true constants (e.g., `MEMORY_SIZE`, `MAX_INPUT_LENGTH`)
|
|
51
|
+
- **Files**: kebab-case for JavaScript files (e.g., `brainfuck-interpreter.js`, `hanoi-solver.js`)
|
|
52
|
+
- **Classes**: PascalCase if used (e.g., `BFInterpreter`, `MemoryManager`)
|
|
53
|
+
|
|
54
|
+
### Type Handling
|
|
55
|
+
- Use `Uint8Array` for memory buffers (fixed-size, typed arrays)
|
|
56
|
+
- Use `String.fromCharCode()` for character conversions
|
|
57
|
+
- Use `charCodeAt()` for string to character code conversion
|
|
58
|
+
- Avoid implicit type coercion; use explicit comparisons (`===` not `==`)
|
|
59
|
+
- Validate input types at function boundaries with early returns
|
|
60
|
+
- Use `typeof` and `instanceof` for type checking
|
|
61
|
+
|
|
62
|
+
### Import Patterns
|
|
63
|
+
- Use CommonJS `require()` for module imports (not ES6 imports)
|
|
64
|
+
- Use `module.exports` or `exports` for module exports
|
|
65
|
+
- Import modules at the top of files (no inline requires)
|
|
66
|
+
- Group related imports together
|
|
67
|
+
- Avoid circular dependencies
|
|
68
|
+
|
|
69
|
+
### Error Handling
|
|
70
|
+
- Use `try/catch` for synchronous operations that may throw
|
|
71
|
+
- Return error objects with descriptive messages instead of throwing when appropriate
|
|
72
|
+
- Handle edge cases explicitly (empty input, null values, undefined values)
|
|
73
|
+
- Validate array bounds before access operations to prevent errors
|
|
74
|
+
- Use defensive programming techniques for external inputs
|
|
75
|
+
- Log errors with sufficient context for debugging
|
|
76
|
+
|
|
77
|
+
### Code Structure
|
|
78
|
+
- Keep functions focused and small (target under 50 lines when possible)
|
|
79
|
+
- Use early returns to reduce nesting and improve readability
|
|
80
|
+
- Group related logic together in the same function
|
|
81
|
+
- Extract complex logic into separate, well-named helper functions
|
|
82
|
+
- Add Chinese comments for complex algorithms (per project convention)
|
|
83
|
+
- Document public APIs with JSDoc-style comments
|
|
84
|
+
|
|
85
|
+
### BF Interpreter Specific Guidelines
|
|
86
|
+
- Memory size: 10,000 bytes (fixed, `Uint8Array(10000)`)
|
|
87
|
+
- Input handling: character-by-character with fallback to null/0
|
|
88
|
+
- Loop matching: Use stack-based approach for bracket pairs (`[` and `]`)
|
|
89
|
+
- Pointer movement: Validate pointer bounds (0 to 9999)
|
|
90
|
+
- Output: Support both array collection and stream output modes
|
|
91
|
+
- Command filtering: Skip non-BF commands silently (optimization)
|
|
92
|
+
|
|
93
|
+
### Git Workflow
|
|
94
|
+
- Use `npm run cz` for commits to follow conventional changelog
|
|
95
|
+
- Write meaningful commit messages describing the "why" not just the "what"
|
|
96
|
+
- Keep commits atomic and focused on a single change
|
|
97
|
+
- Review changes before committing
|
|
98
|
+
- Push changes regularly to avoid data loss
|
|
99
|
+
|
|
100
|
+
## Testing Guidelines
|
|
101
|
+
- Write tests for new functionality in the `tests/` directory
|
|
102
|
+
- Use descriptive test names that explain the behavior being tested
|
|
103
|
+
- Test edge cases: empty input, maximum input, invalid characters
|
|
104
|
+
- Test both success and error paths
|
|
105
|
+
- Mock external dependencies when necessary
|
|
106
|
+
|
|
107
|
+
## Development Workflow
|
|
108
|
+
1. Create a feature branch for changes
|
|
109
|
+
2. Write code following the style guidelines
|
|
110
|
+
3. Add or update tests for the functionality
|
|
111
|
+
4. Run `npm test` to verify all tests pass
|
|
112
|
+
5. Use `npm run cz` to create a proper commit message
|
|
113
|
+
6. Push changes and create a pull request when ready
|
package/README.md
CHANGED
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
Turing Complete Towers of Hanoi
|
|
4
4
|
|
|
5
5
|
```javascript
|
|
6
|
+
npm i bfbf
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
visualize.html 可视化html
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const bf = require('bfbf)
|
|
6
15
|
const input = ''
|
|
7
|
-
|
|
8
|
-
console.log(
|
|
16
|
+
console.log(bf(hanoiCode, input, 1)) // 第三个参数是是否使用流模式输出
|
|
17
|
+
// console.log(bf(helloCode, input).output) // 输出hello world
|
|
9
18
|
```
|
|
19
|
+
|
|
20
|
+
<video width="640" height="480" controls>
|
|
21
|
+
<source src="https://github.com/kongnet/bf/raw/main/hanoi.mp4" type="video/mp4">
|
|
22
|
+
Your browser does not support the video tag.
|
|
23
|
+
</video>
|
package/hanoi.mp4
ADDED
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// 实现图灵完备语言BF
|
|
2
|
+
// 浏览器兼容版本
|
|
3
|
+
(function(root, factory) {
|
|
4
|
+
if (typeof module === 'object' && module.exports) {
|
|
5
|
+
module.exports = factory()
|
|
6
|
+
} else {
|
|
7
|
+
root.js2BF = factory()
|
|
8
|
+
}
|
|
9
|
+
}(typeof self !== 'undefined' ? self : this, function() {
|
|
10
|
+
function js2BF (code, input, stream = false) {
|
|
11
|
+
const memory = new Uint8Array(10000)
|
|
12
|
+
let ptr = 0
|
|
13
|
+
let inputPtr = 0
|
|
14
|
+
let output = []
|
|
15
|
+
|
|
16
|
+
const stack = []
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < code.length; i++) {
|
|
19
|
+
const char = code[i]
|
|
20
|
+
switch (char) {
|
|
21
|
+
case '>':
|
|
22
|
+
ptr++
|
|
23
|
+
break
|
|
24
|
+
case '<':
|
|
25
|
+
ptr--
|
|
26
|
+
break
|
|
27
|
+
case '+':
|
|
28
|
+
memory[ptr]++
|
|
29
|
+
break
|
|
30
|
+
case '-':
|
|
31
|
+
memory[ptr]--
|
|
32
|
+
break
|
|
33
|
+
case '.':
|
|
34
|
+
output.push(String.fromCharCode(memory[ptr]))
|
|
35
|
+
stream && process.stdout.write(String.fromCharCode(memory[ptr]))
|
|
36
|
+
break
|
|
37
|
+
case ',':
|
|
38
|
+
if (inputPtr < input.length) {
|
|
39
|
+
memory[ptr] = input.charCodeAt(inputPtr)
|
|
40
|
+
inputPtr++
|
|
41
|
+
} else {
|
|
42
|
+
memory[ptr] = 0
|
|
43
|
+
}
|
|
44
|
+
break
|
|
45
|
+
case '[':
|
|
46
|
+
if (memory[ptr] === 0) {
|
|
47
|
+
let count = 1
|
|
48
|
+
while (count > 0) {
|
|
49
|
+
i++
|
|
50
|
+
if (code[i] === '[') count++
|
|
51
|
+
else if (code[i] === ']') count--
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
stack.push(i)
|
|
55
|
+
}
|
|
56
|
+
break
|
|
57
|
+
case ']':
|
|
58
|
+
if (memory[ptr] !== 0) {
|
|
59
|
+
i = stack[stack.length - 1]
|
|
60
|
+
} else {
|
|
61
|
+
stack.pop()
|
|
62
|
+
}
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
output: output.map(x => String.fromCharCode(x)).join(''),
|
|
69
|
+
ptr,
|
|
70
|
+
memory
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return js2BF
|
|
74
|
+
}))
|
package/package.json
CHANGED
package/{turing.js → sample.js}
RENAMED
|
@@ -1,70 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
function js2BF (code, input, stream = false) {
|
|
3
|
-
const memory = new Uint8Array(10000) // bf使用的内存空间大小
|
|
4
|
-
let ptr = 0 // 内存指针
|
|
5
|
-
let inputPtr = 0 // 输入指针
|
|
6
|
-
let output = [] // 输出
|
|
7
|
-
|
|
8
|
-
const stack = [] // 栈,'[' , ']' 匹配
|
|
9
|
-
|
|
10
|
-
for (let i = 0; i < code.length; i++) {
|
|
11
|
-
const char = code[i]
|
|
12
|
-
switch (char) {
|
|
13
|
-
case '>':
|
|
14
|
-
ptr++
|
|
15
|
-
break
|
|
16
|
-
case '<':
|
|
17
|
-
ptr--
|
|
18
|
-
break
|
|
19
|
-
case '+':
|
|
20
|
-
memory[ptr]++
|
|
21
|
-
break
|
|
22
|
-
case '-':
|
|
23
|
-
memory[ptr]--
|
|
24
|
-
break
|
|
25
|
-
case '.':
|
|
26
|
-
output.push(memory[ptr]) // += String.fromCharCode(memory[ptr]) 压入数组为了可以输出观察内存模式
|
|
27
|
-
stream && process.stdout.write(String.fromCharCode(memory[ptr])) // 要兼容网页输出,改这里
|
|
28
|
-
break
|
|
29
|
-
case ',':
|
|
30
|
-
if (inputPtr < input.length) {
|
|
31
|
-
memory[ptr] = input.charCodeAt(inputPtr)
|
|
32
|
-
inputPtr++
|
|
33
|
-
} else {
|
|
34
|
-
memory[ptr] = 0
|
|
35
|
-
}
|
|
36
|
-
break
|
|
37
|
-
case '[':
|
|
38
|
-
if (memory[ptr] === 0) {
|
|
39
|
-
let count = 1
|
|
40
|
-
while (count > 0) {
|
|
41
|
-
i++
|
|
42
|
-
if (code[i] === '[') count++
|
|
43
|
-
else if (code[i] === ']') count--
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
stack.push(i)
|
|
47
|
-
}
|
|
48
|
-
break
|
|
49
|
-
case ']':
|
|
50
|
-
if (memory[ptr] !== 0) {
|
|
51
|
-
i = stack[stack.length - 1]
|
|
52
|
-
} else {
|
|
53
|
-
stack.pop()
|
|
54
|
-
}
|
|
55
|
-
break
|
|
56
|
-
default:
|
|
57
|
-
break
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
output: output.map(x => String.fromCharCode(x)).join(''),
|
|
63
|
-
ptr,
|
|
64
|
-
memory
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
1
|
+
const bf = require('./index.js')
|
|
68
2
|
// 以下是例子
|
|
69
3
|
let iloveyou = `
|
|
70
4
|
+++++ +++++
|
|
@@ -767,6 +701,6 @@ let helloCode = `>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.
|
|
|
767
701
|
|
|
768
702
|
// 3个例子
|
|
769
703
|
const input = ''
|
|
770
|
-
|
|
771
|
-
console.log(js2BF(helloCode, input).output) // 输出Hello World!\n 返回{ output, ptr, memory} 3个对象
|
|
704
|
+
console.log(bf(hanoiCode, input, 1)) // 汉诺塔 命令行动态演示 第三个参数是是否使用流模式输出
|
|
705
|
+
// console.log(js2BF(helloCode, input).output) // 输出Hello World!\n 返回{ output, ptr, memory} 3个对象
|
|
772
706
|
// console.log(js2BF(iloveyou, input).output)
|
package/visualize.html
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Brainfuck 可视化解释器</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
+
background: #1a1a2e;
|
|
17
|
+
color: #eee;
|
|
18
|
+
min-height: 100vh;
|
|
19
|
+
padding: 20px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
h1 {
|
|
23
|
+
text-align: center;
|
|
24
|
+
margin-bottom: 20px;
|
|
25
|
+
color: #00d4ff;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.container {
|
|
29
|
+
max-width: 1200px;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: 1fr 1fr;
|
|
33
|
+
gap: 20px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.panel {
|
|
37
|
+
background: #16213e;
|
|
38
|
+
border-radius: 10px;
|
|
39
|
+
padding: 20px;
|
|
40
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.panel h2 {
|
|
44
|
+
color: #00d4ff;
|
|
45
|
+
margin-bottom: 15px;
|
|
46
|
+
font-size: 1.2rem;
|
|
47
|
+
border-bottom: 2px solid #0f3460;
|
|
48
|
+
padding-bottom: 10px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.code-display {
|
|
52
|
+
font-family: 'Courier New', monospace;
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
line-height: 1.8;
|
|
55
|
+
background: #0f3460;
|
|
56
|
+
padding: 15px;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
white-space: pre-wrap;
|
|
59
|
+
word-break: break-all;
|
|
60
|
+
max-height: 300px;
|
|
61
|
+
overflow-y: auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#codeInput {
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 120px;
|
|
67
|
+
background: #0f3460;
|
|
68
|
+
border: 1px solid #00d4ff;
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
color: #eee;
|
|
71
|
+
padding: 15px;
|
|
72
|
+
font-family: 'Courier New', monospace;
|
|
73
|
+
font-size: 14px;
|
|
74
|
+
resize: vertical;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.code-display .char {
|
|
78
|
+
display: inline-block;
|
|
79
|
+
padding: 2px 4px;
|
|
80
|
+
margin: 1px;
|
|
81
|
+
border-radius: 3px;
|
|
82
|
+
transition: all 0.1s ease;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.code-display .current {
|
|
86
|
+
background: #ffd700;
|
|
87
|
+
color: #000;
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
box-shadow: 0 0 10px #ffd700;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.memory-grid {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: repeat(10, 1fr);
|
|
95
|
+
gap: 5px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cell {
|
|
99
|
+
background: #0f3460;
|
|
100
|
+
padding: 10px 5px;
|
|
101
|
+
text-align: center;
|
|
102
|
+
border-radius: 5px;
|
|
103
|
+
font-family: 'Courier New', monospace;
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
transition: all 0.3s ease;
|
|
106
|
+
border: 2px solid transparent;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.cell .addr {
|
|
110
|
+
font-size: 10px;
|
|
111
|
+
color: #666;
|
|
112
|
+
margin-bottom: 3px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.cell .value {
|
|
116
|
+
font-size: 16px;
|
|
117
|
+
font-weight: bold;
|
|
118
|
+
color: #00d4ff;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cell.active {
|
|
122
|
+
background: #ffd700;
|
|
123
|
+
border-color: #ff9500;
|
|
124
|
+
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.cell.active .addr,
|
|
128
|
+
.cell.active .value {
|
|
129
|
+
color: #000;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cell.changed {
|
|
133
|
+
animation: pulse 0.3s ease;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@keyframes pulse {
|
|
137
|
+
0% {
|
|
138
|
+
transform: scale(1);
|
|
139
|
+
}
|
|
140
|
+
50% {
|
|
141
|
+
transform: scale(1.1);
|
|
142
|
+
background: #4ade80;
|
|
143
|
+
}
|
|
144
|
+
100% {
|
|
145
|
+
transform: scale(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.io-section {
|
|
150
|
+
grid-column: 1 / -1;
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: 1fr 1fr;
|
|
153
|
+
gap: 20px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.input-area,
|
|
157
|
+
.output-area {
|
|
158
|
+
background: #0f3460;
|
|
159
|
+
padding: 15px;
|
|
160
|
+
border-radius: 8px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.input-area textarea {
|
|
164
|
+
width: 100%;
|
|
165
|
+
height: 80px;
|
|
166
|
+
background: #16213e;
|
|
167
|
+
border: 1px solid #00d4ff;
|
|
168
|
+
border-radius: 5px;
|
|
169
|
+
color: #eee;
|
|
170
|
+
padding: 10px;
|
|
171
|
+
font-family: 'Courier New', monospace;
|
|
172
|
+
resize: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.output-area pre {
|
|
176
|
+
background: #16213e;
|
|
177
|
+
padding: 10px;
|
|
178
|
+
border-radius: 5px;
|
|
179
|
+
min-height: 60px;
|
|
180
|
+
font-family: 'Courier New', monospace;
|
|
181
|
+
white-space: pre-wrap;
|
|
182
|
+
word-break: break-all;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.controls {
|
|
186
|
+
grid-column: 1 / -1;
|
|
187
|
+
display: flex;
|
|
188
|
+
gap: 15px;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
align-items: center;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
button {
|
|
195
|
+
padding: 12px 30px;
|
|
196
|
+
font-size: 16px;
|
|
197
|
+
border: none;
|
|
198
|
+
border-radius: 8px;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
transition: all 0.3s ease;
|
|
201
|
+
font-weight: bold;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.btn-start {
|
|
205
|
+
background: #4ade80;
|
|
206
|
+
color: #000;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.btn-start:hover {
|
|
210
|
+
background: #22c55e;
|
|
211
|
+
transform: translateY(-2px);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.btn-step {
|
|
215
|
+
background: #00d4ff;
|
|
216
|
+
color: #000;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.btn-step:hover {
|
|
220
|
+
background: #0ea5e9;
|
|
221
|
+
transform: translateY(-2px);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.btn-reset {
|
|
225
|
+
background: #ef4444;
|
|
226
|
+
color: #fff;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.btn-reset:hover {
|
|
230
|
+
background: #dc2626;
|
|
231
|
+
transform: translateY(-2px);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.speed-control {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 10px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.speed-control input {
|
|
241
|
+
width: 150px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.status {
|
|
245
|
+
grid-column: 1 / -1;
|
|
246
|
+
text-align: center;
|
|
247
|
+
padding: 15px;
|
|
248
|
+
background: #0f3460;
|
|
249
|
+
border-radius: 8px;
|
|
250
|
+
font-family: 'Courier New', monospace;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.status .highlight {
|
|
254
|
+
color: #ffd700;
|
|
255
|
+
font-weight: bold;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.legend {
|
|
259
|
+
grid-column: 1 / -1;
|
|
260
|
+
display: flex;
|
|
261
|
+
gap: 20px;
|
|
262
|
+
justify-content: center;
|
|
263
|
+
flex-wrap: wrap;
|
|
264
|
+
font-size: 14px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.legend span {
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
gap: 8px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.legend .dot {
|
|
274
|
+
width: 20px;
|
|
275
|
+
height: 20px;
|
|
276
|
+
border-radius: 4px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.legend .dot.code {
|
|
280
|
+
background: #ffd700;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.legend .dot.cell {
|
|
284
|
+
background: #ffd700;
|
|
285
|
+
border: 2px solid #ff9500;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.legend .dot.changed {
|
|
289
|
+
background: #4ade80;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.presets {
|
|
293
|
+
margin-bottom: 10px;
|
|
294
|
+
display: flex;
|
|
295
|
+
gap: 8px;
|
|
296
|
+
flex-wrap: wrap;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.presets button {
|
|
300
|
+
padding: 6px 12px;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
}
|
|
303
|
+
</style>
|
|
304
|
+
</head>
|
|
305
|
+
<body>
|
|
306
|
+
<h1>🧠 Brainfuck 可视化解释器</h1>
|
|
307
|
+
|
|
308
|
+
<div class="container">
|
|
309
|
+
<div class="panel">
|
|
310
|
+
<h2>📝 代码</h2>
|
|
311
|
+
<div class="presets">
|
|
312
|
+
<button onclick="loadPreset('hello')">Hello World</button>
|
|
313
|
+
<button onclick="loadPreset('love')">I love your</button>
|
|
314
|
+
</div>
|
|
315
|
+
<textarea id="codeInput" placeholder="输入 Brainfuck 代码..."></textarea>
|
|
316
|
+
<div class="code-display" id="codeDisplay" style="margin-top: 10px"></div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<div class="panel">
|
|
320
|
+
<h2>💾 内存 (0-99)</h2>
|
|
321
|
+
<div class="memory-grid" id="memoryGrid"></div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div class="panel io-section">
|
|
325
|
+
<div class="input-area">
|
|
326
|
+
<h2>📥 输入</h2>
|
|
327
|
+
<textarea id="inputArea" placeholder="在此输入字符..."></textarea>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="output-area">
|
|
330
|
+
<h2>📤 输出</h2>
|
|
331
|
+
<pre id="outputArea"></pre>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div class="controls">
|
|
336
|
+
<button class="btn-start" onclick="run()">▶️ 运行</button>
|
|
337
|
+
<button class="btn-step" onclick="step()">👆 单步</button>
|
|
338
|
+
<button class="btn-reset" onclick="reset()">🔄 重置</button>
|
|
339
|
+
<div class="speed-control">
|
|
340
|
+
<label>速度:</label>
|
|
341
|
+
<input type="range" id="speedSlider" min="10" max="500" value="50" />
|
|
342
|
+
<span id="speedValue">50ms</span>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div class="status" id="status">
|
|
347
|
+
准备就绪 - 输入 Brainfuck 代码并点击运行
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div class="legend">
|
|
351
|
+
<span
|
|
352
|
+
><div class="dot code"></div>
|
|
353
|
+
当前代码</span
|
|
354
|
+
>
|
|
355
|
+
<span
|
|
356
|
+
><div class="dot cell"></div>
|
|
357
|
+
当前内存</span
|
|
358
|
+
>
|
|
359
|
+
<span
|
|
360
|
+
><div class="dot changed"></div>
|
|
361
|
+
值已更改</span
|
|
362
|
+
>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<script src="index.js"></script>
|
|
367
|
+
<script>
|
|
368
|
+
const presets = {
|
|
369
|
+
hello: `>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.
|
|
370
|
+
+++++++..+++.>>>++++++++[<++++>-]<.>>>++++++++++[<+++++++++>-]<---.
|
|
371
|
+
<<<<.+++.------.--------.>>+.>++++++++++.`,
|
|
372
|
+
love: `
|
|
373
|
+
+++++ +++++
|
|
374
|
+
[->+++++++>+++>++++++++++<<<]
|
|
375
|
+
>+++.>++.>++++++++.+++.++
|
|
376
|
+
+++++.---------------
|
|
377
|
+
--.<.>+++++++++++
|
|
378
|
+
+++++++++.---
|
|
379
|
+
-------.+
|
|
380
|
+
+++++
|
|
381
|
+
.`
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let code = presets.hello
|
|
385
|
+
let input = ''
|
|
386
|
+
let memory = new Uint8Array(100)
|
|
387
|
+
let ptr = 0
|
|
388
|
+
let codePtr = 0
|
|
389
|
+
let output = []
|
|
390
|
+
let inputPtr = 0
|
|
391
|
+
let stack = []
|
|
392
|
+
let isRunning = false
|
|
393
|
+
let timer = null
|
|
394
|
+
let stepCount = 0
|
|
395
|
+
|
|
396
|
+
const codeInput = document.getElementById('codeInput')
|
|
397
|
+
const codeDisplay = document.getElementById('codeDisplay')
|
|
398
|
+
const memoryGrid = document.getElementById('memoryGrid')
|
|
399
|
+
const inputArea = document.getElementById('inputArea')
|
|
400
|
+
const outputArea = document.getElementById('outputArea')
|
|
401
|
+
const status = document.getElementById('status')
|
|
402
|
+
const speedSlider = document.getElementById('speedSlider')
|
|
403
|
+
const speedValue = document.getElementById('speedValue')
|
|
404
|
+
|
|
405
|
+
// 获取解释器函数 (index.js 暴露的全局变量)
|
|
406
|
+
const bf = window.js2BF
|
|
407
|
+
|
|
408
|
+
function init() {
|
|
409
|
+
codeInput.value = code
|
|
410
|
+
renderCode()
|
|
411
|
+
renderMemory()
|
|
412
|
+
inputArea.value = ''
|
|
413
|
+
outputArea.textContent = ''
|
|
414
|
+
status.innerHTML =
|
|
415
|
+
'准备就绪 - <span class="highlight">输入 Brainfuck 代码并点击运行</span>'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function loadPreset(name) {
|
|
419
|
+
code = presets[name]
|
|
420
|
+
codeInput.value = code
|
|
421
|
+
reset()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderCode() {
|
|
425
|
+
let html = ''
|
|
426
|
+
for (let i = 0; i < code.length; i++) {
|
|
427
|
+
const char = code[i]
|
|
428
|
+
const classes = ['char']
|
|
429
|
+
if (i === codePtr) classes.push('current')
|
|
430
|
+
const displayChar =
|
|
431
|
+
char === '>' ? '>' : char === '<' ? '<' : char
|
|
432
|
+
html += `<span class="${classes.join(' ')}">${displayChar}</span>`
|
|
433
|
+
}
|
|
434
|
+
codeDisplay.innerHTML = html
|
|
435
|
+
|
|
436
|
+
if (codePtr < code.length) {
|
|
437
|
+
codeDisplay.children[codePtr]?.scrollIntoView({
|
|
438
|
+
behavior: 'smooth',
|
|
439
|
+
block: 'nearest',
|
|
440
|
+
inline: 'center'
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function renderMemory() {
|
|
446
|
+
let html = ''
|
|
447
|
+
for (let i = 0; i < 100; i++) {
|
|
448
|
+
const isActive = i === ptr
|
|
449
|
+
html += `
|
|
450
|
+
<div class="cell ${isActive ? 'active' : ''}" id="cell-${i}">
|
|
451
|
+
<div class="addr">${i.toString().padStart(3, '0')}</div>
|
|
452
|
+
<div class="value">${memory[i]}</div>
|
|
453
|
+
</div>
|
|
454
|
+
`
|
|
455
|
+
}
|
|
456
|
+
memoryGrid.innerHTML = html
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function updateCell(index) {
|
|
460
|
+
const cell = document.getElementById(`cell-${index}`)
|
|
461
|
+
if (cell) {
|
|
462
|
+
cell.classList.add('changed')
|
|
463
|
+
cell.querySelector('.value').textContent = memory[index]
|
|
464
|
+
setTimeout(() => cell.classList.remove('changed'), 300)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getMatchingBracket(code, pos) {
|
|
469
|
+
const char = code[pos]
|
|
470
|
+
const isForward = char === '['
|
|
471
|
+
const target = isForward ? ']' : '['
|
|
472
|
+
const direction = isForward ? 1 : -1
|
|
473
|
+
let count = 0
|
|
474
|
+
|
|
475
|
+
for (
|
|
476
|
+
let i = pos + direction;
|
|
477
|
+
i >= 0 && i < code.length;
|
|
478
|
+
i += direction
|
|
479
|
+
) {
|
|
480
|
+
if (code[i] === char) count++
|
|
481
|
+
else if (code[i] === target) {
|
|
482
|
+
count--
|
|
483
|
+
if (count === 0) return i
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return -1
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function executeCommand() {
|
|
490
|
+
if (codePtr >= code.length) {
|
|
491
|
+
stopExecution()
|
|
492
|
+
status.innerHTML = `执行完成! 共 <span class="highlight">${stepCount}</span> 步`
|
|
493
|
+
return false
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const char = code[codePtr]
|
|
497
|
+
let nextPtr = codePtr
|
|
498
|
+
|
|
499
|
+
switch (char) {
|
|
500
|
+
case '>':
|
|
501
|
+
ptr = Math.min(99, ptr + 1)
|
|
502
|
+
break
|
|
503
|
+
case '<':
|
|
504
|
+
ptr = Math.max(0, ptr - 1)
|
|
505
|
+
break
|
|
506
|
+
case '+':
|
|
507
|
+
memory[ptr] = (memory[ptr] + 1) % 256
|
|
508
|
+
updateCell(ptr)
|
|
509
|
+
break
|
|
510
|
+
case '-':
|
|
511
|
+
memory[ptr] = (memory[ptr] + 255) % 256
|
|
512
|
+
updateCell(ptr)
|
|
513
|
+
break
|
|
514
|
+
case '.':
|
|
515
|
+
output.push(String.fromCharCode(memory[ptr]))
|
|
516
|
+
outputArea.textContent = output.join('')
|
|
517
|
+
break
|
|
518
|
+
case ',':
|
|
519
|
+
if (inputPtr < input.length) {
|
|
520
|
+
memory[ptr] = input.charCodeAt(inputPtr)
|
|
521
|
+
inputPtr++
|
|
522
|
+
updateCell(ptr)
|
|
523
|
+
} else {
|
|
524
|
+
memory[ptr] = 0
|
|
525
|
+
updateCell(ptr)
|
|
526
|
+
}
|
|
527
|
+
break
|
|
528
|
+
case '[':
|
|
529
|
+
if (memory[ptr] === 0) {
|
|
530
|
+
nextPtr = getMatchingBracket(code, codePtr)
|
|
531
|
+
} else {
|
|
532
|
+
stack.push(codePtr)
|
|
533
|
+
}
|
|
534
|
+
break
|
|
535
|
+
case ']':
|
|
536
|
+
if (memory[ptr] !== 0) {
|
|
537
|
+
nextPtr = stack[stack.length - 1] || codePtr
|
|
538
|
+
} else {
|
|
539
|
+
stack.pop()
|
|
540
|
+
}
|
|
541
|
+
break
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
codePtr = nextPtr + 1
|
|
545
|
+
stepCount++
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function updateStatus() {
|
|
550
|
+
const cmds = {
|
|
551
|
+
'>': '向右移动指针',
|
|
552
|
+
'<': '向左移动指针',
|
|
553
|
+
'+': '增加当前单元格值',
|
|
554
|
+
'-': '减少当前单元格值',
|
|
555
|
+
'.': '输出字符',
|
|
556
|
+
',': '输入字符',
|
|
557
|
+
'[': '循环开始 (值为0时跳过)',
|
|
558
|
+
']': '循环结束 (值不为0时回跳)'
|
|
559
|
+
}
|
|
560
|
+
const currentCmd = code[codePtr]
|
|
561
|
+
const desc = cmds[currentCmd] || '未知命令'
|
|
562
|
+
status.innerHTML = `步骤 <span class="highlight">${stepCount}</span> | 当前命令: <span class="highlight">${currentCmd}</span> - ${desc} | 指针: <span class="highlight">${ptr}</span> | 值: <span class="highlight">${memory[ptr]}</span>`
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function run() {
|
|
566
|
+
if (isRunning) return
|
|
567
|
+
code = codeInput.value
|
|
568
|
+
input = inputArea.value
|
|
569
|
+
reset()
|
|
570
|
+
isRunning = true
|
|
571
|
+
executeStep()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function executeStep() {
|
|
575
|
+
if (!isRunning) return
|
|
576
|
+
if (!executeCommand()) return
|
|
577
|
+
renderCode()
|
|
578
|
+
renderMemory()
|
|
579
|
+
updateStatus()
|
|
580
|
+
|
|
581
|
+
const speed = parseInt(speedSlider.value)
|
|
582
|
+
timer = setTimeout(executeStep, speed)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function step() {
|
|
586
|
+
if (isRunning) {
|
|
587
|
+
clearTimeout(timer)
|
|
588
|
+
isRunning = false
|
|
589
|
+
}
|
|
590
|
+
code = codeInput.value
|
|
591
|
+
input = inputArea.value
|
|
592
|
+
if (executeCommand()) {
|
|
593
|
+
renderCode()
|
|
594
|
+
renderMemory()
|
|
595
|
+
updateStatus()
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function stopExecution() {
|
|
600
|
+
isRunning = false
|
|
601
|
+
if (timer) {
|
|
602
|
+
clearTimeout(timer)
|
|
603
|
+
timer = null
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function reset() {
|
|
608
|
+
stopExecution()
|
|
609
|
+
memory = new Uint8Array(100)
|
|
610
|
+
ptr = 0
|
|
611
|
+
codePtr = 0
|
|
612
|
+
inputPtr = 0
|
|
613
|
+
output = []
|
|
614
|
+
stack = []
|
|
615
|
+
stepCount = 0
|
|
616
|
+
code = codeInput.value
|
|
617
|
+
input = inputArea.value
|
|
618
|
+
renderCode()
|
|
619
|
+
renderMemory()
|
|
620
|
+
outputArea.textContent = ''
|
|
621
|
+
status.innerHTML =
|
|
622
|
+
'已重置 - <span class="highlight">点击运行开始执行</span>'
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
speedSlider.addEventListener('input', e => {
|
|
626
|
+
speedValue.textContent = `${e.target.value}ms`
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
init()
|
|
630
|
+
</script>
|
|
631
|
+
</body>
|
|
632
|
+
</html>
|