{"id":15517319,"url":"https://github.com/skx/foth","last_synced_at":"2025-04-15T11:08:23.299Z","repository":{"id":41310366,"uuid":"295496207","full_name":"skx/foth","owner":"skx","description":"Tutorial-style FORTH implementation written in golang","archived":false,"fork":false,"pushed_at":"2024-02-16T18:51:21.000Z","size":1335,"stargazers_count":83,"open_issues_count":0,"forks_count":6,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-15T11:08:05.752Z","etag":null,"topics":["forth","forth-like","go","golang","interpreter","scripting-language","tutorial","tutorial-code"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"skx","custom":"https://steve.fi/donate/"}},"created_at":"2020-09-14T17:56:33.000Z","updated_at":"2025-03-11T09:06:04.000Z","dependencies_parsed_at":"2022-08-19T02:51:11.476Z","dependency_job_id":"fda3e36b-97db-4e2c-8389-f4281655cc1d","html_url":"https://github.com/skx/foth","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Ffoth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Ffoth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Ffoth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Ffoth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skx","download_url":"https://codeload.github.com/skx/foth/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249058370,"owners_count":21205910,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["forth","forth-like","go","golang","interpreter","scripting-language","tutorial","tutorial-code"],"created_at":"2024-10-02T10:12:29.966Z","updated_at":"2025-04-15T11:08:23.278Z","avatar_url":"https://github.com/skx.png","language":"Go","readme":"[![GoDoc](https://img.shields.io/static/v1?label=godoc\u0026message=reference\u0026color=blue)](https://pkg.go.dev/github.com/skx/foth@v0.3.0/foth?tab=overview)\n[![Go Report Card](https://goreportcard.com/badge/github.com/skx/foth)](https://goreportcard.com/report/github.com/skx/foth)\n[![license](https://img.shields.io/github/license/skx/foth.svg)](https://github.com/skx/foth/blob/master/LICENSE)\n[![Release](https://img.shields.io/github/release/skx/foth.svg)](https://github.com/skx/foth/releases/latest)\n\n* [foth](#foth)\n  * [Features](#features)\n  * [Installation](#installation)\n  * [Embedded Usage](#embedded-usage)\n  * [Anti-Features](#anti-features)\n  * [Implementation Approach](#implementation-approach)\n  * [Implementation Overview](#implementation-overview)\n    * [Part 1](#part-1) - Minimal initial-implementation.\n    * [Part 2](#part-2) - Hard-coded recursive word definitions.\n    * [Part 3](#part-3) - Allow defining minimal words via the REPL.\n    * [Part 4](#part-4) - Allow defining improved words via the REPL.\n    * [Part 5](#part-5) - Allow executing loops via `do`/`loop`.\n    * [Part 6](#part-6) - Allow conditional execution via `if`/`then`.\n    * [Part 7](#part-7) - Added minimal support for strings.\n    * [Final Revision](#final-revision) - Idiomatic Go, test-cases, and many new words\n  * [BUGS](#bugs)\n    * [loops](#loops) - zero expected-iterations actually runs once\n  * [See Also](#see-also)\n  * [Github Setup](#github-setup)\n\n\n\n\n# foth\n\nA simple implementation of a FORTH-like language, hence _foth_ which is\nclose to _forth_.\n\nIf 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:\n\n* [Forth in 7 easy steps](https://jeelabs.org/article/1612b/)\n  * Just ignore any mention of the return-stack!\n* [Starting FORTH](https://www.forth.com/starting-forth/)\n  * A complete book, but the navigation of this site is non-obvious.\n\nIn 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.\n\nThis 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:\n\n* https://news.ycombinator.com/item?id=13082825\n\nThe comment-thread shows example-code and pseudo-code in C, of course this repository is written in Go.\n\n\n\n## Features\n\nThe 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:\n\n* Comments between `(` and `)` are ignored, as expected.\n  * Single-line comments `\\` to the end of the line are also supported.\n* Support for floating-point numbers (anything that will fit inside a `float64`).\n* Reverse-Polish mathematical operations.\n  * Including support for `abs`, `min`, `max`, etc.\n* Support for printing the top-most stack element (`.`, or `print`).\n* Support for outputting ASCII characters (`emit`).\n* Support for outputting strings (`.\" Hello, World \"`).\n  * Some additional string-support for counting lengths, etc.\n* Support for basic stack operations (`clearstack`, `drop`, `dup`, `over`, `swap`, `.s`)\n* Support for loops, via `do`/`loop`.\n* Support for conditional-execution, via `if`, `else`, and `then`.\n* Support for declaring variables with `variable`, and getting/setting their values with `@` and `!` respectively.\n* Execute files specified on the command-line.\n  * If no arguments are supplied run a simple REPL instead.\n* A standard library is loaded, from the present directory, if it is present.\n  * See what we load by default in [foth/foth.4th](foth/foth.4th).\n* The use of recursive definitions, for example:\n  * `: factorial recursive  dup 1 \u003e  if  dup 1 -  factorial *  then  ;`\n\n\n\n## Installation\n\nYou 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.\n\nEither run this to download and install the binary:\n\n```\n$ go get github.com/skx/foth/foth@v0.5.0\n\n```\n\nOr clone this repository, and build the executable like so:\n\n```\ncd foth\ngo build .\n./foth\n```\n\nThe 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.\n\n\n\n## Embedded Usage\n\nAlthough this is a minimal interpreter it _can_ be embedded within a Golang host-application, allowing users to write scripts to control it.\n\nAs an example of this I put together a simple demo:\n\n* [https://github.com/skx/turtle](https://github.com/skx/turtle)\n\nThis 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).\n\n\n\n## Anti-Features\n\nThe 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\").\n\nWe 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.\n\nBasically 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:\n\n* So we implement `if` or `do`/`loop` in a hard-coded fashion.\n  * That means we can't allow a user to define `while`, or similar.\n  * But otherwise our language is flexible enough to allow _real_ work to be done with it.\n\n\n\n## Implementation Approach\n\nThe 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.\n\nWhile it would certainly be possible to further improve the implementation I'm going to declare this project as \"almost complete\" for my own tastes:\n\n* I'll make minor changes, as they occur to me.\n* Comments, test-cases, and similar are fair game.\n* Outright crashes will be resolved, if I spot any.\n* But no major new features will be added.\n\nIf **you** wanted to extend things further then there are some obvious things to work upon:\n\n* Adding more of the \"standard\" FORTH-words.\n  * For example we're missing `pow`, etc.\n* Enhanced the string-support, to allow an input/read from the user, and other primitives.\n  * strcat, strstr, and similar C-like operations would be useful.\n* Simplify the conditional/loop handling.\n  * Both of these probably involve using a proper return-stack.\n  * This would have the side-effect of allowing new control-flow primitives to be added.\n  * As well as more meta-programming.\n\nPull-requests adding additional functionality will be accepted with thanks.\n\n\n\n## Implementation Overview\n\nEach subdirectory within this repository gets a bit further down the comment-chain.\n\nIn terms of implementation two files are _largely_ unchanged in each example:\n\n* `stack.go`, which contains a simple stack of `float64` numbers.\n* `main.go`, contains a simple REPL/driver.\n  * The final few examples will also allow loading a startup-file, if present.\n\nEach example builds upon the previous ones, with a pair of implementation files that change:\n\n* `builtins.go` contains the forth-words implemented in golang.\n* `eval.go` is the workhorse which implements to FORTH-like interpreter.\n  * This allows executing existing words, and defining new ones.\n\n\n### Part 1\n\nPart one of the implementation only deals with hard-coded execution\nof \"words\".  It only supports the basic mathematical operations, along\nwith the ability to print the top-most entry of the stack:\n\n     cd part1\n     go build .\n     ./part1\n     \u003e 2 3 + 4 5 + * print\n     45.000000\n     ^D\n\nSee [part1/](part1/) for details.\n\n\n### Part 2\n\nPart two allows the definition of new words in terms of existing ones,\nwhich can even happen recursively.\n\nWe've added `dup` to pop an item off the stack, and push it back twice, which\nhas the ultimate effect of duplicating it.\n\nTo demonstrate the self-definition there is the new function `square` which\nsquares the number at the top of the stack.\n\n     cd part2\n     go build .\n     ./part2\n     \u003e 3 square .\n     9.000000\n     \u003e 3 dup + .\n     6.000000\n     ^D\n\nSee [part2/](part2/) for details.\n\n\n### Part 3\n\nPart three allows the user to define their own words, right from within the\nREPL!\n\nThis means we've removed the `square` implementation, because you can add your own:\n\n     cd part3\n     go build .\n     ./part3\n     \u003e : square dup * ;\n     \u003e : cube dup square * ;\n     \u003e 3 cube .\n     27.000000\n     \u003e 25 square .\n     625.000000\n     ^D\n\nSee [part3/](part3/) for details.\n\n**NOTE**: We don't support using numbers in definitions, yet.  That will come in part4!\n\n\n### Part 4\n\nPart 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\".\n\nTo support this we switched our `Words` array from `int` to `float64`, specifically to ensure that we could continue to support floating-point numbers.\n\n     cd part4\n     go build .\n     ./part4\n     \u003e : add1 1 + ;\n     \u003e -100 add1 .\n     -99.000000\n     \u003e 4 add1 .\n     5.000000\n     ^D\n\nSee [part4/](part4/) for details.\n\n\n### Part 5\n\nThis part adds `do` and `loop`, allowing simple loops, and `emit` which outputs the ASCII character stored in the topmost stack-entry.\n\nSample usage would look like this:\n\n```\n\u003e : cr 10 emit ;\n\u003e : star 42 emit ;\n\u003e : stars 0 do star loop cr ;\n\u003e 4 stars\n****\n\u003e 5 stars\n*****\n\u003e 1 stars\n*\n\u003e 10 stars\n**********\n^D\n```\n\nHere we've defined two new words `cr` to print a return, and `star` to output a single star.\n\nWe then defined the `stars` word to use a loop to print the given number of stars.\n\n\n(Note that the character `*` has the ASCII code 42).\n\n`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:\n\n* Top-most entry is the current index.\n* Second entry is the limit.\n\nSo to write out numbers you could try something like this, using `dup` to duplicate the current offset within the loop:\n\n     \u003e : l 10 0 do dup . loop ;\n     \u003e l\n     0.000000\n     1.000000\n     2.000000\n     ..\n     8.000000\n     9.000000\n\n     \u003e : nums 10 0 do dup 48 + emit loop ;\n     \u003e nums\n     0123456789\u003e\n\nSee [part5/](part5/) for details.\n\n\n### Part 6\n\nThis update adds a lot of new primitives to our dictionary of predefined words:\n\n* `drop` - Removes an item from the stack.\n* `swap` - Swaps the top-most two stack-items.\n* `words` - Outputs a list of all defined words.\n* `\u003c`, `\u003c=`, `=` (`==` as a synonym), `\u003e`, `\u003e=`\n  * Remove two items from the stack, and compare them appropriately.\n  * If the condition is true push `1` onto the stack, otherwise `0`.\n* The biggest feature here is the support for using `if` \u0026 `then`, which allow conditional actions to be carried out.\n  * (These are why we added the comparison operations.)\n\nIn 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.\n\nSample usage:\n\n    cd part6\n    go build .\n    ./part6\n    \u003e : hot 72 emit 111 emit 116 emit 10 emit ;\n    \u003e : cold 67 emit 111 emit 108 emit 100 emit 10 emit ;\n    \u003e : test_hot  0 \u003e if hot then ;\n    \u003e : test_cold  0 \u003c= if cold then ;\n    \u003e : test dup test_hot test_cold ;\n    \u003e 10 test\n    Hot\n    \u003e 0 test\n    Cold\n    \u003e -1 test\n    Cold\n    \u003e 10 test_hot\n    Hot\n    \u003e 10 test_cold\n    \u003e -1 test_cold\n    Cold\n    ^D\n\nSee [part6/](part6/) for the code.\n\n**NOTE**: The `if` handler allows:\n\n    : foo $COND IF word1 [word2 .. wordN] then [more_word1 more_word2 ..] ;\n\nThis 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.\n\nAs an example:\n\n    \u003e : foo 0 \u003e if star star then star star cr ;\n\nIf the test-passes, because you give a positive number, you'll see FOUR stars.  if it fails you just get TWO:\n\n     \u003e 2 foo\n     ****\n     \u003e 1 foo\n     ****\n     \u003e 0 foo\n     **\n     \u003e -1 foo\n     **\n\nThis is because the code is synonymous with the following C-code:\n\n     if ( x \u003e 0 ) {\n        printf(\"*\");\n        printf(\"*\");\n     }\n     printf(\"*\");\n     printf(\"*\");\n     printf(\"\\n\");\n\nI found this page useful, it also documents `invert` which I added for completeness:\n\n* https://www.forth.com/starting-forth/4-conditional-if-then-statements/\n\n\n### Part 7\n\nThis update adds a basic level of support for strings.\n\n* When we see a string we store it in an array of strings.\n* We then push the offset of the new string entry onto the stack.\n* This allows it to be referenced and used.\n* Three new words are added:\n  * `strings` Return the number of strings we've seen/stored.\n  * `strlen` show the length of the string at the given address.\n  * `strprn` print the string at the given address.\n\nSample usage:\n\n    cd part7\n    go build .\n    ./part7\n    \u003e : steve \"steve\" ;\n    \u003e steve strlen .\n    5\n    \u003e steve strprn .\n    steve\n    ^D\n\nSee [part7/](part7/) for the code.\n\n\n### Final Revision\n\nThe 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:\n\n* We've added near 100% test-coverage.\n* We've added a simple [lexer](foth/lexer/) to tokenize our input.\n  * This was required to allow us to ignore comments, and handle string literals.\n  * Merely splitting input-strings at whitespace characters would have made either of those impossible to handle correctly.\n* The `if` handling has been updated to support an `else`-branch, the general form is now:\n  * `$COND IF word1 [ .. wordN ] else alt_word1 [.. altN] then [more_word1 more_word2 ..]`\n* It is now possible to use `if`, `else`, `then`, `do`, and `loop` outside word-definitions.\n  * i.e. Immediately in the REPL.\n* `do`/`loop` loops can be nested.\n  * And the new words `i` and `m` used to return the current index and maximum index, respectively.\n* There were many new words defined in the go-core:\n  * `.s` to show the stack-contents.\n  * `clearstack` to clear the stack.\n  * `debug` to change the debug-flag.\n  * `debug?` to reveal the status.\n  * `dump` dumps the compiled form of the given word.\n    * You can view the definitions of all available words this:\n    * `#words 0 do i dump loop`\n  * `#words` to return the number of defined words.\n  * Variables can be declared, by name, with `variable`, and the value of the variable can be set/retrieved with `@` and `!` respectively.\n    * See this demonstrated in the [standard library](foth/foth.4th)\n* There were some new words defined in the [standard library](foth/foth.4th)\n  * e.g. `abs`, `even?`, `negate`, `odd?`,\n* Removed all calls to `os.Exit()`\n  * We now return `error` objects where appropriate, allowing the caller to detect problems.\n* It is now possible to redefine existing words.\n* Execute any files specified on the command line.\n  * If no files are specified run the REPL.\n* We've added support for recursive definitions, in #16 for example allowing:\n  * `: factorial recursive  dup 1 \u003e  if  dup 1 -  factorial *  then  ;`\n\nSee [foth/](foth/) for the implementation.\n\n\n\n## BUGS\n\nA brief list of known-issues:\n\n\n### Loops\n\nThe handling of loops isn't correct when there should be zero-iterations:\n\n```\n     \u003e : star 42 emit ;\n     \u003e : stars 0 do star loop 10 emit ;\n     \u003e 3 stars\n     ***\n     \u003e 1 stars\n     *\n     \u003e 0 stars\n     *\n     ^D\n```\n\n**NOTE**: In `gforth` the result of `0 0 do ... loop` is actually an __infinite__ loop, which is perhaps worse!\n\nIn our `stars` definition we handle this case by explicitly testing the loop\nvalue before we proceed, only running the loop if the value is non-zero.\n\n\n\n\n# See Also\n\nThis 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).\n\nI've also played around with a couple of compilers which might be interesting to refer to:\n\n* Brainfuck compiler:\n  * [https://github.com/skx/bfcc/](https://github.com/skx/bfcc/)\n* A math-compiler:\n  * [https://github.com/skx/math-compiler](https://github.com/skx/math-compiler)\n\n\n\n\n# Github Setup\n\nThis 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.\n","funding_links":["https://github.com/sponsors/skx","https://steve.fi/donate/"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Ffoth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskx%2Ffoth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Ffoth/lists"}