diff options
Diffstat (limited to 'forth')
-rw-r--r-- | forth/factorial.forth | 11 | ||||
-rw-r--r-- | forth/foreforthfourth/README.md | 420 | ||||
-rw-r--r-- | forth/foreforthfourth/debug-string2.js | 32 | ||||
-rw-r--r-- | forth/foreforthfourth/forth-documented.js | 1076 | ||||
-rw-r--r-- | forth/foreforthfourth/forth.js | 1973 | ||||
-rw-r--r-- | forth/foreforthfourth/index.html | 381 | ||||
-rw-r--r-- | forth/foreforthfourth/test-advanced.js | 94 | ||||
-rw-r--r-- | forth/foreforthfourth/test-cross-stack-complete.js | 373 | ||||
-rw-r--r-- | forth/foreforthfourth/test-forth.js | 77 | ||||
-rw-r--r-- | forth/foreforthfourth/test-help-full.js | 33 | ||||
-rw-r--r-- | forth/foreforthfourth/test-help.js | 52 | ||||
-rw-r--r-- | forth/guesser.fth | 100 | ||||
-rw-r--r-- | forth/pf_ref.md | 1213 | ||||
-rw-r--r-- | forth/pf_tut.md | 1345 |
14 files changed, 7180 insertions, 0 deletions
diff --git a/forth/factorial.forth b/forth/factorial.forth new file mode 100644 index 0000000..359a642 --- /dev/null +++ b/forth/factorial.forth @@ -0,0 +1,11 @@ +( n -- n! ) +: FACTORIAL + \ If n is 0, the loop won't run and the initial 1 is returned. + 1 SWAP \ Put initial result 1 on stack, ( 1 n ) + 1+ 1 \ Setup loop bounds, ( 1 n+1 1 ) + DO + I * \ Multiply accumulator by loop index + LOOP ; + +5 FACTORIAL . +10 FACTORIAL . diff --git a/forth/foreforthfourth/README.md b/forth/foreforthfourth/README.md new file mode 100644 index 0000000..29c3b5e --- /dev/null +++ b/forth/foreforthfourth/README.md @@ -0,0 +1,420 @@ +# 4-Stack Toy Forth Interpreter + +A pure functional implementation of a toy Forth interpreter with a unique 4-stack architecture, written in JavaScript. + +## Architecture + +This Forth interpreter features **4 separate stacks** that users can juggle between, unlike traditional single-stack Forth implementations: + +- **Stack 1 (Red)** - Default stack for most operations +- **Stack 2 (Teal)** - Secondary stack for data organization +- **Stack 3 (Blue)** - Tertiary stack for complex operations +- **Stack 4 (Yellow)** - Quaternary stack for additional data + +## Features + +### Core Forth Operations +- **Stack Manipulation**: `dup`, `swap`, `drop`, `2dup`, `2drop`, `over`, `rot`, `-rot` +- **Arithmetic**: `+`, `-`, `*`, `/`, `mod` +- **Comparison**: `=`, `<`, `>`, `and`, `or`, `not` +- **Math Utilities**: `abs`, `negate`, `min`, `max` +- **Stack Inspection**: `.s` (non-destructive), `depth` +- **String Operations**: `." ... "` (print), `s" ... "` (push), `strlen`, `strcat`, `char+` +- **Control Flow**: `if ... then`, `if ... else ... then`, `begin ... until` +- **Help System**: `help` (comprehensive help), `doc <word>` (word documentation), `words` (word list) + +### Multi-Stack Operations +- **Stack Focus System**: `focus.red`, `focus.teal`, `focus.blue`, `focus.yellow` (or `focus.1`, `focus.2`, `focus.3`, `focus.4`) +- **Move Operations**: `move.red`, `move.teal`, `move.blue`, `move.yellow` (or `move.1`, `move.2`, `move.3`, `move.4`) +- **Pop Operations**: `pop.red`, `pop.teal`, `pop.blue`, `pop.yellow` (or `pop.1`, `pop.2`, `pop.3`, `pop.4`) +- **Copy Operations**: `copy.red`, `copy.teal`, `copy.blue`, `copy.yellow` (or `copy.1`, `copy.2`, `copy.3`, `copy.4`) +- **Move Operations**: `move` (interactive stack-to-stack movement), `move.red`, `move.teal`, `move.blue`, `move.yellow` (or `move.1`, `move.2`, `move.3`, `move.4`) +- **Clear Operations**: `clear.all` (clear all stacks), `clear.focused` (clear focused stack) +- **Cross-Stack Operations**: `dup.stacks`, `over.stacks`, `swap.stacks`, `nip.stacks`, `tuck.stacks`, `rot.stacks`, `2dup.stacks`, `2over.stacks`, `2swap.stacks` + +### Word Definition System +- **Define Words**: `: name ... ;` syntax +- **List Words**: `words` command shows all available words +- **User Dictionary**: Persistent storage of custom words + +## Project Structure + +``` +foreforthfourth/ +├── index.html # Web interface +├── forth.js # Core Forth interpreter (standalone) +├── test-forth.js # Test suite for the interpreter +└── README.md # This file +``` + +## Testing + +### Run Tests +```bash +node test-forth.js +``` + +### Complete Test Suite +We have comprehensive test coverage including: +- **Core Operations**: Stack manipulation, arithmetic, comparison, logic +- **Focus System**: All focus commands and stack operations on different stacks +- **String Operations**: String literals, manipulation, and type checking +- **Control Flow**: Conditional execution and loops +- **Multi-Stack**: Operations across all 4 stacks with focus system +- **Error Handling**: Enhanced error messages and edge cases +- **Help System**: Help commands and word documentation +- **Word Definition**: User-defined words and compilation +- **Enhanced Features**: Clear operations, move operations, focus persistence + +### Test Results +- **Total Tests**: 16 comprehensive test cases +- **Success Rate**: 100% ✅ +- **Coverage**: Complete feature coverage with edge case testing + +## Web Interface + +Open `index.html` in a web browser to use the interactive Forth interpreter with: +- Visual representation of all 4 stacks +- Real-time command execution +- Output history +- Responsive design for mobile and desktop + +## Usage Examples + +### Basic Stack Operations +```forth +5 3 2 .s # Push numbers and show stack +dup over # Duplicate top, copy second over top +2dup # Duplicate top two items +``` + +### Arithmetic +```forth +10 3 / # Integer division (result: 3) +10 3 mod # Modulo (result: 1) +-5 abs # Absolute value (result: 5) +5 negate # Negate (result: -5) +``` + +### Multi-Stack Juggling +```forth +5 3 2 # Push to red stack +move.teal # Move top of red to teal stack +move.blue # Move top of red to blue stack +``` + +### Stack Focus System +The interpreter now supports operating on any of the 4 stacks using a focus system: + +```forth +focus.red # Set focus to Red stack (Stack 1) +focus.teal # Set focus to Teal stack (Stack 2) +focus.blue # Set focus to Blue stack (Stack 3) +focus.yellow # Set focus to Yellow stack (Stack 4) + +# Number aliases also work: +focus.1 # Same as focus.red +focus.2 # Same as focus.teal +focus.3 # Same as focus.blue +focus.4 # Same as focus.yellow + +focus.show # Show which stack is currently focused +``` + +**All stack operations** (dup, swap, drop, +, -, *, /, etc.) now work on the **focused stack** instead of just the Red stack. This makes the multi-stack architecture truly powerful! + +**Number and Color Aliases**: All focus, push, and pop commands support both color names and numbers: +```forth +# Focus commands +focus.1 # Same as focus.red +focus.2 # Same as focus.teal +focus.3 # Same as focus.blue +focus.4 # Same as focus.yellow + +# Move commands +move.1 # Same as move.red +move.2 # Same as move.teal +move.3 # Same as move.blue +move.4 # Same as move.yellow + +# Copy commands +copy.1 # Same as copy.red +copy.2 # Same as copy.teal +copy.3 # Same as copy.blue +copy.4 # Same as copy.yellow + +# Pop commands +pop.1 # Same as pop.red +pop.2 # Same as pop.teal +pop.3 # Same as pop.blue +pop.4 # Same as pop.yellow +``` + +### Enhanced Clear Operations +```forth +clear.all # Clear all stacks (same as clear) +clear.focused # Clear only the currently focused stack + +# Example workflow: +focus.teal # Focus on Teal stack +15 20 25 # Add items to Teal stack +clear.focused # Clear only Teal stack +focus.red # Switch to Red stack +5 10 # Add items to Red stack +clear.focused # Clear only Red stack +``` + +### Cross-Stack Operations +The interpreter now provides comprehensive cross-stack manipulation using familiar Forth words with `.stacks` suffix: + +#### **Basic Cross-Stack Operations** +```forth +# Duplicate top item to another stack +focus.red +15 # Add item to Red stack +dup.stacks # Start dup.stacks operation +2 # Enter target stack (Teal) +# Result: 15 is duplicated to Teal stack + +# Copy second item to another stack +focus.blue +10 20 30 # Add items to Blue stack +over.stacks # Start over.stacks operation +1 # Enter target stack (Red) +# Result: 20 is copied to Red stack + +# Swap top items between stacks +focus.red +5 10 # Add items to Red stack +swap.stacks # Start swap.stacks operation +3 # Enter target stack (Blue) +# Result: Top items are swapped between Red and Blue stacks +``` + +#### **Advanced Cross-Stack Operations** +```forth +# Move second item to another stack (remove from source) +focus.teal +100 200 300 # Add items to Teal stack +nip.stacks # Start nip.stacks operation +4 # Enter target stack (Yellow) +# Result: 200 moved to Yellow stack, 100 and 300 remain on Teal + +# Tuck top item under second item on another stack +focus.red +5 10 # Add items to Red stack +tuck.stacks # Start tuck.stacks operation +2 # Enter target stack (Teal) +# Result: 5 tucked under 10 on Teal stack + +# Rotate top 3 items between stacks +focus.blue +1 2 3 # Add items to Blue stack +rot.stacks # Start rot.stacks operation +1 # Enter target stack (Red) +# Result: Top 3 items rotated between Blue and Red stacks + +# Duplicate top 2 items to another stack +focus.yellow +50 60 # Add items to Yellow stack +2dup.stacks # Start 2dup.stacks operation +3 # Enter target stack (Blue) +# Result: 50 and 60 duplicated to Blue stack + +# Copy second pair of items to another stack +focus.red +10 20 30 40 # Add items to Red stack +2over.stacks # Start 2over.stacks operation +2 # Enter target stack (Teal) +# Result: 20 and 30 copied to Teal stack + +# Swap top 2 pairs between stacks +focus.teal +1 2 3 4 # Add items to Teal stack +2swap.stacks # Start 2swap.stacks operation +1 # Enter target stack (Red) +# Result: Top 2 pairs swapped between Teal and Red stacks +``` + +#### **Cross-Stack Operation Workflow** +All cross-stack operations follow this pattern: +1. **Set focus** to the source stack +2. **Add items** to the source stack +3. **Execute operation** (e.g., `dup.stacks`) +4. **Enter target stack** number (1-4) when prompted +5. **Operation completes** automatically + +This provides **true multi-stack power** while maintaining familiar Forth semantics! + +### Move Operations (No Duplication) +The interpreter provides several ways to **move** items between stacks without duplication: + +#### **Interactive Move Command** +The `move` command is a **two-step interactive operation** that moves the top item from one stack to another: + +```forth +move # Start move operation +1 # Source stack (Red/Stack 1) +3 # Destination stack (Blue/Stack 3) +# Result: Top item moved from Red to Blue stack +``` + +**Workflow:** +1. Type `move` to start the operation +2. Enter the **source stack number** (1-4) +3. Enter the **destination stack number** (1-4) +4. The item is **removed** from source and **added** to destination + +#### **Move Commands with Focus System** +Use `move.` commands to move items from the focused stack to a specific target stack: + +```forth +focus.red # Focus on Red stack (1) +42 # Add item to Red stack +move.3 # Move top item to Blue stack (3) +# Result: 42 moved from Red to Blue stack + +focus.teal # Focus on Teal stack (2) +100 # Add item to Teal stack +move.yellow # Move top item to Yellow stack (4) +# Result: 100 moved from Teal to Yellow stack +``` + +#### **Number and Color Aliases** +All move and copy commands support both number and color naming: + +```forth +# Move commands (remove from source) +move.1 # Move to Red stack (1) +move.2 # Move to Teal stack (2) +move.3 # Move to Blue stack (3) +move.4 # Move to Yellow stack (4) + +# Copy commands (keep in source) +copy.1 # Copy to Red stack (1) +copy.2 # Copy to Teal stack (2) +copy.3 # Copy to Blue stack (3) +copy.4 # Copy to Yellow stack (4) + +# Color aliases +move.red # Move to Red stack (1) +move.teal # Move to Teal stack (2) +move.blue # Move to Blue stack (3) +move.yellow # Move to Yellow stack (4) + +copy.red # Copy to Red stack (1) +copy.teal # Copy to Teal stack (2) +copy.blue # Copy to Blue stack (3) +copy.yellow # Copy to Yellow stack (4) +``` + +#### **Comparison: Move vs Copy Operations** + +| Operation | Effect | Duplication | Use Case | +|-----------|--------|-------------|----------| +| `move` | **Moves** item from source to destination | ❌ No | Relocate items between stacks | +| `move.{stack}` | **Moves** item from focused stack to target | ❌ No | Move from focused stack to specific stack | +| `copy.{stack}` | **Copies** item from focused stack to target | ✅ Yes | Keep item on source, copy to target | +| `dup.stacks` | **Copies** item from focused stack to target | ✅ Yes | Keep item on source, copy to target | +| `over.stacks` | **Copies** second item from focused stack to target | ✅ Yes | Copy second item without affecting top | + +#### **Quick Reference: All Move and Copy Operations** + +| Command | From | To | Effect | +|---------|------|----|---------| +| `move` + source + dest | Any stack | Any stack | Move top item between specified stacks | +| `move.red` / `move.1` | Focused stack | Red stack (1) | Move top item to Red stack | +| `move.teal` / `move.2` | Focused stack | Teal stack (2) | Move top item to Teal stack | +| `move.blue` / `move.3` | Focused stack | Blue stack (3) | Move top item to Blue stack | +| `move.yellow` / `move.4` | Focused stack | Yellow stack (4) | Move top item to Yellow stack | +| `copy.red` / `copy.1` | Focused stack | Red stack (1) | Copy top item to Red stack | +| `copy.teal` / `copy.2` | Focused stack | Teal stack (2) | Copy top item to Teal stack | +| `copy.blue` / `copy.3` | Focused stack | Blue stack (3) | Copy top item to Blue stack | +| `copy.yellow` / `copy.4` | Focused stack | Yellow stack (4) | Copy top item to Yellow stack | + +#### **Complete Move Example** +```forth +# Setup: Add items to different stacks +focus.red +42 # Red stack: [42] +focus.teal +100 # Teal stack: [100] +focus.blue +200 # Blue stack: [200] + +# Move items between stacks +focus.red +move.2 # Move 42 from Red to Teal +# Red stack: [], Teal stack: [100, 42] + +focus.teal +move.3 # Move 100 from Teal to Blue +# Teal stack: [42], Blue stack: [200, 100] + +# Use interactive move for complex operations +move # Start move operation +3 # Source: Blue stack (3) +1 # Destination: Red stack (1) +# Result: 200 moved from Blue to Red stack +# Red stack: [200], Blue stack: [100] +``` + +### Word Definition +```forth +: double dup + ; # Define 'double' word +5 double # Use the word (result: 10) +``` + +### Help System +```forth +help # Show comprehensive help for all words +s" dup" doc # Show detailed documentation for 'dup' +words # List all available words +``` + +### Comparison and Logic +```forth +5 3 > # 5 > 3 (result: -1 for true) +5 3 < # 5 < 3 (result: 0 for false) +5 3 > not # NOT (5 > 3) (result: 0) +``` + +## Design Principles + +### Pure Functional +- **Immutable State**: All state updates return new state objects +- **No Side Effects**: Functions are pure and predictable +- **Functional Composition**: Operations compose naturally + +### 4-Stack Architecture +- **Stack Independence**: Each stack operates independently +- **Flexible Data Flow**: Move data between stacks as needed +- **Organized Workflows**: Use different stacks for different purposes + +### ANS Forth Compatibility +- **Standard Words**: Implements core ANS Forth words +- **Familiar Syntax**: Standard Forth syntax and semantics +- **Extensible**: Easy to add new words and functionality +- **Enhanced Error Messages**: Helpful, actionable error messages with stack context and solutions + +## Current Status + +### **Fully Implemented Features** +- **Control Flow**: `IF ... THEN`, `IF ... ELSE ... THEN`, `BEGIN ... UNTIL` constructs +- **String Operations**: String literals (`."` and `s"`), manipulation (`strlen`, `strcat`, `char+`, `type`, `count`) +- **Stack Focus System**: Operate on any of the 4 stacks using focus commands +- **Enhanced Error Messages**: Helpful, actionable error messages with stack context +- **Help System**: Comprehensive help (`help`) and word documentation (`doc`) +- **Multi-Stack Operations**: Full support for all 4 stacks with focus system +- **Enhanced Clear Operations**: `clear.all` and `clear.focused` commands +- **Move Operations**: Interactive `move` command and `move.{stack}` commands for moving items between stacks +- **Copy Operations**: `copy.{stack}` commands for copying items between stacks without removal +- **Number Aliases**: All focus, move, copy, and pop commands support both color names and numbers (1-4) +- **Math Utilities**: `abs`, `negate`, `min`, `max` operations + +### **Advanced Capabilities** +- **Universal Stack Operations**: All built-in words work on any focused stack +- **Dual Naming System**: Both color names and numbers work for all commands +- **Professional Error Handling**: Context-aware error messages with solutions +- **Visual Focus Indicators**: UI shows which stack is currently focused +- **Complete Test Coverage**: 100% test coverage of all features \ No newline at end of file diff --git a/forth/foreforthfourth/debug-string2.js b/forth/foreforthfourth/debug-string2.js new file mode 100644 index 0000000..01a42aa --- /dev/null +++ b/forth/foreforthfourth/debug-string2.js @@ -0,0 +1,32 @@ +// Debug string literals with actual command format +const ForthInterpreter = require('./forth.js'); + +console.log('🔍 Debugging String Literals - Command Format\n'); + +let state = ForthInterpreter.createInitialState(); + +console.log('Testing: ." Hello World"'); +state = ForthInterpreter.parseAndExecute(state, '." Hello World"'); +console.log('Result:', { + stringMode: state.stringMode, + currentString: state.currentString, + stacks: state.stacks[0] +}); + +console.log('\nTesting: ." Hello"'); +state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '." Hello"'); +console.log('Result:', { + stringMode: state.stringMode, + currentString: state.currentString, + stacks: state.stacks[0] +}); + +console.log('\nTesting: ." Test"'); +state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '." Test"'); +console.log('Result:', { + stringMode: state.stringMode, + currentString: state.currentString, + stacks: state.stacks[0] +}); diff --git a/forth/foreforthfourth/forth-documented.js b/forth/foreforthfourth/forth-documented.js new file mode 100644 index 0000000..56ccc17 --- /dev/null +++ b/forth/foreforthfourth/forth-documented.js @@ -0,0 +1,1076 @@ +// Pure functional approach to Forth interpreter state +const createInitialState = () => ({ + stacks: [[], [], [], []], + dictionary: new Map(), + output: [], + compilingWord: null, + compilingDefinition: [], + stringMode: false, + currentString: '', + stringPushMode: false, + skipMode: false, + skipCount: 0, + loopStart: null, + loopBack: false +}); + +// Pure function to update state +const updateState = (state, updates) => ({ + ...state, + ...updates +}); + +// Stack operations +const pushToStack = (stacks, stackIndex, value) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? [...stack, value] : stack + ); + return newStacks; +}; + +const popFromStack = (stacks, stackIndex) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? stack.slice(0, -1) : stack + ); + const value = stacks[stackIndex][stacks[stackIndex].length - 1]; + return { stacks: newStacks, value }; +}; + +const moveBetweenStacks = (stacks, fromStack, toStack) => { + if (stacks[fromStack].length === 0) return { stacks, value: null }; + const { stacks: newStacks, value } = popFromStack(stacks, fromStack); + return { stacks: pushToStack(newStacks, toStack, value), value }; +}; + +const popAndPrint = (state, stackIndex) => { + if (state.stacks[stackIndex].length === 0) { + return updateState(state, { output: [...state.output, `Stack ${stackIndex + 1} is empty`] }); + } + const { stacks, value } = popFromStack(state.stacks, stackIndex); + return updateState(state, { + stacks, + output: [...state.output, `Stack ${stackIndex + 1}: ${value}`] + }); +}; + +// Built-in words with documentation +const builtinWords = { + // Stack manipulation for stack 1 (default) + 'dup': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on dup'] }); + } + const top = state.stacks[0][state.stacks[0].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, top) + }); + }, + doc: 'Duplicate the top item on the stack', + stack: '( x -- x x )' + }, + + 'swap': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on swap'] }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[0].pop(); + const b = newStacks[0].pop(); + newStacks[0].push(a); + newStacks[0].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Exchange the top two items on the stack', + stack: '( x1 x2 -- x2 x1 )' + }, + + 'drop': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on drop'] }); + } + const { stacks } = popFromStack(state.stacks, 0); + return updateState(state, { stacks }); + }, + doc: 'Remove the top item from the stack', + stack: '( x -- )' + }, + + '2drop': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on 2drop'] }); + } + const { stacks: stacks1 } = popFromStack(state.stacks, 0); + const { stacks } = popFromStack(stacks1, 0); + return updateState(state, { stacks }); + }, + doc: 'Remove the top two items from the stack', + stack: '( x1 x2 -- )' + }, + + 'over': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on over'] }); + } + const second = state.stacks[0][state.stacks[0].length - 2]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, second) + }); + }, + doc: 'Copy the second item on the stack to the top', + stack: '( x1 x2 -- x1 x2 x1 )' + }, + + '2dup': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on 2dup'] }); + } + const top = state.stacks[0][state.stacks[0].length - 1]; + const second = state.stacks[0][state.stacks[0].length - 2]; + const newStacks = pushToStack(state.stacks, 0, second); + return updateState(state, { + stacks: pushToStack(newStacks, 0, top) + }); + }, + doc: 'Duplicate the top two items on the stack', + stack: '( x1 x2 -- x1 x2 x1 x2 )' + }, + + 'rot': { + fn: (state) => { + if (state.stacks[0].length < 3) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on rot'] }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[0].pop(); + const b = newStacks[0].pop(); + const c = newStacks[0].pop(); + newStacks[0].push(b); + newStacks[0].push(a); + newStacks[0].push(c); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack', + stack: '( x1 x2 x3 -- x2 x3 x1 )' + }, + + '-rot': { + fn: (state) => { + if (state.stacks[0].length < 3) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on -rot'] }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[0].pop(); + const b = newStacks[0].pop(); + const c = newStacks[0].pop(); + newStacks[0].push(a); + newStacks[0].push(c); + newStacks[0].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack (reverse of rot)', + stack: '( x1 x2 x3 -- x3 x1 x2 )' + }, + + // Arithmetic operations on stack 1 + '+': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on +'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a + b) + }); + }, + doc: 'Add the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '-': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on -'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a - b) + }); + }, + doc: 'Subtract the top number from the second number on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '*': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on *'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a * b) + }); + }, + doc: 'Multiply the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '/': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on /'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, 0, a), 0, b), + output: [...state.output, 'Error: Division by zero'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, 0, Math.floor(a / b)) + }); + }, + doc: 'Divide the second number by the top number on the stack (integer division)', + stack: '( n1 n2 -- n3 )' + }, + + 'mod': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on mod'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, 0, a), 0, b), + output: [...state.output, 'Error: Modulo by zero'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, 0, a % b) + }); + }, + doc: 'Return the remainder of dividing the second number by the top number', + stack: '( n1 n2 -- n3 )' + }, + + 'abs': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on abs'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 0, Math.abs(value)) + }); + }, + doc: 'Return the absolute value of the top number on the stack', + stack: '( n -- |n| )' + }, + + 'min': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on min'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, Math.min(a, b)) + }); + }, + doc: 'Return the smaller of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + 'max': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on max'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, Math.max(a, b)) + }); + }, + doc: 'Return the larger of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + // Comparison and logic + '=': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on ='] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a === b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the top two numbers are equal, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '<': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on <'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a < b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is less than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '>': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on >'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a > b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is greater than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + 'and': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on and'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a && b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if both top two values are true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'or': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on or'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a || b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if either of the top two values is true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'not': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on not'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 0, value ? 0 : -1) + }); + }, + doc: 'Return true (-1) if the top value is false, false (0) if it is true', + stack: '( x -- flag )' + }, + + // Stack inspection + '.s': { + fn: (state) => { + const stackStr = state.stacks[0].length === 0 ? 'empty' : state.stacks[0].join(' '); + return updateState(state, { + output: [...state.output, `Stack 1 (red): ${stackStr}`] + }); + }, + doc: 'Display the contents of the red stack (non-destructive)', + stack: '( -- )' + }, + + 'depth': { + fn: (state) => { + return updateState(state, { + stacks: pushToStack(state.stacks, 0, state.stacks[0].length) + }); + }, + doc: 'Push the number of items on the red stack', + stack: '( -- n )' + }, + + '.': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on .'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks, + output: [...state.output, value.toString()] + }); + }, + doc: 'Pop and print the top item from the red stack', + stack: '( x -- )' + }, + + // Multi-stack operations + 'push.red': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.red'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 0, value) + }); + }, + doc: 'Move top item from red stack to red stack (no-op, for consistency)', + stack: '( x -- x )' + }, + + 'push.teal': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.teal'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 1, value) + }); + }, + doc: 'Move top item from red stack to teal stack', + stack: '( x -- )' + }, + + 'push.blue': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.blue'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 2, value) + }); + }, + doc: 'Move top item from red stack to blue stack', + stack: '( x -- )' + }, + + 'push.yellow': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.yellow'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 3, value) + }); + }, + doc: 'Move top item from red stack to yellow stack', + stack: '( x -- )' + }, + + 'pop.red': { + fn: (state) => popAndPrint(state, 0), + doc: 'Pop and print top item from red stack', + stack: '( x -- )' + }, + + 'pop.teal': { + fn: (state) => popAndPrint(state, 1), + doc: 'Pop and print top item from teal stack', + stack: '( x -- )' + }, + + 'pop.blue': { + fn: (state) => popAndPrint(state, 2), + doc: 'Pop and print top item from blue stack', + stack: '( x -- )' + }, + + 'pop.yellow': { + fn: (state) => popAndPrint(state, 3), + doc: 'Pop and print top item from yellow stack', + stack: '( x -- )' + }, + + // Utility words + 'clear': { + fn: (state) => updateState(state, { + stacks: [[], [], [], []], + output: [...state.output, 'All stacks cleared'] + }), + doc: 'Clear all four stacks', + stack: '( -- )' + }, + + // String operations + '."': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: false + }); + }, + doc: 'Begin a string literal that will be printed to output', + stack: '( -- )' + }, + + 's"': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: true + }); + }, + doc: 'Begin a string literal that will be pushed to the stack', + stack: '( -- )' + }, + + 'type': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on type'] }); + } + const { stacks: stacks1, value: length } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: stacks2, + output: [...state.output, string.toString()] + }); + }, + doc: 'Print a string from the stack (takes length and string address)', + stack: '( addr len -- )' + }, + + 'count': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on count'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (typeof value === 'string') { + const newStacks = pushToStack(stacks, 0, value.length); + return updateState(state, { + stacks: pushToStack(newStacks, 0, value) + }); + } else { + const newStacks = pushToStack(stacks, 0, 0); + return updateState(state, { + stacks: pushToStack(newStacks, 0, '') + }); + } + }, + doc: 'Extract string info from counted string (returns length and address)', + stack: '( c-addr -- c-addr u )' + }, + + 'char+': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on char+'] }); + } + const { stacks: stacks1, value: offset } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + if (typeof string === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, 0, string + String.fromCharCode(offset)) + }); + } else { + return updateState(state, { + stacks: pushToStack(stacks2, 0, string), + output: [...state.output, 'Error: char+ requires string on stack'] + }); + } + }, + doc: 'Add a character to a string using ASCII offset', + stack: '( c-addr1 char -- c-addr2 )' + }, + + 'strlen': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on strlen'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (typeof value === 'string') { + return updateState(state, { + stacks: pushToStack(stacks, 0, value.length) + }); + } else { + return updateState(state, { + stacks: pushToStack(stacks, 0, 0), + output: [...state.output, 'Error: strlen requires string on stack'] + }); + } + }, + doc: 'Get the length of a string on the stack', + stack: '( str -- len )' + }, + + 'strcat': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on strcat'] }); + } + const { stacks: stacks1, value: str2 } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: str1 } = popFromStack(stacks1, 0); + if (typeof str1 === 'string' && typeof str2 === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, 0, str1 + str2) + }); + } else { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, 0, str1), 0, str2), + output: [...state.output, `Error: strcat requires two strings, got ${typeof str1} and ${typeof str2}`] + }); + } + }, + doc: 'Concatenate two strings from the stack', + stack: '( str1 str2 -- str3 )' + }, + + // Control flow + 'if': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on if'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (value === 0) { + // Skip until THEN or ELSE + return updateState(state, { + stacks, + skipMode: true, + skipCount: 0 + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'Begin conditional execution - if top of stack is false (0), skip to THEN', + stack: '( flag -- )' + }, + + 'else': { + fn: (state) => { + if (state.skipMode) { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } + // Skip until THEN + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + }, + doc: 'Begin alternative branch in conditional execution', + stack: '( -- )' + }, + + 'then': { + fn: (state) => { + if (state.skipMode && state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else if (state.skipMode) { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + return state; + }, + doc: 'End conditional execution block', + stack: '( -- )' + }, + + 'begin': { + fn: (state) => { + return updateState(state, { + loopStart: state.output.length + }); + }, + doc: 'Mark the beginning of a loop', + stack: '( -- )' + }, + + 'until': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on until'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (value === 0) { + // Loop back to BEGIN + return updateState(state, { + stacks, + loopBack: true + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'End a loop - if top of stack is false (0), loop back to BEGIN', + stack: '( flag -- )' + }, + + // Help and documentation + 'help': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.map(name => { + const word = builtinWords[name]; + return `${name} ${word.stack} - ${word.doc}`; + }).join('\n'); + + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + '=== 4-Stack Forth Interpreter Help ===', + '', + 'Built-in words:', + builtinList, + '', + 'User defined words: ' + userList, + '', + 'Total words: ' + allWords.length, + '', + 'Use "doc <word>" to get detailed help for a specific word', + 'Use "words" to see just the word names' + ] + }); + }, + doc: 'Display comprehensive help information for all available words', + stack: '( -- )' + }, + + 'doc': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on doc'] }); + } + const { stacks, value: wordName } = popFromStack(state.stacks, 0); + + // Check built-in words first + if (builtinWords[wordName]) { + const word = builtinWords[wordName]; + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Stack effect: ${word.stack}`, + `Description: ${word.doc}`, + `Type: Built-in word` + ] + }); + } + + // Check user-defined words + if (state.dictionary.has(wordName)) { + const definition = state.dictionary.get(wordName); + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Definition: ${definition.join(' ')}`, + `Type: User-defined word` + ] + }); + } + + return updateState(state, { + stacks, + output: [...state.output, `Word '${wordName}' not found`] + }); + }, + doc: 'Display documentation for a specific word', + stack: '( "word" -- )' + }, + + 'words': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.join(' '); + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + 'Built-in words: ' + builtinList, + 'User defined words: ' + userList, + 'Total words: ' + allWords.length + ] + }); + }, + doc: 'List all available words (built-in and user-defined)', + stack: '( -- )' + } +}; + +// Parse and execute Forth input +const parseAndExecute = (state, input) => { + const tokens = input.trim().split(/\s+/).filter(token => token.length > 0); + return tokens.reduce(executeToken, state); +}; + +// Execute a single token +const executeToken = (state, token) => { + // Handle string mode + if (state.stringMode) { + // Check if this token contains the closing quote + const quoteIndex = token.indexOf('"'); + if (quoteIndex !== -1) { + // Token contains closing quote + const beforeQuote = token.substring(0, quoteIndex); + const afterQuote = token.substring(quoteIndex + 1); + + // Add the part before the quote to the string + const finalString = state.currentString + (state.currentString ? ' ' : '') + beforeQuote; + + // End string mode and handle based on mode + let newState; + if (state.stringPushMode) { + // Push mode: add string to stack + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + stacks: pushToStack(state.stacks, 0, finalString) + }); + } else { + // Print mode: add to output + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + output: [...state.output, finalString] + }); + } + + // If there's content after the quote, process it + if (afterQuote.trim()) { + newState = ForthInterpreter.parseAndExecute(newState, afterQuote); + } + + return newState; + } else { + // Add to current string + return updateState(state, { + currentString: state.currentString + (state.currentString ? ' ' : '') + token + }); + } + } + + // Handle skip mode (for control flow) + if (state.skipMode) { + if (token === 'if' || token === 'begin') { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } else if (token === 'then' || token === 'until') { + if (state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + } else if (token === 'else') { + if (state.skipCount === 0) { + // Switch to skipping ELSE branch + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + } + } + // Skip this token + return state; + } + + // Handle move operation state machine + if (state.moveInProgress) { + if (state.moveFromStack === null) { + // Expecting source stack number + const from = parseInt(token); + if (isNaN(from) || from < 1 || from > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, 'Error: Invalid source stack. Must be 1-4'] + }); + } + if (state.stacks[from - 1].length === 0) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Stack ${from} is empty`] + }); + } + return updateState(state, { + moveInProgress: true, + moveFromStack: from - 1, // Convert to 0-based index + output: [...state.output, `Moving from stack ${from}. Enter destination stack (1-4):`] + }); + } else { + // Expecting destination stack number + const to = parseInt(token); + if (isNaN(to) || to < 1 || to > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, 'Error: Invalid destination stack. Must be 1-4'] + }); + } + const toIndex = to - 1; // Convert to 0-based index + const fromIndex = state.moveFromStack; + + // Reset move state + const newState = updateState(state, { + moveInProgress: false, + moveFromStack: null + }); + + // Perform the move + const { stacks, value } = popFromStack(newState.stacks, fromIndex); + return updateState(newState, { + stacks: pushToStack(stacks, toIndex, value), + output: [...newState.output, `Moved ${value} from stack ${fromIndex + 1} to stack ${toIndex + 1}`] + }); + } + } + + // Handle word definition compilation + if (state.compilingWord !== null) { + if (token === ';') { + const newDictionary = new Map(state.dictionary); + newDictionary.set(state.compilingWord, [...state.compilingDefinition]); + return updateState(state, { + dictionary: newDictionary, + compilingWord: null, + compilingDefinition: [], + output: [...state.output, `Word '${state.compilingWord}' defined`] + }); + } + + // If we're expecting a name, capture it + if (state.compilingWord === 'EXPECTING_NAME') { + return updateState(state, { + compilingWord: token, + compilingDefinition: [] + }); + } + + // Otherwise, add to definition + return updateState(state, { + compilingDefinition: [...state.compilingDefinition, token] + }); + } + + // Handle word definition start + if (token === ':') { + return updateState(state, { + compilingWord: 'EXPECTING_NAME', + compilingDefinition: [] + }); + } + + // Check if it's a built-in word + if (builtinWords[token]) { + return builtinWords[token].fn(state); + } + + // Check if it's a user-defined word + if (state.dictionary.has(token)) { + const definition = state.dictionary.get(token); + return definition.reduce(executeToken, state); + } + + // Check if it's a number + const num = parseFloat(token); + if (!isNaN(num)) { + return updateState(state, { + stacks: pushToStack(state.stacks, 0, num) + }); + } + + // Check if it's a move command + if (token === 'move') { + return updateState(state, { + moveInProgress: true, + moveFromStack: null + }); + } + + // Unknown token + return updateState(state, { + output: [...state.output, `Error: Unknown word '${token}'`] + }); +}; + +// Export for use in other modules or testing +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} else if (typeof window !== 'undefined') { + // Browser environment + window.ForthInterpreter = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} diff --git a/forth/foreforthfourth/forth.js b/forth/foreforthfourth/forth.js new file mode 100644 index 0000000..af133ea --- /dev/null +++ b/forth/foreforthfourth/forth.js @@ -0,0 +1,1973 @@ +// Pure functional approach to Forth interpreter state +const createInitialState = () => ({ + stacks: [[], [], [], []], + dictionary: new Map(), + output: [], + compilingWord: null, + compilingDefinition: [], + stringMode: false, + currentString: '', + stringPushMode: false, + skipMode: false, + skipCount: 0, + loopStart: null, + loopBack: false, + focusedStack: 0, // New: 0=Red, 1=Teal, 2=Blue, 3=Yellow + moveInProgress: false, // For move operation + moveFromStack: null, // For move operation + crossStackInProgress: false, // For cross-stack operations + crossStackOperation: null, // Type of cross-stack operation + crossStackData: null // Data for cross-stack operation +}); + +// Pure function to update state +const updateState = (state, updates) => ({ + ...state, + ...updates +}); + +// Stack operations +const pushToStack = (stacks, stackIndex, value) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? [...stack, value] : stack + ); + return newStacks; +}; + +const popFromStack = (stacks, stackIndex) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? stack.slice(0, -1) : stack + ); + const value = stacks[stackIndex][stacks[stackIndex].length - 1]; + return { stacks: newStacks, value }; +}; + +const moveBetweenStacks = (stacks, fromStack, toStack) => { + if (stacks[fromStack].length === 0) return { stacks, value: null }; + const { stacks: newStacks, value } = popFromStack(stacks, fromStack); + return { stacks: pushToStack(newStacks, toStack, value), value }; +}; + +const popAndPrint = (state, stackIndex) => { + if (state.stacks[stackIndex].length === 0) { + return updateState(state, { output: [...state.output, `Stack ${stackIndex + 1} is empty`] }); + } + const { stacks, value } = popFromStack(state.stacks, stackIndex); + return updateState(state, { + stacks, + output: [...state.output, `Stack ${stackIndex + 1}: ${value}`] + }); +}; + +// Helper function to get focused stack name +const getFocusedStackName = (focusedStack) => { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return stackNames[focusedStack]; +}; + +// Helper function to execute cross-stack operations +const executeCrossStackOperation = (state, sourceIndex, targetIndex) => { + const operation = state.crossStackOperation; + const data = state.crossStackData; + + switch (operation) { + case 'dup': + return updateState(state, { + stacks: pushToStack(state.stacks, targetIndex, data.top), + output: [...state.output, `Duplicated ${data.top} from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'over': + return updateState(state, { + stacks: pushToStack(state.stacks, targetIndex, data.second), + output: [...state.output, `Copied ${data.second} from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'swap': + // Create new stacks array + const newStacks = state.stacks.map((stack, i) => [...stack]); + + // Get top 2 items from source stack (don't remove) + const sourceTop = newStacks[sourceIndex][newStacks[sourceIndex].length - 1]; + const sourceSecond = newStacks[sourceIndex][newStacks[sourceIndex].length - 2]; + + // Add source items to target stack in order: top, second + newStacks[targetIndex].push(sourceTop); + newStacks[targetIndex].push(sourceSecond); + + // Source stack remains unchanged (no popping/pushing) + + return updateState(state, { + stacks: newStacks, + output: [...state.output, `Copied top 2 items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'nip': + // Create new stacks array + const nipStacks = state.stacks.map((stack, i) => [...stack]); + + // Remove second item from source stack + nipStacks[sourceIndex].splice(-2, 1); + + // Add second item to target stack + nipStacks[targetIndex].push(data.second); + + return updateState(state, { + stacks: nipStacks, + output: [...state.output, `Moved second item ${data.second} from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'tuck': + // Create new stacks array + const tuckStacks = state.stacks.map((stack, i) => [...stack]); + + // Remove top item from source stack + const tuckTop = tuckStacks[sourceIndex].pop(); + + // Add items to target stack in tuck order: top, second, top + tuckStacks[targetIndex].push(tuckTop); + tuckStacks[targetIndex].push(data.second); + tuckStacks[targetIndex].push(tuckTop); + + // Don't add top item back to source stack - tuck removes it + + return updateState(state, { + stacks: tuckStacks, + output: [...state.output, `Tucked ${tuckTop} under ${data.second} on ${getFocusedStackName(targetIndex)}`] + }); + + case 'rot': + // Create new stacks array + const rotStacks = state.stacks.map((stack, i) => [...stack]); + + // Remove top 3 items from source stack + // For stack [1, 2, 3], top=3, second=2, third=1 + const rotTop = rotStacks[sourceIndex].pop(); // 3 + const rotSecond = rotStacks[sourceIndex].pop(); // 2 + const rotThird = rotStacks[sourceIndex].pop(); // 1 + + // Add 3 items to target stack in rotated order: third, first, second + rotStacks[targetIndex].push(rotThird); + rotStacks[targetIndex].push(rotTop); + rotStacks[targetIndex].push(rotSecond); + + // Add 3 items back to source stack in rotated order: third, first, second + rotStacks[sourceIndex].push(rotThird); + rotStacks[sourceIndex].push(rotTop); + rotStacks[sourceIndex].push(rotSecond); + + return updateState(state, { + stacks: rotStacks, + output: [...state.output, `Rotated top 3 items between ${getFocusedStackName(sourceIndex)} and ${getFocusedStackName(targetIndex)}`] + }); + + case '2dup': + // Create new stacks array + const dup2Stacks = state.stacks.map((stack, i) => [...stack]); + + // Get top 2 items from source stack (don't remove) + const dup2Top = dup2Stacks[sourceIndex][dup2Stacks[sourceIndex].length - 1]; + const dup2Second = dup2Stacks[sourceIndex][dup2Stacks[sourceIndex].length - 2]; + + // Add 2 items to target stack (preserve order: second, top) + dup2Stacks[targetIndex].push(dup2Second); + dup2Stacks[targetIndex].push(dup2Top); + + // Source stack remains unchanged (no popping/pushing) + + return updateState(state, { + stacks: dup2Stacks, + output: [...state.output, `Duplicated top 2 items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case '2over': + // Create new stacks array + const over2Stacks = state.stacks.map((stack, i) => [...stack]); + + // Get second pair of items from source stack (don't remove) + // For stack [10, 20, 30, 40], second pair is [20, 30] + const over2Second = over2Stacks[sourceIndex][over2Stacks[sourceIndex].length - 3]; + const over2Third = over2Stacks[sourceIndex][over2Stacks[sourceIndex].length - 2]; + + // Add 2 items to target stack (preserve order: third, second) + over2Stacks[targetIndex].push(over2Third); + over2Stacks[targetIndex].push(over2Second); + + return updateState(state, { + stacks: over2Stacks, + output: [...state.output, `Copied second pair of items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case '2swap': + // Create new stacks array + const swap2Stacks = state.stacks.map((stack, i) => [...stack]); + + // Get top 4 items from source stack (don't remove) + const sourceItems = [ + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 1], // top + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 2], // second + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 3], // third + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 4] // fourth + ]; + + // Add source items to target stack in order: fourth, third, second, top + swap2Stacks[targetIndex].push(sourceItems[3]); // fourth + swap2Stacks[targetIndex].push(sourceItems[2]); // third + swap2Stacks[targetIndex].push(sourceItems[1]); // second + swap2Stacks[targetIndex].push(sourceItems[0]); // top + + // Source stack remains unchanged (no popping/pushing) + + return updateState(state, { + stacks: swap2Stacks, + output: [...state.output, `Copied top 2 pairs of items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + default: + return updateState(state, { + output: [...state.output, `Error: Unknown cross-stack operation: ${operation}`] + }); + } +}; + +// Built-in words with documentation +const builtinWords = { + // Stack focus commands + 'focus.red': { + fn: (state) => updateState(state, { + focusedStack: 0, + output: [...state.output, 'Focus set to Red stack (Stack 1)'] + }), + doc: 'Set focus to Red stack (Stack 1)', + stack: '( -- )' + }, + 'focus.1': { + fn: (state) => updateState(state, { + focusedStack: 0, + output: [...state.output, 'Focus set to Red stack (Stack 1)'] + }), + doc: 'Set focus to Red stack (Stack 1)', + stack: '( -- )' + }, + 'focus.teal': { + fn: (state) => updateState(state, { + focusedStack: 1, + output: [...state.output, 'Focus set to Teal stack (Stack 2)'] + }), + doc: 'Set focus to Teal stack (Stack 2)', + stack: '( -- )' + }, + 'focus.2': { + fn: (state) => updateState(state, { + focusedStack: 1, + output: [...state.output, 'Focus set to Teal stack (Stack 2)'] + }), + doc: 'Set focus to Teal stack (Stack 2)', + stack: '( -- )' + }, + 'focus.blue': { + fn: (state) => updateState(state, { + focusedStack: 2, + output: [...state.output, 'Focus set to Blue stack (Stack 3)'] + }), + doc: 'Set focus to Blue stack (Stack 3)', + stack: '( -- )' + }, + 'focus.3': { + fn: (state) => updateState(state, { + focusedStack: 2, + output: [...state.output, 'Focus set to Blue stack (Stack 3)'] + }), + doc: 'Set focus to Blue stack (Stack 3)', + stack: '( -- )' + }, + 'focus.yellow': { + fn: (state) => updateState(state, { + focusedStack: 3, + output: [...state.output, 'Focus set to Yellow stack (Stack 4)'] + }), + doc: 'Set focus to Yellow stack (Stack 4)', + stack: '( -- )' + }, + 'focus.4': { + fn: (state) => updateState(state, { + focusedStack: 3, + output: [...state.output, 'Focus set to Yellow stack (Stack 4)'] + }), + doc: 'Set focus to Yellow stack (Stack 4)', + stack: '( -- )' + }, + 'focus.show': { + fn: (state) => { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return updateState(state, { + output: [...state.output, `Currently focused on: ${stackNames[state.focusedStack]}`] + }); + }, + doc: 'Show which stack is currently focused', + stack: '( -- )' + }, + + // Stack manipulation for focused stack + 'dup': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return updateState(state, { + output: [...state.output, `Error: Stack underflow on dup - ${stackNames[state.focusedStack]} is empty. Use numbers or strings to add items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, top) + }); + }, + doc: 'Duplicate the top item on the stack', + stack: '( x -- x x )' + }, + + 'swap': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return updateState(state, { + output: [...state.output, `Error: Stack underflow on swap - ${stackNames[state.focusedStack]} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[state.focusedStack].pop(); + const b = newStacks[state.focusedStack].pop(); + newStacks[state.focusedStack].push(a); + newStacks[state.focusedStack].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Exchange the top two items on the stack', + stack: '( x1 x2 -- x2 x1 )' + }, + + 'drop': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on drop - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to remove.`] + }); + } + const { stacks } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { stacks }); + }, + doc: 'Remove the top item from the stack', + stack: '( x -- )' + }, + + '2drop': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2drop - Stack 1 (Red) needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const { stacks: stacks1 } = popFromStack(state.stacks, state.focusedStack); + const { stacks } = popFromStack(stacks1, 0); + return updateState(state, { stacks }); + }, + doc: 'Remove the top two items from the stack', + stack: '( x1 x2 -- )' + }, + + 'over': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on over - Stack 1 (Red) needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, second) + }); + }, + doc: 'Copy the second item on the stack to the top', + stack: '( x1 x2 -- x1 x2 x1 )' + }, + + '2dup': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2dup - Stack 1 (Red) needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + const newStacks = pushToStack(state.stacks, state.focusedStack, second); + return updateState(state, { + stacks: pushToStack(newStacks, state.focusedStack, top) + }); + }, + doc: 'Duplicate the top two items on the stack', + stack: '( x1 x2 -- x1 x2 x1 x2 )' + }, + + 'rot': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 3) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on rot - Stack 1 (Red) needs 3 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[state.focusedStack].pop(); + const b = newStacks[state.focusedStack].pop(); + const c = newStacks[state.focusedStack].pop(); + newStacks[state.focusedStack].push(b); + newStacks[state.focusedStack].push(a); + newStacks[state.focusedStack].push(c); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack', + stack: '( x1 x2 x3 -- x2 x3 x1 )' + }, + + '-rot': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 3) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on -rot - Stack 1 (Red) needs 3 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[state.focusedStack].pop(); + const b = newStacks[state.focusedStack].pop(); + const c = newStacks[state.focusedStack].pop(); + newStacks[state.focusedStack].push(a); + newStacks[state.focusedStack].push(c); + newStacks[state.focusedStack].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack (reverse of rot)', + stack: '( x1 x2 x3 -- x3 x1 x2 )' + }, + + // Cross-stack operations + 'dup.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on dup.stacks - ${getFocusedStackName(state.focusedStack)} is empty. Add an item first.`] + }); + } + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'dup', + crossStackData: { top: state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1] }, + output: [...state.output, `Enter destination stack (1-4) to duplicate to:`] + }); + }, + doc: 'Duplicate top item from focused stack to another stack', + stack: '( x -- x x ) (interactive)' + }, + + 'over.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on over.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'over', + crossStackData: { second }, + output: [...state.output, `Enter destination stack (1-4) to copy second item to:`] + }); + }, + doc: 'Copy second item from focused stack to another stack', + stack: '( x1 x2 -- x1 x2 x1 ) (interactive)' + }, + + 'swap.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on swap.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'swap', + crossStackData: { top, second }, + output: [...state.output, `Enter target stack (1-4) to swap with:`] + }); + }, + doc: 'Swap top items between focused stack and another stack', + stack: '( x1 x2 -- x2 x1 ) (interactive)' + }, + + 'nip.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on nip.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'nip', + crossStackData: { top, second }, + output: [...state.output, `Enter destination stack (1-4) to move second item to:`] + }); + }, + doc: 'Move second item from focused stack to another stack (remove from source)', + stack: '( x1 x2 -- x1 ) (interactive)' + }, + + 'tuck.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on tuck.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'tuck', + crossStackData: { top, second }, + output: [...state.output, `Enter destination stack (1-4) to tuck top item under second item:`] + }); + }, + doc: 'Tuck top item under second item on another stack', + stack: '( x1 x2 -- x2 x1 x2 ) (interactive)' + }, + + 'rot.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 3) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on rot.stacks - ${getFocusedStackName(state.focusedStack)} needs 3 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const first = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 3]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + const third = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'rot', + crossStackData: { first, second, third }, + output: [...state.output, `Enter destination stack (1-4) to rotate with:`] + }); + }, + doc: 'Rotate top 3 items between focused stack and another stack', + stack: '( x1 x2 x3 -- x2 x3 x1 ) (interactive)' + }, + + '2dup.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2dup.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: '2dup', + crossStackData: { top, second }, + output: [...state.output, `Enter destination stack (1-4) to duplicate top 2 items to:`] + }); + }, + doc: 'Duplicate top 2 items from focused stack to another stack', + stack: '( x1 x2 -- x1 x2 x1 x2 ) (interactive)' + }, + + '2over.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 4) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2over.stacks - ${getFocusedStackName(state.focusedStack)} needs 4 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: '2over', + crossStackData: {}, + output: [...state.output, `Enter destination stack (1-4) to copy second pair of items to:`] + }); + }, + doc: 'Copy second pair of items from focused stack to another stack', + stack: '( x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2 ) (interactive)' + }, + + '2swap.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 4) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2swap.stacks - ${getFocusedStackName(state.focusedStack)} needs 4 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const first = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 4]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 3]; + const third = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + const fourth = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: '2swap', + crossStackData: { first, second, third, fourth }, + output: [...state.output, `Enter destination stack (1-4) to swap top 2 pairs with:`] + }); + }, + doc: 'Swap top 2 pairs of items between focused stack and another stack', + stack: '( x1 x2 x3 x4 -- x3 x4 x1 x2 ) (interactive)' + }, + + // Arithmetic operations on stack 1 + '+': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on + - ${getFocusedStackName(state.focusedStack)} needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a + b) + }); + }, + doc: 'Add the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '-': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on - - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a - b) + }); + }, + doc: 'Subtract the top number from the second number on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '*': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on * - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a * b) + }); + }, + doc: 'Multiply the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '/': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on / - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, a), state.focusedStack, b), + output: [...state.output, 'Error: Division by zero - Cannot divide by zero. Check your divisor before dividing.'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, Math.floor(a / b)) + }); + }, + doc: 'Divide the second number by the top number on the stack (integer division)', + stack: '( n1 n2 -- n3 )' + }, + + 'mod': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on mod - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, a), state.focusedStack, b), + output: [...state.output, 'Error: Modulo by zero - Cannot calculate remainder when dividing by zero. Check your divisor first.'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a % b) + }); + }, + doc: 'Return the remainder of dividing the second number by the top number', + stack: '( n1 n2 -- n3 )' + }, + + 'abs': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on abs - Stack 1 (Red) is empty. Add a number first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, Math.abs(value)) + }); + }, + doc: 'Return the absolute value of the top number on the stack', + stack: '( n -- |n| )' + }, + + 'negate': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on negate - ${getFocusedStackName(state.focusedStack)} is empty. Add a number first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, -value) + }); + }, + doc: 'Return the negative of the top number on the stack', + stack: '( n -- -n )' + }, + + 'min': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on min - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, Math.min(a, b)) + }); + }, + doc: 'Return the smaller of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + 'max': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on max - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, Math.max(a, b)) + }); + }, + doc: 'Return the larger of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + // Comparison and logic + '=': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on = - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a === b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the top two numbers are equal, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '<': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on < - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a < b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is less than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '>': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on > - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a > b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is greater than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + 'and': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on and - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a && b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if both top two values are true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'or': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on or - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a || b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if either of the top two values is true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'not': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on not - Stack 1 (Red) is empty. Add a value first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, value ? 0 : -1) + }); + }, + doc: 'Return true (-1) if the top value is false, false (0) if it is true', + stack: '( x -- flag )' + }, + + // Stack inspection + '.s': { + fn: (state) => { + const stackStr = state.stacks[state.focusedStack].length === 0 ? 'empty' : state.stacks[0].join(' '); + return updateState(state, { + output: [...state.output, `Stack 1 (red): ${stackStr}`] + }); + }, + doc: 'Display the contents of the red stack (non-destructive)', + stack: '( -- )' + }, + + 'depth': { + fn: (state) => { + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, state.stacks[state.focusedStack].length) + }); + }, + doc: 'Push the number of items on the red stack', + stack: '( -- n )' + }, + + '.': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on . - Stack 1 (Red) is empty. Nothing to print.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks, + output: [...state.output, value.toString()] + }); + }, + doc: 'Pop and print the top item from the red stack', + stack: '( x -- )' + }, + + // Multi-stack operations + 'move.red': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 0, value) + }); + }, + doc: 'Move top item from focused stack to red stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.1': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 0, value) + }); + }, + doc: 'Move top item from focused stack to red stack (Stack 1) (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.teal': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 1, value) + }); + }, + doc: 'Move top item from focused stack to teal stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.2': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 1, value) + }); + }, + doc: 'Move top item from focused stack to teal stack (Stack 2) (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.blue': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 2, value) + }); + }, + doc: 'Move top item from focused stack to blue stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.3': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 2, value) + }); + }, + doc: 'Move top item from focused stack to blue stack (Stack 3) (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.yellow': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 3, value) + }); + }, + doc: 'Move top item from focused stack to yellow stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.4': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 3, value) + }); + }, + doc: 'Move top item from focused stack to yellow stack (Stack 4) (removes from focused stack)', + stack: '( x -- )' + }, + + // Copy operations (keep item in source stack) + 'copy.red': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, top) + }); + }, + doc: 'Copy top item from focused stack to red stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.1': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, top) + }); + }, + doc: 'Copy top item from focused stack to red stack (Stack 1) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.teal': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 1, top) + }); + }, + doc: 'Copy top item from focused stack to teal stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.2': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 1, top) + }); + }, + doc: 'Copy top item from focused stack to teal stack (Stack 2) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.blue': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 2, top) + }); + }, + doc: 'Copy top item from focused stack to blue stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.3': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 2, top) + }); + }, + doc: 'Copy top item from focused stack to blue stack (Stack 3) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.yellow': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 3, top) + }); + }, + doc: 'Copy top item from focused stack to yellow stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.4': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 3, top) + }); + }, + doc: 'Copy top item from focused stack to yellow stack (Stack 4) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'pop.red': { + fn: (state) => popAndPrint(state, 0), + doc: 'Pop and print top item from red stack', + stack: '( x -- )' + }, + + 'pop.1': { + fn: (state) => popAndPrint(state, 0), + doc: 'Pop and print top item from red stack (Stack 1)', + stack: '( x -- )' + }, + + 'pop.teal': { + fn: (state) => popAndPrint(state, 1), + doc: 'Pop and print top item from teal stack', + stack: '( x -- )' + }, + + 'pop.2': { + fn: (state) => popAndPrint(state, 1), + doc: 'Pop and print top item from teal stack (Stack 2)', + stack: '( x -- )' + }, + + 'pop.blue': { + fn: (state) => popAndPrint(state, 2), + doc: 'Pop and print top item from blue stack', + stack: '( x -- )' + }, + + 'pop.3': { + fn: (state) => popAndPrint(state, 2), + doc: 'Pop and print top item from blue stack (Stack 3)', + stack: '( x -- )' + }, + + 'pop.yellow': { + fn: (state) => popAndPrint(state, 3), + doc: 'Pop and print top item from yellow stack', + stack: '( x -- )' + }, + + 'pop.4': { + fn: (state) => popAndPrint(state, 3), + doc: 'Pop and print top item from yellow stack (Stack 4)', + stack: '( x -- )' + }, + + // Move operations + 'move': { + fn: (state) => { + return updateState(state, { + moveInProgress: true, + moveFromStack: null, + output: [...state.output, 'Enter source stack (1-4) to move from:'] + }); + }, + doc: 'Move top item from one stack to another (interactive: source, then destination)', + stack: '( -- ) (interactive)' + }, + + // Utility words + 'clear': { + fn: (state) => updateState(state, { + stacks: [[], [], [], []], + output: [...state.output, 'All stacks cleared'] + }), + doc: 'Clear all four stacks', + stack: '( -- )' + }, + + 'clear.all': { + fn: (state) => updateState(state, { + stacks: [[], [], [], []], + output: [...state.output, 'All stacks cleared'] + }), + doc: 'Clear all four stacks (alias for clear)', + stack: '( -- )' + }, + + 'clear.focused': { + fn: (state) => { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + const newStacks = state.stacks.map((stack, i) => + i === state.focusedStack ? [] : stack + ); + return updateState(state, { + stacks: newStacks, + output: [...state.output, `${stackNames[state.focusedStack]} cleared`] + }); + }, + doc: 'Clear only the currently focused stack', + stack: '( -- )' + }, + + // String operations + '."': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: false + }); + }, + doc: 'Begin a string literal that will be printed to output', + stack: '( -- )' + }, + + 's"': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: true + }); + }, + doc: 'Begin a string literal that will be pushed to the stack', + stack: '( -- )' + }, + + 'type': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on type - Stack 1 (Red) needs 2 items (address and length), but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const { stacks: stacks1, value: length } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: stacks2, + output: [...state.output, string.toString()] + }); + }, + doc: 'Print a string from the stack (takes length and string address)', + stack: '( addr len -- )' + }, + + 'count': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on count - Stack 1 (Red) is empty. Add a string first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (typeof value === 'string') { + const newStacks = pushToStack(stacks, state.focusedStack, value.length); + return updateState(state, { + stacks: pushToStack(newStacks, state.focusedStack, value) + }); + } else { + const newStacks = pushToStack(stacks, state.focusedStack, 0); + return updateState(state, { + stacks: pushToStack(newStacks, state.focusedStack, '') + }); + } + }, + doc: 'Extract string info from counted string (returns length and address)', + stack: '( c-addr -- c-addr u )' + }, + + 'char+': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on char+ - Stack 1 (Red) needs 2 items (string and offset), but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const { stacks: stacks1, value: offset } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + if (typeof string === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, string + String.fromCharCode(offset)) + }); + } else { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, string), 0, offset), + output: [...state.output, `Error: char+ requires string on stack - Got ${typeof string}, expected string. Use s" to create strings.`] + }); + } + }, + doc: 'Add a character to a string using ASCII offset', + stack: '( c-addr1 char -- c-addr2 )' + }, + + 'strlen': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on strlen - Stack 1 (Red) is empty. Add a string first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (typeof value === 'string') { + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, value.length) + }); + } else { + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, 0), + output: [...state.output, `Error: strlen requires string on stack - Got ${typeof value}, expected string. Use s" to create strings.`] + }); + } + }, + doc: 'Get the length of a string on the stack', + stack: '( str -- len )' + }, + + 'strcat': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on strcat - Stack 1 (Red) needs 2 strings, but has ${state.stacks[state.focusedStack].length}. Add more strings first.`] + }); + } + const { stacks: stacks1, value: str2 } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: str1 } = popFromStack(stacks1, 0); + if (typeof str1 === 'string' && typeof str2 === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, str1 + str2) + }); + } else { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, str1), 0, str2), + output: [...state.output, `Error: strcat requires two strings - Got ${typeof str1} and ${typeof str2}. Use s" to create strings.`] + }); + } + }, + doc: 'Concatenate two strings from the stack', + stack: '( str1 str2 -- str3 )' + }, + + // Control flow + 'if': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on if - Stack 1 (Red) is empty. Add a condition value first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (value === 0) { + // Skip until THEN or ELSE + return updateState(state, { + stacks, + skipMode: true, + skipCount: 0 + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'Begin conditional execution - if top of stack is false (0), skip to THEN', + stack: '( flag -- )' + }, + + 'else': { + fn: (state) => { + if (state.skipMode) { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } + // Skip until THEN + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + }, + doc: 'Begin alternative branch in conditional execution', + stack: '( -- )' + }, + + 'then': { + fn: (state) => { + if (state.skipMode && state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else if (state.skipMode) { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + return state; + }, + doc: 'End conditional execution block', + stack: '( -- )' + }, + + 'begin': { + fn: (state) => { + return updateState(state, { + loopStart: state.output.length + }); + }, + doc: 'Mark the beginning of a loop', + stack: '( -- )' + }, + + 'until': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on until - Stack 1 (Red) is empty. Add a condition value first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (value === 0) { + // Loop back to BEGIN + return updateState(state, { + stacks, + loopBack: true + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'End a loop - if top of stack is false (0), loop back to BEGIN', + stack: '( flag -- )' + }, + + // Help and documentation + 'help': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.map(name => { + const word = builtinWords[name]; + return `${name} ${word.stack} - ${word.doc}`; + }); + + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + '=== 4-Stack Forth Interpreter Help ===', + '', + 'Built-in words:', + ...builtinList, + '', + 'User defined words: ' + userList, + '', + 'Total words: ' + allWords.length, + '', + 'Use "doc <word>" to get detailed help for a specific word', + 'Use "words" to see just the word names' + ] + }); + }, + doc: 'Display comprehensive help information for all available words', + stack: '( -- )' + }, + + 'doc': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on doc - Stack 1 (Red) is empty. Add a word name first (e.g., s" dup" doc).`] + }); + } + const { stacks, value: wordName } = popFromStack(state.stacks, state.focusedStack); + + // Check built-in words first + if (builtinWords[wordName]) { + const word = builtinWords[wordName]; + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Stack effect: ${word.stack}`, + `Description: ${word.doc}`, + `Type: Built-in word` + ] + }); + } + + // Check user-defined words + if (state.dictionary.has(wordName)) { + const definition = state.dictionary.get(wordName); + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Definition: ${definition.join(' ')}`, + `Type: User-defined word` + ] + }); + } + + return updateState(state, { + stacks, + output: [...state.output, `Word '${wordName}' not found`] + }); + }, + doc: 'Display documentation for a specific word', + stack: '( "word" -- )' + }, + + 'words': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.join(' '); + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + 'Built-in words: ' + builtinList, + 'User defined words: ' + userWords.join(' '), + 'Total words: ' + allWords.length + ] + }); + }, + doc: 'List all available words (built-in and user-defined)', + stack: '( -- )' + } +}; + +// Help and documentation commands (defined outside builtinWords to avoid circular reference) +const helpCommands = { + 'help': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.map(name => { + const word = builtinWords[name]; + return `${name} ${word.stack} - ${word.doc}`; + }); + + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + '=== 4-Stack Forth Interpreter Help ===', + '', + 'Built-in words:', + ...builtinList, + '', + 'User defined words: ' + userList, + '', + 'Total words: ' + allWords.length, + '', + 'Use "doc <word>" to get detailed help for a specific word', + 'Use "words" to see just the word names' + ] + }); + }, + doc: 'Display comprehensive help information for all available words', + stack: '( -- )' + }, + + 'doc': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on doc - Stack 1 (Red) is empty. Add a word name first (e.g., s" dup" doc).`] + }); + } + const { stacks, value: wordName } = popFromStack(state.stacks, state.focusedStack); + + // Check built-in words first + if (builtinWords[wordName]) { + const word = builtinWords[wordName]; + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Stack effect: ${word.stack}`, + `Description: ${word.doc}`, + `Type: Built-in word` + ] + }); + } + + // Check user-defined words + if (state.dictionary.has(wordName)) { + const definition = state.dictionary.get(wordName); + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Definition: ${definition.join(' ')}`, + `Type: User-defined word` + ] + }); + } + + return updateState(state, { + stacks, + output: [...state.output, `Word '${wordName}' not found`] + }); + }, + doc: 'Display documentation for a specific word', + stack: '( "word" -- )' + } +}; + +// Parse and execute Forth input +const parseAndExecute = (state, input) => { + const tokens = input.trim().split(/\s+/).filter(token => token.length > 0); + return tokens.reduce(executeToken, state); +}; + +// Execute a single token +const executeToken = (state, token) => { + // Handle string mode + if (state.stringMode) { + // Check if this token contains the closing quote + const quoteIndex = token.indexOf('"'); + if (quoteIndex !== -1) { + // Token contains closing quote + const beforeQuote = token.substring(0, quoteIndex); + const afterQuote = token.substring(quoteIndex + 1); + + // Add the part before the quote to the string + const finalString = state.currentString + (state.currentString ? ' ' : '') + beforeQuote; + + // End string mode and handle based on mode + let newState; + if (state.stringPushMode) { + // Push mode: add string to stack + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + stacks: pushToStack(state.stacks, state.focusedStack, finalString) + }); + } else { + // Print mode: add to output + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + output: [...state.output, finalString] + }); + } + + // If there's content after the quote, process it + if (afterQuote.trim()) { + newState = ForthInterpreter.parseAndExecute(newState, afterQuote); + } + + return newState; + } else { + // Add to current string + return updateState(state, { + currentString: state.currentString + (state.currentString ? ' ' : '') + token + }); + } + } + + // Handle skip mode (for control flow) + if (state.skipMode) { + if (token === 'if' || token === 'begin') { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } else if (token === 'then' || token === 'until') { + if (state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + } else if (token === 'else') { + if (state.skipCount === 0) { + // Switch to skipping ELSE branch + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + } + } + // Skip this token + return state; + } + + // Handle move operation state machine + if (state.moveInProgress) { + if (state.moveFromStack === null) { + // Expecting source stack number + const from = parseInt(token); + if (isNaN(from) || from < 1 || from > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Invalid source stack ${from} - Must be 1, 2, 3, or 4.`] + }); + } + if (state.stacks[from - 1].length === 0) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Stack ${from} is empty - Nothing to move. Add items to stack ${from} first.`] + }); + } + return updateState(state, { + moveInProgress: true, + moveFromStack: from - 1, // Convert to 0-based index + output: [...state.output, `Moving from stack ${from}. Enter destination stack (1-4):`] + }); + } else { + // Expecting destination stack number + const to = parseInt(token); + if (isNaN(to) || to < 1 || to > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Invalid destination stack ${to} - Must be 1, 2, 3, or 4.`] + }); + } + const toIndex = to - 1; // Convert to 0-based index + const fromIndex = state.moveFromStack; + + // Reset move state + const newState = updateState(state, { + moveInProgress: false, + moveFromStack: null + }); + + // Perform the move + const { stacks, value } = popFromStack(newState.stacks, fromIndex); + return updateState(newState, { + stacks: pushToStack(stacks, toIndex, value), + output: [...newState.output, `Moved ${value} from stack ${fromIndex + 1} to stack ${toIndex + 1}`] + }); + } + } + + // Handle cross-stack operations state machine + if (state.crossStackInProgress) { + if (state.crossStackOperation === null) { + // This shouldn't happen, but handle gracefully + return updateState(state, { + crossStackInProgress: false, + crossStackOperation: null, + crossStackData: null, + output: [...state.output, 'Error: Cross-stack operation state corrupted'] + }); + } + + // Expecting target stack number + const target = parseInt(token); + if (isNaN(target) || target < 1 || target > 4) { + return updateState(state, { + crossStackInProgress: false, + crossStackOperation: null, + crossStackData: null, + output: [...state.output, `Error: Invalid target stack ${target} - Must be 1, 2, 3, or 4.`] + }); + } + + const targetIndex = target - 1; // Convert to 0-based index + const sourceIndex = state.focusedStack; + + // Execute the cross-stack operation (don't clear state yet) + const result = executeCrossStackOperation(state, sourceIndex, targetIndex); + + // Clear cross-stack state after execution + return updateState(result, { + crossStackInProgress: false, + crossStackOperation: null, + crossStackData: null + }); + } + + // Handle word definition compilation + if (state.compilingWord !== null) { + if (token === ';') { + const newDictionary = new Map(state.dictionary); + newDictionary.set(state.compilingWord, [...state.compilingDefinition]); + return updateState(state, { + dictionary: newDictionary, + compilingWord: null, + compilingDefinition: [], + output: [...state.output, `Word '${state.compilingWord}' defined`] + }); + } + + // If we're expecting a name, capture it + if (state.compilingWord === 'EXPECTING_NAME') { + return updateState(state, { + compilingWord: token, + compilingDefinition: [] + }); + } + + // Otherwise, add to definition + return updateState(state, { + compilingDefinition: [...state.compilingDefinition, token] + }); + } + + // Handle word definition start + if (token === ':') { + return updateState(state, { + compilingWord: 'EXPECTING_NAME', + compilingDefinition: [] + }); + } + + // Check if it's a built-in word + if (builtinWords[token]) { + return builtinWords[token].fn(state); + } + + // Check if it's a user-defined word + if (state.dictionary.has(token)) { + const definition = state.dictionary.get(token); + return definition.reduce(executeToken, state); + } + + // Check if it's a number + const num = parseFloat(token); + if (!isNaN(num)) { + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, num) + }); + } + + + + // Check if it's a cross-stack operation + if (token.endsWith('.stacks')) { + const baseOperation = token.replace('.stacks', ''); + if (builtinWords[token]) { + return builtinWords[token].fn(state); + } + } + + // Unknown token + return updateState(state, { + output: [...state.output, `Error: Unknown word '${token}' - Use 'help' to see all available words, or 'doc <word>' for specific help.`] + }); +}; + +// Export for use in other modules or testing +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} else if (typeof window !== 'undefined') { + // Browser environment + window.ForthInterpreter = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} diff --git a/forth/foreforthfourth/index.html b/forth/foreforthfourth/index.html new file mode 100644 index 0000000..a4400f3 --- /dev/null +++ b/forth/foreforthfourth/index.html @@ -0,0 +1,381 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>4-Stack Forth</title> + <style> + body { + font-family: 'Courier New', monospace; + margin: 0; + padding: 10px; + background-color: #000; + color: #fff; + min-height: 100vh; + display: flex; + flex-direction: column; + } + + .container { + max-width: 100%; + flex: 1; + display: flex; + flex-direction: column; + } + + h1 { + font-size: 18px; + margin: 0 0 10px 0; + text-align: center; + } + + .help { + background-color: #111; + border: 1px solid #333; + padding: 10px; + margin-bottom: 10px; + font-size: 12px; + line-height: 1.3; + } + + .help h2 { + margin: 0 0 5px 0; + font-size: 13px; + } + + .help p { + margin: 0; + } + + .stacks-container { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 10px; + flex: 1; + min-height: 200px; + } + + .stack { + border: 2px solid; + padding: 8px; + display: flex; + flex-direction: column; + background-color: #111; + position: relative; + transition: all 0.3s ease; + } + + .stack.focused { + border-width: 4px; + box-shadow: 0 0 15px rgba(255, 255, 255, 0.3); + transform: scale(1.02); + } + + .stack-1 { border-color: #ff6b6b; } + .stack-2 { border-color: #4ecdc4; } + .stack-3 { border-color: #45b7d1; } + .stack-4 { border-color: #f9ca24; } + + .stack h3 { + margin: 0 0 8px 0; + text-align: center; + font-size: 12px; + color: #fff; + } + + .stack-items { + flex: 1; + display: flex; + flex-direction: column-reverse; + gap: 2px; + align-items: stretch; + } + + .stack-item { + background-color: #222; + padding: 4px 6px; + text-align: center; + border: 1px solid #555; + font-size: 12px; + word-break: break-all; + } + + .input-section { + margin-bottom: 10px; + } + + .input-container { + display: flex; + gap: 5px; + margin-bottom: 8px; + } + + #forth-input { + flex: 1; + padding: 8px; + font-family: 'Courier New', monospace; + font-size: 14px; + border: 1px solid #555; + background-color: #111; + color: #fff; + } + + #forth-input:focus { + outline: none; + border-color: #fff; + } + + button { + padding: 8px 12px; + font-family: 'Courier New', monospace; + font-size: 12px; + border: 1px solid #fff; + background-color: #000; + color: #fff; + cursor: pointer; + } + + button:hover, button:active { + background-color: #fff; + color: #000; + } + + .output { + background-color: #111; + border: 1px solid #555; + padding: 8px; + height: 300px; + overflow-y: auto; + font-family: 'Courier New', monospace; + font-size: 12px; + line-height: 1.3; + } + + /* Semantic section styling */ + section { + margin-bottom: 10px; + } + + section h2 { + margin: 0 0 8px 0; + font-size: 14px; + color: #fff; + } + + .output-section h2 { + margin-bottom: 5px; + } + + .error { + color: #fff; + } + + .success { + color: #fff; + } + + /* Screen reader only content */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + /* Mobile optimizations */ + @media (max-width: 768px) { + body { padding: 5px; } + + h1 { font-size: 16px; } + + .help { + font-size: 11px; + padding: 8px; + } + + .help h2 { + font-size: 12px; + } + + .stacks-container { + gap: 4px; + min-height: 150px; + } + + .stack { + padding: 6px; + } + + .stack h3 { + font-size: 11px; + margin-bottom: 6px; + } + + .stack-item { + font-size: 11px; + padding: 3px 4px; + } + + #forth-input { + font-size: 16px; /* Prevents zoom on iOS */ + padding: 6px; + } + + button { + font-size: 11px; + padding: 6px 10px; + } + + .output { + height: 80px; + font-size: 11px; + padding: 6px; + } + } + + @media (max-width: 480px) { + .stacks-container { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + } + + .input-container { + flex-direction: column; + } + + button { + width: 100%; + } + } + </style> +</head> +<body> + <header> + <h1>4-Stack Forth</h1> + </header> + + <main class="container"> + <section class="help" aria-label="Quick help"> + <h2>Quick Help</h2> + <p>Type <strong>help</strong> to see all available words with documentation and stack effects. Add a word to the stack like <code>s" dup"</code> and then run <code>doc</code> to see documentation for that word.</p> + <p><strong>Stack Focus:</strong> Use <code>focus.red</code>, <code>focus.teal</code>, <code>focus.blue</code>, or <code>focus.yellow</code> (or <code>focus.1</code>, <code>focus.2</code>, <code>focus.3</code>, <code>focus.4</code>) to switch which stack operations target. Currently focused: <span id="current-focus">Red (1)</span></p> + </section> + + <section class="stacks-container" aria-label="Data stacks"> + <h2 class="sr-only">Data Stacks</h2> + <div class="stack stack-1" role="region" aria-label="Stack 1 (Red)" id="stack-1"> + <h3>Stack 1 (Red)</h3> + <div class="stack-items" id="stack-1-items" aria-live="polite"></div> + </div> + <div class="stack stack-2" role="region" aria-label="Stack 2 (Teal)" id="stack-2"> + <h3>Stack 2 (Teal)</h3> + <div class="stack-items" id="stack-2-items" aria-live="polite"></div> + </div> + <div class="stack stack-3" role="region" aria-label="Stack 3 (Blue)" id="stack-3"> + <h3>Stack 3 (Blue)</h3> + <div class="stack-items" id="stack-3-items" aria-live="polite"></div> + </div> + <div class="stack stack-4" role="region" aria-label="Stack 4 (Yellow)" id="stack-4"> + <h3>Stack 4 (Yellow)</h3> + <div class="stack-items" id="stack-4-items" aria-live="polite"></div> + </div> + </section> + + <section class="input-section" aria-label="Command input"> + <h2 class="sr-only">Command Input</h2> + <div class="input-container"> + <label for="forth-input" class="sr-only">Forth command input</label> + <input type="text" id="forth-input" placeholder="Enter Forth commands here..." aria-describedby="input-help" /> + <button type="button" onclick="executeForth()" aria-label="Execute command">Run</button> + <button type="button" onclick="clearAll()" aria-label="Clear all stacks">Clear</button> + </div> + <div id="input-help" class="sr-only">Press Enter to execute commands</div> + </section> + + <section class="output-section" aria-label="Command output"> + <h2 class="sr-only">Output</h2> + <div class="output" id="output" role="log" aria-live="polite" aria-label="Forth interpreter output"></div> + </section> + </main> + + <script src="forth.js"></script> + <script> + let forthState = ForthInterpreter.createInitialState(); + + + + // Update visual display + const updateDisplay = () => { + // Update stacks + forthState.stacks.forEach((stack, index) => { + const container = document.getElementById(`stack-${index + 1}-items`); + container.innerHTML = ''; + stack.forEach(item => { + const div = document.createElement('div'); + div.className = 'stack-item'; + div.textContent = item.toString(); + container.appendChild(div); + }); + }); + + // Update focus indicator + document.querySelectorAll('.stack').forEach((stack, index) => { + if (index === forthState.focusedStack) { + stack.classList.add('focused'); + } else { + stack.classList.remove('focused'); + } + }); + + // Update focus text + const focusNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + document.getElementById('current-focus').textContent = focusNames[forthState.focusedStack]; + + // Update output + const outputElement = document.getElementById('output'); + outputElement.innerHTML = forthState.output + .slice(-100) // Show last 100 messages for better help display + .map(msg => { + const className = msg.startsWith('Error:') ? 'error' : 'success'; + return `<div class="${className}">${msg}</div>`; + }) + .join(''); + outputElement.scrollTop = outputElement.scrollHeight; + }; + + // Execute Forth commands + const executeForth = () => { + const input = document.getElementById('forth-input'); + const command = input.value.trim(); + + if (command) { + forthState = ForthInterpreter.parseAndExecute(forthState, command); + updateDisplay(); + input.value = ''; + } + }; + + // Clear all stacks + const clearAll = () => { + forthState = ForthInterpreter.createInitialState(); + updateDisplay(); + }; + + // Handle Enter key in input + document.getElementById('forth-input').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + executeForth(); + } + }); + + // Initialize display + updateDisplay(); + </script> +</body> +</html> \ No newline at end of file diff --git a/forth/foreforthfourth/test-advanced.js b/forth/foreforthfourth/test-advanced.js new file mode 100644 index 0000000..330fd81 --- /dev/null +++ b/forth/foreforthfourth/test-advanced.js @@ -0,0 +1,94 @@ +// Advanced test file for string operations and control flow +// Run with: node test-advanced.js + +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Testing Advanced 4-Stack Forth Features\n'); + +// Test 1: String literals +console.log('Test 1: String literals'); +let state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '." Hello World"'); +console.log('Stack 1 after string literal:', state.stacks[0]); +console.log('Expected: ["Hello World"]\n'); + +// Test 2: String operations +console.log('Test 2: String operations'); +state = ForthInterpreter.parseAndExecute(state, 'dup strlen'); +console.log('Stack 1 after strlen:', state.stacks[0]); +console.log('Expected: ["Hello World", 11]\n'); + +// Test 3: String concatenation +console.log('Test 3: String concatenation'); +state = ForthInterpreter.parseAndExecute(state, '." from Forth" strcat'); +console.log('Stack 1 after strcat:', state.stacks[0]); +console.log('Expected: ["Hello World from Forth"]\n'); + +// Test 4: Basic control flow - IF THEN +console.log('Test 4: Basic control flow - IF THEN'); +state = ForthInterpreter.parseAndExecute(state, '5 3 > if ." Greater" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Greater"\n'); + +// Test 5: Control flow with false condition +console.log('Test 5: Control flow with false condition'); +state = ForthInterpreter.parseAndExecute(state, '3 5 > if ." Greater" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: No output (condition was false)\n'); + +// Test 6: IF ELSE THEN +console.log('Test 6: IF ELSE THEN'); +state = ForthInterpreter.parseAndExecute(state, '5 3 > if ." Greater" else ." Less or Equal" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Greater"\n'); + +// Test 7: IF ELSE THEN with false condition +console.log('Test 7: IF ELSE THEN with false condition'); +state = ForthInterpreter.parseAndExecute(state, '3 5 > if ." Greater" else ." Less or Equal" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Less or Equal"\n'); + +// Test 8: BEGIN UNTIL loop +console.log('Test 8: BEGIN UNTIL loop'); +state = ForthInterpreter.parseAndExecute(state, '5 begin dup ." Loop " 1 - dup 0 = until drop'); +console.log('Output:', state.output.slice(-5)); +console.log('Expected: 5 loop iterations\n'); + +// Test 9: Complex control flow +console.log('Test 9: Complex control flow'); +state = ForthInterpreter.parseAndExecute(state, '10 begin dup 0 > if dup ." Count: " . 1 - else drop 0 then dup 0 = until'); +console.log('Output:', state.output.slice(-10)); +console.log('Expected: Countdown from 10 to 1\n'); + +// Test 10: String manipulation with control flow +console.log('Test 10: String manipulation with control flow'); +state = ForthInterpreter.parseAndExecute(state, '." Test" dup strlen 5 > if ." Long string" else ." Short string" then'); +console.log('Output:', state.output.slice(-3)); +console.log('Expected: "Test", "Short string"\n'); + +// Test 11: Nested control flow +console.log('Test 11: Nested control flow'); +state = ForthInterpreter.parseAndExecute(state, '7 dup 5 > if dup 10 > if ." Very large" else ." Large" then else dup 3 > if ." Medium" else ." Small" then then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Large"\n'); + +// Test 12: String operations with numbers +console.log('Test 12: String operations with numbers'); +state = ForthInterpreter.parseAndExecute(state, '." Hello" 32 char+'); +console.log('Stack 1 after char+:', state.stacks[0]); +console.log('Expected: ["Hello "]\n'); + +console.log('✅ All advanced tests completed!'); +console.log('\nFinal state:'); +console.log('Stack 1:', state.stacks[0]); +console.log('Stack 2:', state.stacks[1]); +console.log('Stack 3:', state.stacks[2]); +console.log('Stack 4:', state.stacks[3]); +console.log('Dictionary size:', state.dictionary.size); +console.log('Output messages:', state.output.length); + +// Show some example outputs +console.log('\nSample outputs:'); +state.output.slice(-5).forEach((msg, i) => { + console.log(`${i + 1}. ${msg}`); +}); diff --git a/forth/foreforthfourth/test-cross-stack-complete.js b/forth/foreforthfourth/test-cross-stack-complete.js new file mode 100644 index 0000000..22f7a16 --- /dev/null +++ b/forth/foreforthfourth/test-cross-stack-complete.js @@ -0,0 +1,373 @@ +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Comprehensive Cross-Stack Operations Test Suite\n'); + +let state = ForthInterpreter.createInitialState(); +let testCount = 0; +let passCount = 0; + +// Test helper function +const test = (name, testFn) => { + testCount++; + try { + testFn(); + console.log(`✅ ${name}`); + passCount++; + } catch (error) { + console.log(`❌ ${name}: ${error.message}`); + } +}; + +// Test 1: Basic Cross-Stack Operations +test('dup.stacks - Basic Functionality', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '42'); + + // Execute dup.stacks + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + if (state.crossStackOperation !== 'dup') throw new Error('Should set operation to dup'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '2'); // Target Teal stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[0].length !== 1) throw new Error('Source stack should still have 1 item'); + if (state.stacks[1].length !== 1) throw new Error('Target stack should have 1 item'); + if (state.stacks[0][state.stacks[0].length - 1] !== 42) throw new Error('Source stack should still have 42'); + if (state.stacks[1][state.stacks[1].length - 1] !== 42) throw new Error('Target stack should have 42'); +}); + +test('over.stacks - Copy Second Item', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack with multiple items + state = ForthInterpreter.parseAndExecute(state, 'focus.blue'); + state = ForthInterpreter.parseAndExecute(state, '10'); + state = ForthInterpreter.parseAndExecute(state, '20'); + state = ForthInterpreter.parseAndExecute(state, '30'); + + // Execute over.stacks + state = ForthInterpreter.parseAndExecute(state, 'over.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[2].length !== 3) throw new Error('Source stack should still have 3 items'); + if (state.stacks[0].length !== 1) throw new Error('Target stack should have 1 item'); + if (state.stacks[0][state.stacks[0].length - 1] !== 20) throw new Error('Target stack should have second item (20)'); +}); + +test('swap.stacks - Swap Top Items', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.yellow'); + state = ForthInterpreter.parseAndExecute(state, '100'); + state = ForthInterpreter.parseAndExecute(state, '200'); + + // Execute swap.stacks + state = ForthInterpreter.parseAndExecute(state, 'swap.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '3'); // Target Blue stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[3].length !== 2) throw new Error('Source stack should still have 2 items'); + if (state.stacks[2].length !== 2) throw new Error('Target stack should have 2 items'); + if (state.stacks[3][state.stacks[3].length - 1] !== 200) throw new Error('Source stack top should be 200'); + if (state.stacks[3][state.stacks[3].length - 2] !== 100) throw new Error('Source stack second should be 100'); + if (state.stacks[2][state.stacks[2].length - 1] !== 100) throw new Error('Target stack top should be 100'); + if (state.stacks[2][state.stacks[2].length - 2] !== 200) throw new Error('Target stack second should be 200'); +}); + +test('nip.stacks - Move Second Item', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.teal'); + state = ForthInterpreter.parseAndExecute(state, '50'); + state = ForthInterpreter.parseAndExecute(state, '60'); + + // Execute nip.stacks + state = ForthInterpreter.parseAndExecute(state, 'nip.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '4'); // Target Yellow stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[1].length !== 1) throw new Error('Source stack should have 1 item after nip'); + if (state.stacks[3].length !== 1) throw new Error('Target stack should have 1 item'); + if (state.stacks[1][state.stacks[1].length - 1] !== 60) throw new Error('Source stack should keep top item (60)'); + if (state.stacks[3][state.stacks[3].length - 1] !== 50) throw new Error('Target stack should have second item (50)'); +}); + +test('tuck.stacks - Tuck Operation', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '7'); + state = ForthInterpreter.parseAndExecute(state, '8'); + + // Execute tuck.stacks + state = ForthInterpreter.parseAndExecute(state, 'tuck.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '2'); // Target Teal stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[0].length !== 1) throw new Error('Source stack should have 1 item after tuck'); + if (state.stacks[1].length !== 3) throw new Error('Target stack should have 3 items'); + if (state.stacks[0][state.stacks[0].length - 1] !== 7) throw new Error('Source stack should keep top item (7)'); + if (state.stacks[1][state.stacks[1].length - 1] !== 8) throw new Error('Target stack should have 8 at top'); + if (state.stacks[1][state.stacks[1].length - 2] !== 7) throw new Error('Target stack should have 7 in middle'); + if (state.stacks[1][state.stacks[1].length - 3] !== 8) throw new Error('Target stack should have 8 at bottom'); +}); + +test('rot.stacks - Rotate Top 3 Items', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.blue'); + state = ForthInterpreter.parseAndExecute(state, '1'); + state = ForthInterpreter.parseAndExecute(state, '2'); + state = ForthInterpreter.parseAndExecute(state, '3'); + + // Execute rot.stacks + state = ForthInterpreter.parseAndExecute(state, 'rot.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[2].length !== 3) throw new Error('Source stack should still have 3 items'); + if (state.stacks[0].length !== 3) throw new Error('Target stack should have 3 items'); + // Source stack should be rotated: [1, 3, 2] (top=2, second=3, third=1) + if (state.stacks[2][state.stacks[2].length - 1] !== 2) throw new Error('Source stack top should be 2'); + if (state.stacks[2][state.stacks[2].length - 2] !== 3) throw new Error('Source stack second should be 3'); + if (state.stacks[2][state.stacks[2].length - 3] !== 1) throw new Error('Source stack third should be 1'); + // Target stack should have rotated items: [1, 3, 2] (top=2, second=3, third=1) + if (state.stacks[0][state.stacks[0].length - 1] !== 2) throw new Error('Target stack top should be 2'); + if (state.stacks[0][state.stacks[0].length - 2] !== 3) throw new Error('Target stack second should be 3'); + if (state.stacks[0][state.stacks[0].length - 3] !== 1) throw new Error('Target stack third should be 1'); +}); + +test('2dup.stacks - Duplicate Top 2 Items', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.yellow'); + state = ForthInterpreter.parseAndExecute(state, '25'); + state = ForthInterpreter.parseAndExecute(state, '35'); + + // Execute 2dup.stacks + state = ForthInterpreter.parseAndExecute(state, '2dup.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '3'); // Target Blue stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[3].length !== 2) throw new Error('Source stack should still have 2 items'); + if (state.stacks[2].length !== 2) throw new Error('Target stack should have 2 items'); + if (state.stacks[3][state.stacks[3].length - 1] !== 35) throw new Error('Source stack top should be 35'); + if (state.stacks[3][state.stacks[3].length - 2] !== 25) throw new Error('Source stack second should be 25'); + if (state.stacks[2][state.stacks[2].length - 1] !== 35) throw new Error('Target stack top should be 35'); + if (state.stacks[2][state.stacks[2].length - 2] !== 25) throw new Error('Target stack second should be 25'); +}); + +test('2over.stacks - Copy Second Pair', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack with 4 items + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '10'); + state = ForthInterpreter.parseAndExecute(state, '20'); + state = ForthInterpreter.parseAndExecute(state, '30'); + state = ForthInterpreter.parseAndExecute(state, '40'); + + // Execute 2over.stacks + state = ForthInterpreter.parseAndExecute(state, '2over.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '2'); // Target Teal stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[0].length !== 4) throw new Error('Source stack should still have 4 items'); + if (state.stacks[1].length !== 2) throw new Error('Target stack should have 2 items'); + if (state.stacks[1][state.stacks[1].length - 1] !== 20) throw new Error('Target stack top should be 20'); + if (state.stacks[1][state.stacks[1].length - 2] !== 30) throw new Error('Target stack second should be 30'); +}); + +test('2swap.stacks - Swap Top 2 Pairs', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack with 4 items + state = ForthInterpreter.parseAndExecute(state, 'focus.teal'); + state = ForthInterpreter.parseAndExecute(state, '1'); + state = ForthInterpreter.parseAndExecute(state, '2'); + state = ForthInterpreter.parseAndExecute(state, '3'); + state = ForthInterpreter.parseAndExecute(state, '4'); + + // Execute 2swap.stacks + state = ForthInterpreter.parseAndExecute(state, '2swap.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[1].length !== 4) throw new Error('Source stack should still have 4 items'); + if (state.stacks[0].length !== 4) throw new Error('Target stack should have 4 items'); + // Source stack should be: [1, 2, 3, 4] (top=4, second=3, third=2, fourth=1) + if (state.stacks[1][state.stacks[1].length - 1] !== 4) throw new Error('Source stack top should be 4'); + if (state.stacks[1][state.stacks[1].length - 2] !== 3) throw new Error('Source stack second should be 3'); + if (state.stacks[1][state.stacks[1].length - 3] !== 2) throw new Error('Source stack third should be 2'); + if (state.stacks[1][state.stacks[1].length - 4] !== 1) throw new Error('Source stack fourth should be 1'); + // Target stack should be: [1, 2, 3, 4] (top=4, second=3, third=2, fourth=1) + if (state.stacks[0][state.stacks[0].length - 1] !== 4) throw new Error('Target stack top should be 4'); + if (state.stacks[0][state.stacks[0].length - 2] !== 3) throw new Error('Target stack second should be 3'); + if (state.stacks[0][state.stacks[0].length - 3] !== 2) throw new Error('Target stack third should be 2'); + if (state.stacks[0][state.stacks[0].length - 4] !== 1) throw new Error('Target stack fourth should be 1'); +}); + +// Test 2: Error Handling +test('Error Handling - Stack Underflow', () => { + state = ForthInterpreter.createInitialState(); + + // Try dup.stacks on empty stack + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + + const lastOutput = state.output[state.output.length - 1]; + if (!lastOutput.includes('Error: Stack underflow')) { + throw new Error('Should show stack underflow error'); + } +}); + +test('Error Handling - Invalid Target Stack', () => { + state = ForthInterpreter.createInitialState(); + + // Set up valid operation + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '42'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + + // Try invalid target stack + state = ForthInterpreter.parseAndExecute(state, '5'); // Invalid stack number + + const lastOutput = state.output[state.output.length - 1]; + if (!lastOutput.includes('Error: Invalid target stack')) { + throw new Error('Should show invalid target stack error'); + } + if (state.crossStackInProgress) { + throw new Error('Should clear cross-stack mode on error'); + } +}); + +// Test 3: Edge Cases +test('Edge Cases - Single Item Operations', () => { + state = ForthInterpreter.createInitialState(); + + // Test over.stacks with only 1 item + state = ForthInterpreter.parseAndExecute(state, 'focus.blue'); + state = ForthInterpreter.parseAndExecute(state, '100'); + state = ForthInterpreter.parseAndExecute(state, 'over.stacks'); + + const lastOutput = state.output[state.output.length - 1]; + if (!lastOutput.includes('Error: Stack underflow')) { + throw new Error('Should show stack underflow error for over.stacks with 1 item'); + } +}); + +test('Edge Cases - Focus Persistence', () => { + state = ForthInterpreter.createInitialState(); + + // Set focus and perform operation + state = ForthInterpreter.parseAndExecute(state, 'focus.yellow'); + const originalFocus = state.focusedStack; + + state = ForthInterpreter.parseAndExecute(state, '50'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + state = ForthInterpreter.parseAndExecute(state, '1'); + + if (state.focusedStack !== originalFocus) { + throw new Error('Focus should persist through cross-stack operations'); + } +}); + +// Test 4: Complex Workflows +test('Complex Workflows - Multi-Operation Chain', () => { + state = ForthInterpreter.createInitialState(); + + // Set up multiple stacks + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '100'); + state = ForthInterpreter.parseAndExecute(state, '200'); + + state = ForthInterpreter.parseAndExecute(state, 'focus.teal'); + state = ForthInterpreter.parseAndExecute(state, '300'); + + // Chain operations + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red + + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, 'over.stacks'); + state = ForthInterpreter.parseAndExecute(state, '3'); // Target Blue + + // Verify final state + if (state.stacks[0].length !== 3) throw new Error('Red stack should have 3 items'); + if (state.stacks[1].length !== 1) throw new Error('Teal stack should have 1 item'); + if (state.stacks[2].length !== 1) throw new Error('Blue stack should have 1 item'); +}); + +// Test 5: State Management +test('State Management - Cross-Stack Mode Reset', () => { + state = ForthInterpreter.createInitialState(); + + // Start operation + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '42'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Complete operation + state = ForthInterpreter.parseAndExecute(state, '2'); + + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + if (state.crossStackOperation !== null) throw new Error('Should clear operation'); + if (state.crossStackData !== null) throw new Error('Should clear data'); +}); + +console.log(`\n📊 Test Results: ${passCount}/${testCount} tests passed`); +console.log(`🎯 Success Rate: ${((passCount / testCount) * 100).toFixed(1)}%`); + +if (passCount === testCount) { + console.log('\n🎉 All cross-stack operation tests passed!'); +} else { + console.log('\n⚠️ Some tests failed. Please review the implementation.'); +} + +console.log('\n🚀 Cross-stack operations test suite complete!'); diff --git a/forth/foreforthfourth/test-forth.js b/forth/foreforthfourth/test-forth.js new file mode 100644 index 0000000..54f9963 --- /dev/null +++ b/forth/foreforthfourth/test-forth.js @@ -0,0 +1,77 @@ +// Simple test file for the Forth interpreter +// Run with: node test-forth.js + +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Testing 4-Stack Toy Forth Interpreter\n'); + +// Test 1: Basic number pushing +console.log('Test 1: Basic number pushing'); +let state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '5 3 2'); +console.log('Stack 1 after "5 3 2":', state.stacks[0]); +console.log('Expected: [5, 3, 2]\n'); + +// Test 2: Basic arithmetic +console.log('Test 2: Basic arithmetic'); +state = ForthInterpreter.parseAndExecute(state, '+'); +console.log('Stack 1 after "+":', state.stacks[0]); +console.log('Expected: [5, 5] (3+2=5)\n'); + +// Test 3: Stack manipulation +console.log('Test 3: Stack manipulation'); +state = ForthInterpreter.parseAndExecute(state, 'dup over'); +console.log('Stack 1 after "dup over":', state.stacks[0]); +console.log('Expected: [5, 5, 5, 5] (dup then over)\n'); + +// Test 4: Stack inspection +console.log('Test 4: Stack inspection'); +state = ForthInterpreter.parseAndExecute(state, '.s'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: <4> 5 5 5 5\n'); + +// Test 5: Comparison operators +console.log('Test 5: Comparison operators'); +state = ForthInterpreter.parseAndExecute(state, '5 3 >'); +console.log('Stack 1 after "5 3 >":', state.stacks[0]); +console.log('Expected: [5, 5, 5, 5, -1] (5 > 3 = true = -1)\n'); + +// Test 6: Word definition +console.log('Test 6: Word definition'); +state = ForthInterpreter.parseAndExecute(state, ': double dup + ;'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: Word \'double\' defined\n'); + +// Test 7: Using defined word +console.log('Test 7: Using defined word'); +state = ForthInterpreter.parseAndExecute(state, 'double'); +console.log('Stack 1 after "double":', state.stacks[0]); +console.log('Expected: [5, 5, 5, 5, -1, 10] (double of 5 = 10)\n'); + +// Test 8: List all words +console.log('Test 8: List all words'); +state = ForthInterpreter.parseAndExecute(state, 'words'); +console.log('Output:', state.output.slice(-3)); +console.log('Expected: Built-in words, User defined words, Total words count\n'); + +// Test 9: Stack juggling +console.log('Test 9: Stack juggling'); +state = ForthInterpreter.parseAndExecute(state, 'push.teal'); +console.log('Stack 1 after "push.teal":', state.stacks[0]); +console.log('Stack 2 after "push.teal":', state.stacks[1]); +console.log('Expected: Stack 1: [5, 5, 5, 5, -1], Stack 2: [10]\n'); + +// Test 10: Error handling +console.log('Test 10: Error handling'); +state = ForthInterpreter.parseAndExecute(state, 'drop drop drop drop drop drop drop drop drop drop drop'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: Error: Stack underflow on drop\n'); + +console.log('✅ All tests completed!'); +console.log('\nFinal state:'); +console.log('Stack 1:', state.stacks[0]); +console.log('Stack 2:', state.stacks[1]); +console.log('Stack 3:', state.stacks[2]); +console.log('Stack 4:', state.stacks[3]); +console.log('Dictionary size:', state.dictionary.size); +console.log('Output messages:', state.output.length); diff --git a/forth/foreforthfourth/test-help-full.js b/forth/foreforthfourth/test-help-full.js new file mode 100644 index 0000000..bf257ec --- /dev/null +++ b/forth/foreforthfourth/test-help-full.js @@ -0,0 +1,33 @@ +// Test to see the complete help output +const ForthInterpreter = require('./forth.js'); + +console.log('🔍 Testing Complete Help Output\n'); + +let state = ForthInterpreter.createInitialState(); + +// Run help command +console.log('Running help command...'); +state = ForthInterpreter.parseAndExecute(state, 'help'); + +console.log('\n=== COMPLETE HELP OUTPUT ==='); +state.output.forEach((line, i) => { + console.log(`${i + 1}: ${line}`); +}); + +console.log('\n=== ANALYSIS ==='); +console.log('Total output lines:', state.output.length); + +// Count built-in words in help output +const helpLines = state.output.filter(line => line.includes(' - ')); +console.log('Lines with word documentation:', helpLines.length); + +// Check if specific words are present +const expectedWords = ['dup', 'swap', 'drop', '+', '-', '*', '/', 'mod', 'if', 'then', 'begin', 'until']; +expectedWords.forEach(word => { + const found = helpLines.some(line => line.startsWith(word)); + console.log(`${word}: ${found ? '✅' : '❌'}`); +}); + +// Show first few documented words +console.log('\nFirst 10 documented words:'); +helpLines.slice(0, 10).forEach(line => console.log(line)); diff --git a/forth/foreforthfourth/test-help.js b/forth/foreforthfourth/test-help.js new file mode 100644 index 0000000..b3aa28e --- /dev/null +++ b/forth/foreforthfourth/test-help.js @@ -0,0 +1,52 @@ +// Test the new help system +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Testing Help System\n'); + +let state = ForthInterpreter.createInitialState(); + +// Test 1: Basic help command +console.log('Test 1: Basic help command'); +state = ForthInterpreter.parseAndExecute(state, 'help'); +console.log('Help output (first 10 lines):'); +state.output.slice(-10).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 2: Document specific word +console.log('\nTest 2: Document specific word'); +state = ForthInterpreter.parseAndExecute(state, 's" dup"'); +state = ForthInterpreter.parseAndExecute(state, 'doc'); +console.log('Doc output:'); +state.output.slice(-5).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 3: Document another word +console.log('\nTest 3: Document another word'); +state = ForthInterpreter.parseAndExecute(state, 's" +"'); +state = ForthInterpreter.parseAndExecute(state, 'doc'); +console.log('Doc output:'); +state.output.slice(-5).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 4: Document non-existent word +console.log('\nTest 4: Document non-existent word'); +state = ForthInterpreter.parseAndExecute(state, 's" nonexistent"'); +state = ForthInterpreter.parseAndExecute(state, 'doc'); +console.log('Doc output:'); +state.output.slice(-3).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 5: Words command +console.log('\nTest 5: Words command'); +state = ForthInterpreter.parseAndExecute(state, 'words'); +console.log('Words output:'); +state.output.slice(-3).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +console.log('\n✅ Help system tests completed!'); +console.log('Total output lines:', state.output.length); diff --git a/forth/guesser.fth b/forth/guesser.fth new file mode 100644 index 0000000..01d84a0 --- /dev/null +++ b/forth/guesser.fth @@ -0,0 +1,100 @@ +\ --- Number Guessing Game for pForth --- + +\ First, we need some helper words for reliable numeric input. +\ These are based on the pForth tutorial. +( -- $addr ) +: INPUT$ ( A word to get a line of text from the user ) + PAD 1+ ( addr --- , leave room on the scratchpad for a byte count ) + 80 ACCEPT ( addr maxbytes -- numbytes , get up to 80 chars ) + PAD C! ( numbytes --- , store the count in the first byte ) + PAD ( -- $addr , leave the address of the counted string ) +; + +( -- n true | false ) +: INPUT# ( Read a line and convert it to a number ) + INPUT$ ( -- $addr ) + NUMBER? ( $addr -- d_num true | false , convert string to double ) + IF + SWAP DROP TRUE ( d_num true -- n true , drop high part of double ) + ELSE + FALSE ( -- false ) + THEN +; + +\ --- Game Logic --- + +VARIABLE SECRET# \ A place to store the secret number. + +( -- ) +: INIT-SECRET# ( Generate and store a random number ) + 501 CHOOSE ( -- rand , CHOOSE gives a number from 0 to n-1 ) + SECRET# ! ( rand -- , store it in our variable ) +; + +( -- n ) +: GET-GUESS ( Loop until the user enters a valid number ) + BEGIN + CR ." Your guess (0-500)? " + INPUT# ( -- n true | false ) + UNTIL ( loops until flag is true ) +; + +( guess -- correct? ) +: CHECK-GUESS ( Compares guess to the secret number, gives a hint ) + SECRET# @ ( guess -- guess secret# ) + 2DUP = ( guess secret# -- guess secret# flag ) + IF ( guess is equal to secret# ) + ." You got it!" CR + 2DROP TRUE \ --> Make sure this 2DROP is here. + ELSE ( guess is not equal ) + 2DUP < ( guess secret# -- guess secret# flag ) + IF + ." Too low!" CR + 2DROP FALSE \ --> And this one. + ELSE + ." Too high!" CR + 2DROP FALSE \ --> And this one. + THEN + THEN +; + +( -- ) +: SEED-BY-WAITING ( Uses user's reaction time to seed the PRNG ) + CR ." Press any key to begin..." + BEGIN + 501 CHOOSE DROP \ "Burn" a random number from the sequence + ?TERMINAL \ Check if a key has been pressed + UNTIL + CR +; + +( -- ) +: GUESSING-GAME ( The main word to run the game ) + SEED-BY-WAITING \ Call our new interactive seeder + INIT-SECRET# + CR ." I'm thinking of a number between 0 and 500." + CR ." You have 5 chances to guess it." + + FALSE ( -- user-won? , place a 'false' flag on the stack ) + + 5 0 DO ( loop 5 times, from 0 to 4 ) + CR 5 I - . ." guesses left." + GET-GUESS + CHECK-GUESS ( guess -- correct? ) + IF ( a correct guess was made ) + DROP TRUE ( replace the 'user-won?' flag with 'true' ) + LEAVE ( and exit the loop immediately ) + THEN + LOOP + + ( The 'user-won?' flag is now on top of the stack ) + IF + CR ." Congratulations!" CR + ELSE + CR ." Sorry, you ran out of guesses." + CR ." The number was " SECRET# @ . CR + THEN +; + +GUESSING-GAME \ This line executes the game word we just defined. +BYE \ This tells pForth to exit when the game is over. diff --git a/forth/pf_ref.md b/forth/pf_ref.md new file mode 100644 index 0000000..92ebbb6 --- /dev/null +++ b/forth/pf_ref.md @@ -0,0 +1,1213 @@ +::::::::::::: {#container} +:::::: {#header} +::: {#leftheader} +[{width="200" height="100" +border="0"}](/) +::: + +::: {#rightheader} +::: + +::: {#midheader} +# SoftSynth + +## \... music and computers \... +::: + +\ +:::::: + +:::: {#leftside} +::: {#leftside_inner} +- [Home](/index.php) +- [Products](/products.php) +- [JSyn](/jsyn/index.php) +- [Syntona](/syntona/index.php) +- [pForth](/pforth/index.php) +- [Music](/music/index.php) +- [Info](/info/index.php) +- [News](/news/index.php) +- [Links](/links/index.php) +- [Contact Us](/contacts.php) +- [About Us](/aboutus.php) +::: +:::: + +:::: {#rightside} +::: {#rightside_inner} +### Projects + + --------------------------------------------------------------------------------------------------- + [JSyn](/jsyn/) - modular synthesis API for Java. + [JMSL](https://www.algomusic.com/jmsl/){target="_blank"} - Java Music Specification Language + [PortAudio](https://www.portaudio.com/){target="_blank"} - cross platform audio I/O API for \'C\' + --------------------------------------------------------------------------------------------------- +::: +:::: + +::: {#content} +[pForth](/pforth/index.php) + : [GitHub](https://github.com/philburk/pforth/) + \| [Tutorial](/pforth/pf_tut.php) \| [Reference]{.current_link} + \| [Links](/forthlinks.php) + +------------------------------------------------------------------------ + +# pForth Reference Manual + +------------------------------------------------------------------------ + +### pForth - a Portable ANSI style Forth written in ANSI \'C\'. + +### **Last updated: July 21, 2016 V23** + +by Phil Burk with Larry Polansky, David Rosenboom. Special thanks to +contributors Darren Gibbs, Herb Maeder, Gary Arakaki, Mike Haas. + +Back to [pForth Home Page](../pforth) + +## LEGAL NOTICE + +The pForth software code is dedicated to the public domain, and any +third party may reproduce, distribute and modify the pForth software +code or any derivative works thereof without any compensation or +license. The pForth software code is provided on an \"as is\" basis +without any warranty of any kind, including, without limitation, the +implied warranties of merchantability and fitness for a particular +purpose and their equivalents under the laws of any jurisdiction. + +------------------------------------------------------------------------ + +## Table of Contents + +- [What is pForth?](#what-is) +- [Compiling pForth for your System](#Compiling-pForth-System) + - [Description of Source Files](#Description-Files) +- [Running pForth](#Running-pForth) +- [ANSI Compliance](#ANSI-Compliance) +- [pForth Special Features](#pForth-Features) + - [Compiling from a File - INCLUDE](#Compiling-File) + - [Saving Precompiled Dictionaries](#Saving-Dictionaries) + - [Creating Turnkey Applications](#Turnkey-Apps) + - [Recompiling Code - ANEW INCLUDE?](#Recompiling-Code) + - [Customising Forget with \[FORGET\]](#Customising-FORGET) + - [Smart Conditionals](#Smart-Conditionals) + - [Development Tools](#Development-Tools) + - [WORDS.LIKE](#WORDS.LIKE) + - [FILE?](#FILEQ) + - [SEE](#SEE) + - [Single Step Trace and Debug](#single-step-trace) + - [Conditional Compilation - \[IF\] \[ELSE\] + \[THEN\]](#Conditional-Compilation) + - [Miscellaneous Handy Words](#Miscellaneous-Words) + - [Local Variables { foo \-- }](#Local-Variables) + - [\'C\' like Structures. :STRUCT](#C-Structures) + - [Vectorred execution - DEFER](#Vectorred-Execution) + - [Floating Point](#Floating-Point) +- [pForth Design](#pForth-Design) + - [\'C\' kernel](#C-kernel) + - [Dictionary Structures](#Dictionary-Structures) +- [Compiling pForth](#Compiling-pForth) + - [Compiler Options](#Compiler-Options) + - [Building pForth on Supported Hosts](#Building-pForth-Hosts) + - [Compiling for Embedded Systems](#Compiling-Embedded) + - [Linking with Custom \'C\' Functions](#Link-Custom-C) + - [Testing your Compiled pForth](#Testing-pForth) + +------------------------------------------------------------------------ + +## []{#what-is}What is pForth? + +PForth is an ANSI style Forth designed to be portable across many +platforms. The \'P\' in pForth stands for \"Portable\". PForth is based +on a Forth kernel written in ANSI standard \'C\'. + +### What is Forth? + +Forth is a stack based language invented by astronomer Charles Moore for +controlling telescopes. Forth is an interactive language. You can enter +commands at the keyboard and have them be immediately executed, similar +to BASIC or LISP. Forth has a dictionary of words that can be executed +or used to construct new words that are then added to the dictionary. +Forth words operate on a data stack that contains numbers and addresses. + +To learn more about Forth, see the [Forth Tutorial](pf_tut.php). + +### The Origins of pForth + +PForth began as a JSR threaded 68000 Forth called HForth that was used +to support [HMSL](/hmsl/), the Hierarchical Music Specification +Language. HMSL was a music experimentation language developed by Phil +Burk, Larry Polansky and David Rosenboom while working at the Mills +College Center for Contemporary Music. Phil moved from Mills to the 3DO +Company where he ported the Forth kernel to \'C\'. It was used +extensively at 3DO as a tool for verifying ASIC design and for bringing +up new hardware platforms. At 3DO, the Forth had to run on many systems +including SUN, SGI, Macintosh, PC, Amiga, the 3DO ARM based Opera +system, and the 3DO PowerPC based M2 system. + +### pForth Design Goals + +PForth has been designed with portability as the primary design goal. As +a result, pForth avoids any fancy UNIX calls. pForth also avoids using +any clever and original ways of constructing the Forth dictionary. It +just compiles its kernel from ANSI compatible \'C\' code then loads ANS +compatible Forth code to build the dictionary. Very boring but very +likely to work on almost any platform. + +The dictionary files that can be saved from pForth are almost host +independent. They can be compiled on one processor, and then run on +another processor. as long as the endian-ness is the same. In other +words, dictionaries built on a PC will only work on a PC. Dictionaries +built on almost any other computer will work on almost any other +computer. + +PForth can be used to bring up minimal hardware systems that have very +few system services implemented. It is possible to compile pForth for +systems that only support routines to send and receive a single +character. If malloc() and free() are not available, equivalent +functions are available in standard \'C\' code. If file I/O is not +available, the dictionary can be saved as a static data array in \'C\' +source format on a host system. The dictionary in \'C\' source form is +then compiled with a custom pForth kernel to avoid having to read the +dictionary from disk. + +------------------------------------------------------------------------ + +## []{#Compiling-pForth-System}Compiling pForth for your System + +Up-to-date instructions on compiling, possibly with comments from the +community, may be found at: + +> [https://github.com/philburk/pforth/wiki/Compiling-on-Unix](https://github.com/philburk/pforth/wiki/Compiling-on-Unix){target="_blank"} + +The process of building pForth involves several steps. This process is +typically handled automatically by the Makefile or IDE Project. + +1. Compile the \'C\' based pForth kernel called \"pforth\" or + \"pforth.exe\". +2. Execute \"pforth\" with the -i option to build the dictionary from + scratch. Compile the \"system.fth\" file which will add all the top + level Forth words. This can be done in one command by entering + \"pforth -i system.fth\". +3. Save the compiled dictionary as \"pforth.dic\". +4. The next time you run pforth, the precompiled pforth.dic file will + be loaded automatically. + +### Unix and Max OS X + +A Makefile has been provided that should work on most Unix based +platforms. + +1. cd to \"platforms/unix\" folder. +2. Enter: make all +3. Enter: ./pforth + +Note that the platforms folder used to be called build. + +### []{#Description-Files}Description of Source Files + +#### Forth Source in /fth/ + + ansilocs.fth = support for ANSI (LOCAL) word + c_struct.fth = 'C' like data structures + case.fth = CASE OF ENDOF ENDCASE + catch.fth = CATCH and THROW + condcomp.fth = [IF] [ELSE] [THEN] conditional compiler + filefind.fth = FILE? + floats.fth = floating point support + forget.fth = FORGET [FORGET] IF.FORGOTTEN + loadp4th.fth = loads basic dictionary + locals.fth = { } style locals using (LOCAL) + math.fth = misc math words + member.fth = additional 'C' like data structure support + misc1.fth = miscellaneous words + misc2.fth = miscellaneous words + numberio.fth = formatted numeric input/output + private.fth = hide low level words + quit.fth = QUIT EVALUATE INTERPRET in high level + smart_if.fth = allows conditionals outside colon definition + see.fth = Forth "disassembler". Eg. SEE SPACES + strings.fth = string support + system.fth = bootstraps pForth dictionary + trace.fth = single step trace for debugging + +#### \'C\' Source in /csrc/ + + pfcompil.c = pForth compiler support + pfcustom.c = example of 'C' functions callable from pForth + pfinnrfp.h = float extensions to interpreter + pforth.h = include this in app that embeds pForth + pf_cglue.c = glue for pForth calling 'C' + pf_clib.c = replacement routines for 'C' stdlib + pf_core.c = primary words called from 'C' app that embeds pForth + pf_float.h = defines PF_FLOAT, and the floating point math functions such as fp_sin + pf_inner.c = inner interpreter + pf_guts.h = primary include file, define structures + pf_io.c = input/output + pf_main.c = basic application for standalone pForth + pf_mem.c = optional malloc() implementation + pf_save.c = save and load dictionaries + pf_text.c = string tools, error message text + pf_words.c = miscellaneous pForth words implemented + +------------------------------------------------------------------------ + +## []{#Running-pForth}Running pForth + +PForth can be run from a shell or by double clicking on its icon, +depending on the system you are using. The execution options for pForth +are described assuming that you are running it from a shell. + +Usage: + +- pforth [-i] [-dDictionaryFilename] [SourceFilename] + +-i + +Initialize pForth by building dictionary from scratch. Used when +building pForth or when debugging pForth on new systems. + + + +-dDictionaryFilename + +Specify a custom dictionary to be loaded in place of the default +\"pforth.dic\". For example: + +- - pforth -dgame.dic + +SourceFilename + +A Forth source file can be automatically compiled by passing its name to +pForth. This is useful when using Forth as an assembler or for automated +hardware testing. Remember that the source file can compile code and +execute it all in the same file. + +#### Quick Verification of pForth + +To verify that PForth is working, enter: + +- 3 4 + . + +It should print \"7 ok\". Now enter: + +- WORDS + +You should see a long list of all the words in the pForth dictionary. +Don\'t worry. You won\'t need to learn all of these. More tests are +described in the README.txt file. + +If you want to learn how to program in Forth, try our +[tutorial](pf_tut.php). + +------------------------------------------------------------------------ + +## []{#ANSI-Compliance}ANSI Compliance + +This Forth is intended to be ANS compatible. I will not claim that it is +compatible until more people bang on it. If you find areas where it +deviates from the standard, please let me know. + +Word sets supported include: + +- FLOAT +- LOCAL with support for { lv1 lv2 \| lv3 \-- } style locals +- EXCEPTION but standard throw codes not implemented +- FILE ACCESS +- MEMORY ALLOCATION + +Here are the areas that I know are not compatible: + +The ENVIRONMENT queries are not implemented. + +Word sets NOT supported include: + +- BLOCK - a matter of religion +- SEARCH ORDER +- PROGRAMMING TOOLS - only has .S ? DUMP WORDS BYE +- STRING - only has CMOVE CMOVE\> COMPARE +- DOUBLE NUMBER - but cell is 32 bits + +------------------------------------------------------------------------ + +## []{#pForth-Features}pForth Special Features + +These features are not part of the ANS standard for Forth. They have +been added to assist developers. + +### []{#Compiling-File}Compiling from a File + +Use INCLUDE to compile source code from a file: + +- INCLUDE filename + +You can nest calls to INCLUDE. INCLUDE simply redirects Forth to takes +its input from the file instead of the keyboard so you can place any +legal Forth code in the source code file. + +### []{#Saving-Dictionaries}Saving Precompiled Dictionaries + +Use SAVE-FORTH save your precompiled code to a file. To save the current +dictionary to a file called \"custom.dic\", enter: + +- c" custom.dic" SAVE-FORTH + +You can then leave pForth and use your custom dictionary by entering: + +- pforth -dcustom.dic + +On icon based systems, you may wish to name your custom dictionary +\"pforth.dic\" so that it will be loaded automatically. + +Be careful that you do not leave absolute addresses stored in the +dictionary because they will not work when you reload pForth at a +different address. Use A! to store an address in a variable in a +relocatable form and A@ to get it back if you need to. + +- VARIABLE DATA-PTR + CREATE DATA 100 ALLOT + DATA DATA-PTR ! \ storing absolute address! BAD + DATA DATA-PTR A! \ storing relocatable address! GOOD + DATA-PTR A@ \ fetch relocatable address + +### []{#Turnkey-Apps}Creating Turnkey Applications + +Use TURNKEY to save a dictionary with a word that will run +automatically. The headers (names) will be discarded to save space in +the dictionary. Suppose you have defined a word called MYAPP to prints +the ASCII code when you press a key on the keyboard. + +- : MYAPP ( -- , print key codes ) + BEGIN ." #" key dup ascii q = not + WHILE . cr REPEAT ; + +Save a dictionary named \"turnkey.dic\" that will run MYAPP. Other names +are OK. + +- c" turnkey.dic" ' MYAPP TURNKEY + +Run the app. Press some letters to see the code. Then press \'q\' to +exit. + +- ./pforth -dturnkey.dic + +### []{#Recompiling-Code}Recompiling Code - ANEW INCLUDE? + +When you are testing a file full of code, you will probably recompile +many times. You will probably want to FORGET the old code before loading +the new code. You could put a line at the beginning of your file like +this: + +- FORGET XXXX-MINE : XXXX-MINE ; + +This would automatically FORGET for you every time you load. +Unfortunately, you must define XXXX-MINE before you can ever load this +file. We have a word that will automatically define a word for you the +first time, then FORGET and redefine it each time after that. It is +called ANEW and can be found at the beginning of most Forth source +files. We use a prefix of TASK- followed by the filename just to be +consistent. This TASK-name word is handy when working with INCLUDE? as +well. Here is an example: + +- \ Start of file + INCLUDE? TASK-MYTHING.FTH MYTHING.FTH + ANEW TASK-THISFILE.FTH + \ the rest of the file follows... + +Notice that the INCLUDE? comes before the call to ANEW so that we don\'t +FORGET MYTHING.FTH every time we recompile. + +FORGET allows you to get rid of code that you have already compiled. +This is an unusual feature in a programming language. It is very +convenient in Forth but can cause problems. Most problems with FORGET +involve leaving addresses that point to the forgotten code that are not +themselves forgotten. This can occur if you set a deferred system word +to your word then FORGET your word. The system word which is below your +word in the dictionary is pointing up to code that no longer exists. It +will probably crash if called. (See discussion of DEFER below.) Another +problem is if your code allocates memory, opens files, or opens windows. +If your code is forgotten you may have no way to free or close these +thing. You could also have a problems if you add addresses from your +code to a table that is below your code. This might be a jump table or +data table. + +Since this is a common problem we have provided a tool for handling it. +If you have some code that you know could potentially cause a problem if +forgotten, then write a cleanup word that will eliminate the problem. +This word could UNdefer words, free memory, etc. Then tell the system to +call this word if the code is forgotten. Here is how: + +- : MY.CLEANUP ( -- , do whatever ) + MY-MEM @ FREE DROP + 0 MY-MEM ! + ; + IF.FORGOTTEN MY.CLEANUP + +IF.FORGOTTEN creates a linked list node containing your CFA that is +checked by FORGET. Any nodes that end up above HERE (the Forth pointer +to the top of the dictionary) after FORGET is done are executed. + +### []{#Customising-FORGET}Customising FORGET with \[FORGET\] + +Sometimes, you may need to extend the way that FORGET works. FORGET is +not deferred, however, because that could cause some real problems. +Instead, you can define a new version of \[FORGET\] which is searched +for and executed by FORGET. You MUST call \[FORGET\] from your program +or FORGET will not actually FORGET. Here is an example. + +- : [FORGET] ( -- , my version ) + ." Change things around!" CR + [FORGET] ( must be called ) + ." Now put them back!" CR + ; + : FOO ." Hello!" ; + FORGET FOO ( Will print "Change things around!", etc.) + +This is recommended over redefining FORGET because words like ANEW that +call FORGET will now pick up your changes. + +### []{#Smart-Conditionals}Smart Conditionals + +In pForth, you can use IF THEN DO LOOP and other conditionals outside of +colon definitions. PForth will switch temporarily into the compile +state, then automatically execute the conditional code. (Thank you Mitch +Bradley) For example, just enter this at the keyboard. + +- 10 0 DO I . LOOP + +### []{#Development-Tools}Development Tools + +#### []{#WORDS.LIKE}WORDS.LIKE + +If you cannot remember the exact name of a word, you can use WORDS.LIKE +to search the dictionary for all words that contain a substring. For an +example, enter: + +- WORDS.LIKE FOR + WORDS.LIKE EMIT + +#### []{#FILEQ}FILE? + +You can use FILE? to find out what file a word was compiled from. If a +word was defined in multiple files then it will list each file. The +execution token of each definition of the word is listed on the same +line. + +- FILE? IF + FILE? AUTO.INIT + +#### []{#SEE}SEE + +You can use SEE to \"disassemble\" a word in the pForth dictionary. SEE +will attempt to print out Forth source in a form that is similar to the +source code. SEE will give you some idea of how the word was defined but +is not perfect. Certain compiler words, like BEGIN and LITERAL, are +difficult to disassemble and may not print properly. For an example, +enter: + +- SEE SPACES + SEE WORDS + +#### []{#single-step-trace}Single Step Trace and Debug + +It is often useful to proceed step by step through your code when +debugging. PForth provides a simple single step trace facility for this +purpose. Here is an example of using TRACE to debug a simple program. +Enter the following program:\ + + +- : SQUARE ( n -- n**2 ) + DUP * + ; + : TSQ ( n -- , test square ) + ." Square of " DUP . + ." is " SQUARE . CR + ; + +Even though this program should work, let\'s pretend it doesn\'t and try +to debug it. Enter: + +- 7 TRACE TSQ + +You should see: + +- 7 trace tsq + << TSQ +0 <10:1> 7 || (.") Square of " >> ok + +The \"TSQ +0\" means that you are about to execute code at an offset of +\"+0\" from the beginning of TSQ. The \<10:1\> means that we are in +base 10, and that there is 1 item on the stack, which is shown to be +\"7\". The (.\") is the word that is about to be executed. (.\") is the +word that is compiled when use use .\". Now to single step, enter: + +- s + +You should see: + +- Square of + << TSQ +16 <10:1> 7 || DUP >> ok + +The \"Square os\" was printed by (.\"). We can step multiple times using +the \"sm\" command. Enter: + +- 3 sm + +You should see: + +- << TSQ +20 <10:2> 7 7 || . >> 7 + << TSQ +24 <10:1> 7 || (.") is " >> is + << TSQ +32 <10:1> 7 || SQUARE >> ok + +The \"7\" after the \"\>\>\" was printed by the . word. If we entered +\"s\", we would step over the SQUARE word. If we want to dive down into +SQUARE, we can enter: + +- sd + +You should see: + +- << SQUARE +0 <10:1> 7 || DUP >> ok + +To step once in SQUARE, enter: + +- s + +You should see: + +- << SQUARE +4 <10:2> 7 7 || * >> ok + +To go to the end of the current word, enter: + +- g + +You should see: + +- << SQUARE +8 <10:1> 49 || EXIT >> + << TSQ +36 <10:1> 49 || . >> ok + +EXIT is compiled at the end of every Forth word. For more information on +TRACE, enter TRACE.HELP: + +- TRACE ( i*x <name> -- , setup trace for Forth word ) + S ( -- , step over ) + SM ( many -- , step over many times ) + SD ( -- , step down ) + G ( -- , go to end of word ) + GD ( n -- , go down N levels from current level, + stop at end of this level ) + +### []{#Conditional-Compilation}Conditional Compilation \[IF\] \[ELSE\] \[THEN\] + +PForth supports conditional compilation words similar to \'C\'\'s #if, +#else, and #endif. + +\[IF\] ( flag \-- , if true, skip to \[ELSE\] or \[THEN\] ) + +\[ELSE\] ( \-- , skip to \[THEN\] ) + +\[THEN\] ( \-- , noop, used to terminate \[IF\] and \[ELSE\] section ) + +For example: + +- TRUE constant USE_FRENCH + + USE_FRENCH [IF] + : WELCOME ." Bienvenue!" cr ; + [ELSE] + : WELCOME ." Welcome!" cr ; + [THEN] + +Here is how to conditionally compile within a colon definition by using +\[ and \]. + +- : DOIT ( -- ) + START.REACTOR + IF + [ USE_FRENCH [IF] ] ." Zut alors!" + [ [ELSE] ] ." Uh oh!" + [THEN] + THEN cr + ; + +### []{#Miscellaneous-Words}Miscellaneous Handy Words + +.HEX ( n \-- , print N as hex number ) + +CHOOSE ( n \-- rand , select random number between 0 and N-1 ) + +MAP ( \-- , print dictionary information ) + +### []{#Local-Variables}Local Variables { foo \--} + +In a complicated Forth word it is sometimes hard to keep track of where +things are on the stack. If you find you are doing a lot of stack +operations like DUP SWAP ROT PICK etc. then you may want to use local +variables. They can greatly simplify your code. You can declare local +variables for a word using a syntax similar to the stack diagram. These +variables will only be accessible within that word. Thus they are +\"local\" as opposed to \"global\" like regular variables. Local +variables are self-fetching. They automatically put their values on the +stack when you give their name. You don\'t need to @ the contents. Local +variables do not take up space in the dictionary. They reside on the +return stack where space is made for them as needed. Words written with +them can be reentrant and recursive. + +Consider a word that calculates the difference of two squares, Here are +two ways of writing the same word. + +- : DIFF.SQUARES ( A B -- A*A-B*B ) + DUP * + SWAP DUP * + SWAP - + ; + ( or ) + : DIFF.SQUARES { A B -- A*A-B*B } + A A * + B B * - + ; + 3 2 DIFF.SQUARES ( would return 5 ) + +In the second definition of DIFF.SQUARES the curly bracket \'{\' told +the compiler to start declaring local variables. Two locals were +defined, A and B. The names could be as long as regular Forth words if +desired. The \"\--\" marked the end of the local variable list. When the +word is executed, the values will automatically be pulled from the stack +and placed in the local variables. When a local variable is executed it +places its value on the stack instead of its address. This is called +self-fetching. Since there is no address, you may wonder how you can +store into a local variable. There is a special operator for local +variables that does a store. It looks like -\> and is pronounced \"to\". + +Local variables need not be passed on the stack. You can declare a local +variable by placing it after a \"vertical bar\" ( \| )character. These +are automatically set to zero when created. Here is a simple example +that uses -\> and \| in a word: + +- : SHOW2* + { loc1 | unvar -- , 1 regular, 1 uninitialized } + LOC1 2* -> UNVAR + (set unver to 2*LOC1 ) + UNVAR . ( print UNVAR ) + ; + 3 SHOW2* ( pass only 1 parameter, prints 6 ) + +Since local variable often used as counters or accumulators, we have a +special operator for adding to a local variable It is +-\> which is +pronounced \"plus to\". These next two lines are functionally equivalent +but the second line is faster and smaller: + +- ACCUM 10 + -> ACCUM + 10 +-> ACCUM + +If you name a local variable the same as a Forth word in the dictionary, +eg. INDEX or COUNT, you will be given a warning message. The local +variable will still work but one could easily get confused so we warn +you about this. Other errors that can occur include, missing a closing +\'}\', missing \'\--\', or having too many local variables. + +### []{#C-Structures}\'C\' like Structures. :STRUCT + +You can define \'C\' like data structures in pForth using :STRUCT. For +example: + +- :STRUCT SONG + LONG SONG_NUMNOTES \ define 32 bit structure member named SONG_NUMNOTES + SHORT SONG_SECONDS \ define 16 bit structure member + BYTE SONG_QUALITY \ define 8 bit member + LONG SONG_NUMBYTES \ auto aligns after SHORT or BYTE + RPTR SONG_DATA \ relocatable pointer to data + ;STRUCT + + SONG HAPPY \ define a song structure called happy + + 400 HAPPY S! SONG_NUMNOTES \ set number of notes to 400 + 17 HAPPY S! SONG_SECONDS \ S! works with all size members + + CREATE SONG-DATA 23 , 17 , 19 , 27 , + SONG-DATA HAPPY S! SONG_DATA \ store pointer in relocatable form + + HAPPY DST SONG \ dump HAPPY as a SONG structure + + HAPPY S@ SONG_NUMNOTES . \ fetch numnotes and print + +See the file \"c_struct.fth\" for more information. + +### []{#Vectorred-Execution}Vectorred Execution - DEFER + +Using DEFER for vectored words. In Forth and other languages you can +save the address of a function in a variable. You can later fetch from +that variable and execute the function it points to.This is called +vectored execution. PForth provides a tool that simplifies this process. +You can define a word using DEFER. This word will contain the execution +token of another Forth function. When you execute the deferred word, it +will execute the function it points to. By changing the contents of this +deferred word, you can change what it will do. There are several words +that support this process. + +DEFER ( \<name\> \-- , define a deferred word ) + +IS ( CFA \<name\> \-- , set the function for a deferred word ) + +WHAT\'S ( \<name\> \-- CFA , return the CFA set by IS ) + +Simple way to see the name of what\'s in a deferred word: + +- - WHAT'S EMIT >NAME ID. + +should print name of current word that\'s in EMIT. + +Here is an example that uses a deferred word. + +- DEFER PRINTIT + ' . IS PRINTIT ( make PRINTIT use . ) + 8 3 + PRINTIT + + : COUNTUP ( -- , call deferred word ) + ." Hit RETURN to stop!" CR + 0 ( first value ) + BEGIN 1+ DUP PRINTIT CR + ?TERMINAL + UNTIL + ; + COUNTUP ( uses simple . ) + + : FANCY.PRINT ( N -- , print in DECIMAL and HEX) + DUP ." DECIMAL = " . + ." , HEX = " .HEX + ; + ' FANCY.PRINT IS PRINTIT ( change printit ) + WHAT'S PRINTIT >NAME ID. ( shows use of WHAT'S ) + 8 3 + PRINTIT + COUNTUP ( notice that it now uses FANCY.PRINT ) + +Many words in the system have been defined using DEFER which means that +we can change how they work without recompiling the entire system. Here +is a partial list of those words + +- ABORT EMIT NUMBER? + +#### Potential Problems with Defer + +Deferred words are very handy to use, however, you must be careful with +them. One problem that can occur is if you initialize a deferred system +more than once. In the below example, suppose we called STUTTER twice. +The first time we would save the original EMIT vector in OLD-EMIT and +put in a new one. The second time we called it we would take our new +function from EMIT and save it in OLD-EMIT overwriting what we had saved +previously. Thus we would lose the original vector for EMIT . You can +avoid this if you check to see whether you have already done the defer. +Here\'s an example of this technique. + +- DEFER OLD-EMIT + ' QUIT IS OLD-EMIT ( set to known value ) + : EEMMIITT ( char --- , our fun EMIT ) + DUP OLD-EMIT OLD-EMIT + ; + : STUTTER ( --- ) + WHAT'S OLD-EMIT 'C QUIT = ( still the same? ) + IF ( this must be the first time ) + WHAT'S EMIT ( get the current value of EMIT ) + IS OLD-EMIT ( save this value in OLD-EMIT ) + 'C EEMMIITT IS EMIT + ELSE ." Attempt to STUTTER twice!" CR + THEN + ; + : STOP-IT! ( --- ) + WHAT'S OLD-EMIT ' QUIT = + IF ." STUTTER not installed!" CR + + ELSE WHAT'S OLD-EMIT IS EMIT + 'C QUIT IS OLD-EMIT + ( reset to show termination ) + THEN + ; + +In the above example, we could call STUTTER or STOP-IT! as many times as +we want and still be safe. + +Suppose you forget your word that EMIT now calls. As you compile new +code you will overwrite the code that EMIT calls and it will crash +miserably. You must reset any deferred words that call your code before +you FORGET your code. The easiest way to do this is to use the word +IF.FORGOTTEN to specify a cleanup word to be called if you ever FORGET +the code in question. In the above example using EMIT , we could have +said: + +- IF.FORGOTTEN STOP-IT! + +### []{#Floating-Point}Floating Point + +PForth supports the FLOAT word set and much of the FLOATEXT word set as +a compile time option. You can select single or double precision as the +default by changing the typedef of PF_FLOAT. + +PForth has several options for floating point output. + +FS. ( r -f- , prints in scientific/exponential format ) + +FE. ( r -f- , prints in engineering format, exponent if multiple of 3 ) + +FG. ( r -f- , prints in normal or exponential format depending on size ) + +F. ( r -f- , as defined by the standard ) + + + +Here is an example of output from each word for a number ranging from +large to very small. + + FS. FE. FG. F. + 1.234000e+12 1.234000e+12 1.234e+12 1234000000000. + 1.234000e+11 123.4000e+09 1.234e+11 123400000000. + 1.234000e+10 12.34000e+09 1.234e+10 12340000000. + 1.234000e+09 1.234000e+09 1.234e+09 1234000000. + 1.234000e+08 123.4000e+06 1.234e+08 123400000. + 1.234000e+07 12.34000e+06 1.234e+07 12340000. + 1.234000e+06 1.234000e+06 1234000. 1234000. + 1.234000e+05 123.4000e+03 123400. 123400.0 + 1.234000e+04 12.34000e+03 12340. 12340.00 + 1.234000e+03 1.234000e+03 1234. 1234.000 + 1.234000e+02 123.4000e+00 123.4 123.4000 + 1.234000e+01 12.34000e+00 12.34 12.34000 + 1.234000e+00 1.234000e+00 1.234 1.234000 + 1.234000e-01 123.4000e-03 0.1234 0.1234000 + 1.234000e-02 12.34000e-03 0.01234 0.0123400 + 1.234000e-03 1.234000e-03 0.001234 0.0012340 + 1.234000e-04 123.4000e-06 0.0001234 0.0001234 + 1.234000e-05 12.34000e-06 1.234e-05 0.0000123 + 1.234000e-06 1.234000e-06 1.234e-06 0.0000012 + 1.234000e-07 123.4000e-09 1.234e-07 0.0000001 + 1.234000e-08 12.34000e-09 1.234e-08 0.0000000 + 1.234000e-09 1.234000e-09 1.234e-09 0.0000000 + 1.234000e-10 123.4000e-12 1.234e-10 0.0000000 + 1.234000e-11 12.34000e-12 1.234e-11 0.0000000 + + 1.234568e+12 1.234568e+12 1.234568e+12 1234567890000. + 1.234568e+11 123.4568e+09 1.234568e+11 123456789000. + 1.234568e+10 12.34568e+09 1.234568e+10 12345678900. + 1.234568e+09 1.234568e+09 1.234568e+09 1234567890. + 1.234568e+08 123.4568e+06 1.234568e+08 123456789. + 1.234568e+07 12.34568e+06 1.234568e+07 12345679. + 1.234568e+06 1.234568e+06 1234568. 1234568. + 1.234568e+05 123.4568e+03 123456.8 123456.8 + 1.234568e+04 12.34568e+03 12345.68 12345.68 + 1.234568e+03 1.234568e+03 1234.568 1234.568 + 1.234568e+02 123.4568e+00 123.4568 123.4568 + 1.234568e+01 12.34568e+00 12.34568 12.34568 + 1.234568e+00 1.234568e+00 1.234568 1.234568 + 1.234568e-01 123.4568e-03 0.1234568 0.1234568 + 1.234568e-02 12.34568e-03 0.01234568 0.0123456 + 1.234568e-03 1.234568e-03 0.001234568 0.0012345 + 1.234568e-04 123.4568e-06 0.0001234568 0.0001234 + 1.234568e-05 12.34568e-06 1.234568e-05 0.0000123 + 1.234568e-06 1.234568e-06 1.234568e-06 0.0000012 + 1.234568e-07 123.4568e-09 1.234568e-07 0.0000001 + 1.234568e-08 12.34568e-09 1.234568e-08 0.0000000 + 1.234568e-09 1.234568e-09 1.234568e-09 0.0000000 + 1.234568e-10 123.4568e-12 1.234568e-10 0.0000000 + 1.234568e-11 12.34568e-12 1.234568e-11 0.0000000 + +## []{#pForth-Design}pForth Design + +### []{#C-kernel}\'C\' kernel + +The pForth kernel is written in \'C\' for portability. The inner +interpreter is implemented in the function ExecuteToken() which is in +pf_inner.c. + +- void pfExecuteToken( ExecToken XT ); + +It is passed an execution token the same as EXECUTE would accept. It +handles threading of secondaries and also has a large switch() case +statement to interpret primitives. It is in one huge routine to take +advantage of register variables, and to reduce calling overhead. +Hopefully, your compiler will optimise the switch() statement into a +jump table so it will run fast. + +### []{#Dictionary-Structures}Dictionary Structures + +This Forth supports multiple dictionaries. Each dictionary consists of a +header segment and a seperate code segment. The header segment contains +link fields and names. The code segment contains tokens and data. The +headers, as well as some entire dictionaries such as the compiler +support words, can be discarded when creating a stand-alone app. + +\[NOT IMPLEMENTED\] Dictionaries can be split so that the compile time +words can be placed above the main dictionary. Thus they can use the +same relative addressing but be discarded when turnkeying. + +Execution tokens are either an index of a primitive ( n \< +NUM_PRIMITIVES), or the offset of a secondary in the code segment. ( n +\>= NUM_PRIMITIVES ) + +The NAME HEADER portion of the dictionary contains a structure for each +named word in the dictionary. It contains the following fields: + +- bytes + 4 Link Field = relative address of previous name header + 4 Code Pointer = relative address of corresponding code + n Name Field = name as counted string Headers are quad byte aligned. + +The CODE portion of the dictionary consists of the following structures: + +#### Primitive + +No Forth code. \'C\' code in \"pf_inner.c\". + +#### Secondary + +- 4*n Parameter Field containing execution tokens + 4 ID_NEXT = 0 terminates secondary + +#### CREATE DOES\> + +- 4 ID_CREATE_P token + 4 Token for optional DOES> code, OR ID_NEXT = 0 + 4 ID_NEXT = 0 + n Body = arbitrary data + +#### Deferred Word + +- 4 ID_DEFER_P same action as ID_NOOP, identifies deferred words + 4 Execution Token of word to execute. + 4 ID_NEXT = 0 + +#### Call to custom \'C\' function. + +- 4 ID_CALL_C + 4 Pack C Call Info Bits + + - 0-15 = Function Index Bits + 16-23 = FunctionTable Index (Unused) Bits + 24-30 = NumParams Bit + 31 = 1 if function returns value + + <!-- --> + + 4 ID_NEXT = 0 + +------------------------------------------------------------------------ + +## []{#Compiling-pForth}Compiling pForth + +A makefile is supplied that will help you compile pForth for your +environment. You can customize the build by setting various compiler +options. + +### []{#Compiler-Options}Compiler Options + +There are several versions of PForth that can be built. By default, the +full kernel will be built. For custom builds, define the following +options in the Makefile before compiling the \'C\' code: + +PF_DEFAULT_DICTIONARY=\"filename\" + +> Specify a dictionary to use in place of the default \"pforth.dic\", +> for example \"/usr/lib/pforth/pforth.dic\". + +PF_NO_INIT + +- Don\'t compile the code used to initially build the dictionary. This + can be used to save space if you already have a prebuilt dictionary. + +PF_NO_SHELL + +- Don\'t compile the outer interpreter and Forth compiler. This can be + used with Cloned dictionaries. + +PF_NO_MALLOC + +- Replace malloc() and free() function with pForth\'s own version. See + pf_mem.c for more details. + +PF_USER_MALLOC=\'\"filename.h\"\' + +- Replace malloc() and free() function with users custom version. See + pf_mem.h for details. + +PF_MEM_POOL_SIZE=numbytes + +- Size of array in bytes used by pForth custom allocator. + +PF_NO_GLOBAL_INIT + +- Define this if you want pForth to not rely on initialization of global + variables by the loader. This may be required for some embedded + systems that may not have a fully functioning loader. Take a look in + \"pfcustom.c\" for an example of its use. + +PF_USER_INC1=\'\"filename.h\"\' + +- File to include BEFORE other include files. Generally set to host + dependent files such as \"pf_mac.h\". + +PF_USER_INC2=\'\"filename.h\"\' + +- File to include AFTER other include files. Generally used to #undef + and re#define symbols. See \"pf_win32.h\" for an example. + +PF_NO_CLIB + +- Replace \'C\' lib calls like toupper and memcpy with pForth\'s own + version. This is useful for embedded systems. + +PF_USER_CLIB=\'\"filename.h\"\' + +- Rreplace \'C\' lib calls like toupper and memcpy with users custom + version. See pf_clib.h for details. + +PF_NO_FILEIO + +- System does not support standard file I/O so stub it out. Setting this + flag will automatically set PF_STATIC_DIC. + +PF_USER_CHARIO=\'\"filename.h\"\' + +- Replace stdio terminal calls like getchar() and putchar() with users + custom version. See pf_io.h for details. + +PF_USER_FILEIO=\'\"filename.h\"\' + +- Replace stdio file calls like fopen and fread with users custom + version. See pf_io.h for details. + +PF_USER_FLOAT=\'\"filename.h\"\' + +- Replace floating point math calls like sin and pow with users custom + version. Also defines PF_FLOAT. + +PF_USER_INIT=MyInit() + +- Call a user defined initialization function that returns a negative + error code if it fails. + +PF_USER_TERM=MyTerm() + +- Call a user defined void termination function. + +PF_STATIC_DIC + +- Compile in static dictionary instead of loading dictionary. from file. + Use \"utils/savedicd.fth\" to save a dictionary as \'C\' source code + in a file called \"pfdicdat.h\". + +PF_SUPPORT_FP + +- Compile ANSI floating point support. + +### []{#Building-pForth-Hosts}Building pForth on Supported Hosts + +To build on UNIX, do nothing, system will default to \"pf_unix.h\". + +To build on Macintosh: + +- -DPF_USER_INC1='"pf_mac.h"' + +To build on PCs: + +- -DPF_USER_INC2='"pf_win32.h"' + +To build a system that only runs turnkey or cloned binaries: + +- -DPF_NO_INIT -DPF_NO_SHELL + +### []{#Compiling-Embedded}Compiling for Embedded Systems + +You may want to create a version of pForth that can be run on a small +system that does not support file I/O. This is useful when bringing up +new computer systems. On UNIX systems, you can use the supplied gmake +target. Simply enter: + +- gmake pfemb + +For other systems, here are the steps to create an embedded pForth. + +1. Determine whether your target system has a different endian-ness + than your host system. If the address of a long word is the address + of the most significant byte, then it is \"big endian\". Examples of + big endian processors are Sparc, Motorola 680x0 and PowerPC60x. If + the address of a long word is the address of the least significant + byte, then it is \"Little Endian\". Examples of little endian + processors are Intel 8088 and derivatives such as the Intel Pentium, + X86. ARM processors can be configured as either big or little + endian. +2. If your target system has a different endian-ness than your host + system, then you must compile a version of pForth for your host that + matches the target. Rebuild pForth with either PF_BIG_ENDIAN_DIC or + PF_LITTLE_ENDIAN_DIC defined. You will need to rebuild pforth.dic + as well as the executable Forth. If you do not specify one of these + variables, then the dictionary will match the native endian-ness of + the host processor. +3. Execute pForth. Notice the message regarding the endian-ness of the + dictionary. +4. Compile your custom Forth words on the host development system. +5. Compile the pForth utulity \"utils/savedicd.fth\". +6. Enter in pForth: SDAD +7. SDAD will generate a file called \"pfdicdat.h\" that contains your + dictionary in source code form. +8. Rewrite the character primitives sdTerminalOut(), sdTerminalIn() and + sdTerminalFlush() defined in pf_io.h to use your new computers + communications port. +9. Write a \"user_chario.h\" file based on the API defined in + \"pf_io.h\". +10. Compile a new version of pForth for your target machine with the + following options: + 1. -DPF_NO_INIT -DPF_NO_MALLOC -DPF_NO_FILEIO \ + -DPF_USER_CHARIO="user_chario.h" \ + -DPF_NO_CLIB -DPF_STATIC_DIC +11. The file \"pfdicdat.h\" will be compiled into this executable and + your dictionary will thus be included in the pForth executable as a + static array. +12. Burn a ROM with your new pForth and run it on your target machine. +13. If you compiled a version of pForth with different endian-ness than + your host system, do not use it for daily operation because it will + be much slower than a native version. + +### []{#Link-Custom-C}Linking with Custom \'C\' Functions + +You can call the pForth interpreter as an embedded tool in a \'C\' +application. For an example of this, see the file pf_main.c. This +application does nothing but load the dictionary and call the pForth +interpreter. + +You can call \'C\' from pForth by adding your own custom \'C\' functions +to a dispatch table, and then adding Forth words to the dictionary that +call those functions. See the file \"pfcustom.c\" for more information. + +### []{#Testing-pForth}Testing your Compiled pForth + +Once you have compiled pForth, you can test it using the small +verification suite we provide. The first test you should run was +written by John Hayes at John Hopkins University. Enter: + +- pforth + include tester.fth + include coretest.fth + bye + +The output will be self explanatory. There are also a number of tests +that I have added that print the number of successes and failures. +Enter: + +- pforth t_corex.fth + pforth t_locals.fth + pforth t_strings.fth + pforth t_floats.ft + +Note that t_corex.fth reveals an expected error because SAVE-INPUT is +not fully implemented. (FIXME)\ + +------------------------------------------------------------------------ + +\ +PForth source code is freely available and is in the public domain. + +Back to [pForth Home Page](../pforth)\ +::: + +::: {#footer} +\(C\) 1997-2015 Mobileer Inc - All Rights Reserved - [Contact +Us](/contacts.php) +::: +::::::::::::: diff --git a/forth/pf_tut.md b/forth/pf_tut.md new file mode 100644 index 0000000..5b4d608 --- /dev/null +++ b/forth/pf_tut.md @@ -0,0 +1,1345 @@ +::::::::::::: {#container} +:::::: {#header} +::: {#leftheader} +[{width="200" height="100" +border="0"}](/) +::: + +::: {#rightheader} +::: + +::: {#midheader} +# SoftSynth + +## \... music and computers \... +::: + +\ +:::::: + +:::: {#leftside} +::: {#leftside_inner} +- [Home](/index.php) +- [Products](/products.php) +- [JSyn](/jsyn/index.php) +- [Syntona](/syntona/index.php) +- [pForth](/pforth/index.php) +- [Music](/music/index.php) +- [Info](/info/index.php) +- [News](/news/index.php) +- [Links](/links/index.php) +- [Contact Us](/contacts.php) +- [About Us](/aboutus.php) +::: +:::: + +:::: {#rightside} +::: {#rightside_inner} +### Projects + + --------------------------------------------------------------------------------------------------- + [JSyn](/jsyn/) - modular synthesis API for Java. + [JMSL](https://www.algomusic.com/jmsl/){target="_blank"} - Java Music Specification Language + [PortAudio](https://www.portaudio.com/){target="_blank"} - cross platform audio I/O API for \'C\' + --------------------------------------------------------------------------------------------------- +::: +:::: + +::: {#content} +[pForth](/pforth/index.php) + : [GitHub](https://github.com/philburk/pforth/) + \| [Tutorial]{.current_link} \| [Reference](/pforth/pf_ref.php) + \| [Links](/forthlinks.php) + +------------------------------------------------------------------------ + +# Forth Tutorial + +------------------------------------------------------------------------ + +Translations: +[Chinese](http://vision.twbbs.org/%7Eletoh/forth/pf_tuttw.html){target="_blank"} +by +[Letoh](http://vision.twbbs.org/%7Eletoh/blog/?page_id=169){target="_blank"} + +by [Phil Burk](http://www.softsynth.com/philburk.html) of +[SoftSynth.com](http://www.softsynth.com) + +## Table of Contents + +- [Forth Syntax](#Forth%20Syntax) +- [Stack Manipulation](#The%20Stack) +- [Arithmetic](#Arithmetic) +- [Defining a New Word](#Defining%20a%20New%20Word) +- [More Arithmetic](#More%20Arithmetic) + - [Arithmetic Overflow](#Arithmetic%20Overflow) + - [Convert Algebraic Expressions to + Forth](#Convert%20Algebraic%20Expressions%20to%20Forth) +- [Character Input and Output](#Character%20Input%20and%20Output) +- [Compiling from Files](#Compiling%20from%20Files) +- [Variables](#Variables) +- [Constants](#Constants) +- [Logical Operators](#Logical%20Operators) +- [Conditionals - IF ELSE THEN + CASE](#Conditionals%20-%20IF%20ELSE%20THEN%20CASE) +- [Loops](#Loops) +- [Text Input and Output](#Text%20Input%20and%20Output) +- [Changing Numeric Base](#Changing%20Numeric%20Base) +- [Answers to Problems](#Answers%20to%20Problems) + +The intent of this tutorial is to provide a series of experiments that +will introduce you to the major concepts of Forth. It is only a starting +point. Feel free to deviate from the sequences I provide. A free form +investigation that is based on your curiosity is probably the best way +to learn any language. Forth is especially well adapted to this type of +learning. + +This tutorial is written for the PForth implementation of the ANS Forth +standard. I have tried to restrict this tutorial to words that are part +of the ANS standard but some PForth specific words may have crept in. + +In the tutorials, I will print the things you need to type in upper +case, and indent them. You can enter them in upper or lower case. At the +end of each line, press the RETURN (or ENTER) key; this causes Forth to +interpret what you\'ve entered. + +## []{#Forth Syntax}Forth Syntax + +Forth has one of the simplest syntaxes of any computer language. The +syntax can be stated as follows, \"**Forth code is a bunch of words with +spaces between them.**\" This is even simpler than English! Each *word* +is equivalent to a function or subroutine in a language like \'C\'. They +are executed in the order they appear in the code. The following +statement, for example, could appear in a Forth program: + +- WAKE.UP EAT.BREAKFAST WORK EAT.DINNER PLAY SLEEP + +Notice that WAKE.UP has a dot between the WAKE and UP. The dot has no +particular meaning to the Forth compiler. I simply used a dot to connect +the two words together to make one word. Forth word names can have any +combination of letters, numbers, or punctuation. We will encounter words +with names like: + +- ." #S SWAP ! @ ACCEPT . * + +They are all called *words*. The word **\$%%-GL7OP** is a legal Forth +name, although not a very good one. It is up to the programmer to name +words in a sensible manner. + +Now it is time to run your Forth and begin experimenting. Please consult +the manual for your Forth for instructions on how to run it. + +## []{#The Stack}Stack Manipulation + +The Forth language is based on the concept of a *stack*. Imagine a stack +of blocks with numbers on them. You can add or remove numbers from the +top of the stack. You can also rearrange the order of the numbers. Forth +uses several stacks. The *DataStack* is the one used for passing data +between Forth words so we will concentrate our attention there. The +*Return Stack* is another Forth stack that is primarily for internal +system use. In this tutorial, when we refer to the \"stack,\" we will be +referring to the Data Stack. + +The stack is initially empty. To put some numbers on the stack, enter: + +- 23 7 9182 + +Let\'s now print the number on top of the stack using the Forth word \' +**.** \', which is pronounced \" dot \". This is a hard word to write +about in a manual because it is a single period. + +Enter: **. ** + +You should see the last number you entered, 9182 , printed. Forth has a +very handy word for showing you what\'s on the stack. It is **.S** , +which is pronounced \"dot S\". The name was constructed from \"dot\" for +print, and \"S\" for stack. (PForth will automatically print the stack +after every line if the TRACE-STACK variable is set to TRUE.) If you +enter: + +- .S + +you will see your numbers in a list. The number at the far right is the +one on top of the stack. + +You will notice that the 9182 is not on the stack. The word \' . \' +removes the number on top of the stack before printing it. In contrast, +\' .S \' leaves the stack untouched. + +We have a way of documenting the effect of words on the stack with a +*stack diagram*. A stack diagram is contained in parentheses. In Forth, +the parentheses indicate a comment. In the examples that follow, you do +not need to type in the comments. When you are programming, of course, +we encourage the use of comments and stack diagrams to make your code +more readable. In this manual, we often indicate stack diagrams in +**bold text** like the one that follows. Do not type these in. The stack +diagram for a word like \' . \' would be: + +**`. ( N -- , print number on top of stack )`** + +The symbols to the left of \-- describe the parameters that a word +expects to process. In this example, N stands for any integer number. To +the right of \--, up to the comma, is a description of the stack +parameters when the word is finished, in this case there are none +because \'dot\' \"eats\" the N that was passed in. (Note that the stack +descriptions are not necessary, but they are a great help when learning +other peoples programs.) + +The text following the comma is an English description of the word. You +will note that after the \-- , N is gone. You may be concerned about the +fact that there were other numbers on the stack, namely 23 and 7 . The +stack diagram, however, only describes the portion of the stack that is +affected by the word. For a more detailed description of the stack +diagrams, there is a special section on them in this manual right before +the main glossary section. + +Between examples, you will probably want to clear the stack. If you +enter **0SP**, pronounced \"zero S P\", then the stack will be cleared. + +Since the stack is central to Forth, it is important to be able to alter +the stack easily. Let\'s look at some more words that manipulate the +stack. Enter: + +- 0SP .S \ That's a 'zero' 0, not an 'oh' O. + 777 DUP .S + +You will notice that there are two copies of 777 on the stack. The word +**DUP** duplicates the top item on the stack. This is useful when you +want to use the number on top of the stack and still have a copy. The +stack diagram for DUP would be: + +**`DUP ( n -- n n , DUPlicate top of stack )`** + +Another useful word, is **SWAP**. Enter: + +- 0SP + 23 7 .S + SWAP .S + SWAP .S + +The stack diagram for SWAP would be: + +**`SWAP ( a b -- b a , swap top two items on stack )`** + +Now enter: + +- OVER .S + OVER .S + +The word **OVER** causes a copy of the second item on the stack to +leapfrog over the first. It\'s stack diagram would be: + +**`OVER ( a b -- a b a , copy second item on stack )`** + +Here is another commonly used Forth word: + +**`DROP ( a -- , remove item from the stack )`** + +Can you guess what we will see if we enter: + +- 0SP 11 22 .S + DROP .S + +Another handy word for manipulating the stack is **ROT**. Enter: + +- 0SP + 11 22 33 44 .S + ROT .S + +The stack diagram for ROT is, therefore: + +**`ROT ( a b c -- b c a , ROTate third item to top ) `** + +You have now learned the more important stack manipulation words. You +will see these in almost every Forth program. I should caution you that +if you see too many stack manipulation words being used in your code +then you may want to reexamine and perhaps reorganize your code. You +will often find that you can avoid excessive stack manipulations by +using *local or global VARIABLES* which will be discussed later. + +If you want to grab any arbitrary item on the stack, use **PICK** . Try +entering: + +- 0SP + 14 13 12 11 10 + 3 PICK . ( prints 13 ) + 0 PICK . ( prints 10 ) + 4 PICK . + +PICK makes a copy of the Nth item on the stack. The numbering starts +with zero, therefore: + +- `0 PICK is equivalent to DUP`\ + `1 PICK is equivalent to OVER` + +**`PICK ( ... v3 v2 v1 v0 N -- ... v3 v2 v1 v0 vN ) `** + +(Warning. The Forth-79 and FIG Forth standards differ from the ANS and +Forth \'83 standard in that their PICK numbering starts with one, not +zero.) + +I have included the stack diagrams for some other useful stack +manipulation words. Try experimenting with them by putting numbers on +the stack and calling them to get a feel for what they do. Again, the +text in parentheses is just a comment and need not be entered. + +**`DROP ( n -- , remove top of stack ) `** + +**`?DUP ( n -- n n | 0 , duplicate only if non-zero, '|' means OR ) `** + +**`-ROT ( a b c -- c a b , rotate top to third position ) `** + +**`2SWAP ( a b c d -- c d a b , swap pairs ) `** + +**`2OVER ( a b c d -- a b c d a b , leapfrog pair ) `** + +**`2DUP ( a b -- a b a b , duplicate pair ) `** + +**`2DROP ( a b -- , remove pair ) `** + +**`NIP ( a b -- b , remove second item from stack ) `** + +**`TUCK ( a b -- b a b , copy top item to third position ) `** + +### []{#Problems - Stack}Problems: + +Start each problem by entering: + +- 0SP 11 22 33 + +Then use the stack manipulation words you have learned to end up with +the following numbers on the stack: + +- 1) 11 33 22 22 + + 2) 22 33 + + 3) 22 33 11 11 22 + + 4) 11 33 22 33 11 + + 5) 33 11 22 11 22 + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Arithmetic}Arithmetic + +Great joy can be derived from simply moving numbers around on a stack. +Eventually, however, you\'ll want to do something useful with them. This +section describes how to perform arithmetic operations in Forth. + +The Forth arithmetic operators work on the numbers currently on top of +the stack. If you want to add the top two numbers together, use the +Forth word **+** , pronounced \"plus\". Enter: + +- 2 3 + . + 2 3 + 10 + . + +This style of expressing arithmetic operations is called *Reverse Polish +Notation,* or *RPN*. It will already be familiar to those of you with HP +calculators. In the following examples, I have put the algebraic +equivalent representation in a comment. + +Some other arithmetic operators are **- \* /** . Enter: + +- 30 5 - . ( 25=30-5 ) + 30 5 / . ( 6=30/5 ) + 30 5 * . ( 150=30*5 ) + 30 5 + 7 / . \ 5=(30+5)/7 + +Some combinations of operations are very common and have been coded in +assembly language for speed. For example, **2\*** is short for 2 \* . +You should use these whenever possible to increase the speed of your +program. These include: + +- 1+ 1- 2+ 2- 2* 2/ + +Try entering: + +- 10 1- . + 7 2* 1+ . ( 15=7*2+1 ) + +One thing that you should be aware of is that when you are doing +division with integers using / , the remainder is lost. Enter: + +- 15 5 / . + 17 5 / . + +This is true in all languages on all computers. Later we will examine +**/MOD** and **MOD** which do give the remainder. + +## []{#Defining a New Word}Defining a New Word + +It\'s now time to write a *small program* in Forth. You can do this by +defining a new word that is a combination of words we have already +learned. Let\'s define and test a new word that takes the average of two +numbers. + +We will make use of two new words, **:** ( \"colon\"), and **;** ( +\"semicolon\") . These words start and end a typical *Forth definition*. +Enter: + +- : AVERAGE ( a b -- avg ) + 2/ ; + +Congratulations. You have just written a Forth program. Let\'s look more +closely at what just happened. The colon told Forth to add a new word to +its list of words. This list is called the Forth dictionary. The name of +the new word will be whatever name follows the colon. Any Forth words +entered after the name will be compiled into the new word. This +continues until the semicolon is reached which finishes the definition. + +Let\'s test this word by entering: + +- 10 20 AVERAGE . ( should print 15 ) + +Once a word has been defined, it can be used to define more words. +Let\'s write a word that tests our word.. Enter: + +- : TEST ( --) 50 60 AVERAGE . ; + TEST + +Try combining some of the words you have learned into new Forth +definitions of your choice. If you promise not to be overwhelmed, you +can get a list of the words that are available for programming by +entering: + +- WORDS + +Don\'t worry, only a small fraction of these will be used directly in +your programs. + +## []{#More Arithmetic}More Arithmetic + +When you need to know the remainder of a divide operation. /MOD will +return the remainder as well as the quotient. the word MOD will only +return the remainder. Enter: + +- 0SP + 53 10 /MOD .S + 0SP + 7 5 MOD .S + +Two other handy words are **MIN** and **MAX** . They accept two numbers +and return the MINimum or MAXimum value respectively. Try entering the +following: + +- 56 34 MAX . + 56 34 MIN . + -17 0 MIN . + +Some other useful words are: + +**`ABS ( n -- abs(n) , absolute value of n ) `** + +**`NEGATE ( n -- -n , negate value, faster then -1 * ) `** + +**`LSHIFT ( n c -- n<<c , left shift of n ) `** + +**`RSHIFT ( n c -- n>>c , logical right shift of n ) `** + +**`ARSHIFT ( n c -- n>>c ) , arithmetic right shift of n ) `** + +ARSHIFT or LSHIFT can be used if you have to multiply quickly by a power +of 2 . A right shift is like doing a divide by 2. This is often faster +than doing a regular multiply or divide. Try entering: + +- : 256* 8 LSHIFT ; + 3 256* . + +### []{#Arithmetic Overflow}Arithmetic Overflow + +If you are having problems with your calculation overflowing the 32-bit +precision of the stack, then you can use **\*/** . This produces an +intermediate result that is 64 bits long. Try the following three +methods of doing the same calculation. Only the one using \*/ will yield +the correct answer, 5197799. + +- 34867312 99154 * 665134 / . + 34867312 665134 / 99154 * . + 34867312 99154 665134 */ . + +#### []{#Convert Algebraic Expressions to Forth}Convert Algebraic Expressions to Forth + +How do we express complex algebraic expressions in Forth? For example: +20 + (3 \* 4) + +To convert this to Forth you must order the operations in the order of +evaluation. In Forth, therefore, this would look like: + +- 3 4 * 20 + + +Evaluation proceeds from left to right in Forth so there is no +ambiguity. Compare the following algebraic expressions and their Forth +equivalents: (Do **not** enter these!) + +- (100+50)/2 ==> 100 50 + 2/ + ((2*7) + (13*5)) ==> 2 7 * 13 5 * + + +If any of these expressions puzzle you, try entering them one word at a +time, while viewing the stack with .S . + +### []{#Problems - Square}Problems: + +Convert the following algebraic expressions to their equivalent Forth +expressions. (Do **not** enter these because they are not Forth code!) + +- (12 * ( 20 - 17 )) + + (1 - ( 4 * (-18) / 6) ) + + ( 6 * 13 ) - ( 4 * 2 * 7 ) + +Use the words you have learned to write these new words: + +- SQUARE ( N -- N*N , calculate square ) + + DIFF.SQUARES ( A B -- A*A-B*B , difference of squares ) + + AVERAGE4 ( A B C D -- [A+B+C+D]/4 ) + + HMS>SECONDS ( HOURS MINUTES SECONDS -- TOTAL-SECONDS , convert ) + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Character Input and Output}Character Input and Output + +The numbers on top of the stack can represent anything. The top number +might be how many blue whales are left on Earth or your weight in +kilograms. It can also be an ASCII character. Try entering the +following: + +- 72 EMIT 105 EMIT + +You should see the word \"Hi\" appear before the OK. The 72 is an ASCII +\'H\' and 105 is an \'i\'. EMIT takes the number on the stack and +outputs it as a character. If you want to find the ASCII value for any +character, you can use the word ASCII . Enter: + +- CHAR W . + CHAR % DUP . EMIT + CHAR A DUP . + 32 + EMIT + +Here is a complete [ASCII chart](http://www.asciitable.com/). + +Notice that the word CHAR is a bit unusual because its input comes not +from the stack, but from the following text. In a stack diagram, we +represent that by putting the input in angle brackets, \<input\>. Here +is the stack diagram for CHAR. + +**`CHAR ( <char> -- char , get ASCII value of a character ) `** + +Using EMIT to output character strings would be very tedious. Luckily +there is a better way. Enter: + +- : TOFU ." Yummy bean curd!" ; + TOFU + +The word **.\"** , pronounced \"dot quote\", will take everything up to +the next quotation mark and print it to the screen. Make sure you leave +a space after the first quotation mark. When you want to have text begin +on a new line, you can issue a carriage return using the word **CR** . +Enter: + +- : SPROUTS ." Miniature vegetables." ; + : MENU + CR TOFU CR SPROUTS CR + ; + MENU + +You can emit a blank space with **SPACE** . A number of spaces can be +output with SPACES . Enter: + +- CR TOFU SPROUTS + CR TOFU SPACE SPROUTS + CR 10 SPACES TOFU CR 20 SPACES SPROUTS + +For character input, Forth uses the word **KEY** which corresponds to +the word EMIT for output. KEY waits for the user to press a key then +leaves its value on the stack. Try the following. + +- : TESTKEY ( -- ) + ." Hit a key: " KEY CR + ." That = " . CR + ; + TESTKEY + +\[Note: On some computers, the input if buffered so you will need to hit +the ENTER key after typing your character.\] + +**`EMIT ( char -- , output character ) `** + +**`KEY ( -- char , input character ) `** + +**`SPACE ( -- , output a space ) `** + +**`SPACES ( n -- , output n spaces ) `** + +**`CHAR ( <char> -- char , convert to ASCII ) `** + +**`CR ( -- , start new line , carriage return ) `** + +**`." ( -- , output " delimited text ) `** + +## []{#Compiling from Files}Compiling from Files + +PForth can read read from ordinary text files so you can use any editor +that you wish to write your programs. + +### Sample Program + +Enter into your file, the following code. + +- \ Sample Forth Code + \ Author: your name + + : SQUARE ( n -- n*n , square number ) + DUP * + ; + + : TEST.SQUARE ( -- ) + CR ." 7 squared = " + 7 SQUARE . CR + ; + +Now save the file to disk. + +The text following the **\\** character is treated as a comment. This +would be a REM statement in BASIC or a /\*\-\--\*/ in \'C\'. The text in +parentheses is also a comment. + +### Using INCLUDE + +\"INCLUDE\" in Forth means to compile from a file. + +You can compile this file using the INCLUDE command. If you saved your +file as WORK:SAMPLE, then compile it by entering: + +- INCLUDE SAMPLE.FTH + +Forth will compile your file and tell you how many bytes it has added to +the dictionary. To test your word, enter: + +- TEST.SQUARE + +Your two words, SQUARE and TEST.SQUARE are now in the Forth dictionary. +We can now do something that is very unusual in a programming language. +We can \"uncompile\" the code by telling Forth to **FORGET** it. Enter: + +- FORGET SQUARE + +This removes SQUARE and everything that follows it, ie. TEST.SQUARE, +from the dictionary. If you now try to execute TEST.SQUARE it won\'t be +found. + +Now let\'s make some changes to our file and reload it. Go back into the +editor and make the following changes: (1) Change TEST.SQUARE to use 15 +instead of 7 then (2) Add this line right before the definition of +SQUARE: + +- ANEW TASK-SAMPLE.FTH + +Now Save your changes and go back to the Forth window. + +You\'re probably wondering what the line starting with **ANEW** was for. +ANEW is always used at the beginning of a file. It defines a special +marker word in the dictionary before the code. The word typically has +\"TASK-\" as a prefix followed by the name of the file. When you +ReInclude a file, ANEW will automatically FORGET the old code starting +after the ANEW statement. This allows you to Include a file over and +over again without having to manually FORGET the first word. If the code +was not forgotten, the dictionary would eventually fill up. + +If you have a big project that needs lots of files, you can have a file +that will load all the files you need. Sometimes you need some code to +be loaded that may already be loaded. The word **INCLUDE?** will only +load code if it isn\'t already in the dictionary. In this next example, +I assume the file is on the volume WORK: and called SAMPLE. If not, +please substitute the actual name. Enter: + +- FORGET TASK-SAMPLE.FTH + INCLUDE? SQUARE WORK:SAMPLE + INCLUDE? SQUARE WORK:SAMPLE + +Only the first INCLUDE? will result in the file being loaded. + +## []{#Variables}Variables + +Forth does not rely as heavily on the use of variables as other compiled +languages. This is because values normally reside on the stack. There +are situations, of course, where variables are required. To create a +variable, use the word **VARIABLE** as follows: + +- VARIABLE MY-VAR + +This created a variable named MY-VAR . A space in memory is now reserved +to hold its 32-bit value. The word VARIABLE is what\'s known as a +\"defining word\" since it creates new words in the dictionary. Now +enter: + +- MY-VAR . + +The number you see is the address, or location, of the memory that was +reserved for MY-VAR. To store data into memory you use the word **!** , +pronounced \"store\". It looks like an exclamation point, but to a Forth +programmer it is the way to write 32-bit data to memory. To read the +value contained in memory at a given address, use the Forth word **@** , +pronounced \"fetch\". Try entering the following: + +- 513 MY-VAR ! + MY-VAR @ . + +This sets the variable MY-VAR to 513 , then reads the value back and +prints it. The stack diagrams for these words follows: + +**`@ ( address -- value , FETCH value FROM address in memory ) `** + +**`! ( value address -- , STORE value TO address in memory )`** + +**`VARIABLE ( <name> -- , define a 4 byte memory storage location)`** + +A handy word for checking the value of a variable is **?** , pronounced +\"question\". Try entering: + +- MY-VAR ? + +If ? wasn\'t defined, we could define it as: + +- : ? ( address -- , look at variable ) + @ . + ; + +Imagine you are writing a game and you want to keep track of the highest +score. You could keep the highest score in a variable. When you reported +a new score, you could check it aginst the highest score. Try entering +this code in a file as described in the previous section: + +- VARIABLE HIGH-SCORE + + : REPORT.SCORE ( score -- , print out score ) + DUP CR ." Your Score = " . CR + HIGH-SCORE @ MAX ( calculate new high ) + DUP ." Highest Score = " . CR + HIGH-SCORE ! ( update variable ) + ; + +Save the file to disk, then compile this code using the INCLUDE word. +Test your word as follows: + +- 123 REPORT.SCORE + 9845 REPORT.SCORE + 534 REPORT.SCORE + +The Forth words @ and ! work on 32-bit quantities. Some Forths are +\"16-bit\" Forths. They fetch and store 16-bit quantities. Forth has +some words that will work on 8 and 16-bit values. C@ and C! work +characters which are usually for 8-bit bytes. The \'C\' stands for +\"Character\" since ASCII characters are 8-bit numbers. Use W@ and W! +for 16-bit \"Words.\" + +Another useful word is **+!** , pronounced \"plus store.\" It adds a +value to a 32-bit value in memory. Try: + +- 20 MY-VAR ! + 5 MY-VAR +! + MY-VAR @ . + +Forth also provides some other words that are similar to VARIABLE. Look +in the glossary for VALUE and ARRAY. Also look at the section on +\"[local variables](pf_ref.php#Local%20Variables%20%7B%20foo%20--%7D?)\" +which are variables which only exist on the stack while a Forth word is +executing. + +*A word of warning about fetching and storing to memory*: You have now +learned enough about Forth to be dangerous. The operation of a computer +is based on having the right numbers in the right place in memory. You +now know how to write new numbers to any place in memory. Since an +address is just a number, you could, but shouldn\'t, enter: + +- 73 253000 ! ( Do NOT do this. ) + +The 253000 would be treated as an address and you would set that memory +location to 73. I have no idea what will happen after that, maybe +nothing. This would be like firing a rifle through the walls of your +apartment building. You don\'t know who or what you are going to hit. +Since you share memory with other programs including the operating +system, you could easily cause the computer to behave strangely, even +crash. Don\'t let this bother you too much, however. Crashing a +computer, unlike crashing a car, does not hurt the computer. You just +have to reboot. The worst that could happen is that if you crash while +the computer is writing to a disk, you could lose a file. That\'s why we +make backups. This same potential problem exists in any powerful +language, not just Forth. This might be less likely in BASIC, however, +because BASIC protects you from a lot of things, including the danger of +writing powerful programs. + +Another way to get into trouble is to do what\'s called an \"odd address +memory access.\" The 68000 processor arranges words and longwords, 16 +and 32 bit numbers, on even addresses. If you do a **@** or **!** , or +**W@** or **W!** , to an odd address, the 68000 processor will take +exception to this and try to abort. + +Forth gives you some protection from this by trapping this exception and +returning you to the OK prompt. If you really need to access data on an +odd address, check out the words **ODD@** and **ODD!** in the glossary. +**C@** and **C!** work fine on both odd and even addresses. + +## []{#Constants}Constants + +If you have a number that is appearing often in your program, we +recommend that you define it as a \"constant.\" Enter: + +- 128 CONSTANT MAX_CHARS + MAX_CHARS . + +We just defined a word called MAX_CHARS that returns the value on the +stack when it was defined. It cannot be changed unless you edit the +program and recompile. Using **CONSTANT** can improve the readability of +your programs and reduce some bugs. Imagine if you refer to the number +128 very often in your program, say 8 times. Then you decide to change +this number to 256. If you globally change 128 to 256 you might change +something you didn\'t intend to. If you change it by hand you might miss +one, especially if your program occupies more than one file. Using +CONSTANT will make it easy to change. The code that results is equally +as fast and small as putting the numbers in directly. I recommend +defining a constant for almost any number. + +## []{#Logical Operators}Logical Operators + +These next two sections are concerned with decision making. This first +section deals with answering questions like \"Is this value too large?\" +or \"Does the guess match the answer?\". The answers to questions like +these are either TRUE or FALSE. Forth uses a 0 to represent **FALSE** +and a -1 to represent **TRUE**. TRUE and FALSE have been capitalized +because they have been defined as Forth constants. Try entering: + +- 23 71 = . + 18 18 = . + +You will notice that the first line printed a 0, or FALSE, and the +second line a -1, or TRUE. The equal sign in Forth is used as a +question, not a statement. It asks whether the top two items on the +stack are equal. It does not set them equal. There are other questions +that you can ask. Enter: + +- 23 198 < . + 23 198 > . + 254 15 > . + +In California, the drinking age for alcohol is 21. You could write a +simple word now to help bartenders. Enter: + +- : DRINK? ( age -- flag , can this person drink? ) + 20 > + ; + + 20 DRINK? . + 21 DRINK? . + 43 DRINK? . + +The word FLAG in the stack diagram above refers to a logical value. + +Forth provides special words for comparing a number to 0. They are +**0=** **0\>** and **0\<** . Using 0\> is faster than calling 0 and \> +separately. Enter: + +- `23 0> . ( print -1 )`\ + `-23 0> . ( print 0 )`\ + `23 0= . ( print 0 )` + +For more complex decisions, you can use the *Boolean* operators **OR** , +**AND** , and **NOT** . OR returns a TRUE if either one or both of the +top two stack items are true. + +- TRUE TRUE OR . + TRUE FALSE OR . + FALSE FALSE OR . + +AND only returns a TRUE if both of them are true. + +- TRUE TRUE AND . + TRUE FALSE AND . + +NOT reverses the value of the flag on the stack. Enter: + +- TRUE . + TRUE NOT . + +Logical operators can be combined. + +- 56 3 > 56 123 < AND . + 23 45 = 23 23 = OR . + +Here are stack diagrams for some of these words. See the glossary for a +more complete list. + +**`< ( a b -- flag , flag is true if A is less than B )`** + +**`> ( a b -- flag , flag is true if A is greater than B )`** + +**`= ( a b -- flag , flag is true if A is equal to B )`** + +**`0= ( a -- flag , true if a equals zero )`** + +**`OR ( a b -- a||b , perform logical OR of bits in A and B )`** + +**`AND ( a b -- a&b , perform logical AND of bits in A and B )`** + +**`NOT ( flag -- opposite-flag , true if false, false if true )`** + +### []{#Problems - Logical}Problems: + +1\) Write a word called LOWERCASE? that returns TRUE if the number on +top of the stack is an ASCII lowercase character. An ASCII \'a\' is 97 . +An ASCII \'z\' is 122 . Test using the characters \" A \` a q z { \". + +- CHAR A LOWERCASE? . ( should print 0 ) + CHAR a LOWERCASE? . ( should print -1 ) + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Conditionals - IF ELSE THEN CASE}Conditionals - IF ELSE THEN CASE + +You will now use the TRUE and FALSE flags you learned to generate in the +last section. The \"flow of control\" words accept flags from the stack, +and then possibly \"branch\" depending on the value. Enter the following +code. + +- : .L ( flag -- , print logical value ) + IF ." True value on stack!" + ELSE ." False value on stack!" + THEN + ; + + 0 .L + FALSE .L + TRUE .L + 23 7 < .L + +You can see that when a TRUE was on the stack, the first part got +executed. If a FALSE was on the stack, then the first part was skipped, +and the second part was executed. One thing you will find interesting is +that if you enter: + +- 23 .L + +the value on the stack will be treated as true. The flow of control +words consider any value that does not equal zero to be TRUE. + +The **ELSE** word is optional in the **IF\...THEN** construct. Try the +following: + +- : BIGBUCKS? ( amount -- ) + 1000 > + IF ." That's TOO expensive!" + THEN + ; + + 531 BIGBUCKS? + 1021 BIGBUCKS? + +Many Forths also support a **CASE** statement similar to switch() in +\'C\'. Enter: + +- : TESTCASE ( N -- , respond appropriately ) + CASE + 0 OF ." Just a zero!" ENDOF + 1 OF ." All is ONE!" ENDOF + 2 OF WORDS ENDOF + DUP . ." Invalid Input!" + ENDCASE CR + ; + + 0 TESTCASE + 1 TESTCASE + 5 TESTCASE + +See CASE in the glossary for more information. + +### []{#Problems - Conditionals}Problems: + +1\) Write a word called DEDUCT that subtracts a value from a variable +containing your checking account balance. Assume the balance is in +dollars. Print the balance. Print a warning if the balance is negative. + +- VARIABLE ACCOUNT + + : DEDUCT ( n -- , subtract N from balance ) + ????????????????????????????????? ( you fill this in ) + ; + + 300 ACCOUNT ! ( initial funds ) + 40 DEDUCT ( prints 260 ) + 200 DEDUCT ( print 60 ) + 100 DEDUCT ( print -40 and give warning! ) + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Loops}Loops + +Another useful pair of words is **BEGIN\...UNTIL** . These are used to +loop until a given condition is true. Try this: + +- : COUNTDOWN ( N -- ) + BEGIN + DUP . CR ( print number on top of stack ) + 1- DUP 0< ( loop until we go negative ) + UNTIL + ; + + 16 COUNTDOWN + +This word will count down from N to zero. + +If you know how many times you want a loop to execute, you can use the +**DO\...LOOP** construct. Enter: + +- : SPELL + ." ba" + 4 0 DO + ." na" + LOOP + ; + +This will print \"ba\" followed by four occurrences of \"na\". The +ending value is placed on the stack before the beginning value. Be +careful that you don\'t pass the values in reverse. Forth will go \"the +long way around\" which could take awhile. The reason for this order is +to make it easier to pass the loop count into a word on the stack. +Consider the following word for doing character graphics. Enter: + +- : PLOT# ( n -- ) + 0 DO + [CHAR] - EMIT + LOOP CR + ; + + CR 9 PLOT# 37 PLOT# + +If you want to access the loop counter you can use the word I . Here is +a simple word that dumps numbers and their associated ASCII characters. + +- : .ASCII ( end start -- , dump characters ) + DO + CR I . I EMIT + LOOP CR + ; + + 80 64 .ASCII + +If you want to leave a DO LOOP before it finishes, you can use the word +**LEAVE**. Enter: + +- : TEST.LEAVE ( -- , show use of leave ) + 100 0 + DO + I . CR \ print loop index + I 20 > \ is I over 20 + IF + LEAVE + THEN + LOOP + ; + TEST.LEAVE \ will print 0 to 20 + +Please consult the manual to learn about the following words **+LOOP** +and **RETURN** . FIXME + +Another useful looping construct is the **BEGIN WHILE REPEAT** loop. +This allows you to make a test each time through the loop before you +actually do something. The word WHILE will continue looping if the flag +on the stack is True. Enter: + +- : SUM.OF.N ( N -- SUM[N] , calculate sum of N integers ) + 0 \ starting value of SUM + BEGIN + OVER 0> \ Is N greater than zero? + WHILE + OVER + \ add N to sum + SWAP 1- SWAP \ decrement N + REPEAT + SWAP DROP \ get rid on N + ; + + 4 SUM.OF.N \ prints 10 ( 1+2+3+4 ) + +### []{#Problems - Loops}Problems: + +1\) Rewrite SUM.OF.N using a DO LOOP. + +2\) Rewrite SUM.OF.N using BEGIN UNTIL. + +3\) For bonus points, write SUM.OF.N without using any looping or +conditional construct! + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Text Input and Output}Text Input and Output + +You learned earlier how to do single character I/O. This section +concentrates on using strings of characters. You can embed a text string +in your program using S\". Note that you must follow the S\" by one +space. The text string is terminated by an ending \" .Enter: + +- : TEST S" Hello world!" ; + TEST .S + +Note that TEST leaves two numbers on the stack. The first number is the +address of the first character. The second number is the number of +characters in the string. You can print the characters of the string as +follows. + +- TEST DROP \ get rid of number of characters + DUP C@ EMIT \ prints first character, 'H' + CHAR+ DUP C@ EMIT \ prints second character, 'e' + \ and so on + +CHAR+ advances the address to the next character. You can print the +entire string using TYPE. + +- TEST TYPE + TEST 2/ TYPE \ print half of string + +It would be nice if we could simply use a single address to describe a +string and not have to pass the number of characters around. \'C\' does +this by putting a zero at the end of the string to show when it ends. +Forth has a different solution. A text string in Forth consists of a +character count in the first byte, followed immediately by the +characters themselves. This type of character string can be created +using the Forth word C\" , pronounced \'c quote\'. Enter: + +- : T2 C" Greetings Fred" ; + T2 . + +The number that was printed was the address of the start of the string. +It should be a byte that contains the number of characters. Now enter: + +- T2 C@ . + +You should see a 14 printed. Remember that C@ fetches one character/byte +at the address on the stack. You can convert a counted Forth string to +an address and count using COUNT. + +- T2 COUNT .S + TYPE + +The word **COUNT** extracts the number of characters and their starting +address. COUNT will only work with strings of less than 256 characters, +since 255 is the largest number that can be stored in the count byte. +TYPE will, however, work with longer strings since the length is on the +stack. Their stack diagrams follow: + +**`CHAR+ ( address -- address' , add the size of one character )`** + +**`COUNT ( $addr -- addr #bytes , extract string information ) `** + +**`TYPE ( addr #bytes -- , output characters at addr )`** + +The \$addr is the address of a count byte. The dollar sign is often used +to mark words that relate to strings. + +You can easily input a string using the word **ACCEPT**. (You may want +to put these upcoming examples in a file since they are very handy.) The +word **ACCEPT** receives characters from the keyboard and places them at +any specified address. **ACCEPT** takes input characters until a maximum +is reached or an end of line character is entered. **ACCEPT** returns +the number of characters entered. You can write a word for entering +text. Enter: + +- : INPUT$ ( -- $addr ) + PAD 1+ ( leave room for byte count ) + 127 ACCEPT ( recieve a maximum of 127 chars ) + PAD C! ( set byte count ) + PAD ( return address of string ) + ; + + INPUT$ COUNT TYPE + +Enter a string which should then be echoed. You could use this in a +program that writes form letters. + +- : FORM.LETTER ( -- ) + ." Enter customer's name." CR + INPUT$ + CR ." Dear " DUP COUNT TYPE CR + ." Your cup that says " COUNT TYPE + ." is in the mail!" CR + ; + +**`ACCEPT ( addr maxbytes -- numbytes , input text, save at address ) `** + +You can use your word INPUT\$ to write a word that will read a number +from the keyboard. Enter: + +- : INPUT# ( -- N true | false ) + INPUT$ ( get string ) + NUMBER? ( convert to a string if valid ) + IF DROP TRUE ( get rid of high cell ) + ELSE FALSE + THEN + ; + +This word will return a single-precision number and a TRUE, or it will +just return FALSE. The word **NUMBER?** returns a double precision +number if the input string contains a valid number. Double precision +numbers are 64-bit so we DROP the top 32 bits to get a single-precision +32 bit number. + +## []{#Changing Numeric Base}Changing Numeric Base + +For day-to-day life, the numbering system we use is decimal, or \"base +10.\" That means each digit get multiplied by a power of 10. Thus a +number like 527 is equal to (5\*100 + 2\*10 + 7\*1). The use of 10 for +the numeric base is a completely arbitrary decision. It no doubt has +something to do with the fact that most people have 10 fingers +(including thumbs). The Babylonians used base 60, which is where we got +saddled with the concept of 60 minutes in an hour. Computer hardware +uses base 2, or \"binary\". The computer number \"1101\" is equal to +(1\*8 + 1\*4 + 0\*2 + 1\*1). If you add these up, you get 8+4+1=13 . The +binary number \"10\" is (1\*2 + 0\*1), or 2. Likewise the numeric string +\"10\" in any base N is N. + +Forth makes it very easy to explore different numeric bases because it +can work in any base. Try entering the following: + +- DECIMAL 6 BINARY . + 1 1 + . + 1101 DECIMAL . + +Another useful numeric base is *hexadecimal*. which is base 16. One +problem with bases over 10 is that our normal numbering system only has +digits 0 to 9. For hex numbers we use the letters A to F for the digits +10 to 15. Thus the hex number \"3E7\" is equal to (3\*256 + 14\*16 + +7\*1). Try entering: + +- DECIMAL 12 HEX . \ print C + DECIMAL 12 256 * 7 16 * + 10 + .S + DUP BINARY . + HEX . + +A variable called **BASE** is used to keep track of the current numeric +base. The words **HEX** , **DECIMAL** , and **BINARY** work by changing +this variable. You can change the base to anything you want. Try: + +- 7 BASE ! + 6 1 + . + BASE @ . \ surprise! + +You are now in base 7 . When you fetched and printed the value of BASE, +it said \"10\" because 7, in base 7, is \"10\". + +PForth defines a word called .HEX that prints a number as hexadecimal +regardless of the current base. + +- DECIMAL 14 .HEX + +You could define a word like .HEX for any base. What is needed is a way +to temporarily set the base while a number is printed, then restore it +when we are through. Try the following word: + +- : .BIN ( N -- , print N in Binary ) + BASE @ ( save current base ) + 2 BASE ! ( set to binary ) + SWAP . ( print number ) + BASE ! ( restore base ) + ; + + DECIMAL + 13 .BIN + 13 . + +## []{#Answers to Problems}Answers to Problems + +If your answer doesn\'t exactly match these but it works, don\'t fret. +In Forth, there are usually many ways to the same thing. + +### [Stack Manipulations](#Problems%20-%20Stack) + +- 1) SWAP DUP + 2) ROT DROP + 3) ROT DUP 3 PICK + 4) SWAP OVER 3 PICK + 5) -ROT 2DUP + +### [Arithmetic](#Problems%20-%20Square) + +- (12 * (20 - 17)) ==> 20 17 - 12 * + (1 - (4 * (-18) / 6)) ==> 1 4 -18 * 6 / - + (6 * 13) - (4 * 2 * 7) ==> 6 13 * 4 2 * 7 * - + + : SQUARE ( N -- N*N ) + DUP * + ; + + : DIFF.SQUARES ( A B -- A*A-B*B ) + SWAP SQUARE + SWAP SQUARE - + ; + + : AVERAGE4 ( A B C D -- [A+B+C+D]/4 ) + + + + ( add'em up ) + 4 / + ; + + : HMS>SECONDS ( HOURS MINUTES SECONDS -- TOTAL-SECONDS ) + -ROT SWAP ( -- seconds minutes hours ) + 60 * + ( -- seconds total-minutes ) + 60 * + ( -- seconds ) + ; + +### [Logical Operators](#Problems%20-%20Logical) + +- : LOWERCASE? ( CHAR -- FLAG , true if lowercase ) + DUP 123 < + SWAP 96 > AND + ; + +### [Conditionals](#Problems%20-%20Conditionals) + +- : DEDUCT ( n -- , subtract from account ) + ACCOUNT @ ( -- n acc ) + SWAP - DUP ACCOUNT ! ( -- acc' , update variable ) + ." Balance = $" DUP . CR ( -- acc' ) + 0< ( are we broke? ) + IF ." Warning!! Your account is overdrawn!" CR + THEN + ; + +### [`Loops`](#Problems%20-%20Loops) + +- : SUM.OF.N.1 ( N -- SUM[N] ) + 0 SWAP \ starting value of SUM + 1+ 0 \ set indices for DO LOOP + ?DO \ safer than DO if N=0 + I + + LOOP + ; + + : SUM.OF.N.2 ( N -- SUM[N] ) + 0 \ starting value of SUM + BEGIN ( -- N' SUM ) + OVER + + SWAP 1- SWAP + OVER 0< + UNTIL + SWAP DROP + ; + + : SUM.OF.N.3 ( NUM -- SUM[N] , Gauss' method ) + DUP 1+ \ SUM(N) = N*(N+1)/2 + * 2/ + ; + +Back to [pForth Home Page](../pforth)\ +::: + +::: {#footer} +\(C\) 1997-2015 Mobileer Inc - All Rights Reserved - [Contact +Us](/contacts.php) +::: +::::::::::::: |