├── .github ├── FUNDING.yml ├── build ├── run-tests.sh └── workflows │ ├── codeql-analysis.yml │ ├── pull_request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── foth ├── README.md ├── eval │ ├── builtins.go │ ├── builtins_test.go │ ├── eval.go │ └── eval_test.go ├── foth.4th ├── go.mod ├── lexer │ ├── lexer.go │ └── lexer_test.go ├── main.go └── stack │ ├── stack.go │ └── stack_test.go ├── go.mod ├── part1 ├── README.md ├── builtins.go ├── eval.go ├── main.go └── stack.go ├── part2 ├── README.md ├── builtins.go ├── eval.go ├── main.go └── stack.go ├── part3 ├── README.md ├── builtins.go ├── eval.go ├── main.go └── stack.go ├── part4 ├── README.md ├── builtins.go ├── eval.go ├── main.go └── stack.go ├── part5 ├── README.md ├── builtins.go ├── eval.go ├── main.go └── stack.go ├── part6 ├── README.md ├── builtins.go ├── eval.go ├── foth.4th ├── main.go └── stack.go └── part7 ├── README.md ├── builtins.go ├── eval.go ├── foth.4th ├── main.go └── stack.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: skx 3 | custom: https://steve.fi/donate/ 4 | -------------------------------------------------------------------------------- /.github/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The basename of our binary 4 | BASE="foth" 5 | 6 | cd ./foth/ 7 | 8 | # I don't even .. 9 | go env -w GOFLAGS="-buildvcs=false" 10 | 11 | # 12 | # We build on multiple platforms/archs 13 | # 14 | BUILD_PLATFORMS="linux darwin freebsd" 15 | BUILD_ARCHS="amd64 386" 16 | 17 | # For each platform 18 | for OS in ${BUILD_PLATFORMS[@]}; do 19 | 20 | # For each arch 21 | for ARCH in ${BUILD_ARCHS[@]}; do 22 | 23 | # Setup a suffix for the binary 24 | SUFFIX="${OS}" 25 | 26 | # i386 is better than 386 27 | if [ "$ARCH" = "386" ]; then 28 | SUFFIX="${SUFFIX}-i386" 29 | else 30 | SUFFIX="${SUFFIX}-${ARCH}" 31 | fi 32 | 33 | echo "Building for ${OS} [${ARCH}] -> ${BASE}-${SUFFIX}" 34 | 35 | # Run the build 36 | export GOARCH=${ARCH} 37 | export GOOS=${OS} 38 | export CGO_ENABLED=0 39 | 40 | # Build the main-binary 41 | go build -ldflags "-X main.version=$(git describe --tags 2>/dev/null || echo 'master')" -o "${BASE}-${SUFFIX}" 42 | done 43 | done 44 | -------------------------------------------------------------------------------- /.github/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # I don't even .. 4 | go env -w GOFLAGS="-buildvcs=false" 5 | 6 | # Install tools to test our code-quality. 7 | go install golang.org/x/lint/golint@master 8 | go install honnef.co/go/tools/cmd/staticcheck@master 9 | 10 | # Run the static-check tool 11 | for i in */; do 12 | echo "Running golint on $i" 13 | cd $i 14 | t=$(mktemp) 15 | staticcheck -checks all ./... > $t 16 | if [ -s $t ]; then 17 | echo "Found errors via 'staticcheck'" 18 | cat $t 19 | rm $t 20 | exit 1 21 | fi 22 | rm $t 23 | cd .. 24 | done 25 | 26 | 27 | 28 | # At this point failures cause aborts 29 | set -e 30 | 31 | # Run the linter-tool 32 | for i in */; do 33 | echo "Running golint on $i" 34 | cd $i 35 | golint -set_exit_status ./... 36 | cd .. 37 | done 38 | 39 | # Run the vet-tool 40 | for i in */; do 41 | echo "Running go vet $i" 42 | cd $i 43 | go vet ./... 44 | cd .. 45 | done 46 | 47 | 48 | # Run our golang tests 49 | cd foth && go test ./... 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 14 * * 5' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Pull Request 3 | jobs: 4 | test: 5 | name: Run tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Test 10 | uses: skx/github-action-tester@master 11 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Push Event 6 | jobs: 7 | test: 8 | name: Run tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Test 13 | uses: skx/github-action-tester@master 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: release 2 | name: Handle Release 3 | jobs: 4 | upload: 5 | name: Upload 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the repository 9 | uses: actions/checkout@master 10 | - name: Generate the artifacts 11 | uses: skx/github-action-build@master 12 | - name: Upload the artifacts 13 | uses: skx/github-action-publish-binaries@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | args: foth/foth-* 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | part?/part? 2 | foth/foth 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/skx/foth@v0.3.0/foth?tab=overview) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/skx/foth)](https://goreportcard.com/report/github.com/skx/foth) 3 | [![license](https://img.shields.io/github/license/skx/foth.svg)](https://github.com/skx/foth/blob/master/LICENSE) 4 | [![Release](https://img.shields.io/github/release/skx/foth.svg)](https://github.com/skx/foth/releases/latest) 5 | 6 | * [foth](#foth) 7 | * [Features](#features) 8 | * [Installation](#installation) 9 | * [Embedded Usage](#embedded-usage) 10 | * [Anti-Features](#anti-features) 11 | * [Implementation Approach](#implementation-approach) 12 | * [Implementation Overview](#implementation-overview) 13 | * [Part 1](#part-1) - Minimal initial-implementation. 14 | * [Part 2](#part-2) - Hard-coded recursive word definitions. 15 | * [Part 3](#part-3) - Allow defining minimal words via the REPL. 16 | * [Part 4](#part-4) - Allow defining improved words via the REPL. 17 | * [Part 5](#part-5) - Allow executing loops via `do`/`loop`. 18 | * [Part 6](#part-6) - Allow conditional execution via `if`/`then`. 19 | * [Part 7](#part-7) - Added minimal support for strings. 20 | * [Final Revision](#final-revision) - Idiomatic Go, test-cases, and many new words 21 | * [BUGS](#bugs) 22 | * [loops](#loops) - zero expected-iterations actually runs once 23 | * [See Also](#see-also) 24 | * [Github Setup](#github-setup) 25 | 26 | 27 | 28 | 29 | # foth 30 | 31 | A simple implementation of a FORTH-like language, hence _foth_ which is 32 | close to _forth_. 33 | 34 | If you're new to FORTH then [the wikipedia page](https://en.wikipedia.org/wiki/Forth_(programming_language)) is a good starting point, and there are more good reads online such as: 35 | 36 | * [Forth in 7 easy steps](https://jeelabs.org/article/1612b/) 37 | * Just ignore any mention of the return-stack! 38 | * [Starting FORTH](https://www.forth.com/starting-forth/) 39 | * A complete book, but the navigation of this site is non-obvious. 40 | 41 | In brief FORTH is a stack-based language, which uses Reverse Polish notation. The basic _thing_ in Forth is the "word", which is a named data item, subroutine, or operator. Programming consists largely of defining new words, which are stored in a so-called "dictionary", in terms of existing ones. Iteratively building up a local DSL suited to your particular task. 42 | 43 | This repository was created by following the brief tutorial posted within the following Hacker News thread, designed to demonstrate how you could implement something _like_ FORTH, in a series of simple steps: 44 | 45 | * https://news.ycombinator.com/item?id=13082825 46 | 47 | The comment-thread shows example-code and pseudo-code in C, of course this repository is written in Go. 48 | 49 | 50 | 51 | ## Features 52 | 53 | The end-result of this work is a simple scripting-language which you could easily embed within your golang application, allowing users to write simple FORTH-like scripts. We implement the kind of features a FORTH-user would expect: 54 | 55 | * Comments between `(` and `)` are ignored, as expected. 56 | * Single-line comments `\` to the end of the line are also supported. 57 | * Support for floating-point numbers (anything that will fit inside a `float64`). 58 | * Reverse-Polish mathematical operations. 59 | * Including support for `abs`, `min`, `max`, etc. 60 | * Support for printing the top-most stack element (`.`, or `print`). 61 | * Support for outputting ASCII characters (`emit`). 62 | * Support for outputting strings (`." Hello, World "`). 63 | * Some additional string-support for counting lengths, etc. 64 | * Support for basic stack operations (`clearstack`, `drop`, `dup`, `over`, `swap`, `.s`) 65 | * Support for loops, via `do`/`loop`. 66 | * Support for conditional-execution, via `if`, `else`, and `then`. 67 | * Support for declaring variables with `variable`, and getting/setting their values with `@` and `!` respectively. 68 | * Execute files specified on the command-line. 69 | * If no arguments are supplied run a simple REPL instead. 70 | * A standard library is loaded, from the present directory, if it is present. 71 | * See what we load by default in [foth/foth.4th](foth/foth.4th). 72 | * The use of recursive definitions, for example: 73 | * `: factorial recursive dup 1 > if dup 1 - factorial * then ;` 74 | 75 | 76 | 77 | ## Installation 78 | 79 | You can find binary releases of the final-version upon the [project release page](https://github.com/skx/foth/releases), but if you prefer you can install from source easily. 80 | 81 | Either run this to download and install the binary: 82 | 83 | ``` 84 | $ go get github.com/skx/foth/foth@v0.5.0 85 | 86 | ``` 87 | 88 | Or clone this repository, and build the executable like so: 89 | 90 | ``` 91 | cd foth 92 | go build . 93 | ./foth 94 | ``` 95 | 96 | The executable will try to load [foth.4th](foth/foth.4th) from the current-directory, so you'll want to fetch that too. But otherwise it should work as you'd expect - the startup-file defines several useful words, so running without it is a little annoying but it isn't impossible. 97 | 98 | 99 | 100 | ## Embedded Usage 101 | 102 | Although this is a minimal interpreter it _can_ be embedded within a Golang host-application, allowing users to write scripts to control it. 103 | 104 | As an example of this I put together a simple demo: 105 | 106 | * [https://github.com/skx/turtle](https://github.com/skx/turtle) 107 | 108 | This embeds the interpreter within an application, and defines some new words to allow the user to create graphics - in the style of [turtle](https://en.wikipedia.org/wiki/Turtle_graphics). 109 | 110 | 111 | 112 | ## Anti-Features 113 | 114 | The obvious omission from this implementation is support for strings in the general case (string support is pretty limited to calling strlen, and printing strings which are constant and "inline"). 115 | 116 | We also lack the meta-programming facilities that FORTH users would expect, in a FORTH system it is possible to implement new control-flow systems, for example, by working with words and the control-flow directly. Instead in this system these things are unavailable, and the implementation of IF/DO/LOOP/ELSE/THEN are handled in the golang-code in a way users cannot modify. 117 | 118 | Basically we ignore the common FORTH-approach of using a return-stack, and implementing a VM with "cells". Instead we just emulate the _behaviour_ of the more advanced words: 119 | 120 | * So we implement `if` or `do`/`loop` in a hard-coded fashion. 121 | * That means we can't allow a user to define `while`, or similar. 122 | * But otherwise our language is flexible enough to allow _real_ work to be done with it. 123 | 124 | 125 | 126 | ## Implementation Approach 127 | 128 | The code evolves through a series of simple steps, [contained in the comment-thread](https://news.ycombinator.com/item?id=13082825), ultimately ending with a [final revision](#final-revision) which is actually useful, usable, and pretty flexible. 129 | 130 | While it would certainly be possible to further improve the implementation I'm going to declare this project as "almost complete" for my own tastes: 131 | 132 | * I'll make minor changes, as they occur to me. 133 | * Comments, test-cases, and similar are fair game. 134 | * Outright crashes will be resolved, if I spot any. 135 | * But no major new features will be added. 136 | 137 | If **you** wanted to extend things further then there are some obvious things to work upon: 138 | 139 | * Adding more of the "standard" FORTH-words. 140 | * For example we're missing `pow`, etc. 141 | * Enhanced the string-support, to allow an input/read from the user, and other primitives. 142 | * strcat, strstr, and similar C-like operations would be useful. 143 | * Simplify the conditional/loop handling. 144 | * Both of these probably involve using a proper return-stack. 145 | * This would have the side-effect of allowing new control-flow primitives to be added. 146 | * As well as more meta-programming. 147 | 148 | Pull-requests adding additional functionality will be accepted with thanks. 149 | 150 | 151 | 152 | ## Implementation Overview 153 | 154 | Each subdirectory within this repository gets a bit further down the comment-chain. 155 | 156 | In terms of implementation two files are _largely_ unchanged in each example: 157 | 158 | * `stack.go`, which contains a simple stack of `float64` numbers. 159 | * `main.go`, contains a simple REPL/driver. 160 | * The final few examples will also allow loading a startup-file, if present. 161 | 162 | Each example builds upon the previous ones, with a pair of implementation files that change: 163 | 164 | * `builtins.go` contains the forth-words implemented in golang. 165 | * `eval.go` is the workhorse which implements to FORTH-like interpreter. 166 | * This allows executing existing words, and defining new ones. 167 | 168 | 169 | ### Part 1 170 | 171 | Part one of the implementation only deals with hard-coded execution 172 | of "words". It only supports the basic mathematical operations, along 173 | with the ability to print the top-most entry of the stack: 174 | 175 | cd part1 176 | go build . 177 | ./part1 178 | > 2 3 + 4 5 + * print 179 | 45.000000 180 | ^D 181 | 182 | See [part1/](part1/) for details. 183 | 184 | 185 | ### Part 2 186 | 187 | Part two allows the definition of new words in terms of existing ones, 188 | which can even happen recursively. 189 | 190 | We've added `dup` to pop an item off the stack, and push it back twice, which 191 | has the ultimate effect of duplicating it. 192 | 193 | To demonstrate the self-definition there is the new function `square` which 194 | squares the number at the top of the stack. 195 | 196 | cd part2 197 | go build . 198 | ./part2 199 | > 3 square . 200 | 9.000000 201 | > 3 dup + . 202 | 6.000000 203 | ^D 204 | 205 | See [part2/](part2/) for details. 206 | 207 | 208 | ### Part 3 209 | 210 | Part three allows the user to define their own words, right from within the 211 | REPL! 212 | 213 | This means we've removed the `square` implementation, because you can add your own: 214 | 215 | cd part3 216 | go build . 217 | ./part3 218 | > : square dup * ; 219 | > : cube dup square * ; 220 | > 3 cube . 221 | 27.000000 222 | > 25 square . 223 | 625.000000 224 | ^D 225 | 226 | See [part3/](part3/) for details. 227 | 228 | **NOTE**: We don't support using numbers in definitions, yet. That will come in part4! 229 | 230 | 231 | ### Part 4 232 | 233 | Part four allows the user to define their own words, including the use of numbers, from within the REPL. Here the magic is handling the input of numbers when in "compiling mode". 234 | 235 | To support this we switched our `Words` array from `int` to `float64`, specifically to ensure that we could continue to support floating-point numbers. 236 | 237 | cd part4 238 | go build . 239 | ./part4 240 | > : add1 1 + ; 241 | > -100 add1 . 242 | -99.000000 243 | > 4 add1 . 244 | 5.000000 245 | ^D 246 | 247 | See [part4/](part4/) for details. 248 | 249 | 250 | ### Part 5 251 | 252 | This part adds `do` and `loop`, allowing simple loops, and `emit` which outputs the ASCII character stored in the topmost stack-entry. 253 | 254 | Sample usage would look like this: 255 | 256 | ``` 257 | > : cr 10 emit ; 258 | > : star 42 emit ; 259 | > : stars 0 do star loop cr ; 260 | > 4 stars 261 | **** 262 | > 5 stars 263 | ***** 264 | > 1 stars 265 | * 266 | > 10 stars 267 | ********** 268 | ^D 269 | ``` 270 | 271 | Here we've defined two new words `cr` to print a return, and `star` to output a single star. 272 | 273 | We then defined the `stars` word to use a loop to print the given number of stars. 274 | 275 | 276 | (Note that the character `*` has the ASCII code 42). 277 | 278 | `do` and `loop` are pretty basic, allowing only loops to be handled which increment by one each iteration. You cannot use the standard `i` token to get the current index, instead you can see them on the stack: 279 | 280 | * Top-most entry is the current index. 281 | * Second entry is the limit. 282 | 283 | So to write out numbers you could try something like this, using `dup` to duplicate the current offset within the loop: 284 | 285 | > : l 10 0 do dup . loop ; 286 | > l 287 | 0.000000 288 | 1.000000 289 | 2.000000 290 | .. 291 | 8.000000 292 | 9.000000 293 | 294 | > : nums 10 0 do dup 48 + emit loop ; 295 | > nums 296 | 0123456789> 297 | 298 | See [part5/](part5/) for details. 299 | 300 | 301 | ### Part 6 302 | 303 | This update adds a lot of new primitives to our dictionary of predefined words: 304 | 305 | * `drop` - Removes an item from the stack. 306 | * `swap` - Swaps the top-most two stack-items. 307 | * `words` - Outputs a list of all defined words. 308 | * `<`, `<=`, `=` (`==` as a synonym), `>`, `>=` 309 | * Remove two items from the stack, and compare them appropriately. 310 | * If the condition is true push `1` onto the stack, otherwise `0`. 311 | * The biggest feature here is the support for using `if` & `then`, which allow conditional actions to be carried out. 312 | * (These are why we added the comparison operations.) 313 | 314 | In addition to these new primitives the driver, `main.go`, was updated to load and evaluate [foth.4th](part6/foth.4th) on-startup if it is present. 315 | 316 | Sample usage: 317 | 318 | cd part6 319 | go build . 320 | ./part6 321 | > : hot 72 emit 111 emit 116 emit 10 emit ; 322 | > : cold 67 emit 111 emit 108 emit 100 emit 10 emit ; 323 | > : test_hot 0 > if hot then ; 324 | > : test_cold 0 <= if cold then ; 325 | > : test dup test_hot test_cold ; 326 | > 10 test 327 | Hot 328 | > 0 test 329 | Cold 330 | > -1 test 331 | Cold 332 | > 10 test_hot 333 | Hot 334 | > 10 test_cold 335 | > -1 test_cold 336 | Cold 337 | ^D 338 | 339 | See [part6/](part6/) for the code. 340 | 341 | **NOTE**: The `if` handler allows: 342 | 343 | : foo $COND IF word1 [word2 .. wordN] then [more_word1 more_word2 ..] ; 344 | 345 | This means if the condition is true then we run `word1`, `word2` .. and otherwise we skip them, and continue running after the `then` statement. Specifically note there is **no support for `else`**. That is why we call the `test_host` and `test_cold` words in our `test` definition. Each word tests separately. 346 | 347 | As an example: 348 | 349 | > : foo 0 > if star star then star star cr ; 350 | 351 | If the test-passes, because you give a positive number, you'll see FOUR stars. if it fails you just get TWO: 352 | 353 | > 2 foo 354 | **** 355 | > 1 foo 356 | **** 357 | > 0 foo 358 | ** 359 | > -1 foo 360 | ** 361 | 362 | This is because the code is synonymous with the following C-code: 363 | 364 | if ( x > 0 ) { 365 | printf("*"); 366 | printf("*"); 367 | } 368 | printf("*"); 369 | printf("*"); 370 | printf("\n"); 371 | 372 | I found this page useful, it also documents `invert` which I added for completeness: 373 | 374 | * https://www.forth.com/starting-forth/4-conditional-if-then-statements/ 375 | 376 | 377 | ### Part 7 378 | 379 | This update adds a basic level of support for strings. 380 | 381 | * When we see a string we store it in an array of strings. 382 | * We then push the offset of the new string entry onto the stack. 383 | * This allows it to be referenced and used. 384 | * Three new words are added: 385 | * `strings` Return the number of strings we've seen/stored. 386 | * `strlen` show the length of the string at the given address. 387 | * `strprn` print the string at the given address. 388 | 389 | Sample usage: 390 | 391 | cd part7 392 | go build . 393 | ./part7 394 | > : steve "steve" ; 395 | > steve strlen . 396 | 5 397 | > steve strprn . 398 | steve 399 | ^D 400 | 401 | See [part7/](part7/) for the code. 402 | 403 | 404 | ### Final Revision 405 | 406 | The final version, stored beneath [foth/](foth/), is pretty similar to the previous part from an end-user point of view, however there have been a lot of changes behind the scenes: 407 | 408 | * We've added near 100% test-coverage. 409 | * We've added a simple [lexer](foth/lexer/) to tokenize our input. 410 | * This was required to allow us to ignore comments, and handle string literals. 411 | * Merely splitting input-strings at whitespace characters would have made either of those impossible to handle correctly. 412 | * The `if` handling has been updated to support an `else`-branch, the general form is now: 413 | * `$COND IF word1 [ .. wordN ] else alt_word1 [.. altN] then [more_word1 more_word2 ..]` 414 | * It is now possible to use `if`, `else`, `then`, `do`, and `loop` outside word-definitions. 415 | * i.e. Immediately in the REPL. 416 | * `do`/`loop` loops can be nested. 417 | * And the new words `i` and `m` used to return the current index and maximum index, respectively. 418 | * There were many new words defined in the go-core: 419 | * `.s` to show the stack-contents. 420 | * `clearstack` to clear the stack. 421 | * `debug` to change the debug-flag. 422 | * `debug?` to reveal the status. 423 | * `dump` dumps the compiled form of the given word. 424 | * You can view the definitions of all available words this: 425 | * `#words 0 do i dump loop` 426 | * `#words` to return the number of defined words. 427 | * Variables can be declared, by name, with `variable`, and the value of the variable can be set/retrieved with `@` and `!` respectively. 428 | * See this demonstrated in the [standard library](foth/foth.4th) 429 | * There were some new words defined in the [standard library](foth/foth.4th) 430 | * e.g. `abs`, `even?`, `negate`, `odd?`, 431 | * Removed all calls to `os.Exit()` 432 | * We now return `error` objects where appropriate, allowing the caller to detect problems. 433 | * It is now possible to redefine existing words. 434 | * Execute any files specified on the command line. 435 | * If no files are specified run the REPL. 436 | * We've added support for recursive definitions, in #16 for example allowing: 437 | * `: factorial recursive dup 1 > if dup 1 - factorial * then ;` 438 | 439 | See [foth/](foth/) for the implementation. 440 | 441 | 442 | 443 | ## BUGS 444 | 445 | A brief list of known-issues: 446 | 447 | 448 | ### Loops 449 | 450 | The handling of loops isn't correct when there should be zero-iterations: 451 | 452 | ``` 453 | > : star 42 emit ; 454 | > : stars 0 do star loop 10 emit ; 455 | > 3 stars 456 | *** 457 | > 1 stars 458 | * 459 | > 0 stars 460 | * 461 | ^D 462 | ``` 463 | 464 | **NOTE**: In `gforth` the result of `0 0 do ... loop` is actually an __infinite__ loop, which is perhaps worse! 465 | 466 | In our `stars` definition we handle this case by explicitly testing the loop 467 | value before we proceed, only running the loop if the value is non-zero. 468 | 469 | 470 | 471 | 472 | # See Also 473 | 474 | This repository was put together after [experimenting with a scripting language](https://github.com/skx/monkey/), an [evaluation engine](https://github.com/skx/evalfilter/), putting together a [TCL-like scripting language](https://github.com/skx/critical), writing a [BASIC interpreter](https://github.com/skx/gobasic) and creating [yet another lisp](https://github.com/skx/yal). 475 | 476 | I've also played around with a couple of compilers which might be interesting to refer to: 477 | 478 | * Brainfuck compiler: 479 | * [https://github.com/skx/bfcc/](https://github.com/skx/bfcc/) 480 | * A math-compiler: 481 | * [https://github.com/skx/math-compiler](https://github.com/skx/math-compiler) 482 | 483 | 484 | 485 | 486 | # Github Setup 487 | 488 | This repository is configured to run tests upon every commit, and when pull-requests are created/updated. The testing is carried out via [.github/run-tests.sh](.github/run-tests.sh) which is used by the [github-action-tester](https://github.com/skx/github-action-tester) action. 489 | -------------------------------------------------------------------------------- /foth/README.md: -------------------------------------------------------------------------------- 1 | # foth 2 | 3 | This is our final version of the code. Compared to the previous revision it has been overhauled quite significantly, but it should still be obvious there is a common ancestry present: 4 | 5 | * We've moved a bit of the code around, into sub-packages. 6 | * We've added test-cases to __everything__. 7 | * We implement a lot of new primitives. 8 | * In [eval/builtins.go](eval/builtins.go). 9 | * And a `quit` word in the REPL, or scripts, which is hardcoded outside the builtin-package. 10 | * And also in a new file loaded at startup: [foth.4th](foth.4th). 11 | * We've reworked our implementation to be a bit more idiomatic go. 12 | * We've improved a lot of implementation details: 13 | * `do`/`loop` now work in the REPL, not just when compiled into words. 14 | * `do`/`loop` can now be nested too. 15 | * `if`/`else`/`then` work now. 16 | * If you recall in [part6](../part6/) we didn't support an else-branch. 17 | 18 | 19 | 20 | ## Building 21 | 22 | To build, and run this version: 23 | 24 | ``` 25 | go build . 26 | ./foth 27 | ``` 28 | 29 | 30 | 31 | ## Implementation 32 | 33 | No real surprises here, but we did make a bunch of changes to ease usage. 34 | 35 | We also moved to using a real `go.mod`, so you could embed the interpreter in your application if you wished: 36 | 37 | * [main.go](main.go) now does this. 38 | * It even defines a "secret word" to show how you could export your application-specific methods to the scripting-language. 39 | 40 | For example the [stack/stack.go](stack/stack.go) gained a bunch of new methods, so that we can dump the stack at run-time we need to see the size, and peek at given offsets: 41 | 42 | ``` 43 | $ ./foth 44 | Welcome to foth! 45 | > 1 2 3 46 | > .s 47 | <3> 1.000000 2.000000 3.000000 48 | > . 49 | 3 50 | > . 51 | 2 52 | > .s 53 | <1> 1.000000 54 | ``` 55 | 56 | Here we see: 57 | 58 | * We added the numbers `1`, `2`, and `3` to the stack. 59 | * Then we used the new `.s` word to dump the stack-contents. 60 | * That showed "<3>" (meaning three entries) then the values. 61 | * We removed two numbers, by printing them. 62 | * Then we dumped again. 63 | 64 | Similarly we decided to add a simple [lexer](lexer/) to allow handling string-immediates, and skipping comments without bothering our interpreter with them. 65 | 66 | The lexer also allowed us to simplify some things. We used "output star" as a simple demo a lot of times, defined like this: 67 | 68 | : star 42 emit ; 69 | 70 | With the lexer we can sneakily allow a different form: 71 | 72 | : star '*' emit ; 73 | 74 | The lexer just turns `'X'` into the ASCII code for the character X. Simple, but useful. 75 | 76 | We added error-testing, at parse/run-time. And allow the state to be dumped. 77 | 78 | * As noted already we can see the list of words via `words`. 79 | * We can see the count of words via `#words .`. 80 | * And we can dump a word with `32 dump`. 81 | * Dumping all words is as simple as: 82 | * `#words 0 do i dump loop` 83 | * We show the debug-state via `debug?` and allow it to be toggled via: 84 | * `debug? invert debug` 85 | * We added variable-support, both defining then getting/setting. 86 | 87 | 88 | 89 | ## Where next? 90 | 91 | Pull-requests for new primitives, cleanups, etc are most welcome. 92 | 93 | But I'm probably done .. although it is quite addictive working with DSL like this! 94 | 95 | Steve 96 | -- 97 | -------------------------------------------------------------------------------- /foth/eval/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | // 5 | // We've added `emit` here, to output the value at the top of the stack 6 | // as an ASCII character, as well as "do" (nop) and "loop". 7 | 8 | package eval 9 | 10 | import ( 11 | "fmt" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | func (e *Eval) binOp(op func(float64, float64) float64) func() error { 17 | return func() error { 18 | a, err := e.Stack.Pop() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | b, err := e.Stack.Pop() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | e.Stack.Push(op(a, b)) 29 | return nil 30 | } 31 | } 32 | 33 | func (e *Eval) add() error { 34 | return e.binOp(func(n float64, m float64) float64 { return n + m })() 35 | } 36 | 37 | func (e *Eval) clearStack() error { 38 | for !e.Stack.IsEmpty() { 39 | e.Stack.Pop() 40 | } 41 | return nil 42 | } 43 | 44 | func (e *Eval) debugSet() error { 45 | 46 | v, err := e.Stack.Pop() 47 | if err != nil { 48 | return err 49 | } 50 | if v == 0 { 51 | e.debug = false 52 | } else { 53 | e.debug = true 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (e *Eval) debugp() error { 60 | if e.debug { 61 | e.Stack.Push(1) 62 | } else { 63 | e.Stack.Push(0) 64 | } 65 | return nil 66 | } 67 | 68 | func (e *Eval) div() error { 69 | return e.binOp(func(n float64, m float64) float64 { return m / n })() 70 | } 71 | 72 | func (e *Eval) drop() error { 73 | _, err := e.Stack.Pop() 74 | return err 75 | } 76 | 77 | func (e *Eval) dump() error { 78 | a, err := e.Stack.Pop() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | e.dumpWord(int(a)) 84 | return nil 85 | } 86 | 87 | func (e *Eval) dup() error { 88 | a, err := e.Stack.Pop() 89 | if err != nil { 90 | return err 91 | } 92 | e.Stack.Push(a) 93 | e.Stack.Push(a) 94 | 95 | return nil 96 | } 97 | 98 | func (e *Eval) emit() error { 99 | a, err := e.Stack.Pop() 100 | if err != nil { 101 | return err 102 | } 103 | e.printString(string(rune(a))) 104 | return nil 105 | } 106 | 107 | func (e *Eval) eq() error { 108 | return e.binOp(func(n float64, m float64) float64 { 109 | if m == n { 110 | return 1 111 | } 112 | return 0 113 | })() 114 | } 115 | 116 | func (e *Eval) getVar() error { 117 | 118 | offset, err := e.Stack.Pop() 119 | if err != nil { 120 | return err 121 | } 122 | val := e.vars[int(offset)] 123 | e.Stack.Push(val.Value) 124 | return nil 125 | } 126 | 127 | func (e *Eval) gt() error { 128 | return e.binOp(func(n float64, m float64) float64 { 129 | if m > n { 130 | return 1 131 | } 132 | return 0 133 | })() 134 | } 135 | 136 | func (e *Eval) gtEq() error { 137 | return e.binOp(func(n float64, m float64) float64 { 138 | if m >= n { 139 | return 1 140 | } 141 | return 0 142 | })() 143 | } 144 | 145 | func (e *Eval) i() error { 146 | if len(e.loops) > 0 { 147 | i := e.loops[len(e.loops)-1].Current 148 | e.Stack.Push(float64(i)) 149 | return nil 150 | } 151 | return fmt.Errorf("you cannot access 'i' outside a loop-body") 152 | } 153 | 154 | func (e *Eval) invert() error { 155 | v, err := e.Stack.Pop() 156 | if err != nil { 157 | return err 158 | } 159 | if v == 0 { 160 | e.Stack.Push(1) 161 | } else { 162 | e.Stack.Push(0) 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (e *Eval) loop() error { 169 | return nil 170 | } 171 | 172 | func (e *Eval) lt() error { 173 | return e.binOp(func(n float64, m float64) float64 { 174 | if m < n { 175 | return 1 176 | } 177 | return 0 178 | })() 179 | } 180 | 181 | func (e *Eval) ltEq() error { 182 | return e.binOp(func(n float64, m float64) float64 { 183 | if m <= n { 184 | return 1 185 | } 186 | return 0 187 | })() 188 | } 189 | 190 | func (e *Eval) max() error { 191 | return e.binOp(func(n float64, m float64) float64 { 192 | if m > n { 193 | return m 194 | } 195 | return n 196 | })() 197 | } 198 | 199 | func (e *Eval) min() error { 200 | return e.binOp(func(n float64, m float64) float64 { 201 | if m < n { 202 | return m 203 | } 204 | return n 205 | })() 206 | } 207 | 208 | func (e *Eval) m() error { 209 | if len(e.loops) > 0 { 210 | m := e.loops[len(e.loops)-1].Max 211 | e.Stack.Push(float64(m)) 212 | return nil 213 | } 214 | 215 | return fmt.Errorf("you cannot access 'm' outside a loop-body") 216 | } 217 | 218 | func (e *Eval) mod() error { 219 | return e.binOp(func(n float64, m float64) float64 { 220 | return float64(int(m) % int(n)) 221 | })() 222 | } 223 | 224 | func (e *Eval) mul() error { 225 | return e.binOp(func(n float64, m float64) float64 { return m * n })() 226 | } 227 | 228 | func (e *Eval) nop() error { 229 | return nil 230 | } 231 | 232 | func (e *Eval) over() error { 233 | a, err := e.Stack.Pop() 234 | if err != nil { 235 | return err 236 | } 237 | b, err := e.Stack.Pop() 238 | if err != nil { 239 | return err 240 | } 241 | 242 | e.Stack.Push(b) 243 | e.Stack.Push(a) 244 | e.Stack.Push(b) 245 | return nil 246 | } 247 | func (e *Eval) print() error { 248 | n, err := e.Stack.Pop() 249 | if err != nil { 250 | return err 251 | } 252 | e.printNumber(n) 253 | e.printString("\n") 254 | return nil 255 | } 256 | 257 | func (e *Eval) setVar() error { 258 | offset, err := e.Stack.Pop() 259 | if err != nil { 260 | return err 261 | } 262 | value, err2 := e.Stack.Pop() 263 | if err2 != nil { 264 | return err2 265 | } 266 | e.vars[int(offset)].Value = value 267 | return nil 268 | } 269 | 270 | func (e *Eval) stackDump() error { 271 | l := e.Stack.Len() 272 | e.printString(fmt.Sprintf(" ", l)) 273 | 274 | c := 0 275 | for c < l { 276 | e.printNumber(e.Stack.At(c)) 277 | e.printString(" ") 278 | c++ 279 | } 280 | e.printString("\n") 281 | return nil 282 | 283 | } 284 | 285 | // startDefinition moves us into compiling-mode 286 | // 287 | // Note the interpreter handles removing this when it sees ";" 288 | func (e *Eval) startDefinition() error { 289 | e.compiling = true 290 | return nil 291 | } 292 | 293 | // strings 294 | func (e *Eval) stringCount() error { 295 | // Return the number of strings we've seen 296 | e.Stack.Push(float64(len(e.strings))) 297 | return nil 298 | } 299 | 300 | // strlen 301 | func (e *Eval) strlen() error { 302 | addr, err := e.Stack.Pop() 303 | if err != nil { 304 | return err 305 | } 306 | 307 | i := int(addr) 308 | 309 | if i < len(e.strings) { 310 | str := e.strings[i] 311 | e.Stack.Push(float64(len(str))) 312 | return nil 313 | } 314 | 315 | return fmt.Errorf("invalid stack offset for string reference") 316 | 317 | } 318 | 319 | // strprn - string printing 320 | func (e *Eval) strprn() error { 321 | addr, err := e.Stack.Pop() 322 | if err != nil { 323 | return err 324 | } 325 | 326 | i := int(addr) 327 | 328 | if i < len(e.strings) { 329 | str := e.strings[i] 330 | e.printString(str) 331 | return nil 332 | } 333 | 334 | return fmt.Errorf("invalid stack offset for string reference") 335 | } 336 | 337 | func (e *Eval) sub() error { 338 | return e.binOp(func(n float64, m float64) float64 { return m - n })() 339 | } 340 | 341 | func (e *Eval) swap() error { 342 | var a, b float64 343 | var err error 344 | b, err = e.Stack.Pop() 345 | if err != nil { 346 | return err 347 | } 348 | a, err = e.Stack.Pop() 349 | if err != nil { 350 | return err 351 | } 352 | e.Stack.Push(b) 353 | e.Stack.Push(a) 354 | 355 | return nil 356 | } 357 | 358 | func (e *Eval) variable() error { 359 | e.defining = true 360 | return nil 361 | } 362 | 363 | func (e *Eval) words() error { 364 | known := []string{} 365 | 366 | for _, entry := range e.Dictionary { 367 | 368 | // Skip any word that contains a " " in its name, 369 | // this covers "$ $" which is a hack to execute 370 | // immediately-compiled words 371 | if !strings.Contains(entry.Name, " ") { 372 | known = append(known, entry.Name) 373 | } 374 | } 375 | 376 | sort.Strings(known) 377 | fmt.Printf("%s\n", strings.Join(known, " ")) 378 | 379 | return nil 380 | } 381 | 382 | func (e *Eval) wordLen() error { 383 | known := []string{} 384 | 385 | for _, entry := range e.Dictionary { 386 | 387 | // Skip any word that contains a " " in its name, 388 | // this covers "$ $" which is a hack to execute 389 | // immediately-compiled words 390 | if !strings.Contains(entry.Name, " ") { 391 | known = append(known, entry.Name) 392 | } 393 | } 394 | 395 | e.Stack.Push(float64(len(known))) 396 | return nil 397 | } 398 | -------------------------------------------------------------------------------- /foth/eval/builtins_test.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestAdd(t *testing.T) { 9 | 10 | e := New() 11 | 12 | // empty stack 13 | err := e.add() 14 | if err == nil { 15 | t.Fatalf("expected error with empty stack") 16 | } 17 | 18 | // only one item 19 | e.Stack.Push(1) 20 | err = e.add() 21 | if err == nil { 22 | t.Fatalf("expected error with empty stack") 23 | } 24 | 25 | // two items 26 | e.Stack.Push(1.2) 27 | e.Stack.Push(1.1) 28 | err = e.add() 29 | if err != nil { 30 | t.Fatalf("expected no error, but got one") 31 | } 32 | 33 | x, _ := e.Stack.Pop() 34 | if x != 2.3 { 35 | t.Fatalf("wrong result for add") 36 | } 37 | } 38 | 39 | func TestDebug(t *testing.T) { 40 | 41 | e := New() 42 | 43 | // debug is off 44 | err := e.debugp() 45 | if err != nil { 46 | t.Fatalf("unexpected error") 47 | } 48 | 49 | var val float64 50 | val, err = e.Stack.Pop() 51 | if err != nil { 52 | t.Errorf("unexpected error") 53 | } 54 | if val != 0 { 55 | t.Fatalf("debug value is wrong") 56 | } 57 | 58 | // empty stack 59 | err = e.debugSet() 60 | if err == nil { 61 | t.Fatalf("expected error with empty stack; got none") 62 | } 63 | 64 | // set debug 65 | e.Stack.Push(1) 66 | err = e.debugSet() 67 | if err != nil { 68 | t.Fatalf("unexpected error") 69 | } 70 | 71 | // Getting it should confirm it is set. 72 | err = e.debugp() 73 | if err != nil { 74 | t.Fatalf("unexpected error") 75 | } 76 | val, err = e.Stack.Pop() 77 | if err != nil { 78 | t.Errorf("unexpected error") 79 | } 80 | if val != 1 { 81 | t.Fatalf("debug value is wrong") 82 | } 83 | 84 | // Now set it off 85 | e.Stack.Push(0) 86 | err = e.debugSet() 87 | if err != nil { 88 | t.Fatalf("unexpected error") 89 | } 90 | 91 | // and it should be off 92 | err = e.debugp() 93 | if err != nil { 94 | t.Fatalf("unexpected error") 95 | } 96 | val, err = e.Stack.Pop() 97 | if err != nil { 98 | t.Errorf("unexpected error") 99 | } 100 | if val != 0 { 101 | t.Fatalf("debug value is wrong") 102 | } 103 | 104 | } 105 | func TestDiv(t *testing.T) { 106 | 107 | e := New() 108 | 109 | // empty stack 110 | err := e.div() 111 | if err == nil { 112 | t.Fatalf("expected error with empty stack") 113 | } 114 | 115 | // only one item 116 | e.Stack.Push(1) 117 | err = e.div() 118 | if err == nil { 119 | t.Fatalf("expected error with empty stack") 120 | } 121 | 122 | // two items 123 | e.Stack.Push(9) 124 | e.Stack.Push(3) 125 | err = e.div() 126 | if err != nil { 127 | t.Fatalf("expected no error, but got one") 128 | } 129 | 130 | x, _ := e.Stack.Pop() 131 | if x != 3 { 132 | t.Fatalf("wrong result for add") 133 | } 134 | } 135 | 136 | func TestDrop(t *testing.T) { 137 | 138 | e := New() 139 | if e.drop() == nil { 140 | t.Fatalf("expected error, got none") 141 | } 142 | 143 | e.Stack.Push(12) 144 | if e.drop() != nil { 145 | t.Fatalf("unexpected error") 146 | } 147 | if !e.Stack.IsEmpty() { 148 | t.Fatalf("stack should be empty now") 149 | } 150 | } 151 | 152 | func TestDump(t *testing.T) { 153 | 154 | e := New() 155 | 156 | // test with empty stack 157 | err := e.dump() 158 | if err == nil { 159 | t.Fatalf("expected error, got none") 160 | } 161 | 162 | // count the words 163 | if e.wordLen() != nil { 164 | t.Fatalf("unexpected error") 165 | } 166 | 167 | count, err := e.Stack.Pop() 168 | if err != nil { 169 | t.Fatalf("stack error") 170 | } 171 | 172 | // for each word 173 | cur := 0 174 | for cur < int(count) { 175 | 176 | e.Stack.Push(float64(cur)) 177 | e.dump() 178 | 179 | if !e.Stack.IsEmpty() { 180 | t.Fatalf("stack surprise") 181 | } 182 | cur++ 183 | } 184 | } 185 | func TestDup(t *testing.T) { 186 | 187 | e := New() 188 | if e.dup() == nil { 189 | t.Fatalf("expected error, got none") 190 | } 191 | 192 | e.Stack.Push(12) 193 | if e.dup() != nil { 194 | t.Fatalf("unexpected error") 195 | } 196 | 197 | // should now have two entries 198 | _, err := e.Stack.Pop() 199 | if err != nil { 200 | t.Errorf("unexpected error") 201 | } 202 | _, err2 := e.Stack.Pop() 203 | if err2 != nil { 204 | t.Errorf("unexpected error") 205 | } 206 | if !e.Stack.IsEmpty() { 207 | t.Fatalf("stack should be empty now") 208 | } 209 | } 210 | 211 | func TestEmit(t *testing.T) { 212 | 213 | e := New() 214 | if e.emit() == nil { 215 | t.Fatalf("expected error, got none") 216 | } 217 | 218 | e.Stack.Push(12) 219 | if e.emit() != nil { 220 | t.Fatalf("unexpected error") 221 | } 222 | } 223 | 224 | func TestEq(t *testing.T) { 225 | 226 | e := New() 227 | 228 | // empty stack 229 | err := e.eq() 230 | if err == nil { 231 | t.Fatalf("expected error with empty stack") 232 | } 233 | 234 | // only one item 235 | e.Stack.Push(1) 236 | err = e.eq() 237 | if err == nil { 238 | t.Fatalf("expected error with empty stack") 239 | } 240 | 241 | // two items 242 | e.Stack.Push(1) 243 | e.Stack.Push(1) 244 | err = e.eq() 245 | if err != nil { 246 | t.Fatalf("expected no error, but got one") 247 | } 248 | 249 | // equal 250 | x, _ := e.Stack.Pop() 251 | if x != 1 { 252 | t.Fatalf("eq() failed") 253 | } 254 | 255 | // two items 256 | e.Stack.Push(12) 257 | e.Stack.Push(1) 258 | err = e.eq() 259 | if err != nil { 260 | t.Fatalf("expected no error, but got one") 261 | } 262 | 263 | // non-equal 264 | x, _ = e.Stack.Pop() 265 | if x != 0 { 266 | t.Fatalf("eq() failed") 267 | } 268 | } 269 | 270 | func TestGetVar(t *testing.T) { 271 | 272 | e := New() 273 | 274 | // Need to have a variable set before it can be retrieved 275 | e.SetVariable("foo", 93.2) 276 | 277 | // empty stack 278 | err := e.getVar() 279 | if err == nil { 280 | t.Fatalf("expected error with empty stack") 281 | } 282 | 283 | // get the first variable. 284 | e.Stack.Push(0) 285 | err = e.getVar() 286 | if err != nil { 287 | t.Fatalf("unexpected error") 288 | } 289 | 290 | // The value should match 291 | val, err := e.Stack.Pop() 292 | if err != nil { 293 | t.Fatalf("stack underflow") 294 | } 295 | 296 | if val != 93.2 { 297 | t.Fatalf("getvar('foo') had %f not %f", val, 93.2) 298 | } 299 | 300 | } 301 | 302 | func TestGt(t *testing.T) { 303 | 304 | e := New() 305 | 306 | // empty stack 307 | err := e.gt() 308 | if err == nil { 309 | t.Fatalf("expected error with empty stack") 310 | } 311 | 312 | // only one item 313 | e.Stack.Push(1) 314 | err = e.gt() 315 | if err == nil { 316 | t.Fatalf("expected error with empty stack") 317 | } 318 | 319 | // two items 320 | e.Stack.Push(10) 321 | e.Stack.Push(1) 322 | err = e.gt() 323 | if err != nil { 324 | t.Fatalf("expected no error, but got one") 325 | } 326 | 327 | // gt 328 | x, _ := e.Stack.Pop() 329 | if x != 1 { 330 | t.Fatalf("gt() failed") 331 | } 332 | 333 | // two items 334 | e.Stack.Push(1) 335 | e.Stack.Push(1) 336 | err = e.gt() 337 | if err != nil { 338 | t.Fatalf("expected no error, but got one") 339 | } 340 | 341 | // not-gt 342 | x, _ = e.Stack.Pop() 343 | if x != 0 { 344 | t.Fatalf("gt() failed") 345 | } 346 | } 347 | 348 | func TestGtEq(t *testing.T) { 349 | 350 | e := New() 351 | 352 | // empty stack 353 | err := e.gtEq() 354 | if err == nil { 355 | t.Fatalf("expected error with empty stack") 356 | } 357 | 358 | // only one item 359 | e.Stack.Push(1) 360 | err = e.gtEq() 361 | if err == nil { 362 | t.Fatalf("expected error with empty stack") 363 | } 364 | 365 | // two items 366 | e.Stack.Push(1) 367 | e.Stack.Push(1) 368 | err = e.gtEq() 369 | if err != nil { 370 | t.Fatalf("expected no error, but got one") 371 | } 372 | 373 | // gt 374 | x, _ := e.Stack.Pop() 375 | if x != 1 { 376 | t.Fatalf(">=() failed") 377 | } 378 | 379 | // two items 380 | e.Stack.Push(-1) 381 | e.Stack.Push(1) 382 | err = e.gtEq() 383 | if err != nil { 384 | t.Fatalf("expected no error, but got one") 385 | } 386 | 387 | // not->= 388 | x, _ = e.Stack.Pop() 389 | if x != 0 { 390 | t.Fatalf("gt() failed") 391 | } 392 | } 393 | 394 | func TestInvert(t *testing.T) { 395 | 396 | e := New() 397 | 398 | // empty stack 399 | err := e.invert() 400 | if err == nil { 401 | t.Fatalf("expected error with empty stack") 402 | } 403 | 404 | // 0 -> 1 405 | e.Stack.Push(0) 406 | e.invert() 407 | out, err2 := e.Stack.Pop() 408 | if err2 != nil { 409 | t.Errorf("unexpected error") 410 | } 411 | if out != 1 { 412 | t.Errorf("unexpected result") 413 | } 414 | 415 | // 10 -> 0 416 | e.Stack.Push(10) 417 | e.invert() 418 | out, err2 = e.Stack.Pop() 419 | if err2 != nil { 420 | t.Errorf("unexpected error") 421 | } 422 | if out != 0 { 423 | t.Errorf("unexpected result") 424 | } 425 | 426 | } 427 | 428 | func TestLt(t *testing.T) { 429 | 430 | e := New() 431 | 432 | // empty stack 433 | err := e.lt() 434 | if err == nil { 435 | t.Fatalf("expected error with empty stack") 436 | } 437 | 438 | // only one item 439 | e.Stack.Push(1) 440 | err = e.lt() 441 | if err == nil { 442 | t.Fatalf("expected error with empty stack") 443 | } 444 | 445 | // two items 446 | e.Stack.Push(10) 447 | e.Stack.Push(22) 448 | err = e.lt() 449 | if err != nil { 450 | t.Fatalf("expected no error, but got one") 451 | } 452 | 453 | // lt 454 | x, _ := e.Stack.Pop() 455 | if x != 1 { 456 | t.Fatalf("lt() failed") 457 | } 458 | 459 | // two items 460 | e.Stack.Push(1) 461 | e.Stack.Push(1) 462 | err = e.lt() 463 | if err != nil { 464 | t.Fatalf("expected no error, but got one") 465 | } 466 | 467 | // not-lt 468 | x, _ = e.Stack.Pop() 469 | if x != 0 { 470 | t.Fatalf("lt() failed") 471 | } 472 | } 473 | 474 | func TestLtEq(t *testing.T) { 475 | 476 | e := New() 477 | 478 | // empty stack 479 | err := e.ltEq() 480 | if err == nil { 481 | t.Fatalf("expected error with empty stack") 482 | } 483 | 484 | // only one item 485 | e.Stack.Push(1) 486 | err = e.ltEq() 487 | if err == nil { 488 | t.Fatalf("expected error with empty stack") 489 | } 490 | 491 | // two items 492 | e.Stack.Push(1) 493 | e.Stack.Push(1) 494 | err = e.ltEq() 495 | if err != nil { 496 | t.Fatalf("expected no error, but got one") 497 | } 498 | 499 | // lt 500 | x, _ := e.Stack.Pop() 501 | if x != 1 { 502 | t.Fatalf("<=() failed") 503 | } 504 | 505 | // two items 506 | e.Stack.Push(10) 507 | e.Stack.Push(-22) 508 | err = e.ltEq() 509 | if err != nil { 510 | t.Fatalf("expected no error, but got one") 511 | } 512 | 513 | // not-<= 514 | x, _ = e.Stack.Pop() 515 | if x != 0 { 516 | t.Fatalf("<=() failed") 517 | } 518 | } 519 | 520 | func TestMod(t *testing.T) { 521 | 522 | e := New() 523 | 524 | // empty stack 525 | err := e.mod() 526 | if err == nil { 527 | t.Fatalf("expected error with empty stack") 528 | } 529 | 530 | // only one item 531 | e.Stack.Push(1) 532 | err = e.mod() 533 | if err == nil { 534 | t.Fatalf("expected error with empty stack") 535 | } 536 | 537 | type TestCase struct { 538 | in float64 539 | out float64 540 | } 541 | 542 | tests := []TestCase{{in: 1, out: 1}, 543 | {in: 2, out: 2}, 544 | {in: 3, out: 3}, 545 | {in: 4, out: 0}, 546 | {in: 5, out: 1}, 547 | {in: 6, out: 2}, 548 | {in: 7, out: 3}, 549 | {in: 8, out: 0}, 550 | {in: 9, out: 1}, 551 | {in: 10, out: 2}, 552 | } 553 | 554 | for _, test := range tests { 555 | // two items 556 | e.Stack.Push(test.in) 557 | e.Stack.Push(4) 558 | err = e.mod() 559 | if err != nil { 560 | t.Fatalf("expected no error, but got one") 561 | } 562 | 563 | x, _ := e.Stack.Pop() 564 | if x != test.out { 565 | t.Fatalf("wrong result %f %% 4. Got %f, not %f", test.in, x, test.out) 566 | } 567 | } 568 | } 569 | 570 | func TestMul(t *testing.T) { 571 | 572 | e := New() 573 | 574 | // empty stack 575 | err := e.mul() 576 | if err == nil { 577 | t.Fatalf("expected error with empty stack") 578 | } 579 | 580 | // only one item 581 | e.Stack.Push(1) 582 | err = e.mul() 583 | if err == nil { 584 | t.Fatalf("expected error with empty stack") 585 | } 586 | 587 | // two items 588 | e.Stack.Push(6) 589 | e.Stack.Push(7) 590 | err = e.mul() 591 | if err != nil { 592 | t.Fatalf("expected no error, but got one") 593 | } 594 | 595 | x, _ := e.Stack.Pop() 596 | if x != 42 { 597 | t.Fatalf("wrong result for mul") 598 | } 599 | } 600 | 601 | func TestNop(t *testing.T) { 602 | 603 | e := New() 604 | if e.nop() != nil { 605 | t.Fatalf("unexpected error") 606 | } 607 | } 608 | 609 | func TestOver(t *testing.T) { 610 | 611 | e := New() 612 | 613 | // empty stack 614 | err := e.over() 615 | if err == nil { 616 | t.Fatalf("expected error with empty stack") 617 | } 618 | 619 | // int 620 | e.Stack.Push(3) 621 | err = e.over() 622 | if err == nil { 623 | t.Fatalf("expected underflow") 624 | } 625 | 626 | // OK now we add 2 1 627 | e.Stack.Push(2) 628 | e.Stack.Push(1) 629 | err = e.over() 630 | if err != nil { 631 | t.Fatalf("unexpected error") 632 | } 633 | 634 | // stack should now be: 2 1 2 635 | v, er := e.Stack.Pop() 636 | if er != nil { 637 | t.Fatalf("unexpected error") 638 | } 639 | if v != 2 { 640 | t.Fatalf("unexpected error") 641 | } 642 | 643 | v, er = e.Stack.Pop() 644 | if er != nil { 645 | t.Fatalf("unexpected error") 646 | } 647 | if v != 1 { 648 | t.Fatalf("unexpected error") 649 | } 650 | 651 | v, er = e.Stack.Pop() 652 | if er != nil { 653 | t.Fatalf("unexpected error") 654 | } 655 | if v != 2 { 656 | t.Fatalf("unexpected error") 657 | } 658 | 659 | if !e.Stack.IsEmpty() { 660 | t.Fatalf("unexpected stack content") 661 | } 662 | 663 | } 664 | func TestPrint(t *testing.T) { 665 | e := New() 666 | 667 | // empty stack 668 | err := e.print() 669 | if err == nil { 670 | t.Fatalf("expected error with empty stack") 671 | } 672 | 673 | // int 674 | e.Stack.Push(3) 675 | err = e.print() 676 | if err != nil { 677 | t.Fatalf("unexpected error") 678 | } 679 | 680 | if !e.Stack.IsEmpty() { 681 | t.Fatalf("stack should be empty now") 682 | 683 | } 684 | 685 | f := 5 / 4.0 686 | 687 | // float 688 | e.Stack.Push(f) 689 | a, _ := e.Stack.Pop() 690 | if a != 1.25 { 691 | t.Fatalf("not a float?: %f", a) 692 | } 693 | e.Stack.Push(f) 694 | err = e.print() 695 | if err != nil { 696 | t.Fatalf("unexpected error") 697 | } 698 | 699 | } 700 | 701 | func TestSetVar(t *testing.T) { 702 | 703 | e := New() 704 | 705 | // Setup a variable 706 | e.SetVariable("name", 6) 707 | 708 | // empty stack 709 | err := e.setVar() 710 | if err == nil { 711 | t.Fatalf("expected error with empty stack") 712 | } 713 | 714 | // only one item 715 | e.Stack.Push(1) 716 | err = e.setVar() 717 | if err == nil { 718 | t.Fatalf("expected error with empty stack") 719 | } 720 | 721 | // Now set 722 | e.Stack.Push(32.1) 723 | e.Stack.Push(0) 724 | err = e.setVar() 725 | if err != nil { 726 | t.Fatalf("unexpected error") 727 | } 728 | 729 | // confirm it worked 730 | v, err := e.GetVariable("name") 731 | if err != nil { 732 | t.Fatalf("unexpected error") 733 | } 734 | if v != 32.1 { 735 | t.Fatalf("value mismatch after setting variable") 736 | } 737 | 738 | } 739 | 740 | // strings counts the string literals, and will return 741 | // a number on the stack 742 | func TestStrings(t *testing.T) { 743 | 744 | e := New() 745 | 746 | // Empty stack on first start 747 | n := e.Stack.Len() 748 | if n != 0 { 749 | t.Fatalf("failing result for stringCount, got %d", n) 750 | } 751 | 752 | // call the function 753 | err := e.stringCount() 754 | if err != nil { 755 | t.Fatalf("unexpected error with stringCount %s", err.Error()) 756 | } 757 | 758 | n = e.Stack.Len() 759 | if n != 1 { 760 | t.Fatalf("failing result for stringCount, got %d", n) 761 | os.Exit(1) 762 | } 763 | } 764 | 765 | func TestStrlen(t *testing.T) { 766 | 767 | e := New() 768 | e.strings = append(e.strings, "Steve") 769 | 770 | // call the function 771 | err := e.strlen() 772 | if err == nil { 773 | t.Fatalf("expected an error, got none") 774 | } 775 | 776 | // Empty stack on first start 777 | n := e.Stack.Len() 778 | if n != 0 { 779 | t.Fatalf("failing result for strlen, got %d", n) 780 | } 781 | 782 | // push an invalid string 783 | e.Stack.Push(100.0) 784 | 785 | // call the function 786 | err = e.strlen() 787 | if err == nil { 788 | t.Fatalf("expected an error, got none") 789 | } 790 | 791 | // Now try to get the length of Steve 792 | e.Stack.Push(0.0) 793 | err = e.strlen() 794 | if err != nil { 795 | t.Fatalf("unexpected error, calling strlen %s", err.Error()) 796 | } 797 | 798 | // Is the result expected? 799 | x, _ := e.Stack.Pop() 800 | if x != 5 { 801 | t.Fatalf("wrong result for strlen, got %f", x) 802 | } 803 | } 804 | 805 | func TestStrPrn(t *testing.T) { 806 | 807 | e := New() 808 | 809 | // We want to avoid spamming stdout, so our string to print is "empty" 810 | e.strings = append(e.strings, "") 811 | 812 | // call the function 813 | err := e.strprn() 814 | if err == nil { 815 | t.Fatalf("expected an error, got none") 816 | } 817 | 818 | // Empty stack on first start 819 | n := e.Stack.Len() 820 | if n != 0 { 821 | t.Fatalf("failing result for strprn, got %d", n) 822 | } 823 | 824 | // push an invalid string 825 | e.Stack.Push(100.0) 826 | 827 | // call the function 828 | err = e.strprn() 829 | if err == nil { 830 | t.Fatalf("expected an error, got none") 831 | } 832 | 833 | // Now try to get the length of Steve 834 | e.Stack.Push(0.0) 835 | err = e.strprn() 836 | if err != nil { 837 | t.Fatalf("unexpected error, calling strprn %s", err.Error()) 838 | } 839 | 840 | // Empty stack, still? 841 | n = e.Stack.Len() 842 | if n != 0 { 843 | t.Fatalf("failing result for strprn, got %d", n) 844 | } 845 | } 846 | 847 | func TestSub(t *testing.T) { 848 | 849 | e := New() 850 | 851 | // empty stack 852 | err := e.sub() 853 | if err == nil { 854 | t.Fatalf("expected error with empty stack") 855 | } 856 | 857 | // only one item 858 | e.Stack.Push(1) 859 | err = e.sub() 860 | if err == nil { 861 | t.Fatalf("expected error with empty stack") 862 | } 863 | 864 | // two items 865 | e.Stack.Push(6) 866 | e.Stack.Push(4) 867 | err = e.sub() 868 | if err != nil { 869 | t.Fatalf("expected no error, but got one") 870 | } 871 | 872 | x, _ := e.Stack.Pop() 873 | if x != 2 { 874 | t.Fatalf("wrong result for sub, got %f", x) 875 | } 876 | } 877 | 878 | func TestSwap(t *testing.T) { 879 | 880 | e := New() 881 | 882 | // empty stack 883 | err := e.swap() 884 | if err == nil { 885 | t.Fatalf("expected error with empty stack") 886 | } 887 | 888 | // only one item 889 | e.Stack.Push(1) 890 | err = e.swap() 891 | if err == nil { 892 | t.Fatalf("expected error with empty stack") 893 | } 894 | 895 | // two items 896 | e.Stack.Push(3) 897 | e.Stack.Push(1) 898 | err = e.swap() 899 | if err != nil { 900 | t.Fatalf("expected no error, but got one") 901 | } 902 | 903 | a, err := e.Stack.Pop() 904 | if err != nil { 905 | t.Fatalf("unexpected error") 906 | } 907 | b, err2 := e.Stack.Pop() 908 | if err2 != nil { 909 | t.Fatalf("unexpected error") 910 | } 911 | if a != 3 { 912 | t.Fatalf("unexpected error, got %f", a) 913 | } 914 | if b != 1 { 915 | t.Fatalf("unexpected error, got %f", b) 916 | } 917 | } 918 | 919 | func TestWords(t *testing.T) { 920 | 921 | e := New() 922 | if e.words() != nil { 923 | t.Fatalf("unexpected error") 924 | } 925 | } 926 | 927 | func TestWordLen(t *testing.T) { 928 | 929 | e := New() 930 | if e.wordLen() != nil { 931 | t.Fatalf("unexpected error") 932 | } 933 | 934 | if e.Stack.IsEmpty() { 935 | t.Fatalf("expected stack entry") 936 | } 937 | 938 | count, err := e.Stack.Pop() 939 | if err != nil { 940 | t.Fatalf("stack error") 941 | } 942 | if int(count) > len(e.Dictionary) { 943 | t.Fatalf("too many wrds") 944 | } 945 | } 946 | -------------------------------------------------------------------------------- /foth/eval/eval.go: -------------------------------------------------------------------------------- 1 | // Package eval contains our simple forth-like interpreter. 2 | package eval 3 | 4 | import ( 5 | "bufio" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/skx/foth/foth/lexer" 13 | "github.com/skx/foth/foth/stack" 14 | ) 15 | 16 | // Loop holds the state of a running do/while loop. 17 | type Loop struct { 18 | // Start is the starting number our loop begins from. 19 | Start int 20 | 21 | // Max holds the terminating number our loop finishes at. 22 | Max int 23 | 24 | // Current holds the current number of the iteration. 25 | Current int 26 | } 27 | 28 | // Variable is the structure for storing variable names, and contents 29 | type Variable struct { 30 | // Name is the name of the variable 31 | Name string 32 | 33 | // Value is the value we store within it. 34 | Value float64 35 | } 36 | 37 | // Word is the structure for a single word. 38 | type Word struct { 39 | // Name is the name of the function "+", "print", etc. 40 | Name string 41 | 42 | // Function is the function-pointer to call to invoke it. 43 | // 44 | // If this is nil then instead we interpret the known-codes 45 | // from previously defined words. 46 | Function func() error 47 | 48 | // Words holds the words we execute if the function-pointer 49 | // is empty. 50 | // 51 | // The indexes here are relative to the Dictionary our evaluator 52 | // holds/maintains 53 | // 54 | // NOTE: We specifically store `float64` here so that we can add 55 | // floats to the stack when compiling. 56 | Words []float64 57 | 58 | // Does this word switch us into immediate-mode? 59 | StartImmediate bool 60 | 61 | // Does this word end us from immediate-mode? 62 | EndImmediate bool 63 | 64 | // Does this word recurse? 65 | Recursive bool 66 | } 67 | 68 | // Eval is our evaluation structure, which holds state of where 69 | // we're executing code from. 70 | // 71 | // The state includes the variables, inline-strings, etc. 72 | type Eval struct { 73 | 74 | // Stack holds our operands. 75 | Stack stack.Stack 76 | 77 | // Dictionary entries 78 | Dictionary []Word 79 | 80 | // STDOUT is the writer used for `.`, `print`, and `emit` words 81 | STDOUT *bufio.Writer 82 | 83 | // Private details 84 | 85 | // Literal strings. As encountered in our program. 86 | strings []string 87 | 88 | // Have we already bumped our immediate-count? 89 | // 90 | // This is a gross-hack to account for the fact that 91 | // compileToken needs to bump the immediate-count when 92 | // it sees a nested do, or similar token. 93 | bumped bool 94 | 95 | // Are we in a compiling mode? 96 | compiling bool 97 | 98 | // Are we in debug-mode? 99 | debug bool 100 | 101 | // Are we in immediate-mode? 102 | immediate int 103 | 104 | // Temporary word we're compiling 105 | tmp Word 106 | 107 | // We keep a stack of the last time we saw a `do` token, 108 | // so we can pair it with the appropriate matching `loop`. 109 | doOpen []int 110 | 111 | // When we generate IF-statements we have to patch one or two 112 | // offsets, depending on whether there is an ELSE branch present 113 | // or not. 114 | // 115 | // Here we keep track of them 116 | ifOffset1 int 117 | ifOffset2 int 118 | 119 | // Loops stores loops which are currently open. 120 | // 121 | // When we compile `do` we add a new one, when the `loop` 122 | // word is completed we remove one. 123 | loops []Loop 124 | 125 | // Variables 126 | vars []Variable 127 | 128 | // Are we currently defining a variable? 129 | defining bool 130 | } 131 | 132 | var ( 133 | // ErrQuit will be used to handle a QUIT from the REPL. 134 | // 135 | // It should be expected as a normal, non-fatal, error from the Eval function. 136 | ErrQuit = errors.New("RETURN") 137 | ) 138 | 139 | // New returns a simple evaluator, which will allow executing forth-like words. 140 | func New() *Eval { 141 | 142 | // Empty structure 143 | e := &Eval{} 144 | 145 | // Are we debugging? 146 | if os.Getenv("DEBUG") != "" { 147 | e.debug = true 148 | } 149 | 150 | // Populate our built-in functions. 151 | e.Dictionary = []Word{ 152 | // comparisons 153 | {Name: "<", Function: e.lt}, 154 | {Name: "<=", Function: e.ltEq}, 155 | {Name: "=", Function: e.eq}, 156 | {Name: "==", Function: e.eq}, // synonym 157 | {Name: ">", Function: e.gt}, 158 | {Name: ">=", Function: e.gtEq}, 159 | 160 | // conditionals 161 | {Name: "else", Function: e.nop}, 162 | {Name: "if", Function: e.nop, StartImmediate: true}, 163 | {Name: "then", Function: e.nop, EndImmediate: true}, 164 | 165 | // debug-handling 166 | {Name: "debug", Function: e.debugSet}, 167 | {Name: "debug?", Function: e.debugp}, 168 | 169 | // I/O 170 | {Name: ".", Function: e.print}, 171 | {Name: ".\"", Function: e.nop}, 172 | {Name: "emit", Function: e.emit}, 173 | {Name: "print", Function: e.print}, 174 | 175 | // loop-handling 176 | {Name: "do", Function: e.nop, StartImmediate: true}, 177 | {Name: "i", Function: e.i}, 178 | {Name: "loop", Function: e.loop, EndImmediate: true}, 179 | {Name: "m", Function: e.m}, 180 | 181 | // mathematical 182 | {Name: "*", Function: e.mul}, 183 | {Name: "+", Function: e.add}, 184 | {Name: "-", Function: e.sub}, 185 | {Name: "/", Function: e.div}, 186 | {Name: "max", Function: e.max}, 187 | {Name: "min", Function: e.min}, 188 | {Name: "mod", Function: e.mod}, 189 | 190 | // misc 191 | {Name: "nop", Function: e.nop}, 192 | 193 | // stack-related 194 | {Name: ".s", Function: e.stackDump}, 195 | {Name: "clearstack", Function: e.clearStack}, 196 | {Name: "drop", Function: e.drop}, 197 | {Name: "dup", Function: e.dup}, 198 | {Name: "invert", Function: e.invert}, 199 | {Name: "over", Function: e.over}, 200 | {Name: "swap", Function: e.swap}, 201 | 202 | // variable-handling 203 | {Name: "!", Function: e.setVar}, 204 | {Name: "@", Function: e.getVar}, 205 | {Name: "variable", Function: e.variable}, 206 | 207 | // word-handling 208 | {Name: "#words", Function: e.wordLen}, 209 | {Name: ":", Function: e.startDefinition}, 210 | {Name: ";", Function: e.nop}, 211 | {Name: "dump", Function: e.dump}, 212 | {Name: "words", Function: e.words}, 213 | 214 | // strings 215 | {Name: "strings", Function: e.stringCount}, 216 | {Name: "strlen", Function: e.strlen}, 217 | {Name: "strprn", Function: e.strprn}, 218 | } 219 | 220 | return e 221 | } 222 | 223 | // Eval evaluates the given expression. 224 | // 225 | // This is the main public-facing the user of this library would be expected 226 | // to use. 227 | func (e *Eval) Eval(input string) error { 228 | 229 | // Lex our input string into a series of tokens. 230 | // 231 | // This is done for two reasons: 232 | // 233 | // 1. We want to remove comments 234 | // 235 | // 2. We support inline strings, such as the following: 236 | // 237 | // ." foo bar " 238 | // 239 | // Blindly splitting on whitespace would screw those up 240 | // 241 | l := lexer.New(input) 242 | args, err := l.Tokens() 243 | if err != nil { 244 | return err 245 | } 246 | 247 | // 248 | // For each token.. 249 | // 250 | for _, token := range args { 251 | 252 | // Get the name of the token. 253 | // 254 | // The name is the only thing we care about, except 255 | // in the case of string-literals 256 | tok := token.Name 257 | 258 | // Are we defining a variable? 259 | if e.defining { 260 | if e.debug { 261 | fmt.Printf("defining variable %s\n", tok) 262 | } 263 | e.vars = append(e.vars, Variable{Name: tok}) 264 | e.defining = false 265 | continue 266 | } 267 | 268 | // Are we in compiling mode? 269 | if e.compiling || (e.immediate > 0) { 270 | 271 | // If so compile the token 272 | err := e.compileToken(token) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | // And loop around. 278 | continue 279 | } 280 | 281 | // Is this an immediate print? If so do it. 282 | if token.Type == lexer.PSTRING { 283 | e.printString(token.Value) 284 | continue 285 | } 286 | 287 | // Quit is also special-cased. 288 | if tok == "quit" { 289 | return ErrQuit 290 | } 291 | 292 | // Lookup this word from our dictionary 293 | idx := e.findWord(tok) 294 | if idx != -1 { 295 | 296 | // Are we starting immediate mode? 297 | if !e.compiling && e.Dictionary[idx].StartImmediate { 298 | e.immediate++ 299 | e.bumped = true 300 | 301 | // Errors here can't happen. 302 | // 303 | // We only compile at the start 304 | // of conditionals, and they can't 305 | // happen. 306 | e.compileToken(token) 307 | } else { 308 | 309 | err := e.evalWord(idx) 310 | if err != nil { 311 | return err 312 | } 313 | } 314 | } else { 315 | 316 | // Is this a variable? If so push the variable offset 317 | idx = e.findVariable(tok) 318 | if idx >= 0 { 319 | e.Stack.Push(float64(idx)) 320 | continue 321 | } 322 | 323 | // String 324 | if token.Type == lexer.STRING { 325 | e.strings = append(e.strings, token.Value) 326 | e.Stack.Push(float64(len(e.strings) - 1)) 327 | continue 328 | } 329 | 330 | // If we didn't handle this as a word, then 331 | // assume it is a number. 332 | i, err := strconv.ParseFloat(tok, 64) 333 | if err != nil { 334 | return fmt.Errorf("11 failed to convert %s to number %s", tok, err.Error()) 335 | } 336 | 337 | e.Stack.Push(i) 338 | } 339 | } 340 | 341 | return nil 342 | } 343 | 344 | // GetVariable returns the contents of the specified variable. 345 | // 346 | // This is designed to be used by host-applications which embed 347 | // this library. 348 | func (e *Eval) GetVariable(name string) (float64, error) { 349 | 350 | idx := e.findVariable(name) 351 | if idx >= 0 { 352 | return e.vars[idx].Value, nil 353 | } 354 | 355 | return 0, fmt.Errorf("variable %s not found", name) 356 | } 357 | 358 | // Reset updates the internal state of the evaluator, which is a useful 359 | // thing to do if `Eval` returns an error 360 | func (e *Eval) Reset() { 361 | 362 | // Clear the stack 363 | for !e.Stack.IsEmpty() { 364 | e.Stack.Pop() 365 | } 366 | 367 | // reset our state 368 | e.defining = false 369 | e.immediate = 0 370 | e.compiling = false 371 | 372 | // we're not defining anything 373 | e.tmp.Name = "" 374 | e.tmp.Words = []float64{} 375 | 376 | // we're not in a do/loop 377 | e.doOpen = []int{} 378 | e.loops = []Loop{} 379 | 380 | // we're not in a conditional 381 | e.ifOffset1 = 0 382 | e.ifOffset2 = 0 383 | } 384 | 385 | // SetVariable stores the specified value in the variable of the given 386 | // name. 387 | // 388 | // This is designed to be used by host-applications which embed 389 | // this library. 390 | func (e *Eval) SetVariable(name string, value float64) { 391 | 392 | idx := e.findVariable(name) 393 | if idx >= 0 { 394 | e.vars[idx].Value = value 395 | return 396 | } 397 | 398 | e.vars = append(e.vars, Variable{Name: name, Value: value}) 399 | } 400 | 401 | // SetWriter allows you to setup a special writer for all STDOUT 402 | // messages this application will produce. 403 | // 404 | // i.e. Writes from `.`, `emit`, `print`, and string-immediates will 405 | // go there. 406 | func (e *Eval) SetWriter(writer *bufio.Writer) { 407 | e.STDOUT = writer 408 | } 409 | 410 | // compileToken is called with a new token, when we're in compiling-mode. 411 | // 412 | // This is called in two ways: 413 | // 414 | // 1. To compile a new word. 415 | // 416 | // 2. In "immediate" mode. Where we compile a fake word, 417 | // with an impossible name (`$ $`), with the expectation 418 | // we'll then immediately execute it. 419 | func (e *Eval) compileToken(token lexer.Token) error { 420 | 421 | tok := token.Name 422 | 423 | // Did we start in immediate-mode? 424 | // 425 | // We keep track of this here, because we might 426 | // disable immediate-mode later and need to know. 427 | imm := (!e.compiling && e.immediate > 0) 428 | 429 | if imm { 430 | 431 | // In immediate mode we are going to compile 432 | // a word which has an illegal-name, which ensures 433 | // we never overwrite a valid user-word 434 | // 435 | // We'll then execute it immediately post-definition. 436 | e.tmp.Name = "$ $" 437 | } 438 | 439 | // If we don't yet have a name 440 | if e.tmp.Name == "" { 441 | 442 | // is the name used? If so remove it 443 | idx := e.findWord(tok) 444 | if idx != -1 { 445 | e.Dictionary[idx].Name = "" 446 | } 447 | 448 | // Set the name for this word. 449 | e.tmp.Name = tok 450 | return nil 451 | } 452 | 453 | // End of a definition? 454 | if tok == ";" { 455 | 456 | // Save the word to our dictionary 457 | e.tmp.Name = strings.ToLower(e.tmp.Name) 458 | e.Dictionary = append(e.Dictionary, e.tmp) 459 | 460 | // Show what we compiled each new definition 461 | // to, when running in debug-mode 462 | if e.debug { 463 | e.dumpWord(len(e.Dictionary) - 1) 464 | } 465 | 466 | // reset for the next definition 467 | e.tmp.Name = "" 468 | e.tmp.Words = []float64{} 469 | e.compiling = false 470 | return nil 471 | } 472 | 473 | if tok == "recursive" { 474 | e.tmp.Recursive = true 475 | return nil 476 | } 477 | 478 | // Is the user adding an existing word to the definition? 479 | idx := e.findWord(tok) 480 | if idx >= 0 { 481 | 482 | // 483 | // We have to do some juggling here because 484 | // when we find a word with `EndImmediate` we 485 | // terminate. 486 | // 487 | // So we have to make sure we don't terminate 488 | // early if something else opened. 489 | // 490 | // i.e. "loop" would usually terminate immediate-mode, 491 | // but we can't stop there for definitions that use 492 | // nested loops 493 | // 494 | if imm && e.Dictionary[idx].StartImmediate { 495 | if e.bumped { 496 | e.immediate-- 497 | e.bumped = false 498 | } 499 | e.immediate++ 500 | } 501 | 502 | // Found the word, add to the end. 503 | e.tmp.Words = append(e.tmp.Words, float64(idx)) 504 | 505 | // 506 | // Now some special cases. 507 | // 508 | // Horrid 509 | // 510 | // If the word was a "DO" 511 | if tok == "do" { 512 | 513 | // keep track of where we are 514 | e.doOpen = append(e.doOpen, len(e.tmp.Words)-1) 515 | 516 | // we compile this into a "new-loop" instruction 517 | e.tmp.Words = append(e.tmp.Words, -10) 518 | e.tmp.Words = append(e.tmp.Words, 99) // dull 519 | } 520 | 521 | // if the word was a "LOOP" 522 | if tok == "loop" { 523 | 524 | // "loop" without a "do" will be a fatal error 525 | if len(e.doOpen) < 1 { 526 | return fmt.Errorf("'loop' without an opening 'do'") 527 | } 528 | 529 | // We load the loop, increment, etc. 530 | e.tmp.Words = append(e.tmp.Words, -11) 531 | e.tmp.Words = append(e.tmp.Words, 99) // dull 532 | 533 | // We've bumped the instance, and pushed 534 | // a result onto the stack now. 535 | // 536 | // So we jump back to repeat if we must. 537 | e.tmp.Words = append(e.tmp.Words, -3) 538 | e.tmp.Words = append(e.tmp.Words, float64(e.doOpen[len(e.doOpen)-1]+3)) 539 | 540 | // We've matched the do-loop pair - drop the 541 | // open-reference 542 | e.doOpen = e.doOpen[:len(e.doOpen)-1] 543 | 544 | } 545 | 546 | // output a string-print operation, in compiled form 547 | if token.Name == ".\"" { 548 | e.strings = append(e.strings, token.Value) 549 | e.tmp.Words = append(e.tmp.Words, -5) 550 | e.tmp.Words = append(e.tmp.Words, float64(len(e.strings))-1) 551 | } 552 | 553 | // 554 | // Conditional support is a bit nasty. 555 | // 556 | // Basically we expect to allow someone to write 557 | // something like: 558 | // 559 | // : foo 0 < if neg else pos then 560 | // 561 | // That translates to: 562 | // 563 | // if foo < 0 { 564 | // neg 565 | // } else { 566 | // pos 567 | // } 568 | // 569 | // We want to convert that to: 570 | // 571 | // COND 572 | // IF 573 | // CONDJUMP xx 574 | // NEG CODE 575 | // JMP end 576 | // ELSE 577 | // xx: 578 | // POS CODE 579 | // THEN 580 | // end: 581 | // 582 | // In short we have to insert a conditional-jump, and 583 | // an unconditional one. 584 | // 585 | // We then use back-patching to fixup the offsets. 586 | // 587 | if tok == "if" { 588 | // reset both possible places to patch 589 | e.ifOffset1 = 0 590 | e.ifOffset2 = 0 591 | 592 | // we add the conditional-jump opcode 593 | e.tmp.Words = append(e.tmp.Words, -3) 594 | // placeholder jump-offset 595 | e.tmp.Words = append(e.tmp.Words, 99) 596 | 597 | // The offset of the last instruction is 598 | // the 99-byte we just added as a placeholder 599 | // record that. 600 | e.ifOffset1 = len(e.tmp.Words) 601 | } 602 | 603 | if tok == "else" { 604 | 605 | // no offsets defined? Then we've got a bug 606 | if e.ifOffset1 == 0 && e.ifOffset2 == 0 { 607 | return fmt.Errorf("'else' without an 'if'") 608 | } 609 | 610 | e.tmp.Words[e.ifOffset1-1] = float64(len(e.tmp.Words) + 2) 611 | 612 | // before we compile the end we have to 613 | // add a jump to after the THEN 614 | e.tmp.Words = append(e.tmp.Words, -4) 615 | e.tmp.Words = append(e.tmp.Words, 999) 616 | 617 | e.ifOffset2 = len(e.tmp.Words) 618 | } 619 | 620 | if tok == "then" { 621 | 622 | // no offsets defined? Then we've got a bug 623 | if e.ifOffset1 == 0 && e.ifOffset2 == 0 { 624 | return fmt.Errorf("'then' without an 'if'") 625 | } 626 | 627 | // back-patch the jump offset to the position of this word 628 | if e.ifOffset2 > 0 { 629 | e.tmp.Words[e.ifOffset2-1] = float64(len(e.tmp.Words)) 630 | } else { 631 | e.tmp.Words[e.ifOffset1-1] = float64(len(e.tmp.Words) - 1) 632 | } 633 | // Reset for future 634 | e.ifOffset2 = 0 635 | e.ifOffset1 = 0 636 | } 637 | 638 | // Did we just end immediate mode? 639 | if e.Dictionary[idx].EndImmediate { 640 | 641 | // If we're inside an immediate 642 | if !e.compiling && e.immediate > 0 { 643 | e.immediate-- 644 | } 645 | 646 | if e.immediate == 0 && imm { 647 | 648 | // We've compiled the word. 649 | e.Dictionary = append(e.Dictionary, e.tmp) 650 | 651 | if e.debug { 652 | fmt.Printf("Completed the temporary word - '$ $'\n") 653 | e.dumpWord(len(e.Dictionary) - 1) 654 | } 655 | 656 | // reset for the next definition 657 | e.tmp.Name = "" 658 | e.tmp.Words = []float64{} 659 | // Run it. 660 | e.evalWord(len(e.Dictionary) - 1) 661 | return nil 662 | } 663 | } 664 | 665 | return nil 666 | } 667 | 668 | // OK so we're compiling something - and it wasn't a word 669 | // was it a variable? 670 | idx = e.findVariable(tok) 671 | if idx >= 0 { 672 | // compile this into something that will push 673 | // the offset of the variable onto the stack 674 | e.tmp.Words = append(e.tmp.Words, -1) 675 | e.tmp.Words = append(e.tmp.Words, float64(idx)) 676 | return nil 677 | } 678 | 679 | // save a string, in compiled form 680 | if token.Name == "\"" { 681 | e.strings = append(e.strings, token.Value) 682 | e.tmp.Words = append(e.tmp.Words, -1) 683 | e.tmp.Words = append(e.tmp.Words, float64(len(e.strings))-1) 684 | return nil 685 | } 686 | 687 | // Convert to float 688 | val, err := strconv.ParseFloat(tok, 64) 689 | if err != nil { 690 | 691 | if e.tmp.Recursive { 692 | e.tmp.Words = append(e.tmp.Words, float64(len(e.Dictionary))) 693 | return nil 694 | } 695 | return fmt.Errorf("22 failed to convert %s to number %s", tok, err.Error()) 696 | } 697 | 698 | // At this point we assume the user entered a number 699 | // so we save a magic "-1" flag in our 700 | // definition, and then the number itself 701 | e.tmp.Words = append(e.tmp.Words, -1) 702 | e.tmp.Words = append(e.tmp.Words, val) 703 | 704 | return nil 705 | } 706 | 707 | // dumpWord dumps the definition of the given word. 708 | func (e *Eval) dumpWord(idx int) { 709 | 710 | // Ensure we have a valid index 711 | if idx >= len(e.Dictionary) || idx < 0 { 712 | fmt.Printf("Invalid index\n") 713 | return 714 | } 715 | 716 | // Lookup the word 717 | word := e.Dictionary[idx] 718 | 719 | // Store temporary data here 720 | codes := []string{} 721 | 722 | // Walk over the opcodes in the word-definition 723 | off := 0 724 | for off < len(word.Words) { 725 | 726 | // Get the actual byte 727 | // 728 | // Values >=0 are references to other words. 729 | // 730 | // Values <0 are "magic", and were created via 731 | // the "compilation" process. 732 | v := word.Words[int(off)] 733 | 734 | if v == -1 { 735 | codes = append(codes, fmt.Sprintf("%d: store %f", off, word.Words[off+1])) 736 | off++ 737 | } else if v == -3 { 738 | codes = append(codes, fmt.Sprintf("%d: [cond-jmp %f]", off, word.Words[off+1])) 739 | off++ 740 | } else if v == -4 { 741 | codes = append(codes, fmt.Sprintf("%d: [jmp %f]", off, word.Words[off+1])) 742 | off++ 743 | } else if v == -5 { 744 | codes = append(codes, fmt.Sprintf("%d: [print-string %f (\"%s\")]", off, word.Words[off+1], e.strings[int(word.Words[off+1])])) 745 | off++ 746 | } else if v == -10 { 747 | codes = append(codes, fmt.Sprintf("%d: [new-loop]", off)) 748 | off++ 749 | } else if v == -11 { 750 | codes = append(codes, fmt.Sprintf("%d: [loop-test]", off)) 751 | off++ 752 | } else { 753 | codes = append(codes, fmt.Sprintf("%d: %s", off, e.Dictionary[int(v)].Name)) 754 | } 755 | 756 | // keep going for further words 757 | off++ 758 | } 759 | 760 | // Didn't decompile? Then it was a native-word 761 | if len(codes) == 0 { 762 | fmt.Printf("Word '%s' - [Native]\n", word.Name) 763 | } else { 764 | // Otherwise show the bytecode. 765 | fmt.Printf("Word '%s'\n %s\n", word.Name, strings.Join(codes, "\n ")) 766 | } 767 | } 768 | 769 | // evalWord evaluates a word, by index from the dictionary 770 | // 771 | // * Functions might contain a pointer to a function implemented in go. 772 | // 773 | // If so we just call that pointer. 774 | // 775 | // - Functions will otherwise have lists of numbers, which point to 776 | // previously defined words. 777 | // 778 | // In addition to the pointers to previously-defined words there are 779 | // also some special values: 780 | // 781 | // "-1" means the next value is a number 782 | // 783 | // "-3" is a conditional-jump, which will change our IP if 784 | // the topmost item on the stack is "0". 785 | // 786 | // "-4" is an unconditional jump, which will change our IP 787 | // 788 | // "-5" prints a string, stored in our literal-area. 789 | // Dynamic strings are not supported. 790 | // 791 | // "-10" creates a new Loop structure. 792 | // (i.e. `do`). 793 | // 794 | // "-11" handles the test/termination of a loop condition. 795 | // (i.e. `loop`). 796 | func (e *Eval) evalWord(index int) error { 797 | 798 | // Lookup the word in our dictionary. 799 | word := e.Dictionary[index] 800 | 801 | // Is this implemented in golang? If so just invoke the function 802 | // and we're done. 803 | if word.Function != nil { 804 | 805 | if e.debug { 806 | fmt.Printf(" calling built-in word %s\n", word.Name) 807 | } 808 | err := word.Function() 809 | return err 810 | } 811 | 812 | if e.debug { 813 | fmt.Printf(" calling dynamic stuff\n") 814 | } 815 | 816 | // 817 | // We use a simple state-machine to handle some of our 818 | // "opcodes". Opcodes are basically indexes into our 819 | // dictionary of previously-defined words, and also some 820 | // special codes (which are less than zero). 821 | // 822 | // The reason this is handled like this, is to avoid poking the 823 | // indexes directly and risking array-overflow on malformed 824 | // word-lists. 825 | // 826 | // (i.e. When we see "[1, 2, -1]" the last instruction should add 827 | // the following number to the stack - but it is missing. We want 828 | // to avoid messing around with the index to avoid that.) 829 | // 830 | 831 | state := "default" 832 | 833 | // We need to allow control-jumps now, so we 834 | // have to store our index manually. 835 | ip := 0 836 | for ip < len(word.Words) { 837 | 838 | // the current opcode 839 | opcode := word.Words[ip] 840 | 841 | // adding a number? 842 | if state == "add-number" { 843 | if e.debug { 844 | fmt.Printf(" storing %f on stack\n", opcode) 845 | } 846 | 847 | // add to stack 848 | e.Stack.Push(opcode) 849 | 850 | state = "default" 851 | } else if state == "string-print" { 852 | // print a string 853 | e.printString(e.strings[int(opcode)]) 854 | state = "default" 855 | 856 | } else if state == "cond-jump" { 857 | // Jump only if 0 is on the top of the stack. 858 | // 859 | // i.e. This is an "if" test. 860 | val, err := e.Stack.Pop() 861 | if err != nil { 862 | return err 863 | } 864 | 865 | if val == 0 { 866 | if e.debug { 867 | 868 | fmt.Printf(" popped %f from stack - jumping to %f\n", val, opcode) 869 | } 870 | // change opcode 871 | ip = int(opcode) 872 | // decrement as it'll get bumped at 873 | // the foot of the loop 874 | ip-- 875 | } else { 876 | if e.debug { 877 | fmt.Printf(" popped %f from stack - not making conditional jump\n", val) 878 | } 879 | } 880 | state = "default" 881 | } else if state == "new-loop" { 882 | 883 | // given the two-values on the stack 884 | // create and save a new Loop structure 885 | // to describe this loop. 886 | cur, err := e.Stack.Pop() 887 | if err != nil { 888 | return err 889 | } 890 | 891 | max, err2 := e.Stack.Pop() 892 | if err2 != nil { 893 | return err2 894 | } 895 | 896 | // new loop 897 | l := Loop{ 898 | Start: int(cur), 899 | Max: int(max), 900 | Current: int(cur), 901 | } 902 | 903 | // save it away 904 | e.loops = append(e.loops, l) 905 | state = "default" 906 | } else if state == "loop-test" { 907 | 908 | // we've working with the last loop 909 | l := len(e.loops) - 1 910 | 911 | // bump the count 912 | e.loops[l].Current++ 913 | 914 | // test to see if the loop is over 915 | if e.loops[l].Current >= e.loops[l].Max { 916 | e.Stack.Push(1) 917 | 918 | // loop is over now 919 | e.loops = e.loops[:len(e.loops)-1] 920 | } else { 921 | e.Stack.Push(0) 922 | } 923 | 924 | state = "default" 925 | } else if state == "jump" { 926 | 927 | // change opcode 928 | ip = int(opcode) 929 | // decrement as it'll get bumped at 930 | // the foot of the loop 931 | ip-- 932 | state = "default" 933 | } else if state == "default" { 934 | 935 | switch opcode { 936 | case -1: 937 | state = "add-number" 938 | case -3: 939 | state = "cond-jump" 940 | case -4: 941 | state = "jump" 942 | case -5: 943 | state = "string-print" 944 | case -10: 945 | state = "new-loop" 946 | case -11: 947 | state = "loop-test" 948 | default: 949 | err := e.evalWord(int(opcode)) 950 | if err != nil { 951 | return err 952 | } 953 | } 954 | } else { 955 | return fmt.Errorf("unknown state '%s'", state) 956 | } 957 | 958 | // next instruction 959 | ip++ 960 | } 961 | 962 | return nil 963 | } 964 | 965 | // findVariable returns the index of the specified variable in our list 966 | // of variables. 967 | // 968 | // Returns -1 if the variable cannot be found. 969 | // 970 | // Yes we store these in an array, rather than a map. That's because 971 | // we want to differentiate between an undefined variable and one with 972 | // no value. 973 | func (e *Eval) findVariable(name string) int { 974 | for i, v := range e.vars { 975 | if v.Name == name { 976 | return i 977 | } 978 | } 979 | return -1 980 | } 981 | 982 | // findWords returns the index of the specified word in our dictionary. 983 | // 984 | // Returns -1 if the word cannot be found. 985 | func (e *Eval) findWord(name string) int { 986 | name = strings.ToLower(name) 987 | 988 | for index, word := range e.Dictionary { 989 | if name == word.Name { 990 | return index 991 | } 992 | } 993 | return -1 994 | } 995 | 996 | // printNumber - outputs a floating-point number. However if the 997 | // value is actually an integer then that is displayed instead. 998 | func (e *Eval) printNumber(n float64) { 999 | 1000 | // If the value on the top of the stack is an integer 1001 | // then show it as one - i.e. without any ".00000". 1002 | if float64(int(n)) == n { 1003 | e.printString(fmt.Sprintf("%d", int(n))) 1004 | return 1005 | } 1006 | 1007 | // OK we have a floating-point result. Show it, but 1008 | // remove any trailing "0". 1009 | // 1010 | // This means we get 1.25 instead of 1.2500000 shown 1011 | // when the user runs `5 4 / .`. 1012 | // 1013 | output := fmt.Sprintf("%f", n) 1014 | 1015 | for strings.HasSuffix(output, "0") { 1016 | output = strings.TrimSuffix(output, "0") 1017 | } 1018 | e.printString(output) 1019 | } 1020 | 1021 | // printString outputs a string, taking into account that 1022 | // STDOUT might have been replaced via `SetWriter`. 1023 | func (e *Eval) printString(str string) { 1024 | 1025 | if e.STDOUT == nil { 1026 | e.STDOUT = bufio.NewWriter(os.Stdout) 1027 | } 1028 | 1029 | e.STDOUT.WriteString(str) 1030 | e.STDOUT.Flush() 1031 | } 1032 | -------------------------------------------------------------------------------- /foth/eval/eval_test.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestBasic(t *testing.T) { 12 | 13 | e := New() 14 | e.debug = true 15 | 16 | // stack-underflow 17 | out := e.Eval(" . ") 18 | if out == nil { 19 | t.Fatalf("expected error, got none") 20 | } 21 | 22 | // quit 23 | out = e.Eval("quit") 24 | if out == nil { 25 | t.Fatalf("expected error, got none") 26 | } 27 | if out != ErrQuit { 28 | t.Fatalf("got error, but the wrong one") 29 | } 30 | 31 | // nested-comments 32 | out = e.Eval(" ( one ( two ) ) ") 33 | if out == nil { 34 | t.Fatalf("expected error, got none") 35 | } 36 | 37 | // no error 38 | out = e.Eval(".\" Hello, World \" ") 39 | if out != nil { 40 | t.Fatalf("unexpected") 41 | } 42 | } 43 | 44 | func TestClearWords(t *testing.T) { 45 | 46 | // create instance 47 | e := New() 48 | e.debug = true 49 | 50 | // Push some stuff 51 | err := e.Eval("1 3 4 5") 52 | if err != nil { 53 | t.Fatalf("unexpected error") 54 | } 55 | 56 | // Ensure it is non-empty 57 | if e.Stack.IsEmpty() { 58 | t.Fatalf("unexpected stack") 59 | } 60 | if e.Stack.Len() != 4 { 61 | t.Fatalf("unexpected stack") 62 | } 63 | 64 | // Clear the stack 65 | err = e.Eval(".s clearstack") 66 | if err != nil { 67 | t.Fatalf("unexpected error") 68 | } 69 | 70 | // Ensure it is non-empty 71 | if !e.Stack.IsEmpty() { 72 | t.Fatalf("unexpected stack") 73 | } 74 | if e.Stack.Len() != 0 { 75 | t.Fatalf("unexpected stack") 76 | } 77 | 78 | } 79 | func TestDumpWords(t *testing.T) { 80 | 81 | // dummy test 82 | os.Setenv("DEBUG", "1") 83 | e := New() 84 | 85 | if e.debug != true { 86 | t.Fatalf("putenv didn't enable debugging") 87 | } 88 | 89 | // test definitions 90 | tests := []string{": star 42 emit ;", 91 | ": stars 0 do star loop 10 emit ;", 92 | ": test_hot 0 > if star then star ;", 93 | ": tests 0 0 = if 1 else 2 then ;", 94 | ": tests 0 0 = if .\" test \" else .\" ok\" ;", 95 | "0 0 = if .\" test \" else .\" ok\"", 96 | } 97 | 98 | for _, str := range tests { 99 | e.Eval(str) 100 | } 101 | 102 | e.dumpWord(0) 103 | os.Setenv("DEBUG", "") 104 | } 105 | 106 | func TestError(t *testing.T) { 107 | 108 | // Some things that will generate errors 109 | tests := []string{": foo . ; foo", 110 | 111 | // two stack-items are expected for `do` 112 | ": foo do i emit loop ; foo", 113 | ": foo 2 do i emit loop ; foo", 114 | 115 | // i & m only within a loop-body 116 | ": foo i ; foo", 117 | ": foo m ; foo", 118 | 119 | // loop without a do 120 | ": foo loop ; foo", 121 | 122 | // else/then without an if 123 | ": foo else ; foo", 124 | ": foo then ; foo", 125 | 126 | // unterminated strings 127 | ".\" this is long", 128 | "\" this is long", 129 | } 130 | 131 | for _, test := range tests { 132 | e := New() 133 | e.debug = true 134 | 135 | err := e.Eval(test) 136 | if err == nil { 137 | t.Fatalf("expected error, got none for: %s", test) 138 | } 139 | } 140 | 141 | } 142 | 143 | // Try running one of each of our test-cases 144 | func TestEvalWord(t *testing.T) { 145 | 146 | // dummy test 147 | e := New() 148 | e.debug = true 149 | 150 | // test definitions 151 | tests := []string{": star 42 emit ;", 152 | ": stars 0 do star loop 10 emit ;", 153 | ": test_hot 0 > if star then ;", 154 | 155 | // deliberately redefine a word 156 | ": test_hot 0 >= if star then ;", 157 | "10 stars", 158 | "star", 159 | "10 test_hot", 160 | "-1 test_hot"} 161 | 162 | for _, str := range tests { 163 | e.Eval(str) 164 | } 165 | 166 | } 167 | 168 | func TestFloatFail(t *testing.T) { 169 | 170 | tests := []string{": foo. 3.2.1.2 emit ; foo.", 171 | "6.7.8.9 ."} 172 | 173 | for _, str := range tests { 174 | e := New() 175 | e.debug = true 176 | err := e.Eval(str) 177 | if err == nil { 178 | t.Fatalf("expected error processing '%s', got none", str) 179 | } 180 | if !strings.Contains(err.Error(), "failed to convert") { 181 | t.Fatalf("got an error, but the wrong one: %s", err.Error()) 182 | } 183 | } 184 | } 185 | 186 | func TestIfThenElse(t *testing.T) { 187 | 188 | type Test struct { 189 | input string 190 | result float64 191 | } 192 | 193 | tests := []Test{ 194 | {input: "3 3 = if 1 then", result: 1}, 195 | {input: "3 3 = if .\" ok \" 1 then", result: 1}, 196 | {input: ": f 3 3 = if 1 then ; f", result: 1}, 197 | {input: ": f 3 3 = if .\" ok \" 1 then ; f", result: 1}, 198 | 199 | {input: "3 3 = invert if 1 else 2 then", result: 2}, 200 | {input: ": f 3 3 = invert if 1 else 2 then ; f", result: 2}, 201 | 202 | {input: "3 31 = if 0 else 3 then", result: 3}, 203 | {input: ": f 3 31 = if 0 else 3 then ; f ", result: 3}, 204 | 205 | {input: "3 31 = if 1 else 12 then", result: 12}, 206 | {input: ": f 3 31 = if 1 else 12 then ; f", result: 12}, 207 | 208 | {input: "3 21 = invert if 221 else 112 then", result: 221}, 209 | {input: ": ff 3 21 = invert if 221 else 112 then ; ff ", result: 221}, 210 | } 211 | 212 | for _, test := range tests { 213 | 214 | e := New() 215 | e.debug = true 216 | err := e.Eval(test.input) 217 | if err != nil { 218 | t.Fatalf("unexpected error processing '%s': %s", test.input, err.Error()) 219 | } 220 | 221 | ret, err2 := e.Stack.Pop() 222 | if err2 != nil { 223 | t.Fatalf("failed to get stack value from %s", test.input) 224 | } 225 | if !e.Stack.IsEmpty() { 226 | t.Fatalf("%s: expected stack to be empty", test.input) 227 | } 228 | if ret != test.result { 229 | t.Fatalf("%s: %f got %f", test.input, test.result, ret) 230 | } 231 | } 232 | 233 | // Test of an if with an empty stack 234 | e := New() 235 | e.debug = true 236 | err := e.Eval(": fail if . then ; fail") 237 | if err == nil { 238 | t.Fatalf("expected error, got none") 239 | } 240 | } 241 | 242 | func TestMaxMin(t *testing.T) { 243 | 244 | errors := []string{ 245 | // no entry 246 | "max", 247 | "min", 248 | 249 | // one too few 250 | "3 max", 251 | "3 min"} 252 | 253 | for _, txt := range errors { 254 | 255 | // create instance 256 | e := New() 257 | e.debug = true 258 | 259 | err := e.Eval(txt) 260 | if err == nil { 261 | t.Fatalf("expected an error, got none") 262 | } 263 | if !strings.Contains(err.Error(), "underflow") { 264 | t.Fatalf("found wrong error: %s", err.Error()) 265 | } 266 | } 267 | 268 | type TestCase struct { 269 | Input string 270 | Result float64 271 | } 272 | 273 | tests := []TestCase{ 274 | {Input: "3 4 max", Result: 4}, 275 | {Input: "4 3 max", Result: 4}, 276 | {Input: "3 3 max", Result: 3}, 277 | {Input: "-4 -30 max", Result: -4}, 278 | 279 | {Input: "3 4 min", Result: 3}, 280 | {Input: "4 3 min", Result: 3}, 281 | {Input: "2 2 min", Result: 2}, 282 | {Input: "-4 -30 min", Result: -30}, 283 | } 284 | 285 | for _, test := range tests { 286 | 287 | // create instance 288 | e := New() 289 | e.debug = true 290 | 291 | err := e.Eval(test.Input) 292 | if err != nil { 293 | t.Fatalf("unexpected error") 294 | } 295 | 296 | ret, err2 := e.Stack.Pop() 297 | if err2 != nil { 298 | t.Fatalf("failed to get stack value") 299 | } 300 | if !e.Stack.IsEmpty() { 301 | t.Fatalf("expected stack to be empty, it wasn't") 302 | } 303 | if ret != test.Result { 304 | t.Fatalf("unexpected result for %s -> %f", test.Input, ret) 305 | } 306 | } 307 | } 308 | 309 | func TestReset(t *testing.T) { 310 | 311 | // create instance 312 | e := New() 313 | e.debug = true 314 | 315 | // Trigger error-state 316 | err := e.Eval(": foo 1 3 + .;") 317 | if err == nil { 318 | t.Fatalf("expected an error, got none") 319 | } 320 | 321 | // Ensure something is on the stack 322 | e.Stack.Push(33.1) 323 | 324 | // Now reset and run something else to confirm it worked 325 | e.Reset() 326 | 327 | // The stack should be empty 328 | if !e.Stack.IsEmpty() { 329 | t.Fatalf("expected stack to be empty, it wasn't") 330 | } 331 | 332 | err = e.Eval(": foo 1 3 + ; foo ") 333 | if err != nil { 334 | t.Fatalf("expected no error, got %s", err.Error()) 335 | } 336 | 337 | ret, err2 := e.Stack.Pop() 338 | if err2 != nil { 339 | t.Fatalf("failed to get stack value") 340 | } 341 | if !e.Stack.IsEmpty() { 342 | t.Fatalf("expected stack to be empty, it wasn't") 343 | } 344 | if ret != 4 { 345 | t.Fatalf("unexpected result, post-recovery") 346 | } 347 | } 348 | 349 | func TestVariables(t *testing.T) { 350 | 351 | // create instance 352 | e := New() 353 | e.debug = true 354 | 355 | var v float64 356 | var err error 357 | 358 | // fetching a variable will fail, as it is not present 359 | _, err = e.GetVariable("unset") 360 | if err == nil { 361 | t.Fatalf("expected error accessing a missing variable") 362 | } 363 | 364 | // Now set it 365 | e.SetVariable("unset", 22) 366 | e.SetVariable("unset", 33) 367 | 368 | v, err = e.GetVariable("unset") 369 | if err != nil { 370 | t.Fatalf("unexpected error accessing variable") 371 | } 372 | if v != 33 { 373 | t.Fatalf("variable has wrong value") 374 | } 375 | 376 | // Run a script to change the variable 377 | err = e.Eval("12 unset !") 378 | if err != nil { 379 | t.Fatalf("error running script") 380 | } 381 | 382 | // Get the value and confirm it is updated 383 | v, err = e.GetVariable("unset") 384 | if err != nil { 385 | t.Fatalf("unexpected error accessing variable") 386 | } 387 | if v != 12 { 388 | t.Fatalf("variable has wrong value") 389 | } 390 | 391 | // Finally declare a variable and set the value 392 | err = e.Eval("variable meow") 393 | if err != nil { 394 | t.Fatalf("unexpected error") 395 | } 396 | err = e.Eval("3 meow !") 397 | if err != nil { 398 | t.Fatalf("unexpected error") 399 | } 400 | 401 | // Get the value and confirm it is updated 402 | v, err = e.GetVariable("meow") 403 | if err != nil { 404 | t.Fatalf("unexpected error accessing variable") 405 | } 406 | if v != 3 { 407 | t.Fatalf("variable has wrong value") 408 | } 409 | 410 | // Double the value of the variable 411 | err = e.Eval(": double meow @ 2 * meow ! ; double") 412 | if err != nil { 413 | t.Fatalf("unexpected error") 414 | } 415 | 416 | v, err = e.GetVariable("meow") 417 | if err != nil { 418 | t.Fatalf("unexpected error accessing variable") 419 | } 420 | if v != 6 { 421 | t.Fatalf("variable has wrong value") 422 | } 423 | } 424 | 425 | func TestSetWriter(t *testing.T) { 426 | 427 | var b bytes.Buffer 428 | out := bufio.NewWriter(&b) 429 | 430 | e := New() 431 | e.debug = true 432 | e.SetWriter(out) 433 | 434 | // write something simple 435 | err := e.Eval("42 emit 42 emit") 436 | if err != nil { 437 | t.Fatalf("unexpected error") 438 | } 439 | 440 | if b.String() != "**" { 441 | t.Fatalf("STDOUT didn't match") 442 | } 443 | 444 | // write something more complex 445 | b.Reset() 446 | 447 | // i is the current loop index 448 | // m is the max 449 | // 450 | // so we're outputting "1/10", "2/10", etc. 451 | // 452 | err = e.Eval("10 0 do i 48 + emit 47 emit m . loop") 453 | if err != nil { 454 | t.Fatalf("unexpected error") 455 | } 456 | 457 | if b.String() != "0/10\n1/10\n2/10\n3/10\n4/10\n5/10\n6/10\n7/10\n8/10\n9/10\n" { 458 | t.Fatalf("STDOUT didn't match, got '%s' for ", b.String()) 459 | } 460 | 461 | // Finally a string literal 462 | b.Reset() 463 | err = e.Eval(".\"Steve\nKemp\"") 464 | if err != nil { 465 | t.Fatalf("unexpected error") 466 | } 467 | 468 | if b.String() != "Steve\nKemp" { 469 | t.Fatalf("STDOUT didn't match, got '%s'", b.String()) 470 | } 471 | } 472 | 473 | func TestStrlenEval(t *testing.T) { 474 | 475 | type Test struct { 476 | input string 477 | result float64 478 | } 479 | 480 | tests := []Test{ 481 | {input: "\"steve\" strlen", result: 5}, 482 | {input: ": foo .\"steve\" 3 ; foo ", result: 3}, 483 | {input: ": bar \"steve\" strlen ; bar ", result: 5}, 484 | {input: ": factorial recursive dup 1 > if dup 1 - factorial * then ; 6 factorial", result: 720}, 485 | } 486 | 487 | for _, test := range tests { 488 | 489 | e := New() 490 | e.debug = true 491 | err := e.Eval(test.input) 492 | if err != nil { 493 | t.Fatalf("unexpected error processing '%s': %s", test.input, err.Error()) 494 | } 495 | 496 | ret, err2 := e.Stack.Pop() 497 | if err2 != nil { 498 | t.Fatalf("failed to get stack value from %s", test.input) 499 | } 500 | if !e.Stack.IsEmpty() { 501 | t.Fatalf("%s: expected stack to be empty", test.input) 502 | } 503 | if ret != test.result { 504 | t.Fatalf("%s: %f got %f", test.input, test.result, ret) 505 | } 506 | } 507 | 508 | } 509 | -------------------------------------------------------------------------------- /foth/foth.4th: -------------------------------------------------------------------------------- 1 | \ 2 | \ This file is loaded on-startup, if it is present. 3 | \ 4 | \ In FORTH there are two kinds of comments: 5 | \ 6 | \ 1. Anything between \ and a newline will be skipped. 7 | \ 8 | \ 2. Anything between brackets `(` + `)` will be skipped. 9 | \ These comments can span lines, although they typically do not. 10 | \ 11 | \ NOTE: Nesting `(` and `)` comments is a syntax error, so the following 12 | \ is explicitly denied: 13 | \ 14 | \ ( comment ( comment again ) ) 15 | \ 16 | 17 | \ 18 | \ Declare a variable named `PI` 19 | \ 20 | variable PI 21 | 22 | \ 23 | \ Set the value of PI to be the expected constant. 24 | \ 25 | \ The following web-reference is useful reading for variable-access, even 26 | \ though our support is slightly different: 27 | \ 28 | \ https://www.forth.com/starting-forth/8-variables-constants-arrays/ 29 | \ 30 | 3.14 PI ! 31 | 32 | \ 33 | \ Variables can be retrieved, and displayed, like so: 34 | \ 35 | \ PI @ . 36 | 37 | 38 | \ 39 | \ Of course you can modify them inside words. 40 | \ 41 | \ The following example sets a variable "val", and allows showing/incrementing 42 | \ it 43 | \ 44 | variable val 45 | 46 | \ 47 | \ A simple helper to get the content of a variable and show it. 48 | \ 49 | : ? @ . ; 50 | 51 | \ 52 | \ Get the value of the variable named "val", and show it 53 | \ 54 | : show_val val ? ; 55 | 56 | \ 57 | \ Get the value, increase by one, and store it back 58 | \ 59 | : inc_val val @ 1 + val ! ; 60 | 61 | \ 62 | \ Get the value, decrement, and store. 63 | \ 64 | : dec_val val @ 1 - val ! ; 65 | 66 | 67 | \ 68 | \ bell: make some noise 69 | \ 70 | : bell 7 emit ; 71 | 72 | \ 73 | \ gnome-terminal won't re-ring the bell, unless there is a delay. 74 | \ 75 | : bells 76 | 3 0 do 77 | bell 78 | 3000000 0 do 79 | nop 80 | loop 81 | loop 82 | ; 83 | 84 | \ 85 | \ We define `=` (and `==`) by default, but we do not have a built-in 86 | \ function for not-equals. We can fix that now: 87 | \ 88 | : != = invert ; 89 | 90 | \ 91 | \ Similarly we do not have a built-in method for abs(N), so we 92 | \ can resolve that here 93 | \ 94 | : abs dup 0 > if 1 else -1 then * ; 95 | 96 | 97 | \ 98 | \ Is closely related to abs, which we've just defined. 99 | \ 100 | : negate ( n - n ) -1 * ; 101 | 102 | 103 | \ 104 | \ On the topic of numbers we can also test if numbers are odd, and 105 | \ even very easily. 106 | \ 107 | : even? 2 mod 0 = ; 108 | : odd? 2 mod 1 = ; 109 | 110 | 111 | \ 112 | \ CR: Output a carrige return (newline). 113 | \ 114 | : cr 10 emit ; 115 | 116 | \ 117 | \ Star: Output a star to the console. 118 | \ 119 | \ Here 42 is the ASCII code for the "*" character. 120 | \ 121 | \ If you prefer you could use: 122 | \ 123 | \ : star '*' emit ; 124 | \ 125 | : star 42 emit ; 126 | 127 | 128 | \ 129 | \ Stars: Show the specified number of stars. 130 | \ 131 | \ e.g. "3 stars" 132 | \ 133 | \ We add a test here to make sure that the user enters > 0 134 | \ as their argument 135 | \ 136 | : stars dup 0 > if 0 do star loop else drop then ; 137 | 138 | 139 | \ 140 | \ `space` and `spaces` are similar to the previous words, 141 | \ except they print space(s) instead of stars. 142 | \ 143 | : space ' ' emit ; 144 | : spaces dup 0 > if 0 do space loop else drop then ; 145 | 146 | 147 | \ 148 | \ Squares: Draw a square of stars 149 | \ 150 | \ e.g. 10 squares 151 | \ 152 | \ Here we define a loop that runs from N to 0, and we use "m" as 153 | \ the maximum-value of the loop. 154 | \ 155 | \ Inside loop-bodies we can access two variables like that: 156 | \ 157 | \ i -> The current value of the loop 158 | \ 159 | \ m -> The maximum value of the loop (which will terminate it). 160 | \ 161 | : squares 0 do 162 | m stars cr 163 | loop ; 164 | 165 | 166 | \ 167 | \ square: Square a number 168 | \ 169 | : square dup * ; 170 | 171 | \ 172 | \ cube: cube a number 173 | \ 174 | : cube dup square * ; 175 | 176 | 177 | \ 178 | \ 1+: add one to a number 179 | \ 180 | : 1+ 1 + ; 181 | 182 | 183 | \ 184 | \ boot: output a message on-startup 185 | \ 186 | : bootup ." Welcome to foth!\n " ; 187 | bootup 188 | 189 | \ 190 | \ IF test 191 | \ 192 | \ This section of the startup-file outputs either "hot" or "cold" depending 193 | \ on whether a number is <0 or not. 194 | \ 195 | \ Here we repeat our work because we did't have support for ELSE when we 196 | \ added this example. 197 | \ 198 | 199 | \ output a hot/cold message 200 | : hot ." Hot\n " ; 201 | : cold ." Cold\n " ; 202 | 203 | : test_hot 0 > if hot then ; 204 | : test_cold 0 <= if cold then ; 205 | : temp? dup test_hot test_cold ; 206 | 207 | 208 | \ 209 | \ Here we have a word that uses IF and ELSE, allowing our test to 210 | \ be simplified compared to the previous version. 211 | \ 212 | 213 | \ Output "frozen\n" 214 | : frozen ." frozen\n " ; 215 | 216 | \ Output "NOT frozen\n" 217 | : non_frozen ." NOT frozen\n " ; 218 | 219 | \ Output the appropiate message. 220 | : frozen? 0 <= if frozen else non_frozen then cr ; 221 | 222 | \ 223 | \ Or we could have written the following word to do everything 224 | \ in one-step: 225 | \ 226 | : frozen2? 0 <= if ." frozen " else ." NOT frozen " then cr ; 227 | 228 | 229 | \ 230 | \ We have to ensure we have a newline on the last line, or it will be 231 | \ ignored 232 | \ 233 | -------------------------------------------------------------------------------- /foth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skx/foth/foth 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /foth/lexer/lexer.go: -------------------------------------------------------------------------------- 1 | // Package lexer parses a string as FORTH 2 | package lexer 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | // Type holds the type of a node 9 | type Type string 10 | 11 | // Types of tokens we return 12 | const ( 13 | 14 | // STRING is a string literal, enclosed in quotes. 15 | // e.g. "Steve" 16 | STRING = "string" 17 | 18 | // PSTRING is a string literal, enclosed in quotes, 19 | // which is printed to STDOUT when encountered. 20 | // e.g. ."Steve" 21 | PSTRING = "pstring" 22 | 23 | // WORD is anything which is not a STRING or PSTRING. 24 | // 25 | // This includes words, numbers, and other symbols which 26 | // are valid. 27 | // 28 | // The interpreter will internally convert the token `3.14' 29 | // from a string to a number, when appropriate, for example, 30 | // so we don't need an INTEGER/NUMBER type. 31 | // 32 | WORD = "word" 33 | ) 34 | 35 | // Token is a single token. 36 | // 37 | // All our tokens have a name and type, only our two string-types have a value 38 | // which is used for anything. 39 | type Token struct { 40 | 41 | // Name holds the name of the token. 42 | Name string 43 | 44 | // Type holds the token-type 45 | Type Type 46 | 47 | // For the case of a string-literal we store the value here. 48 | Value string 49 | } 50 | 51 | // Lexer holds our state. 52 | type Lexer struct { 53 | 54 | // input is the string we were given 55 | input string 56 | 57 | // offset points to the character we're currently looking at 58 | offset int 59 | } 60 | 61 | // New creates a new lexer which allows parsing a string of FORTH tokens 62 | // into an array of tokens that can be interpreted. 63 | func New(input string) *Lexer { 64 | return &Lexer{input: input} 65 | } 66 | 67 | // Tokens returns all the tokens from the given input-string. 68 | func (l *Lexer) Tokens() ([]Token, error) { 69 | 70 | var res []Token 71 | 72 | // We walk the input from start to finish 73 | l.offset = 0 74 | 75 | // Value of the current token - built up character by character. 76 | cur := "" 77 | 78 | for l.offset < len(l.input) { 79 | 80 | c := l.input[l.offset] 81 | switch string(c) { 82 | 83 | case " ", "\n", "\r", "\t": 84 | 85 | // If we've built up a word then we save it away. 86 | if len(cur) != 0 { 87 | res = append(res, Token{Name: cur, Type: WORD}) 88 | cur = "" 89 | } 90 | 91 | case "\\": 92 | // Comment to the end of the line 93 | for l.offset < len(l.input) { 94 | if l.input[l.offset] == '\n' { 95 | break 96 | } 97 | l.offset++ 98 | } 99 | 100 | case "'": 101 | // We parse 'x' as the ASCII code of the character x. 102 | 103 | // can we peek ahead two characters? 104 | if l.offset+2 < len(l.input) { 105 | 106 | // confirm we have a close 107 | if string(l.input[l.offset+2]) == "'" { 108 | 109 | c = l.input[l.offset+1] 110 | d := int(c) 111 | s := fmt.Sprintf("%d", d) 112 | res = append(res, Token{Name: s, Type: WORD}) 113 | l.offset += 2 114 | } else { 115 | return res, fmt.Errorf("syntax error") 116 | } 117 | } else { 118 | return res, fmt.Errorf("unterminated single-character constant") 119 | } 120 | 121 | case "(": 122 | 123 | // skip the "(" 124 | l.offset++ 125 | 126 | // Eat the comment - which is everything 127 | // between the "(" and ")" (inclusive) 128 | // 129 | // NOTE: Nested comments are prohibited 130 | closed := false 131 | for l.offset < len(l.input) { 132 | if l.input[l.offset] == ')' { 133 | closed = true 134 | break 135 | } 136 | if l.input[l.offset] == '(' { 137 | return res, fmt.Errorf("nested comments are illegal") 138 | } 139 | l.offset++ 140 | } 141 | if !closed { 142 | return res, fmt.Errorf("unterminated comment") 143 | } 144 | 145 | // This is for strings 146 | case "\"": 147 | 148 | // skip the opening """ 149 | l.offset++ 150 | 151 | // Read the string, handling control-characters & etc 152 | str, err := l.readString() 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | res = append(res, Token{Name: "\"", Value: str, Type: STRING}) 158 | 159 | // This is for ." xxx " 160 | case ".": 161 | 162 | // ensure we don't walk off the array 163 | if l.offset+1 < len(l.input) { 164 | 165 | // next character is a string? 166 | if l.input[l.offset+1] == '"' { 167 | 168 | l.offset += 2 169 | 170 | // Read the string, handling control-characters & etc 171 | str, err := l.readString() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | res = append(res, Token{Name: ".\"", Value: str, Type: PSTRING}) 177 | } else { 178 | cur = cur + "." 179 | } 180 | } else { 181 | cur = cur + "." 182 | } 183 | default: 184 | cur = cur + string(c) 185 | } 186 | l.offset++ 187 | } 188 | 189 | // end token? 190 | if cur != "" { 191 | res = append(res, Token{Name: cur, Type: WORD}) 192 | } 193 | 194 | // All done. 195 | return res, nil 196 | } 197 | 198 | // readString is called to read until a close of the string 199 | // is encountered. (i.e. "). 200 | func (l *Lexer) readString() (string, error) { 201 | 202 | // We're now inside a string 203 | closed := false 204 | val := "" 205 | 206 | for l.offset < len(l.input) { 207 | 208 | c := l.input[l.offset] 209 | 210 | if c == '"' { 211 | closed = true 212 | l.offset++ 213 | break 214 | } 215 | 216 | // Handle \n, etc. 217 | if c == '\\' { 218 | 219 | // if there is another character 220 | if l.offset+1 < len(l.input) { 221 | 222 | // look at what it is 223 | l.offset++ 224 | c := l.input[l.offset] 225 | 226 | if c == 'n' { 227 | c = '\n' 228 | } 229 | if c == 'r' { 230 | c = '\r' 231 | } 232 | if c == 't' { 233 | c = '\t' 234 | } 235 | if c == '"' { 236 | c = '"' 237 | } 238 | if c == '\\' { 239 | c = '\\' 240 | } 241 | 242 | val += string(c) 243 | l.offset++ 244 | continue 245 | } 246 | } 247 | 248 | // default 249 | val += string(l.input[l.offset]) 250 | l.offset++ 251 | } 252 | 253 | // Failed to close the string? 254 | if !closed { 255 | return val, fmt.Errorf("unterminated string") 256 | } 257 | 258 | // Returned okay 259 | return val, nil 260 | } 261 | -------------------------------------------------------------------------------- /foth/lexer/lexer_test.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | // Test single-character constants 9 | func TestCharacter(t *testing.T) { 10 | 11 | var out []Token 12 | var err error 13 | 14 | // unterminated character 15 | l := New(` '*`) 16 | _, err = l.Tokens() 17 | if err == nil { 18 | t.Fatalf("expected error, got none") 19 | } 20 | if !strings.Contains(err.Error(), "unterminated") { 21 | t.Fatalf("got an error, but the wrong kind") 22 | } 23 | 24 | // single character 25 | l = New(` '*' `) 26 | out, err = l.Tokens() 27 | if err != nil { 28 | t.Fatalf("error lexing: %s", err) 29 | } 30 | if len(out) != 1 { 31 | t.Fatalf("wrong number of tokens") 32 | } 33 | if out[0].Name != "42" { 34 | t.Fatalf("unexpected result: %v", out[0].Name) 35 | } 36 | 37 | // unclosed character 38 | l = New(` '** `) 39 | _, err = l.Tokens() 40 | if err == nil { 41 | t.Fatalf("expected error, got none") 42 | } 43 | if !strings.Contains(err.Error(), "syntax error") { 44 | t.Fatalf("got an error, but the wrong kind") 45 | } 46 | 47 | } 48 | 49 | // Comments should be removed 50 | func TestComment(t *testing.T) { 51 | 52 | l := New(` to \ This is a comment 53 | ( comment here )fo 54 | `) 55 | 56 | out, err := l.Tokens() 57 | if err != nil { 58 | t.Fatalf("error lexing: %s", err) 59 | } 60 | if len(out) != 2 { 61 | t.Fatalf("Unexpected output, got: %v", out) 62 | } 63 | if out[0].Name != "to" { 64 | t.Fatalf("got bad prefix") 65 | } 66 | if out[1].Name != "fo" { 67 | t.Fatalf("got bad suffix") 68 | } 69 | } 70 | 71 | // Nested comments are a bug 72 | func TestCommentNested(t *testing.T) { 73 | 74 | l := New(" ( comment ( here ) ) ") 75 | 76 | _, err := l.Tokens() 77 | if err == nil { 78 | t.Fatalf("expected error, but got none") 79 | } 80 | if !strings.Contains(err.Error(), "nested comments") { 81 | t.Fatalf("got an error, but the wrong one") 82 | } 83 | } 84 | 85 | // Escape characters 86 | func TestEscapeCharacters(t *testing.T) { 87 | 88 | l := New("\"hello\\n\\r\\t\\r\\\"\\\\steve\"") 89 | 90 | toks, err := l.Tokens() 91 | if err != nil { 92 | t.Fatalf("unexpected error %s", err.Error()) 93 | } 94 | 95 | expect := "hello\n\r\t\r\"\\steve" 96 | if toks[0].Value != expect { 97 | t.Fatalf("unexpected value for string; got '%s' not '%s'", toks[0].Value, expect) 98 | } 99 | } 100 | 101 | // Unterminated comments are a bug 102 | func TestCommentUnterminated(t *testing.T) { 103 | 104 | l := New(" ( comment here ") 105 | 106 | _, err := l.Tokens() 107 | if err == nil { 108 | t.Fatalf("expected error, but got none") 109 | } 110 | if !strings.Contains(err.Error(), "unterminated comment") { 111 | t.Fatalf("got an error, but the wrong one") 112 | } 113 | } 114 | 115 | // Empty input should give empty output 116 | func TestEmpty(t *testing.T) { 117 | l := New(" \t \r \n") 118 | out, err := l.Tokens() 119 | if err != nil { 120 | t.Fatalf("error lexing") 121 | } 122 | 123 | if len(out) != 0 { 124 | t.Fatalf("Unexpected output, got: %v", out) 125 | } 126 | 127 | } 128 | 129 | func TestString(t *testing.T) { 130 | 131 | l := New("start .\" foo bar baz \" end") 132 | out, err := l.Tokens() 133 | 134 | if err != nil { 135 | t.Fatalf("error lexing") 136 | } 137 | 138 | if out[0].Name != "start" { 139 | t.Fatalf("got bad prefix") 140 | } 141 | if out[1].Name != ".\"" { 142 | t.Fatalf("got bad string") 143 | } 144 | if out[1].Value != " foo bar baz " { 145 | t.Fatalf("got bad string: '%s'", out[1].Value) 146 | } 147 | if out[2].Name != "end" { 148 | t.Fatalf("got bad suffix") 149 | } 150 | 151 | } 152 | 153 | func TestString2(t *testing.T) { 154 | 155 | l := New("start \" foo bar baz \" end") 156 | out, err := l.Tokens() 157 | 158 | if err != nil { 159 | t.Fatalf("error lexing") 160 | } 161 | 162 | if out[0].Name != "start" { 163 | t.Fatalf("got bad prefix") 164 | } 165 | if out[1].Name != "\"" { 166 | t.Fatalf("got bad string") 167 | } 168 | if out[1].Value != " foo bar baz " { 169 | t.Fatalf("got bad string: '%s'", out[1].Value) 170 | } 171 | if out[2].Name != "end" { 172 | t.Fatalf("got bad suffix") 173 | } 174 | 175 | } 176 | 177 | // Unterminated strings are a bug 178 | func TestStringUnterminated(t *testing.T) { 179 | 180 | l := New(" .\" string here ") 181 | 182 | _, err := l.Tokens() 183 | if err == nil { 184 | t.Fatalf("expected error, but got none") 185 | } 186 | if !strings.Contains(err.Error(), "unterminated string") { 187 | t.Fatalf("got an error, but the wrong one") 188 | } 189 | } 190 | 191 | // Unterminated strings are a bug 192 | func TestStringUnterminated2(t *testing.T) { 193 | 194 | l := New(" \" string here ") 195 | 196 | _, err := l.Tokens() 197 | if err == nil { 198 | t.Fatalf("expected error, but got none") 199 | } 200 | if !strings.Contains(err.Error(), "unterminated string") { 201 | t.Fatalf("got an error, but the wrong one") 202 | } 203 | } 204 | 205 | // Not strings 206 | func TestNotString(t *testing.T) { 207 | 208 | l := New(" .- .") 209 | 210 | out, err := l.Tokens() 211 | if err != nil { 212 | t.Fatalf("unexpected error") 213 | } 214 | 215 | if out[0].Name != ".-" { 216 | t.Fatalf("got bad prefix") 217 | } 218 | if out[1].Name != "." { 219 | t.Fatalf("got bad suffix") 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /foth/main.go: -------------------------------------------------------------------------------- 1 | // foth - final revision, allow if/else/then, neaten-code, and run files 2 | // specified on the command-line. If none run the REPL. 3 | // 4 | // Loads "foth.4th" from cwd, if present, and evaluates it before the REPL 5 | // is launched - otherwise the same as previous versions. 6 | 7 | package main 8 | 9 | import ( 10 | "bufio" 11 | "fmt" 12 | "io" 13 | "os" 14 | "strings" 15 | 16 | "github.com/skx/foth/foth/eval" 17 | ) 18 | 19 | // "secret" word 20 | func secret() error { 21 | fmt.Printf("nothing happens\n") 22 | return nil 23 | } 24 | 25 | // If the given file exists, read the contents, and evaluate it 26 | func doInit(ev *eval.Eval, path string) error { 27 | 28 | handle, err := os.Open(path) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | reader := bufio.NewReader(handle) 34 | line, err := reader.ReadString(byte('\n')) 35 | for err == nil { 36 | 37 | // Trim it 38 | line = strings.TrimSpace(line) 39 | 40 | // Evaluate 41 | err = ev.Eval(line) 42 | if err != nil { 43 | 44 | // This error is generated by the "QUIT" word, and 45 | // means there was an expected early-return. 46 | // It is an error condition that requires handling. 47 | if err == eval.ErrQuit { 48 | return nil 49 | } 50 | 51 | // OK a _real_ error was received. 52 | fmt.Printf("ERROR: %s\n", err.Error()) 53 | 54 | // Reset our state, to allow recovery 55 | ev.Reset() 56 | } 57 | 58 | // Repeat 59 | line, err = reader.ReadString(byte('\n')) 60 | } 61 | 62 | if err != io.EOF { 63 | return err 64 | } 65 | 66 | err = handle.Close() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func main() { 75 | 76 | reader := bufio.NewReader(os.Stdin) 77 | forth := eval.New() 78 | 79 | forth.Dictionary = append(forth.Dictionary, eval.Word{Name: "xyzzy", Function: secret}) 80 | 81 | // Load the init-file if it is present. 82 | // 83 | // i.e. Run the file, but ignore errors. 84 | doInit(forth, "foth.4th") 85 | 86 | // If we got any arguments treat them as files to lead 87 | if len(os.Args) > 1 { 88 | for _, file := range os.Args[1:] { 89 | err := doInit(forth, file) 90 | if err != nil { 91 | fmt.Printf("error running %s: %s\n", file, err.Error()) 92 | return 93 | } 94 | } 95 | return 96 | } 97 | 98 | // No arguments, just run the REPL 99 | for { 100 | fmt.Printf("> ") 101 | 102 | // Read input 103 | text, err := reader.ReadString('\n') 104 | if err != nil { 105 | fmt.Printf("error reading input: %s\n", err.Error()) 106 | return 107 | } 108 | 109 | // Trim it 110 | text = strings.TrimSpace(text) 111 | 112 | err = forth.Eval(text) 113 | if err != nil { 114 | 115 | // This error is generated by the "QUIT" word, and 116 | // means there was an expected early-return. 117 | // It is an error condition that requires handling. 118 | if err == eval.ErrQuit { 119 | return 120 | } 121 | 122 | fmt.Printf("ERROR: %s\n", err.Error()) 123 | 124 | // Reset our state, to allow recovery 125 | forth.Reset() 126 | } 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /foth/stack/stack.go: -------------------------------------------------------------------------------- 1 | // Package stack allows a stack of float64 to be maintained. 2 | package stack 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // At returns the value at the given offset. 12 | func (s *Stack) At(offset int) float64 { 13 | return (*s)[offset] 14 | } 15 | 16 | // IsEmpty checks whether the stack is empty. 17 | func (s *Stack) IsEmpty() bool { 18 | return len(*s) == 0 19 | } 20 | 21 | // Len returns the length of the stack. 22 | func (s *Stack) Len() int { 23 | return len(*s) 24 | } 25 | 26 | // Push adds a new number to the top of the stack. 27 | func (s *Stack) Push(x float64) { 28 | *s = append(*s, x) 29 | } 30 | 31 | // Pop removes, and returns, the top element of stack. 32 | func (s *Stack) Pop() (float64, error) { 33 | if s.IsEmpty() { 34 | return 0, fmt.Errorf("stack underflow") 35 | } 36 | 37 | i := len(*s) - 1 38 | x := (*s)[i] 39 | *s = (*s)[:i] 40 | 41 | return x, nil 42 | } 43 | -------------------------------------------------------------------------------- /foth/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEmpty(t *testing.T) { 8 | 9 | var s Stack 10 | 11 | if !s.IsEmpty() { 12 | t.Fatalf("new stack should be empty") 13 | } 14 | 15 | s.Push(33) 16 | if s.IsEmpty() { 17 | t.Fatalf("populated stack should not be empty") 18 | } 19 | if s.Len() != 1 { 20 | t.Fatalf("stack length was wrong") 21 | } 22 | if s.At(0) != 33 { 23 | t.Fatalf("stack value was wrong") 24 | } 25 | s.Pop() 26 | if !s.IsEmpty() { 27 | t.Fatalf("stack should be back to new") 28 | } 29 | 30 | } 31 | 32 | func TestUnderflow(t *testing.T) { 33 | 34 | var s Stack 35 | 36 | _, err := s.Pop() 37 | 38 | if err == nil { 39 | t.Fatalf("stack was not covered") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skx/foth 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /part1/README.md: -------------------------------------------------------------------------------- 1 | # Part 1 2 | 3 | Part one of the implementation only deals with hard-coded execution 4 | of "words". It only supports the basic mathematical operations, along 5 | with the ability to print the top-most entry of the stack: 6 | 7 | ## Building 8 | 9 | To build, and run this version: 10 | 11 | ``` 12 | go build . 13 | ./part1 14 | > 2 3 + 4 5 + * print 15 | 45.000000 16 | ^D 17 | ``` 18 | 19 | 20 | ## Implementation 21 | 22 | * We have a simple stack implemented in [stack.go](stack.go) 23 | * This allows storing a series of `float64` objects. 24 | * We've defined a `Word` structure to hold known-commands/words 25 | * Each word has a name, and a pointer to the function which contains the implementation. 26 | * Our interpreter, [eval.go](eval.go) has a list of hard-coded Words defined. 27 | * Each token is executed in the same way: 28 | * If the token matches the name of one of the defined Words, then call the function associated with it. 29 | * Otherwise assume the input is a number, and push to the stack. 30 | * [main.go](main.go) just reads STDIN, line by line, and passes to the evaluator 31 | -------------------------------------------------------------------------------- /part1/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | 5 | package main 6 | 7 | import "fmt" 8 | 9 | func (e *Eval) add() { 10 | a := e.Stack.Pop() 11 | b := e.Stack.Pop() 12 | e.Stack.Push(a + b) 13 | } 14 | 15 | func (e *Eval) sub() { 16 | a := e.Stack.Pop() 17 | b := e.Stack.Pop() 18 | e.Stack.Push(b - a) 19 | } 20 | 21 | func (e *Eval) mul() { 22 | a := e.Stack.Pop() 23 | b := e.Stack.Pop() 24 | e.Stack.Push(a * b) 25 | } 26 | 27 | func (e *Eval) div() { 28 | a := e.Stack.Pop() 29 | b := e.Stack.Pop() 30 | e.Stack.Push(b / a) 31 | } 32 | 33 | func (e *Eval) print() { 34 | a := e.Stack.Pop() 35 | fmt.Printf("%f\n", a) 36 | } 37 | -------------------------------------------------------------------------------- /part1/eval.go: -------------------------------------------------------------------------------- 1 | // part1 - only allow a fixed number of hard-coded words 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Word is the structure for a single word 11 | type Word struct { 12 | // Name is the name of the function "+", "print", etc. 13 | Name string 14 | 15 | // Function is the function-pointer to call to invoke it. 16 | Function func() 17 | } 18 | 19 | // Eval is our evaluation structure 20 | type Eval struct { 21 | 22 | // Internal stack 23 | Stack Stack 24 | 25 | // Dictionary entries 26 | Dictionary []Word 27 | } 28 | 29 | // NewEval returns a simple evaluator 30 | func NewEval() *Eval { 31 | 32 | // Empty structure 33 | e := &Eval{} 34 | 35 | // Populate the dictionary of words we have implemented 36 | // which are hard-coded. 37 | e.Dictionary = []Word{ 38 | {"+", e.add}, // 0 39 | {"-", e.sub}, // 1 40 | {"*", e.mul}, // 2 41 | {"/", e.div}, // 3 42 | {"print", e.print}, // 4 43 | {".", e.print}, // 5 44 | } 45 | return e 46 | } 47 | 48 | // Eval processes a list of tokens. 49 | // 50 | // This is invoked by our repl with a line of input at the time. 51 | func (e *Eval) Eval(args []string) { 52 | 53 | // 54 | // token = NextToken() 55 | // if token is in dictionary 56 | // call function from that dict entry 57 | // else if token is a number 58 | // push that number onto the data stack 59 | for _, tok := range args { 60 | 61 | tok = strings.TrimSpace(tok) 62 | if tok == "" { 63 | continue 64 | } 65 | var i float64 66 | var err error 67 | 68 | // Dictionary item? 69 | for _, word := range e.Dictionary { 70 | if tok == word.Name { 71 | word.Function() 72 | goto end 73 | } 74 | } 75 | 76 | // is this a number? 77 | i, err = strconv.ParseFloat(tok, 64) 78 | if err != nil { 79 | fmt.Printf("%s: %s\n", tok, err.Error()) 80 | return 81 | } 82 | 83 | e.Stack.Push(i) 84 | 85 | end: 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /part1/main.go: -------------------------------------------------------------------------------- 1 | // part1 driver 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | 14 | reader := bufio.NewReader(os.Stdin) 15 | forth := NewEval() 16 | 17 | for { 18 | fmt.Printf("> ") 19 | 20 | // Read input 21 | text, err := reader.ReadString('\n') 22 | if err != nil { 23 | fmt.Printf("error reading input: %s\n", err.Error()) 24 | return 25 | } 26 | 27 | // Trim it 28 | text = strings.TrimSpace(text) 29 | 30 | forth.Eval(strings.Split(text, " ")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /part1/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if the stack is empty 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | -------------------------------------------------------------------------------- /part2/README.md: -------------------------------------------------------------------------------- 1 | # Part 2 2 | 3 | Part two of the implementation is very similar to [part1](../part1/) in 4 | the sense that it only allows the execution of hard-coded words. 5 | 6 | However the implementation has become more flexible, because we've updated 7 | our `Word`-structure to allow words to be executed in two ways: 8 | 9 | * A word can contain a reference to a (go) function. 10 | * If present this is called, as before. 11 | * If there is no (go) function reference we do something different. 12 | * We call existing words, based on their dictionary index. 13 | 14 | ## Building 15 | 16 | To build, and run this version: 17 | 18 | ``` 19 | go build . 20 | ./part2 21 | > 2 dup * print 22 | 4.000000 23 | > 4 square print 24 | 16.000000 25 | ^D 26 | ``` 27 | 28 | 29 | ## Implementation 30 | 31 | * We have a simple stack implemented in [stack.go](stack.go) 32 | * This allows storing a series of `float64` objects. 33 | * This is identical to the previous version. 34 | * We've defined a `Word` structure to hold known-commands/words 35 | * Each word has a name, a pointer to a (go) function, and also a series of word-indexes. 36 | * Since we store our words as an array we can access them by number, as well as name. 37 | * Our interpreter, [eval.go](eval.go) has a list of hard-coded Words defined. 38 | * Each token is executed in the same way: 39 | * If the token matches the name of one of the defined Words 40 | * And there is a (go) function-pointer, then call it. 41 | * Otherwise assume there is a list of `Word` indexes. Call each one in turn. 42 | * Otherwise assume the input is a number, and push to the stack. 43 | * [main.go](main.go) just reads STDIN, line by line, and passes to the evaluator 44 | -------------------------------------------------------------------------------- /part2/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | // 5 | // Compared to part1 we've added `dup`. 6 | 7 | package main 8 | 9 | import "fmt" 10 | 11 | func (e *Eval) add() { 12 | a := e.Stack.Pop() 13 | b := e.Stack.Pop() 14 | e.Stack.Push(a + b) 15 | } 16 | 17 | func (e *Eval) sub() { 18 | a := e.Stack.Pop() 19 | b := e.Stack.Pop() 20 | e.Stack.Push(b - a) 21 | } 22 | 23 | func (e *Eval) mul() { 24 | a := e.Stack.Pop() 25 | b := e.Stack.Pop() 26 | e.Stack.Push(a * b) 27 | } 28 | 29 | func (e *Eval) div() { 30 | a := e.Stack.Pop() 31 | b := e.Stack.Pop() 32 | e.Stack.Push(b / a) 33 | } 34 | 35 | func (e *Eval) print() { 36 | a := e.Stack.Pop() 37 | fmt.Printf("%f\n", a) 38 | } 39 | 40 | func (e *Eval) dup() { 41 | a := e.Stack.Pop() 42 | e.Stack.Push(a) 43 | e.Stack.Push(a) 44 | } 45 | -------------------------------------------------------------------------------- /part2/eval.go: -------------------------------------------------------------------------------- 1 | // part2 - allow defining words in terms of others 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Word is the structure for a single word 11 | type Word struct { 12 | // Name is the name of the function "+", "print", etc. 13 | Name string 14 | 15 | // Function is the function-pointer to call to invoke it. 16 | // 17 | // If this is nil then instead we interpret the known-codes 18 | // from previously defined words. 19 | Function func() 20 | 21 | // Words holds the words we execute if the function-pointer 22 | // is empty. 23 | // 24 | // The indexes here are relative to the Dictionary our evaluator 25 | // holds/maintains 26 | Words []int 27 | } 28 | 29 | // Eval is our evaluation structure 30 | type Eval struct { 31 | 32 | // Internal stack 33 | Stack Stack 34 | 35 | // Dictionary entries 36 | Dictionary []Word 37 | } 38 | 39 | // NewEval returns a simple evaluator 40 | func NewEval() *Eval { 41 | 42 | // Empty structure 43 | e := &Eval{} 44 | 45 | // Populate the dictionary of words we have implemented 46 | // which are hard-coded. 47 | // 48 | // All of these are coded in go, except for the new function 49 | // square - which is implemented as just "dup *". The implementation 50 | // just has the offsets in this array of the words used to invoke. 51 | e.Dictionary = []Word{ 52 | {Name: "+", Function: e.add}, // 0 53 | {Name: "-", Function: e.sub}, // 1 54 | {Name: "*", Function: e.mul}, // 2 55 | {Name: "/", Function: e.div}, // 3 56 | {Name: "print", Function: e.print}, // 4 57 | {Name: ".", Function: e.print}, // 5 58 | {Name: "dup", Function: e.dup}, // 6 59 | {Name: "square", Function: nil, Words: []int{6, 2}}, // 7 60 | } 61 | return e 62 | } 63 | 64 | // Eval processes a list of tokens. 65 | // 66 | // This is invoked by our repl with a line of input at the time. 67 | func (e *Eval) Eval(args []string) { 68 | 69 | for _, tok := range args { 70 | 71 | // Trim the leading/trailing spaces, 72 | // and skip any empty tokens 73 | tok = strings.TrimSpace(tok) 74 | if tok == "" { 75 | continue 76 | } 77 | 78 | // Did we handle this as a dictionary item? 79 | handled := false 80 | for index, word := range e.Dictionary { 81 | if tok == word.Name { 82 | e.evalWord(index) 83 | handled = true 84 | } 85 | } 86 | 87 | // If we didn't handle this as a word, then 88 | // assume it is a number. 89 | if !handled { 90 | i, err := strconv.ParseFloat(tok, 64) 91 | if err != nil { 92 | fmt.Printf("%s: %s\n", tok, err.Error()) 93 | return 94 | } 95 | 96 | e.Stack.Push(i) 97 | } 98 | } 99 | } 100 | 101 | // evalWord evaluates a word, by index from the dictionary 102 | func (e *Eval) evalWord(index int) { 103 | 104 | word := e.Dictionary[index] 105 | if word.Function != nil { 106 | word.Function() 107 | } else { 108 | for _, offset := range word.Words { 109 | e.evalWord(offset) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /part2/main.go: -------------------------------------------------------------------------------- 1 | // part2 driver 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | 14 | reader := bufio.NewReader(os.Stdin) 15 | forth := NewEval() 16 | 17 | for { 18 | fmt.Printf("> ") 19 | 20 | // Read input 21 | text, err := reader.ReadString('\n') 22 | if err != nil { 23 | fmt.Printf("error reading input: %s\n", err.Error()) 24 | return 25 | } 26 | 27 | // Trim it 28 | text = strings.TrimSpace(text) 29 | 30 | forth.Eval(strings.Split(text, " ")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /part2/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if our stack is empty. 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | -------------------------------------------------------------------------------- /part3/README.md: -------------------------------------------------------------------------------- 1 | # Part 3 2 | 3 | Part three of the implementation is very similar to [part2](../part2/) in 4 | the sense that we allow execution words which are defined in terms of 5 | pre-existing words. 6 | 7 | Our `Word`-structure looks the same as in the previous part: 8 | 9 | * A word can contain a reference to a (go) function. 10 | * If present this is called, as before. 11 | * If there is no (go) function reference we do something different. 12 | * We call existing words, based on their dictionary index. 13 | 14 | The difference is in our evaluator itself: 15 | 16 | * Now we have a notion of being in a "compiling state". 17 | * When `:` is encountered we flip that switch on. 18 | * When `;` is encountered we flip that switch off. 19 | * When compiling we use a _temporary_ word, and instead of executing words once we've found them we merely append their offsets to the list of words in this new definition. 20 | * There's a bit of magic to setup the name of the new word too 21 | 22 | ## Building 23 | 24 | To build, and run this version: 25 | 26 | ``` 27 | go build . 28 | ./part3 29 | > : square dup * ; 30 | > 3 square . 31 | 9.000000 32 | ^D 33 | ``` 34 | 35 | 36 | ## Implementation 37 | 38 | * We have a simple stack implemented in [stack.go](stack.go) 39 | * This allows storing a series of `float64` objects. 40 | * This is identical to the previous version. 41 | * We've defined a `Word` structure to hold known-commands/words. 42 | * Each word has a name, a pointer to a (go) function, and also a series of word-indexes. 43 | * Since we store our words as an array we can access them by number, as well as name. 44 | * Our interpreter, [eval.go](eval.go) has been updated to set/clear the `compiling` flag, when it sees `:` and `;` 45 | * When not in compiling-mode things work as before 46 | * If the token matches the name of one of the defined Words 47 | * And there is a (go) function-pointer, then call it. 48 | * Otherwise assume there is a list of `Word` indexes. Call each one in turn. 49 | * Otherwise assume the input is a number, and push to the stack. 50 | * When in compiling mode we instead lookup each input word and just store the offset of the found `Word` in the internal space for the new Word 51 | * Once we hit `;` we add this new word to the dictionary of known-words 52 | * [main.go](main.go) just reads STDIN, line by line, and passes to the evaluator 53 | 54 | ## Problems / Omissions 55 | 56 | There is one large omission in this part, if we're in compiling mode we cannot handle numbers! Recall that when we're executing code we add any number to the stack, as we encounter it. 57 | 58 | At the point we're compiling words we don't have any way of using the stack though - that only comes into play when we're _executing_ the word. 59 | 60 | This problem will be resolved in [part4](../part4/). 61 | -------------------------------------------------------------------------------- /part3/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | // 5 | // Compared to part3 we've added a word for ":" to allow defining words 6 | // via manipulation of the "compiling" flag. 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | func (e *Eval) add() { 15 | a := e.Stack.Pop() 16 | b := e.Stack.Pop() 17 | e.Stack.Push(a + b) 18 | } 19 | 20 | func (e *Eval) sub() { 21 | a := e.Stack.Pop() 22 | b := e.Stack.Pop() 23 | e.Stack.Push(b - a) 24 | } 25 | 26 | func (e *Eval) mul() { 27 | a := e.Stack.Pop() 28 | b := e.Stack.Pop() 29 | e.Stack.Push(a * b) 30 | } 31 | 32 | func (e *Eval) div() { 33 | a := e.Stack.Pop() 34 | b := e.Stack.Pop() 35 | e.Stack.Push(b / a) 36 | } 37 | 38 | func (e *Eval) print() { 39 | a := e.Stack.Pop() 40 | fmt.Printf("%f\n", a) 41 | } 42 | 43 | func (e *Eval) dup() { 44 | a := e.Stack.Pop() 45 | e.Stack.Push(a) 46 | e.Stack.Push(a) 47 | } 48 | 49 | // startDefinition moves us into compiling-mode. 50 | // 51 | // Note the interpreter handles removing this when it sees ";" 52 | func (e *Eval) startDefinition() { 53 | e.compiling = true 54 | } 55 | -------------------------------------------------------------------------------- /part3/eval.go: -------------------------------------------------------------------------------- 1 | // part3 - allow defining words in our own environment. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Word is the structure for a single word 12 | type Word struct { 13 | // Name is the name of the function "+", "print", etc. 14 | Name string 15 | 16 | // Function is the function-pointer to call to invoke it. 17 | // 18 | // If this is nil then instead we interpret the known-codes 19 | // from previously defined words. 20 | Function func() 21 | 22 | // Words holds the words we execute if the function-pointer 23 | // is empty. 24 | // 25 | // The indexes here are relative to the Dictionary our evaluator 26 | // holds/maintains 27 | Words []int 28 | } 29 | 30 | // Eval is our evaluation structure 31 | type Eval struct { 32 | 33 | // Internal stack 34 | Stack Stack 35 | 36 | // Dictionary entries 37 | Dictionary []Word 38 | 39 | // Are we in a compiling mode? 40 | compiling bool 41 | 42 | // Temporary word we're compiling 43 | tmp Word 44 | } 45 | 46 | // NewEval returns a simple evaluator 47 | func NewEval() *Eval { 48 | 49 | // Empty structure 50 | e := &Eval{} 51 | 52 | // Populate the dictionary of words we have implemented 53 | // which are hard-coded. 54 | e.Dictionary = []Word{ 55 | {Name: "+", Function: e.add}, // 0 56 | {Name: "-", Function: e.sub}, // 1 57 | {Name: "*", Function: e.mul}, // 2 58 | {Name: "/", Function: e.div}, // 3 59 | {Name: "print", Function: e.print}, // 4 60 | {Name: ".", Function: e.print}, // 5 61 | {Name: "dup", Function: e.dup}, // 6 62 | {Name: ":", Function: e.startDefinition}, // 7 63 | // Note we don't handle ";" here. 64 | } 65 | return e 66 | } 67 | 68 | // Eval processes a list of tokens. 69 | // 70 | // This is invoked by our repl with a line of input at the time. 71 | func (e *Eval) Eval(args []string) { 72 | 73 | for _, tok := range args { 74 | 75 | // Trim the leading/trailing spaces, 76 | // and skip any empty tokens 77 | tok = strings.TrimSpace(tok) 78 | if tok == "" { 79 | continue 80 | } 81 | 82 | // Are we in compiling mode? 83 | if e.compiling { 84 | 85 | // If we don't have a name 86 | if e.tmp.Name == "" { 87 | 88 | // is the name used? 89 | idx := e.findWord(tok) 90 | if idx != -1 { 91 | fmt.Printf("word %s already defined\n", tok) 92 | os.Exit(1) 93 | } 94 | 95 | // save the name 96 | e.tmp.Name = tok 97 | continue 98 | } 99 | 100 | // End of a definition? 101 | if tok == ";" { 102 | e.Dictionary = append(e.Dictionary, e.tmp) 103 | e.tmp.Name = "" 104 | e.tmp.Words = []int{} 105 | e.compiling = false 106 | continue 107 | 108 | } 109 | 110 | // OK we have a name, so lookup the word definition 111 | // for it. 112 | idx := e.findWord(tok) 113 | if idx >= 0 { 114 | // Found it 115 | e.tmp.Words = append(e.tmp.Words, idx) 116 | } 117 | 118 | continue 119 | } 120 | 121 | // Did we handle this as a dictionary item? 122 | handled := false 123 | for index, word := range e.Dictionary { 124 | if tok == word.Name { 125 | e.evalWord(index) 126 | handled = true 127 | } 128 | } 129 | 130 | // If we didn't handle this as a word, then 131 | // assume it is a number. 132 | if !handled { 133 | i, err := strconv.ParseFloat(tok, 64) 134 | if err != nil { 135 | fmt.Printf("%s: %s\n", tok, err.Error()) 136 | return 137 | } 138 | 139 | e.Stack.Push(i) 140 | } 141 | } 142 | } 143 | 144 | // evalWord evaluates a word, by index from the dictionary 145 | func (e *Eval) evalWord(index int) { 146 | 147 | word := e.Dictionary[index] 148 | if word.Function != nil { 149 | word.Function() 150 | } else { 151 | for _, offset := range word.Words { 152 | e.evalWord(offset) 153 | } 154 | } 155 | } 156 | 157 | func (e *Eval) findWord(name string) int { 158 | for index, word := range e.Dictionary { 159 | if name == word.Name { 160 | return index 161 | } 162 | } 163 | return -1 164 | } 165 | -------------------------------------------------------------------------------- /part3/main.go: -------------------------------------------------------------------------------- 1 | // part3 driver 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | 14 | reader := bufio.NewReader(os.Stdin) 15 | forth := NewEval() 16 | 17 | for { 18 | fmt.Printf("> ") 19 | 20 | // Read input 21 | text, err := reader.ReadString('\n') 22 | if err != nil { 23 | fmt.Printf("error reading input: %s\n", err.Error()) 24 | return 25 | } 26 | 27 | // Trim it 28 | text = strings.TrimSpace(text) 29 | 30 | forth.Eval(strings.Split(text, " ")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /part3/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if our stack is empty. 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | -------------------------------------------------------------------------------- /part4/README.md: -------------------------------------------------------------------------------- 1 | # Part 4 2 | 3 | Part four of the implementation is very similar to [part3](../part3/): 4 | 5 | * We still have only a small number of built-in functions. 6 | * We allow you to define new ones. 7 | 8 | The difference here is that we now support compiling words which contain 9 | _numbers_, in addition to references to existing words. 10 | 11 | If you recall from [part3](../part3/): 12 | 13 | * We have a notion of being in a "compiling state". 14 | * When `:` is encountered we flip that switch on. 15 | * When `;` is encountered we flip that switch off. 16 | * When compiling we use a _temporary_ storage, and instead of executing words once we've found them we merely append their offsets to the list of words in this new definition. 17 | 18 | The problem we found is that when the user entered a number in their new definition there was nothing we could do with it - adding it to the stack would be the wrong thing to do, and we couldn't add it to the word-definition. 19 | 20 | We've updated things now so that the list of references to pre-existing words can store numbers too! We add a magic "opcode", -1, and then add the number itself. 21 | 22 | ## Building 23 | 24 | To build, and run this version: 25 | 26 | ``` 27 | go build . 28 | ./part4 29 | > : square dup * ; 30 | > 3 square . 31 | 9.000000 32 | > : +1 1 + ; 33 | > 3 +1 . 34 | 4.000000 35 | ^D 36 | ``` 37 | 38 | 39 | ## Implementation 40 | 41 | * We have a simple stack implemented in [stack.go](stack.go) 42 | * This allows storing a series of `float64` objects. 43 | * This is identical to the previous version. 44 | * We've defined a `Word` structure to hold known-commands/words. 45 | * Each word has a name, a pointer to a (go) function, and also a series of word-indexes. 46 | * Since we store our words as an array we can access them by number, as well as name. 47 | * Our interpreter, still has the `compiling` flag, when it juggles when sees `:` and `;` 48 | * When not in compiling-mode things work as before. 49 | * If the token matches the name of one of the defined Words 50 | * And there is a (go) function-pointer, then call it. 51 | * Otherwise assume there is a list of `Word` indexes: 52 | * But if we see the magic flag `-1` then the next number is a number to push to the stack. 53 | * Otherwise assume the input is a number, and push to the stack. 54 | * When in compiling mode we instead lookup each input word and just store the offset of the found `Word` in the internal space for the new Word. 55 | * If that fails then we assume the user entered a number, and add it to the word-array (prefixed by `-1`). 56 | * Once we hit `;` we add this new word to the dictionary of known-words 57 | * [main.go](main.go) just reads STDIN, line by line, and passes to the evaluator 58 | 59 | **NOTE**: As a consequence of storing numbers inside the definition of the word, prefixed by the `-1` marker, we've had to change the word-array to be an array of `float64` values - not integers. 60 | 61 | 62 | 63 | ## More Details 64 | 65 | If we skip ahead to the [final version](../foth/) we gain access to a `dump` command, which lets us dissassemble the dictionary-entries. 66 | 67 | Using that we can show the compiled version of the `+1` word we defined in this README: 68 | 69 | ``` 70 | $ cd ../foth 71 | $ go build . 72 | $ ./foth 73 | Welcome to foth! 74 | > #words . 75 | 71 76 | > : +1 1 + ; 77 | > #words . 78 | 72 79 | > 71 dump 80 | Word '+1' 81 | 0: store 1.000000 82 | 2: + 83 | ``` 84 | 85 | Here we used `#words` to get a count of all the defined words (`words` would show their names). To start with we see there are 71 words (0-70). Then we define our new one, and confirm the count is updated. 86 | 87 | The final step is to dump the word, and we see that the array containing the implementation contains three entries: 88 | 89 | * 0x00: -1 90 | * 0x01: 1.0 91 | * 0x02: + 92 | 93 | (The -1 is implicit here, and you have to guess that two bytes have been taken by the offset/index in the left-column.) 94 | 95 | When this word is executed the following thing happens: 96 | 97 | * The -1 value is taken to mean "load-word" 98 | * The next value, 1, is then pushed onto the stack 99 | * Then the "+" word is executed. 100 | * And this word is complete. 101 | -------------------------------------------------------------------------------- /part4/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | func (e *Eval) add() { 12 | a := e.Stack.Pop() 13 | b := e.Stack.Pop() 14 | e.Stack.Push(a + b) 15 | } 16 | 17 | func (e *Eval) sub() { 18 | a := e.Stack.Pop() 19 | b := e.Stack.Pop() 20 | e.Stack.Push(b - a) 21 | } 22 | 23 | func (e *Eval) mul() { 24 | a := e.Stack.Pop() 25 | b := e.Stack.Pop() 26 | e.Stack.Push(a * b) 27 | } 28 | 29 | func (e *Eval) div() { 30 | a := e.Stack.Pop() 31 | b := e.Stack.Pop() 32 | e.Stack.Push(b / a) 33 | } 34 | 35 | func (e *Eval) print() { 36 | a := e.Stack.Pop() 37 | fmt.Printf("%f\n", a) 38 | } 39 | 40 | func (e *Eval) dup() { 41 | a := e.Stack.Pop() 42 | e.Stack.Push(a) 43 | e.Stack.Push(a) 44 | } 45 | 46 | // startDefinition moves us into compiling-mode 47 | // 48 | // Note the interpreter handles removing this when it sees ";" 49 | func (e *Eval) startDefinition() { 50 | e.compiling = true 51 | } 52 | -------------------------------------------------------------------------------- /part4/eval.go: -------------------------------------------------------------------------------- 1 | // part4 - allow defining words in our own environment, even with numbers! 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Word is the structure for a single word 12 | type Word struct { 13 | // Name is the name of the function "+", "print", etc. 14 | Name string 15 | 16 | // Function is the function-pointer to call to invoke it. 17 | // 18 | // If this is nil then instead we interpret the known-codes 19 | // from previously defined words. 20 | Function func() 21 | 22 | // Words holds the words we execute if the function-pointer 23 | // is empty. 24 | // 25 | // The indexes here are relative to the Dictionary our evaluator 26 | // holds/maintains 27 | Words []float64 28 | } 29 | 30 | // Eval is our evaluation structure 31 | type Eval struct { 32 | 33 | // Internal stack 34 | Stack Stack 35 | 36 | // Dictionary entries 37 | Dictionary []Word 38 | 39 | // Are we in a compiling mode? 40 | compiling bool 41 | 42 | // Temporary word we're compiling 43 | tmp Word 44 | } 45 | 46 | // NewEval returns a simple evaluator 47 | func NewEval() *Eval { 48 | 49 | // Empty structure 50 | e := &Eval{} 51 | 52 | // Populate the dictionary of words we have implemented 53 | // which are hard-coded. 54 | e.Dictionary = []Word{ 55 | {Name: "+", Function: e.add}, // 0 56 | {Name: "-", Function: e.sub}, // 1 57 | {Name: "*", Function: e.mul}, // 2 58 | {Name: "/", Function: e.div}, // 3 59 | {Name: "print", Function: e.print}, // 4 60 | {Name: ".", Function: e.print}, // 5 61 | {Name: "dup", Function: e.dup}, // 6 62 | {Name: ":", Function: e.startDefinition}, // 7 63 | // Note we don't handle ";" here. 64 | } 65 | return e 66 | } 67 | 68 | // Eval processes a list of tokens. 69 | // 70 | // This is invoked by our repl with a line of input at the time. 71 | func (e *Eval) Eval(args []string) { 72 | 73 | for _, tok := range args { 74 | 75 | // Trim the leading/trailing spaces, 76 | // and skip any empty tokens 77 | tok = strings.TrimSpace(tok) 78 | if tok == "" { 79 | continue 80 | } 81 | 82 | // Are we in compiling mode? 83 | if e.compiling { 84 | 85 | // If we don't have a name 86 | if e.tmp.Name == "" { 87 | 88 | // is the name used? 89 | idx := e.findWord(tok) 90 | if idx != -1 { 91 | fmt.Printf("word %s already defined\n", tok) 92 | os.Exit(1) 93 | } 94 | 95 | // save the name 96 | e.tmp.Name = tok 97 | continue 98 | } 99 | 100 | // End of a definition? 101 | if tok == ";" { 102 | e.Dictionary = append(e.Dictionary, e.tmp) 103 | e.tmp.Name = "" 104 | e.tmp.Words = []float64{} 105 | e.compiling = false 106 | continue 107 | 108 | } 109 | 110 | // OK we have a name, so lookup the word definition 111 | // for it. 112 | idx := e.findWord(tok) 113 | if idx >= 0 { 114 | // Found it 115 | e.tmp.Words = append(e.tmp.Words, float64(idx)) 116 | } else { 117 | 118 | // OK we assume the user entered a number 119 | // so we save a magic "-1" flag in our 120 | // definition, and then the number itself 121 | e.tmp.Words = append(e.tmp.Words, -1) 122 | 123 | // Convert to float 124 | val, err := strconv.ParseFloat(tok, 64) 125 | if err != nil { 126 | fmt.Printf("%s: %s\n", tok, err.Error()) 127 | return 128 | } 129 | e.tmp.Words = append(e.tmp.Words, val) 130 | 131 | } 132 | 133 | continue 134 | } 135 | 136 | // Did we handle this as a dictionary item? 137 | handled := false 138 | for index, word := range e.Dictionary { 139 | if tok == word.Name { 140 | e.evalWord(index) 141 | handled = true 142 | } 143 | } 144 | 145 | // If we didn't handle this as a word, then 146 | // assume it is a number. 147 | if !handled { 148 | i, err := strconv.ParseFloat(tok, 64) 149 | if err != nil { 150 | fmt.Printf("%s: %s\n", tok, err.Error()) 151 | return 152 | } 153 | 154 | e.Stack.Push(i) 155 | } 156 | } 157 | } 158 | 159 | // evalWord evaluates a word, by index from the dictionary 160 | func (e *Eval) evalWord(index int) { 161 | 162 | word := e.Dictionary[index] 163 | if word.Function != nil { 164 | word.Function() 165 | } else { 166 | addNum := false 167 | 168 | for _, offset := range word.Words { 169 | 170 | // adding a number? 171 | if addNum { 172 | // add to stack 173 | e.Stack.Push(offset) 174 | addNum = false 175 | } else { 176 | 177 | // if we see -1 we're adding a number 178 | if offset == -1 { 179 | addNum = true 180 | } else { 181 | 182 | // otherwise eval as usual 183 | e.evalWord(int(offset)) 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | func (e *Eval) findWord(name string) int { 191 | for index, word := range e.Dictionary { 192 | if name == word.Name { 193 | return index 194 | } 195 | } 196 | return -1 197 | } 198 | -------------------------------------------------------------------------------- /part4/main.go: -------------------------------------------------------------------------------- 1 | // part4 driver 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | 14 | reader := bufio.NewReader(os.Stdin) 15 | forth := NewEval() 16 | 17 | for { 18 | fmt.Printf("> ") 19 | 20 | // Read input 21 | text, err := reader.ReadString('\n') 22 | if err != nil { 23 | fmt.Printf("error reading input: %s\n", err.Error()) 24 | return 25 | } 26 | 27 | // Trim it 28 | text = strings.TrimSpace(text) 29 | 30 | forth.Eval(strings.Split(text, " ")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /part4/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if our stack is empty. 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | -------------------------------------------------------------------------------- /part5/README.md: -------------------------------------------------------------------------------- 1 | # Part 5 2 | 3 | Part five of the implementation is very similar to [part4](../part4/), the difference is that we've added _very basic_ support for looping instructions. 4 | 5 | 6 | ## Building 7 | 8 | To build, and run this version: 9 | 10 | ``` 11 | > : cr 10 emit ; 12 | > : star 42 emit ; 13 | > : stars 0 do star loop cr ; 14 | > 4 stars 15 | **** 16 | > 5 stars 17 | ***** 18 | > 1 stars 19 | * 20 | > 10 stars 21 | ********** 22 | ^D 23 | ``` 24 | 25 | Here we've defined two new words `cr` to print a return, and `star` to output a single star. We then use those in our `stars` word to allow showing the user the specified number of stars, followed by a newline. 26 | 27 | 28 | ## Implementation 29 | 30 | The implementation here is as simple as the previous version, we look for `do` and when we find it we remember where it occurred in the input. We then output another "special opcode" when we hit the `loop` word: 31 | 32 | * We use `-2` to mark a loop-test. 33 | * At runtime we pop two items from the stack, and if they're not equal we jump back to the opcode of the `do` instruction. 34 | * This supports our loop. 35 | 36 | This looping implementation is __very basic__. It stores the `do`/`loop` state on the stack, as you can see here: 37 | 38 | ``` 39 | > : bar 5 1 do dup . loop ; 40 | > bar 41 | 1.000000 42 | 2.000000 43 | 3.000000 44 | 4.000000 45 | ``` 46 | -------------------------------------------------------------------------------- /part5/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | // 5 | // We've added `emit` here, to output the value at the top of the stack 6 | // as an ASCII character, as well as "do" (nop) and "loop". 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | func (e *Eval) add() { 15 | a := e.Stack.Pop() 16 | b := e.Stack.Pop() 17 | e.Stack.Push(a + b) 18 | } 19 | 20 | func (e *Eval) sub() { 21 | a := e.Stack.Pop() 22 | b := e.Stack.Pop() 23 | e.Stack.Push(b - a) 24 | } 25 | 26 | func (e *Eval) mul() { 27 | a := e.Stack.Pop() 28 | b := e.Stack.Pop() 29 | e.Stack.Push(a * b) 30 | } 31 | 32 | func (e *Eval) div() { 33 | a := e.Stack.Pop() 34 | b := e.Stack.Pop() 35 | e.Stack.Push(b / a) 36 | } 37 | 38 | func (e *Eval) print() { 39 | a := e.Stack.Pop() 40 | fmt.Printf("%f\n", a) 41 | } 42 | 43 | func (e *Eval) dup() { 44 | a := e.Stack.Pop() 45 | e.Stack.Push(a) 46 | e.Stack.Push(a) 47 | } 48 | 49 | // startDefinition moves us into compiling-mode 50 | // 51 | // Note the interpreter handles removing this when it sees ";" 52 | func (e *Eval) startDefinition() { 53 | e.compiling = true 54 | } 55 | 56 | func (e *Eval) emit() { 57 | a := e.Stack.Pop() 58 | fmt.Printf("%c", rune(a)) 59 | } 60 | 61 | func (e *Eval) do() { 62 | // nop 63 | } 64 | 65 | func (e *Eval) loop() { 66 | cur := e.Stack.Pop() 67 | max := e.Stack.Pop() 68 | 69 | cur++ 70 | 71 | e.Stack.Push(max) 72 | e.Stack.Push(cur) 73 | } 74 | -------------------------------------------------------------------------------- /part5/eval.go: -------------------------------------------------------------------------------- 1 | // part5 - allow loops 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Word is the structure for a single word 12 | type Word struct { 13 | // Name is the name of the function "+", "print", etc. 14 | Name string 15 | 16 | // Function is the function-pointer to call to invoke it. 17 | // 18 | // If this is nil then instead we interpret the known-codes 19 | // from previously defined words. 20 | Function func() 21 | 22 | // Words holds the words we execute if the function-pointer 23 | // is empty. 24 | // 25 | // The indexes here are relative to the Dictionary our evaluator 26 | // holds/maintains 27 | Words []float64 28 | } 29 | 30 | // Eval is our evaluation structure 31 | type Eval struct { 32 | 33 | // Internal stack 34 | Stack Stack 35 | 36 | // Dictionary entries 37 | Dictionary []Word 38 | 39 | // Are we in a compiling mode? 40 | compiling bool 41 | 42 | // Temporary word we're compiling 43 | tmp Word 44 | 45 | // open of the last do 46 | doOpen int 47 | } 48 | 49 | // NewEval returns a simple evaluator 50 | func NewEval() *Eval { 51 | 52 | // Empty structure 53 | e := &Eval{} 54 | 55 | // Populate the dictionary of words we have implemented 56 | // which are hard-coded. 57 | e.Dictionary = []Word{ 58 | {Name: "+", Function: e.add}, // 0 59 | {Name: "-", Function: e.sub}, // 1 60 | {Name: "*", Function: e.mul}, // 2 61 | {Name: "/", Function: e.div}, // 3 62 | {Name: "print", Function: e.print}, // 4 63 | {Name: ".", Function: e.print}, // 5 64 | {Name: "dup", Function: e.dup}, // 6 65 | {Name: ":", Function: e.startDefinition}, // 7 66 | {Name: "emit", Function: e.emit}, // 8 67 | {Name: "do", Function: e.do}, // 9 68 | {Name: "loop", Function: e.loop}, // 10 69 | } 70 | 71 | return e 72 | } 73 | 74 | // Eval processes a list of tokens. 75 | // 76 | // This is invoked by our repl with a line of input at the time. 77 | func (e *Eval) Eval(args []string) { 78 | 79 | for _, tok := range args { 80 | 81 | // Trim the leading/trailing spaces, 82 | // and skip any empty tokens 83 | tok = strings.TrimSpace(tok) 84 | if tok == "" { 85 | continue 86 | } 87 | 88 | // Are we in compiling mode? 89 | if e.compiling { 90 | 91 | // If we don't have a name 92 | if e.tmp.Name == "" { 93 | 94 | // is the name used? 95 | idx := e.findWord(tok) 96 | if idx != -1 { 97 | fmt.Printf("word %s already defined\n", tok) 98 | os.Exit(1) 99 | } 100 | 101 | // save the name 102 | e.tmp.Name = tok 103 | continue 104 | } 105 | 106 | // End of a definition? 107 | if tok == ";" { 108 | e.Dictionary = append(e.Dictionary, e.tmp) 109 | e.tmp.Name = "" 110 | e.tmp.Words = []float64{} 111 | e.compiling = false 112 | 113 | continue 114 | 115 | } 116 | 117 | // OK we have a name, so lookup the word definition 118 | // for it. 119 | idx := e.findWord(tok) 120 | if idx >= 0 { 121 | // Found it 122 | e.tmp.Words = append(e.tmp.Words, float64(idx)) 123 | 124 | // If the word was a "DO" 125 | if tok == "do" { 126 | e.doOpen = len(e.tmp.Words) - 1 127 | } 128 | // if the word was a "LOOP" 129 | if tok == "loop" { 130 | 131 | // offset of do must be present 132 | e.tmp.Words = append(e.tmp.Words, -2) 133 | e.tmp.Words = append(e.tmp.Words, float64(e.doOpen)) 134 | } 135 | } else { 136 | 137 | // OK we assume the user entered a number 138 | // so we save a magic "-1" flag in our 139 | // definition, and then the number itself 140 | e.tmp.Words = append(e.tmp.Words, -1) 141 | 142 | // Convert to float 143 | val, err := strconv.ParseFloat(tok, 64) 144 | if err != nil { 145 | fmt.Printf("%s: %s\n", tok, err.Error()) 146 | return 147 | } 148 | e.tmp.Words = append(e.tmp.Words, val) 149 | } 150 | 151 | continue 152 | } 153 | 154 | // Did we handle this as a dictionary item? 155 | handled := false 156 | for index, word := range e.Dictionary { 157 | if tok == word.Name { 158 | e.evalWord(index) 159 | handled = true 160 | } 161 | } 162 | 163 | // If we didn't handle this as a word, then 164 | // assume it is a number. 165 | if !handled { 166 | i, err := strconv.ParseFloat(tok, 64) 167 | if err != nil { 168 | fmt.Printf("%s: %s\n", tok, err.Error()) 169 | return 170 | } 171 | 172 | e.Stack.Push(i) 173 | } 174 | } 175 | } 176 | 177 | // evalWord evaluates a word, by index from the dictionary 178 | // 179 | // * Functions might have go-pointers 180 | // if so we just call the pointer. 181 | // 182 | // * Functions might have lists of numbers, which point to previous 183 | // definitions. There are two special-cases, "-1" means the next 184 | // value is a number, and "-2" is a jump opcode to move around 185 | // in our bytecode array. 186 | // 187 | func (e *Eval) evalWord(index int) { 188 | 189 | // Lookup the word 190 | word := e.Dictionary[index] 191 | if word.Function != nil { 192 | word.Function() 193 | return 194 | } 195 | 196 | // Adding a number? 197 | addNum := false 198 | 199 | // jumping? 200 | jump := false 201 | 202 | // We need to allow control-jumps now, so we 203 | // have to store our index manually. 204 | inst := 0 205 | for inst < len(word.Words) { 206 | 207 | // the current opcode 208 | opcode := word.Words[inst] 209 | 210 | // adding a number? 211 | if addNum { 212 | // add to stack 213 | e.Stack.Push(opcode) 214 | addNum = false 215 | } else if jump { 216 | // If the two top-most entries 217 | // are not equal, then jump 218 | cur := e.Stack.Pop() 219 | max := e.Stack.Pop() 220 | 221 | if max > cur { 222 | // put them back 223 | e.Stack.Push(max) 224 | e.Stack.Push(cur) 225 | 226 | // change opcode 227 | inst = int(opcode) 228 | // decrement as it'll get bumped at 229 | // the foot of the loop 230 | inst-- 231 | } 232 | 233 | jump = false 234 | } else { 235 | 236 | // if we see -1 we're adding a number 237 | if opcode == -1 { 238 | addNum = true 239 | } else if opcode == -2 { 240 | // -2 is a jump 241 | jump = true 242 | } else { 243 | 244 | // otherwise we evaluate 245 | // otherwise eval as usual 246 | e.evalWord(int(opcode)) 247 | } 248 | } 249 | 250 | // next instruction 251 | inst++ 252 | } 253 | } 254 | 255 | // findWords returns the index in our dictionary of the entry for the 256 | // given-name. Returns -1 on error. 257 | func (e *Eval) findWord(name string) int { 258 | for index, word := range e.Dictionary { 259 | if name == word.Name { 260 | return index 261 | } 262 | } 263 | return -1 264 | } 265 | -------------------------------------------------------------------------------- /part5/main.go: -------------------------------------------------------------------------------- 1 | // part5 driver 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | 14 | reader := bufio.NewReader(os.Stdin) 15 | forth := NewEval() 16 | 17 | for { 18 | fmt.Printf("> ") 19 | 20 | // Read input 21 | text, err := reader.ReadString('\n') 22 | if err != nil { 23 | fmt.Printf("error reading input: %s\n", err.Error()) 24 | return 25 | } 26 | 27 | // Trim it 28 | text = strings.TrimSpace(text) 29 | 30 | forth.Eval(strings.Split(text, " ")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /part5/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if our stack is empty. 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | -------------------------------------------------------------------------------- /part6/README.md: -------------------------------------------------------------------------------- 1 | # Part 6 2 | 3 | Part six of the implementation is very similar to [part5](../part5/), the difference is that we've added support for `if`/`then` instructions. 4 | 5 | **NOTE** We didn't add support for `else`. 6 | 7 | In FORTH `if` looks like this: 8 | 9 | ``` 10 | : one? 1 = if 42 emit 10 emit then ; 11 | ``` 12 | 13 | `if` will execute if the top-most word on the stack is `1`, otherwise it will skip to the word after the `then`. 14 | 15 | (Of course if there were `else` support then that would be jumped to instead!) 16 | 17 | 18 | 19 | ## Building 20 | 21 | To build and run this version: 22 | 23 | ``` 24 | go build . 25 | ./part6 26 | > : one? 1 = if 42 emit 10 emit then ; 27 | > 1 one? 28 | * 29 | > 0 one? 30 | > 3 one? 31 | > 1 one? 32 | * 33 | > ^D 34 | ``` 35 | 36 | Here we've defined a word which takes a number from the stack, compares it with `1` and if equal outputs a star (and newline). 37 | 38 | You'll see that we've implemented a whole bunch of new primitives, specifically to allow new conditional things: 39 | 40 | * `=` is an equality test. It pops two values off the stack. 41 | * If equal it pushes `1`. 42 | * Otherwise it pushes `0`. 43 | * `<` is a less-than test. It pops two values off the stack. 44 | * If the test passes it pushes `1`. 45 | * Otherwise it pushes `0`. 46 | * etc. 47 | 48 | 49 | ## Implementation 50 | 51 | The implementation here is pretty simple again, as suits a tutorial-code. 52 | 53 | When we see an `if` we need to add a conditional-jump opcode, which will skip over the loop body at run-time if the topmost stack value is not 0. 54 | 55 | In C we might write something like this: 56 | 57 | if ( a < b ) { 58 | do stuff 59 | } 60 | 61 | When we implement that we have to have this in our definition: 62 | 63 | a 64 | b 65 | < 66 | if 67 | -3 \ -3 is a JUMP opcode, which conditionally 68 | XXX \ jumps to the specified offset. 69 | do 70 | stuff 71 | XXX: 72 | 73 | Here we're using `-3` as the magic opcode to mean "pop a value from the stack, and if the value is zero we jump to the specified offset in our word-list. 74 | 75 | Hopefully that is clear! 76 | -------------------------------------------------------------------------------- /part6/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | // 5 | // We've added `emit` here, to output the value at the top of the stack 6 | // as an ASCII character, as well as "do" (nop) and "loop". 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | func (e *Eval) add() { 17 | a := e.Stack.Pop() 18 | b := e.Stack.Pop() 19 | e.Stack.Push(a + b) 20 | } 21 | 22 | func (e *Eval) div() { 23 | a := e.Stack.Pop() 24 | b := e.Stack.Pop() 25 | e.Stack.Push(b / a) 26 | } 27 | 28 | func (e *Eval) do() { 29 | // nop 30 | } 31 | 32 | func (e *Eval) drop() { 33 | e.Stack.Pop() 34 | } 35 | 36 | func (e *Eval) dup() { 37 | a := e.Stack.Pop() 38 | e.Stack.Push(a) 39 | e.Stack.Push(a) 40 | } 41 | 42 | func (e *Eval) emit() { 43 | a := e.Stack.Pop() 44 | fmt.Printf("%c", rune(a)) 45 | } 46 | 47 | func (e *Eval) eq() { 48 | a := e.Stack.Pop() 49 | b := e.Stack.Pop() 50 | if a == b { 51 | e.Stack.Push(1) 52 | } else { 53 | e.Stack.Push(0) 54 | } 55 | } 56 | 57 | func (e *Eval) gt() { 58 | b := e.Stack.Pop() 59 | a := e.Stack.Pop() 60 | if a > b { 61 | e.Stack.Push(1) 62 | } else { 63 | e.Stack.Push(0) 64 | } 65 | } 66 | 67 | func (e *Eval) gtEq() { 68 | b := e.Stack.Pop() 69 | a := e.Stack.Pop() 70 | if a >= b { 71 | e.Stack.Push(1) 72 | } else { 73 | e.Stack.Push(0) 74 | } 75 | } 76 | 77 | func (e *Eval) iff() { 78 | // nop 79 | } 80 | 81 | func (e *Eval) invert() { 82 | v := e.Stack.Pop() 83 | if v == 0 { 84 | e.Stack.Push(1) 85 | } else { 86 | e.Stack.Push(0) 87 | } 88 | } 89 | 90 | func (e *Eval) loop() { 91 | cur := e.Stack.Pop() 92 | max := e.Stack.Pop() 93 | 94 | cur++ 95 | 96 | e.Stack.Push(max) 97 | e.Stack.Push(cur) 98 | } 99 | 100 | func (e *Eval) lt() { 101 | b := e.Stack.Pop() 102 | a := e.Stack.Pop() 103 | if a < b { 104 | e.Stack.Push(1) 105 | } else { 106 | e.Stack.Push(0) 107 | } 108 | } 109 | 110 | func (e *Eval) ltEq() { 111 | b := e.Stack.Pop() 112 | a := e.Stack.Pop() 113 | if a <= b { 114 | e.Stack.Push(1) 115 | } else { 116 | e.Stack.Push(0) 117 | } 118 | } 119 | 120 | func (e *Eval) mul() { 121 | a := e.Stack.Pop() 122 | b := e.Stack.Pop() 123 | e.Stack.Push(a * b) 124 | } 125 | 126 | func (e *Eval) print() { 127 | a := e.Stack.Pop() 128 | 129 | // If the value on the top of the stack is an integer 130 | // then show it as one - i.e. without any ".00000". 131 | if float64(int(a)) == a { 132 | fmt.Printf("%d\n", int(a)) 133 | return 134 | } 135 | 136 | // OK we have a floating-point result. Show it, but 137 | // remove any trailing "0". 138 | // 139 | // This means we get 1.25 instead of 1.2500000 shown 140 | // when the user runs `5 4 / .`. 141 | // 142 | output := fmt.Sprintf("%f", a) 143 | for strings.HasSuffix(output, "0") { 144 | output = strings.TrimSuffix(output, "0") 145 | } 146 | fmt.Printf("%s\n", output) 147 | 148 | } 149 | 150 | // startDefinition moves us into compiling-mode 151 | // 152 | // Note the interpreter handles removing this when it sees ";" 153 | func (e *Eval) startDefinition() { 154 | e.compiling = true 155 | } 156 | 157 | func (e *Eval) sub() { 158 | a := e.Stack.Pop() 159 | b := e.Stack.Pop() 160 | e.Stack.Push(b - a) 161 | } 162 | 163 | func (e *Eval) swap() { 164 | a := e.Stack.Pop() 165 | b := e.Stack.Pop() 166 | e.Stack.Push(a) 167 | e.Stack.Push(b) 168 | } 169 | 170 | func (e *Eval) then() { 171 | // nop 172 | } 173 | 174 | func (e *Eval) words() { 175 | known := []string{} 176 | 177 | for _, entry := range e.Dictionary { 178 | known = append(known, entry.Name) 179 | } 180 | 181 | sort.Strings(known) 182 | fmt.Printf("%s\n", strings.Join(known, " ")) 183 | } 184 | -------------------------------------------------------------------------------- /part6/eval.go: -------------------------------------------------------------------------------- 1 | // part6 - allow if, and implement more built-ins to make that useful. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Word is the structure for a single word 12 | type Word struct { 13 | // Name is the name of the function "+", "print", etc. 14 | Name string 15 | 16 | // Function is the function-pointer to call to invoke it. 17 | // 18 | // If this is nil then instead we interpret the known-codes 19 | // from previously defined words. 20 | Function func() 21 | 22 | // Words holds the words we execute if the function-pointer 23 | // is empty. 24 | // 25 | // The indexes here are relative to the Dictionary our evaluator 26 | // holds/maintains 27 | Words []float64 28 | } 29 | 30 | // Eval is our evaluation structure 31 | type Eval struct { 32 | 33 | // Internal stack 34 | Stack Stack 35 | 36 | // Dictionary entries 37 | Dictionary []Word 38 | 39 | // Are we in a compiling mode? 40 | compiling bool 41 | 42 | // Temporary word we're compiling 43 | tmp Word 44 | 45 | // open of the last do 46 | doOpen int 47 | 48 | // offset of the argument to any IF 49 | ifOffset int 50 | } 51 | 52 | // NewEval returns a simple evaluator 53 | func NewEval() *Eval { 54 | 55 | // Empty structure 56 | e := &Eval{} 57 | 58 | // Populate the dictionary of words we have implemented 59 | // which are hard-coded. 60 | e.Dictionary = []Word{ 61 | {Name: "*", Function: e.mul}, 62 | {Name: "+", Function: e.add}, 63 | {Name: "-", Function: e.sub}, 64 | {Name: ".", Function: e.print}, 65 | {Name: "/", Function: e.div}, 66 | {Name: ":", Function: e.startDefinition}, 67 | {Name: "<", Function: e.lt}, 68 | {Name: "<=", Function: e.ltEq}, 69 | {Name: "=", Function: e.eq}, 70 | {Name: "==", Function: e.eq}, 71 | {Name: ">", Function: e.gt}, 72 | {Name: ">=", Function: e.gtEq}, 73 | {Name: "do", Function: e.do}, // NOP 74 | {Name: "if", Function: e.iff}, // NOP 75 | {Name: "invert", Function: e.invert}, 76 | {Name: "drop", Function: e.drop}, 77 | {Name: "dup", Function: e.dup}, 78 | {Name: "emit", Function: e.emit}, 79 | {Name: "loop", Function: e.loop}, 80 | {Name: "print", Function: e.print}, 81 | {Name: "swap", Function: e.swap}, 82 | {Name: "then", Function: e.then}, // NOP 83 | {Name: "words", Function: e.words}, 84 | } 85 | 86 | return e 87 | } 88 | 89 | // Eval processes a list of tokens. 90 | // 91 | // This is invoked by our repl with a line of input at the time. 92 | func (e *Eval) Eval(args []string) { 93 | 94 | for _, tok := range args { 95 | 96 | // Trim the leading/trailing spaces, 97 | // and skip any empty tokens 98 | tok = strings.TrimSpace(tok) 99 | if tok == "" { 100 | continue 101 | } 102 | 103 | // Are we in compiling mode? 104 | if e.compiling { 105 | 106 | // If we don't have a name 107 | if e.tmp.Name == "" { 108 | 109 | // is the name used? 110 | idx := e.findWord(tok) 111 | if idx != -1 { 112 | fmt.Printf("word %s already defined\n", tok) 113 | os.Exit(1) 114 | } 115 | 116 | // save the name 117 | e.tmp.Name = tok 118 | 119 | continue 120 | } 121 | 122 | // End of a definition? 123 | if tok == ";" { 124 | e.Dictionary = append(e.Dictionary, e.tmp) 125 | 126 | e.tmp.Name = "" 127 | e.tmp.Words = []float64{} 128 | e.compiling = false 129 | 130 | continue 131 | 132 | } 133 | 134 | // OK we have a name, so lookup the word definition 135 | // for it. 136 | idx := e.findWord(tok) 137 | if idx >= 0 { 138 | // Found it 139 | e.tmp.Words = append(e.tmp.Words, float64(idx)) 140 | 141 | // If the word was a "DO" 142 | if tok == "do" { 143 | e.doOpen = len(e.tmp.Words) - 1 144 | } 145 | 146 | // if the word was a "LOOP" 147 | if tok == "loop" { 148 | 149 | // offset of do must be present 150 | e.tmp.Words = append(e.tmp.Words, -2) 151 | e.tmp.Words = append(e.tmp.Words, float64(e.doOpen)) 152 | } 153 | 154 | // If the word was a "if" 155 | if tok == "if" { 156 | // we add the conditional-jump opcode 157 | e.tmp.Words = append(e.tmp.Words, -3) 158 | // placeholder jump-offset 159 | e.tmp.Words = append(e.tmp.Words, 99) 160 | 161 | // save the address of our stub, 162 | // so we can back-patch 163 | e.ifOffset = len(e.tmp.Words) 164 | } 165 | 166 | if tok == "then" { 167 | // back - patch the jump offset to the position of this word 168 | e.tmp.Words[e.ifOffset-1] = float64(len(e.tmp.Words) - 1) 169 | } 170 | 171 | } else { 172 | 173 | // OK we assume the user entered a number 174 | // so we save a magic "-1" flag in our 175 | // definition, and then the number itself 176 | e.tmp.Words = append(e.tmp.Words, -1) 177 | 178 | // Convert to float 179 | val, err := strconv.ParseFloat(tok, 64) 180 | if err != nil { 181 | fmt.Printf("%s: %s\n", tok, err.Error()) 182 | return 183 | } 184 | e.tmp.Words = append(e.tmp.Words, val) 185 | } 186 | 187 | continue 188 | } 189 | 190 | // Did we handle this as a dictionary item? 191 | handled := false 192 | for index, word := range e.Dictionary { 193 | if tok == word.Name { 194 | e.evalWord(index) 195 | handled = true 196 | } 197 | } 198 | 199 | // If we didn't handle this as a word, then 200 | // assume it is a number. 201 | if !handled { 202 | i, err := strconv.ParseFloat(tok, 64) 203 | if err != nil { 204 | fmt.Printf("%s: %s\n", tok, err.Error()) 205 | return 206 | } 207 | 208 | e.Stack.Push(i) 209 | } 210 | } 211 | } 212 | 213 | // evalWord evaluates a word, by index from the dictionary 214 | // 215 | // * Functions might contain a pointer to a function implemented in go. 216 | // 217 | // If so we just call that pointer. 218 | // 219 | // * Functions will otherwise have lists of numbers, which point to 220 | // previously defined words. 221 | // 222 | // In addition to the pointers to previously-defined words there are 223 | // also some special values: 224 | // 225 | // "-1" means the next value is a number 226 | // 227 | // "-2" is an unconditional jump, which will change our IP. 228 | // 229 | // "-3" is a conditional-jump, which will change our IP if 230 | // the topmost item on the stack is "0". 231 | // 232 | func (e *Eval) evalWord(index int) { 233 | 234 | // Lookup the word 235 | word := e.Dictionary[index] 236 | 237 | // Is this implemented in golang? If so just invoke the function 238 | // and we're done. 239 | if word.Function != nil { 240 | word.Function() 241 | return 242 | } 243 | 244 | // 245 | // TODO: Improve the way these special cases are handled. 246 | // 247 | // The reason this is handled like this, is to avoid poking the 248 | // indexes directly and risking array-overflow on malformed 249 | // word-lists. 250 | // 251 | // (i.e. When we see "[1, 2, -1]" the last instruction should add 252 | // the following number to the stack - but it is missing. We want 253 | // to avoid messing around with the index to avoid that.) 254 | // 255 | 256 | // Adding a number? 257 | addNum := false 258 | 259 | // jumping? 260 | jump := false 261 | 262 | // jumping if the stack has a false-value? 263 | condJump := false 264 | 265 | // We need to allow control-jumps now, so we 266 | // have to store our index manually. 267 | ip := 0 268 | for ip < len(word.Words) { 269 | 270 | // the current opcode 271 | opcode := word.Words[ip] 272 | 273 | // adding a number? 274 | if addNum { 275 | // add to stack 276 | e.Stack.Push(opcode) 277 | addNum = false 278 | } else if jump { 279 | // If the two top-most entries 280 | // are not equal, then jump 281 | cur := e.Stack.Pop() 282 | max := e.Stack.Pop() 283 | 284 | if max > cur { 285 | // put them back 286 | e.Stack.Push(max) 287 | e.Stack.Push(cur) 288 | 289 | // change opcode 290 | ip = int(opcode) 291 | 292 | // decrement as it'll get bumped at 293 | // the foot of the loop 294 | ip-- 295 | } 296 | 297 | jump = false 298 | } else if condJump { 299 | // Jump only if 0 is on the top of the stack. 300 | // 301 | // i.e. This is an "if" test. 302 | val := e.Stack.Pop() 303 | if val == 0 { 304 | // change opcode 305 | ip = int(opcode) 306 | // decrement as it'll get bumped at 307 | // the foot of the loop 308 | ip-- 309 | } 310 | condJump = false 311 | } else { 312 | 313 | // if we see -1 we're adding a number 314 | if opcode == -1 { 315 | addNum = true 316 | } else if opcode == -2 { 317 | // -2 is a jump 318 | jump = true 319 | } else if opcode == -3 { 320 | // -3 is a conditional-jump 321 | condJump = true 322 | } else { 323 | 324 | // otherwise we evaluate 325 | // otherwise eval as usual 326 | e.evalWord(int(opcode)) 327 | } 328 | } 329 | 330 | // next instruction 331 | ip++ 332 | } 333 | } 334 | 335 | // findWords returns the index in our dictionary of the entry for the 336 | // given-name. Returns -1 if the word cannot be found. 337 | func (e *Eval) findWord(name string) int { 338 | for index, word := range e.Dictionary { 339 | if name == word.Name { 340 | return index 341 | } 342 | } 343 | return -1 344 | } 345 | -------------------------------------------------------------------------------- /part6/foth.4th: -------------------------------------------------------------------------------- 1 | # 2 | # This file is loaded on-startup, if it is present. 3 | # 4 | # NOTE: Lines having a "#"-prefix will be skipped. 5 | # 6 | # This is not a standard approach to FORTH comments, but it makes 7 | # sense for this particular implementation. 8 | # 9 | 10 | 11 | # 12 | # CR: Output a carrige return (newline). 13 | # 14 | : cr 10 emit ; 15 | 16 | # 17 | # Star: Output a star to the console. 18 | # 19 | # Here 42 is the ASCII code for the "*" character. 20 | # 21 | : star 42 emit ; 22 | 23 | 24 | # 25 | # Stars: Show the specified number of stars. 26 | # 27 | # e.g. "3 stars" 28 | # 29 | : stars 0 do star loop 10 emit ; 30 | 31 | 32 | # 33 | # square: Square a number 34 | # 35 | : square dup * ; 36 | 37 | # 38 | # cube: cube a number 39 | # 40 | : cube dup square * ; 41 | 42 | # 43 | # 1+: add one to a number 44 | # 45 | : 1+ 1 + ; 46 | 47 | # 48 | # boot: output a message on-startup 49 | # 50 | : bootup 87 emit 101 emit 108 emit 99 emit 111 emit 109 emit 101 emit 32 emit 116 emit 111 emit 32 emit 102 emit 111 emit 116 emit 104 emit 33 emit 10 emit ; 51 | bootup 52 | -------------------------------------------------------------------------------- /part6/main.go: -------------------------------------------------------------------------------- 1 | // part6 driver 2 | // 3 | // Loads "foth.4th" from cwd, if present, and evaluates it before the REPL 4 | // is launched - otherwise the same as previous versions. 5 | 6 | package main 7 | 8 | import ( 9 | "bufio" 10 | "fmt" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | // If the given file exists, read the contents, and evaluate it 16 | func doInit(eval *Eval, path string) { 17 | 18 | handle, err := os.Open(path) 19 | if err != nil { 20 | return 21 | } 22 | 23 | reader := bufio.NewReader(handle) 24 | line, err := reader.ReadString(byte('\n')) 25 | for err == nil { 26 | 27 | // Trim it 28 | line = strings.TrimSpace(line) 29 | 30 | // Is this isn't comment then execute it 31 | if !strings.HasPrefix(line, "#") { 32 | 33 | // Evaluate 34 | eval.Eval(strings.Split(line, " ")) 35 | } 36 | 37 | // Repeat 38 | line, err = reader.ReadString(byte('\n')) 39 | } 40 | 41 | handle.Close() 42 | } 43 | 44 | func main() { 45 | 46 | reader := bufio.NewReader(os.Stdin) 47 | forth := NewEval() 48 | 49 | // Load the init-file if it is present. 50 | doInit(forth, "foth.4th") 51 | 52 | for { 53 | fmt.Printf("> ") 54 | 55 | // Read input 56 | text, err := reader.ReadString('\n') 57 | if err != nil { 58 | fmt.Printf("error reading input: %s\n", err.Error()) 59 | return 60 | } 61 | 62 | // Trim it 63 | text = strings.TrimSpace(text) 64 | 65 | forth.Eval(strings.Split(text, " ")) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /part6/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if our stack is empty. 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | -------------------------------------------------------------------------------- /part7/README.md: -------------------------------------------------------------------------------- 1 | # Part 7 2 | 3 | Part seven of the implementation is very similar to [part6](../part6/), the difference is that we've added **basic** support for strings. 4 | 5 | The specific problem we have is that our stack, and word definitions, only allow support for storing floating-point numbers. So we cannot store a string on the stack, which means we must be indirect: 6 | 7 | * When we see a string we store it in an array of known strings. 8 | * We then push the offset of the new string entry onto the stack. 9 | * This allows it to be referenced and used. 10 | 11 | However because we don't have arbitrary read/write to RAM opcodes/words we can't do much more than that. We've added some new string-specific words as a proof of concept though: 12 | 13 | * `strlen` - Return the length of a string. 14 | * `strprn` - Print a string. 15 | * `strings` - Return the maximum string index we've seen. 16 | 17 | 18 | 19 | ## Building 20 | 21 | To build and run this version: 22 | 23 | ``` 24 | go build . 25 | ./part7 26 | > : steve "steve" ; 27 | > steve strlen . 28 | 5 29 | > steve strprn . 30 | steve 31 | > "test" strlen . 32 | 4 33 | > 34 | ^D 35 | ``` 36 | 37 | 38 | 39 | ## Implementation 40 | 41 | The implementation here is pretty simple again, as suits tutorial-code. The interpreter already had a string-storing area, added for the string-literal printing support: 42 | 43 | ``` 44 | // Eval is our evaluation structure 45 | type Eval struct { 46 | 47 | .. 48 | // strings contains string-storage 49 | strings []string 50 | } 51 | ``` 52 | 53 | In the past when we saw input we didn't recognize we assumed it was a number, and parsed that with `strconv.ParseFloat`, but now we test if the first character of the token is a `"` character. If it is we : 54 | 55 | * Strip the leading/trailing `"` from it. 56 | * Append the string to our storage array. 57 | * Push the offset of the new entry. 58 | * Using the magic -1 value, if we're in compiling mode. 59 | * If you recall from [part4](../part4/) this is the magic word that allows a number to be read from the word's definition. 60 | 61 | From there there is no special support. The primitives just read from the string area, for example `strlen` looks like this: 62 | 63 | ``` 64 | // strlen 65 | func (e *Eval) strlen() { 66 | addr := e.Stack.Pop() 67 | i := int(addr) 68 | 69 | if i < len(e.strings) { 70 | str := e.strings[i] 71 | e.Stack.Push(float64(len(str))) 72 | } else { 73 | e.Stack.Push(-1) 74 | } 75 | } 76 | ``` 77 | 78 | 79 | 80 | ## Bugs 81 | 82 | Because we don't have a decent lexer we can only handle strings without spaces, or newlines. Our [final version](../foth/) corrects that problem, and adds support for `\t`, `\n`, etc. 83 | -------------------------------------------------------------------------------- /part7/builtins.go: -------------------------------------------------------------------------------- 1 | // This file contains the built-in facilities we have hard-coded. 2 | // 3 | // That means the implementation for "+", "-", "/", "*", and "print". 4 | // 5 | // We've added `emit` here, to output the value at the top of the stack 6 | // as an ASCII character, as well as "do" (nop) and "loop". 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | func (e *Eval) add() { 17 | a := e.Stack.Pop() 18 | b := e.Stack.Pop() 19 | e.Stack.Push(a + b) 20 | } 21 | 22 | func (e *Eval) div() { 23 | a := e.Stack.Pop() 24 | b := e.Stack.Pop() 25 | e.Stack.Push(b / a) 26 | } 27 | 28 | func (e *Eval) do() { 29 | // nop 30 | } 31 | 32 | func (e *Eval) drop() { 33 | e.Stack.Pop() 34 | } 35 | 36 | func (e *Eval) dup() { 37 | a := e.Stack.Pop() 38 | e.Stack.Push(a) 39 | e.Stack.Push(a) 40 | } 41 | 42 | func (e *Eval) emit() { 43 | a := e.Stack.Pop() 44 | fmt.Printf("%c", rune(a)) 45 | } 46 | 47 | func (e *Eval) eq() { 48 | a := e.Stack.Pop() 49 | b := e.Stack.Pop() 50 | if a == b { 51 | e.Stack.Push(1) 52 | } else { 53 | e.Stack.Push(0) 54 | } 55 | } 56 | 57 | func (e *Eval) gt() { 58 | b := e.Stack.Pop() 59 | a := e.Stack.Pop() 60 | if a > b { 61 | e.Stack.Push(1) 62 | } else { 63 | e.Stack.Push(0) 64 | } 65 | } 66 | 67 | func (e *Eval) gtEq() { 68 | b := e.Stack.Pop() 69 | a := e.Stack.Pop() 70 | if a >= b { 71 | e.Stack.Push(1) 72 | } else { 73 | e.Stack.Push(0) 74 | } 75 | } 76 | 77 | func (e *Eval) iff() { 78 | // nop 79 | } 80 | 81 | func (e *Eval) invert() { 82 | v := e.Stack.Pop() 83 | if v == 0 { 84 | e.Stack.Push(1) 85 | } else { 86 | e.Stack.Push(0) 87 | } 88 | } 89 | 90 | func (e *Eval) loop() { 91 | cur := e.Stack.Pop() 92 | max := e.Stack.Pop() 93 | 94 | cur++ 95 | 96 | e.Stack.Push(max) 97 | e.Stack.Push(cur) 98 | } 99 | 100 | func (e *Eval) lt() { 101 | b := e.Stack.Pop() 102 | a := e.Stack.Pop() 103 | if a < b { 104 | e.Stack.Push(1) 105 | } else { 106 | e.Stack.Push(0) 107 | } 108 | } 109 | 110 | func (e *Eval) ltEq() { 111 | b := e.Stack.Pop() 112 | a := e.Stack.Pop() 113 | if a <= b { 114 | e.Stack.Push(1) 115 | } else { 116 | e.Stack.Push(0) 117 | } 118 | } 119 | 120 | func (e *Eval) mul() { 121 | a := e.Stack.Pop() 122 | b := e.Stack.Pop() 123 | e.Stack.Push(a * b) 124 | } 125 | 126 | func (e *Eval) print() { 127 | a := e.Stack.Pop() 128 | 129 | // If the value on the top of the stack is an integer 130 | // then show it as one - i.e. without any ".00000". 131 | if float64(int(a)) == a { 132 | fmt.Printf("%d\n", int(a)) 133 | return 134 | } 135 | 136 | // OK we have a floating-point result. Show it, but 137 | // remove any trailing "0". 138 | // 139 | // This means we get 1.25 instead of 1.2500000 shown 140 | // when the user runs `5 4 / .`. 141 | // 142 | output := fmt.Sprintf("%f", a) 143 | for strings.HasSuffix(output, "0") { 144 | output = strings.TrimSuffix(output, "0") 145 | } 146 | fmt.Printf("%s\n", output) 147 | 148 | } 149 | 150 | // startDefinition moves us into compiling-mode 151 | // 152 | // Note the interpreter handles removing this when it sees ";" 153 | func (e *Eval) startDefinition() { 154 | e.compiling = true 155 | } 156 | 157 | // strings 158 | func (e *Eval) stringCount() { 159 | // Return the number of strings we've seen 160 | e.Stack.Push(float64(len(e.strings))) 161 | } 162 | 163 | // strlen 164 | func (e *Eval) strlen() { 165 | addr := e.Stack.Pop() 166 | i := int(addr) 167 | 168 | if i < len(e.strings) { 169 | str := e.strings[i] 170 | e.Stack.Push(float64(len(str))) 171 | } else { 172 | e.Stack.Push(-1) 173 | } 174 | } 175 | 176 | // strprn - string printing 177 | func (e *Eval) strprn() { 178 | addr := e.Stack.Pop() 179 | i := int(addr) 180 | 181 | if i < len(e.strings) { 182 | str := e.strings[i] 183 | fmt.Printf("%s", str) 184 | } 185 | } 186 | 187 | func (e *Eval) sub() { 188 | a := e.Stack.Pop() 189 | b := e.Stack.Pop() 190 | e.Stack.Push(b - a) 191 | } 192 | 193 | func (e *Eval) swap() { 194 | a := e.Stack.Pop() 195 | b := e.Stack.Pop() 196 | e.Stack.Push(a) 197 | e.Stack.Push(b) 198 | } 199 | 200 | func (e *Eval) then() { 201 | // nop 202 | } 203 | 204 | func (e *Eval) words() { 205 | known := []string{} 206 | 207 | for _, entry := range e.Dictionary { 208 | known = append(known, entry.Name) 209 | } 210 | 211 | sort.Strings(known) 212 | fmt.Printf("%s\n", strings.Join(known, " ")) 213 | } 214 | -------------------------------------------------------------------------------- /part7/eval.go: -------------------------------------------------------------------------------- 1 | // part6 - allow if, and implement more built-ins to make that useful. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Word is the structure for a single word 12 | type Word struct { 13 | // Name is the name of the function "+", "print", etc. 14 | Name string 15 | 16 | // Function is the function-pointer to call to invoke it. 17 | // 18 | // If this is nil then instead we interpret the known-codes 19 | // from previously defined words. 20 | Function func() 21 | 22 | // Words holds the words we execute if the function-pointer 23 | // is empty. 24 | // 25 | // The indexes here are relative to the Dictionary our evaluator 26 | // holds/maintains 27 | Words []float64 28 | } 29 | 30 | // Eval is our evaluation structure 31 | type Eval struct { 32 | 33 | // Internal stack 34 | Stack Stack 35 | 36 | // Dictionary entries 37 | Dictionary []Word 38 | 39 | // Are we in a compiling mode? 40 | compiling bool 41 | 42 | // Temporary word we're compiling 43 | tmp Word 44 | 45 | // open of the last do 46 | doOpen int 47 | 48 | // offset of the argument to any IF 49 | ifOffset int 50 | 51 | // strings contains string-storage 52 | strings []string 53 | } 54 | 55 | // NewEval returns a simple evaluator 56 | func NewEval() *Eval { 57 | 58 | // Empty structure 59 | e := &Eval{} 60 | 61 | // Populate the dictionary of words we have implemented 62 | // which are hard-coded. 63 | e.Dictionary = []Word{ 64 | {Name: "*", Function: e.mul}, 65 | {Name: "+", Function: e.add}, 66 | {Name: "-", Function: e.sub}, 67 | {Name: ".", Function: e.print}, 68 | {Name: "/", Function: e.div}, 69 | {Name: ":", Function: e.startDefinition}, 70 | {Name: "<", Function: e.lt}, 71 | {Name: "<=", Function: e.ltEq}, 72 | {Name: "=", Function: e.eq}, 73 | {Name: "==", Function: e.eq}, 74 | {Name: ">", Function: e.gt}, 75 | {Name: ">=", Function: e.gtEq}, 76 | {Name: "do", Function: e.do}, // NOP 77 | {Name: "if", Function: e.iff}, // NOP 78 | {Name: "invert", Function: e.invert}, 79 | {Name: "drop", Function: e.drop}, 80 | {Name: "dup", Function: e.dup}, 81 | {Name: "emit", Function: e.emit}, 82 | {Name: "loop", Function: e.loop}, 83 | {Name: "print", Function: e.print}, 84 | {Name: "swap", Function: e.swap}, 85 | {Name: "then", Function: e.then}, // NOP 86 | {Name: "words", Function: e.words}, 87 | {Name: "strings", Function: e.stringCount}, 88 | {Name: "strlen", Function: e.strlen}, 89 | {Name: "strprn", Function: e.strprn}, 90 | } 91 | 92 | return e 93 | } 94 | 95 | // Eval processes a list of tokens. 96 | // 97 | // This is invoked by our repl with a line of input at the time. 98 | func (e *Eval) Eval(args []string) { 99 | 100 | for _, tok := range args { 101 | 102 | // Trim the leading/trailing spaces, 103 | // and skip any empty tokens 104 | tok = strings.TrimSpace(tok) 105 | if tok == "" { 106 | continue 107 | } 108 | 109 | // Are we in compiling mode? 110 | if e.compiling { 111 | 112 | // If we don't have a name 113 | if e.tmp.Name == "" { 114 | 115 | // is the name used? 116 | idx := e.findWord(tok) 117 | if idx != -1 { 118 | fmt.Printf("word %s already defined\n", tok) 119 | os.Exit(1) 120 | } 121 | 122 | // save the name 123 | e.tmp.Name = tok 124 | 125 | continue 126 | } 127 | 128 | // End of a definition? 129 | if tok == ";" { 130 | e.Dictionary = append(e.Dictionary, e.tmp) 131 | 132 | e.tmp.Name = "" 133 | e.tmp.Words = []float64{} 134 | e.compiling = false 135 | 136 | continue 137 | 138 | } 139 | 140 | // OK we have a name, so lookup the word definition 141 | // for it. 142 | idx := e.findWord(tok) 143 | if idx >= 0 { 144 | // Found it 145 | e.tmp.Words = append(e.tmp.Words, float64(idx)) 146 | 147 | // If the word was a "DO" 148 | if tok == "do" { 149 | e.doOpen = len(e.tmp.Words) - 1 150 | } 151 | 152 | // if the word was a "LOOP" 153 | if tok == "loop" { 154 | 155 | // offset of do must be present 156 | e.tmp.Words = append(e.tmp.Words, -2) 157 | e.tmp.Words = append(e.tmp.Words, float64(e.doOpen)) 158 | } 159 | 160 | // If the word was a "if" 161 | if tok == "if" { 162 | // we add the conditional-jump opcode 163 | e.tmp.Words = append(e.tmp.Words, -3) 164 | // placeholder jump-offset 165 | e.tmp.Words = append(e.tmp.Words, 99) 166 | 167 | // save the address of our stub, 168 | // so we can back-patch 169 | e.ifOffset = len(e.tmp.Words) 170 | } 171 | 172 | if tok == "then" { 173 | // back - patch the jump offset to the position of this word 174 | e.tmp.Words[e.ifOffset-1] = float64(len(e.tmp.Words) - 1) 175 | } 176 | 177 | } else { 178 | 179 | // If this starts with a " then it is a strings 180 | // Append it, and push the offset 181 | if len(tok) > 0 && tok[0] == '"' { 182 | 183 | // Remove leading/trailing " 184 | str := tok 185 | str = str[1:] 186 | str = str[:(len(str) - 1)] 187 | 188 | // Append the string and get its offset 189 | e.strings = append(e.strings, str) 190 | 191 | offset := len(e.strings) - 1 192 | 193 | // We're pushing an int, which is the offset of the string 194 | e.tmp.Words = append(e.tmp.Words, -1) 195 | e.tmp.Words = append(e.tmp.Words, float64(offset)) 196 | continue 197 | } 198 | 199 | // OK we assume the user entered a number 200 | // so we save a magic "-1" flag in our 201 | // definition, and then the number itself 202 | e.tmp.Words = append(e.tmp.Words, -1) 203 | 204 | // Convert to float 205 | val, err := strconv.ParseFloat(tok, 64) 206 | if err != nil { 207 | fmt.Printf("%s: %s\n", tok, err.Error()) 208 | return 209 | } 210 | e.tmp.Words = append(e.tmp.Words, val) 211 | } 212 | 213 | continue 214 | } 215 | 216 | // Did we handle this as a dictionary item? 217 | handled := false 218 | for index, word := range e.Dictionary { 219 | if tok == word.Name { 220 | e.evalWord(index) 221 | handled = true 222 | } 223 | } 224 | 225 | // If we didn't handle this as a word, then 226 | // assume it is a number. 227 | if !handled { 228 | 229 | // If this starts with a " then it is a strings 230 | // Append it, and push the offset 231 | if len(tok) > 0 && tok[0] == '"' { 232 | 233 | // Remove leading/trailing " 234 | str := tok 235 | str = str[1:] 236 | str = str[:(len(str) - 1)] 237 | e.strings = append(e.strings, str) 238 | e.Stack.Push(float64(len(e.strings) - 1)) 239 | continue 240 | } 241 | 242 | i, err := strconv.ParseFloat(tok, 64) 243 | if err != nil { 244 | fmt.Printf("%s: %s\n", tok, err.Error()) 245 | return 246 | } 247 | 248 | e.Stack.Push(i) 249 | } 250 | } 251 | } 252 | 253 | // evalWord evaluates a word, by index from the dictionary 254 | // 255 | // * Functions might contain a pointer to a function implemented in go. 256 | // 257 | // If so we just call that pointer. 258 | // 259 | // - Functions will otherwise have lists of numbers, which point to 260 | // previously defined words. 261 | // 262 | // In addition to the pointers to previously-defined words there are 263 | // also some special values: 264 | // 265 | // "-1" means the next value is a number 266 | // 267 | // "-2" is an unconditional jump, which will change our IP. 268 | // 269 | // "-3" is a conditional-jump, which will change our IP if 270 | // the topmost item on the stack is "0". 271 | func (e *Eval) evalWord(index int) { 272 | 273 | // Lookup the word 274 | word := e.Dictionary[index] 275 | 276 | // Is this implemented in golang? If so just invoke the function 277 | // and we're done. 278 | if word.Function != nil { 279 | word.Function() 280 | return 281 | } 282 | 283 | // 284 | // TODO: Improve the way these special cases are handled. 285 | // 286 | // The reason this is handled like this, is to avoid poking the 287 | // indexes directly and risking array-overflow on malformed 288 | // word-lists. 289 | // 290 | // (i.e. When we see "[1, 2, -1]" the last instruction should add 291 | // the following number to the stack - but it is missing. We want 292 | // to avoid messing around with the index to avoid that.) 293 | // 294 | 295 | // Adding a number? 296 | addNum := false 297 | 298 | // jumping? 299 | jump := false 300 | 301 | // jumping if the stack has a false-value? 302 | condJump := false 303 | 304 | // We need to allow control-jumps now, so we 305 | // have to store our index manually. 306 | ip := 0 307 | for ip < len(word.Words) { 308 | 309 | // the current opcode 310 | opcode := word.Words[ip] 311 | 312 | // adding a number? 313 | if addNum { 314 | // add to stack 315 | e.Stack.Push(opcode) 316 | addNum = false 317 | } else if jump { 318 | // If the two top-most entries 319 | // are not equal, then jump 320 | cur := e.Stack.Pop() 321 | max := e.Stack.Pop() 322 | 323 | if max > cur { 324 | // put them back 325 | e.Stack.Push(max) 326 | e.Stack.Push(cur) 327 | 328 | // change opcode 329 | ip = int(opcode) 330 | 331 | // decrement as it'll get bumped at 332 | // the foot of the loop 333 | ip-- 334 | } 335 | 336 | jump = false 337 | } else if condJump { 338 | // Jump only if 0 is on the top of the stack. 339 | // 340 | // i.e. This is an "if" test. 341 | val := e.Stack.Pop() 342 | if val == 0 { 343 | // change opcode 344 | ip = int(opcode) 345 | // decrement as it'll get bumped at 346 | // the foot of the loop 347 | ip-- 348 | } 349 | condJump = false 350 | } else { 351 | 352 | // if we see -1 we're adding a number 353 | if opcode == -1 { 354 | addNum = true 355 | } else if opcode == -2 { 356 | // -2 is a jump 357 | jump = true 358 | } else if opcode == -3 { 359 | // -3 is a conditional-jump 360 | condJump = true 361 | } else { 362 | 363 | // otherwise we evaluate 364 | // otherwise eval as usual 365 | e.evalWord(int(opcode)) 366 | } 367 | } 368 | 369 | // next instruction 370 | ip++ 371 | } 372 | } 373 | 374 | // findWords returns the index in our dictionary of the entry for the 375 | // given-name. Returns -1 if the word cannot be found. 376 | func (e *Eval) findWord(name string) int { 377 | for index, word := range e.Dictionary { 378 | if name == word.Name { 379 | return index 380 | } 381 | } 382 | return -1 383 | } 384 | -------------------------------------------------------------------------------- /part7/foth.4th: -------------------------------------------------------------------------------- 1 | # 2 | # This file is loaded on-startup, if it is present. 3 | # 4 | # NOTE: Lines having a "#"-prefix will be skipped. 5 | # 6 | # This is not a standard approach to FORTH comments, but it makes 7 | # sense for this particular implementation. 8 | # 9 | 10 | 11 | # 12 | # CR: Output a carrige return (newline). 13 | # 14 | : cr 10 emit ; 15 | 16 | # 17 | # Star: Output a star to the console. 18 | # 19 | # Here 42 is the ASCII code for the "*" character. 20 | # 21 | : star 42 emit ; 22 | 23 | 24 | # 25 | # Stars: Show the specified number of stars. 26 | # 27 | # e.g. "3 stars" 28 | # 29 | : stars 0 do star loop 10 emit ; 30 | 31 | 32 | # 33 | # square: Square a number 34 | # 35 | : square dup * ; 36 | 37 | # 38 | # cube: cube a number 39 | # 40 | : cube dup square * ; 41 | 42 | # 43 | # 1+: add one to a number 44 | # 45 | : 1+ 1 + ; 46 | 47 | # 48 | # boot: output a message on-startup 49 | # 50 | : bootup 87 emit 101 emit 108 emit 99 emit 111 emit 109 emit 101 emit 32 emit 116 emit 111 emit 32 emit 102 emit 111 emit 116 emit 104 emit 33 emit 10 emit ; 51 | bootup 52 | -------------------------------------------------------------------------------- /part7/main.go: -------------------------------------------------------------------------------- 1 | // part6 driver 2 | // 3 | // Loads "foth.4th" from cwd, if present, and evaluates it before the REPL 4 | // is launched - otherwise the same as previous versions. 5 | 6 | package main 7 | 8 | import ( 9 | "bufio" 10 | "fmt" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | // If the given file exists, read the contents, and evaluate it 16 | func doInit(eval *Eval, path string) { 17 | 18 | handle, err := os.Open(path) 19 | if err != nil { 20 | return 21 | } 22 | 23 | reader := bufio.NewReader(handle) 24 | line, err := reader.ReadString(byte('\n')) 25 | for err == nil { 26 | 27 | // Trim it 28 | line = strings.TrimSpace(line) 29 | 30 | // Is this isn't comment then execute it 31 | if !strings.HasPrefix(line, "#") { 32 | 33 | // Evaluate 34 | eval.Eval(strings.Split(line, " ")) 35 | } 36 | 37 | // Repeat 38 | line, err = reader.ReadString(byte('\n')) 39 | } 40 | 41 | handle.Close() 42 | } 43 | 44 | func main() { 45 | 46 | reader := bufio.NewReader(os.Stdin) 47 | forth := NewEval() 48 | 49 | // Load the init-file if it is present. 50 | doInit(forth, "foth.4th") 51 | 52 | for { 53 | fmt.Printf("> ") 54 | 55 | // Read input 56 | text, err := reader.ReadString('\n') 57 | if err != nil { 58 | fmt.Printf("error reading input: %s\n", err.Error()) 59 | return 60 | } 61 | 62 | // Trim it 63 | text = strings.TrimSpace(text) 64 | 65 | forth.Eval(strings.Split(text, " ")) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /part7/stack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stack holds our numbers. 9 | type Stack []float64 10 | 11 | // IsEmpty checks if our stack is empty. 12 | func (s *Stack) IsEmpty() bool { 13 | return len(*s) == 0 14 | } 15 | 16 | // Push adds a new number to the stack 17 | func (s *Stack) Push(x float64) { 18 | *s = append(*s, x) 19 | } 20 | 21 | // Pop removes and returns the top element of stack. 22 | func (s *Stack) Pop() float64 { 23 | if s.IsEmpty() { 24 | fmt.Printf("stack underflow\n") 25 | os.Exit(1) 26 | } 27 | 28 | i := len(*s) - 1 29 | x := (*s)[i] 30 | *s = (*s)[:i] 31 | 32 | return x 33 | } 34 | --------------------------------------------------------------------------------