Skip to content

Commit

Permalink
Added part7 - with basic string-support (#21)
Browse files Browse the repository at this point in the history
* Added part7 - with basic string-support

This is pretty simple to implement, and understand.
  • Loading branch information
skx authored Feb 15, 2024
1 parent cdb5744 commit 9e9279c
Show file tree
Hide file tree
Showing 13 changed files with 1,210 additions and 9 deletions.
47 changes: 39 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
* [Part 4](#part-4) - Allow defining improved words via the REPL.
* [Part 5](#part-5) - Allow executing loops via `do`/`loop`.
* [Part 6](#part-6) - Allow conditional execution via `if`/`then`.
* [Part 7](#part-7) - Added minimal support for strings.
* [Final Revision](#final-revision) - Idiomatic Go, test-cases, and many new words
* [BUGS](#bugs)
* [loops](#loops) - zero expected-iterations actually runs once
* [See Also](#see-also)
* [Github Setup](#github-setup)




# foth

A simple implementation of a FORTH-like language, hence _foth_ which is
Expand Down Expand Up @@ -57,6 +60,7 @@ The end-result of this work is a simple scripting-language which you could easil
* Support for printing the top-most stack element (`.`, or `print`).
* Support for outputting ASCII characters (`emit`).
* Support for outputting strings (`." Hello, World "`).
* Some additional string-support for counting lengths, etc.
* Support for basic stack operations (`clearstack`, `drop`, `dup`, `over`, `swap`, `.s`)
* Support for loops, via `do`/`loop`.
* Support for conditional-execution, via `if`, `else`, and `then`.
Expand All @@ -69,14 +73,15 @@ The end-result of this work is a simple scripting-language which you could easil
* `: factorial recursive dup 1 > if dup 1 - factorial * then ;`



## Installation

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.

Either run this to download and install the binary:

```
$ go get github.com/skx/foth/foth@v0.4.0
$ go get github.com/skx/foth/foth@v0.5.0
```

Expand All @@ -91,6 +96,7 @@ go build .
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.



## Embedded Usage

Although this is a minimal interpreter it _can_ be embedded within a Golang host-application, allowing users to write scripts to control it.
Expand All @@ -105,7 +111,7 @@ This embeds the interpreter within an application, and defines some new words to

## Anti-Features

The obvious omission from this implementation is support for strings in the general case (string support is limited to outputting a constant-string).
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").

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.

Expand All @@ -116,6 +122,7 @@ Basically we ignore the common FORTH-approach of using a return-stack, and imple
* But otherwise our language is flexible enough to allow _real_ work to be done with it.



## Implementation Approach

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.
Expand All @@ -131,7 +138,8 @@ If **you** wanted to extend things further then there are some obvious things to

* Adding more of the "standard" FORTH-words.
* For example we're missing `pow`, etc.
* Simplify the special-case handling of string-support.
* Enhanced the string-support, to allow an input/read from the user, and other primitives.
* strcat, strstr, and similar C-like operations would be useful.
* Simplify the conditional/loop handling.
* Both of these probably involve using a proper return-stack.
* This would have the side-effect of allowing new control-flow primitives to be added.
Expand Down Expand Up @@ -174,7 +182,6 @@ with the ability to print the top-most entry of the stack:
See [part1/](part1/) for details.



### Part 2

Part two allows the definition of new words in terms of existing ones,
Expand All @@ -198,7 +205,6 @@ squares the number at the top of the stack.
See [part2/](part2/) for details.



### Part 3

Part three allows the user to define their own words, right from within the
Expand All @@ -222,7 +228,6 @@ See [part3/](part3/) for details.
**NOTE**: We don't support using numbers in definitions, yet. That will come in part4!



### Part 4

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".
Expand All @@ -242,7 +247,6 @@ To support this we switched our `Words` array from `int` to `float64`, specifica
See [part4/](part4/) for details.



### Part 5

This part adds `do` and `loop`, allowing simple loops, and `emit` which outputs the ASCII character stored in the topmost stack-entry.
Expand Down Expand Up @@ -294,7 +298,6 @@ So to write out numbers you could try something like this, using `dup` to duplic
See [part5/](part5/) for details.



### Part 6

This update adds a lot of new primitives to our dictionary of predefined words:
Expand Down Expand Up @@ -371,6 +374,31 @@ I found this page useful, it also documents `invert` which I added for completen
* https://www.forth.com/starting-forth/4-conditional-if-then-statements/


### Part 7

This update adds a basic level of support for strings.

* When we see a string we store it in an array of strings.
* We then push the offset of the new string entry onto the stack.
* This allows it to be referenced and used.
* Three new words are added:
* `strings` Return the number of strings we've seen/stored.
* `strlen` show the length of the string at the given address.
* `strprn` print the string at the given address.

Sample usage:

cd part7
go build .
./part7
> : steve "steve" ;
> steve strlen .
5
> steve strprn .
steve
^D

See [part7/](part7/) for the code.


### Final Revision
Expand Down Expand Up @@ -416,6 +444,7 @@ See [foth/](foth/) for the implementation.

A brief list of known-issues:


### Loops

The handling of loops isn't correct when there should be zero-iterations:
Expand All @@ -439,6 +468,7 @@ value before we proceed, only running the loop if the value is non-zero.




# See Also

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).
Expand All @@ -452,6 +482,7 @@ I've also played around with a couple of compilers which might be interesting to




# Github Setup

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.
44 changes: 44 additions & 0 deletions foth/eval/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,50 @@ func (e *Eval) startDefinition() error {
return nil
}

// strings
func (e *Eval) stringCount() error {
// Return the number of strings we've seen
e.Stack.Push(float64(len(e.strings)))
return nil
}

// strlen
func (e *Eval) strlen() error {
addr, err := e.Stack.Pop()
if err != nil {
return err
}

i := int(addr)

if i < len(e.strings) {
str := e.strings[i]
e.Stack.Push(float64(len(str)))
return nil
}

return fmt.Errorf("invalid stack offset for string reference")

}

// strprn - string printing
func (e *Eval) strprn() error {
addr, err := e.Stack.Pop()
if err != nil {
return err
}

i := int(addr)

if i < len(e.strings) {
str := e.strings[i]
fmt.Printf("%s", str)
return nil
}

return fmt.Errorf("invalid stack offset for string reference")
}

func (e *Eval) sub() error {
return e.binOp(func(n float64, m float64) float64 { return m - n })()
}
Expand Down
108 changes: 108 additions & 0 deletions foth/eval/builtins_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eval

import (
"os"
"testing"
)

Expand Down Expand Up @@ -736,6 +737,113 @@ func TestSetVar(t *testing.T) {

}

// strings counts the string literals, and will return
// a number on the stack
func TestStrings(t *testing.T) {

e := New()

// Empty stack on first start
n := e.Stack.Len()
if n != 0 {
t.Fatalf("failing result for stringCount, got %d", n)
}

// call the function
err := e.stringCount()
if err != nil {
t.Fatalf("unexpected error with stringCount %s", err.Error())
}

n = e.Stack.Len()
if n != 1 {
t.Fatalf("failing result for stringCount, got %d", n)
os.Exit(1)
}
}

func TestStrlen(t *testing.T) {

e := New()
e.strings = append(e.strings, "Steve")

// call the function
err := e.strlen()
if err == nil {
t.Fatalf("expected an error, got none")
}

// Empty stack on first start
n := e.Stack.Len()
if n != 0 {
t.Fatalf("failing result for strlen, got %d", n)
}

// push an invalid string
e.Stack.Push(100.0)

// call the function
err = e.strlen()
if err == nil {
t.Fatalf("expected an error, got none")
}

// Now try to get the length of Steve
e.Stack.Push(0.0)
err = e.strlen()
if err != nil {
t.Fatalf("unexpected error, calling strlen %s", err.Error())
}

// Is the result expected?
x, _ := e.Stack.Pop()
if x != 5 {
t.Fatalf("wrong result for strlen, got %f", x)
}
}

func TestStrPrn(t *testing.T) {

e := New()

// We want to avoid spamming stdout, so our string to print is "empty"
e.strings = append(e.strings, "")

// call the function
err := e.strprn()
if err == nil {
t.Fatalf("expected an error, got none")
}

// Empty stack on first start
n := e.Stack.Len()
if n != 0 {
t.Fatalf("failing result for strprn, got %d", n)
}

// push an invalid string
e.Stack.Push(100.0)

// call the function
err = e.strprn()
if err == nil {
t.Fatalf("expected an error, got none")
}

// Now try to get the length of Steve
e.Stack.Push(0.0)
err = e.strprn()
if err != nil {
t.Fatalf("unexpected error, calling strprn %s", err.Error())
}

// Empty stack, still?
n = e.Stack.Len()
if n != 0 {
t.Fatalf("failing result for strprn, got %d", n)
}
}

func TestSub(t *testing.T) {

e := New()
Expand Down
22 changes: 21 additions & 1 deletion foth/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ func New() *Eval {
{Name: ";", Function: e.nop},
{Name: "dump", Function: e.dump},
{Name: "words", Function: e.words},

// strings
{Name: "strings", Function: e.stringCount},
{Name: "strlen", Function: e.strlen},
{Name: "strprn", Function: e.strprn},
}

return e
Expand Down Expand Up @@ -315,6 +320,13 @@ func (e *Eval) Eval(input string) error {
continue
}

// String
if token.Name == "\"" {
e.strings = append(e.strings, token.Value)
e.Stack.Push(float64(len(e.strings) - 1))
continue
}

// If we didn't handle this as a word, then
// assume it is a number.
i, err := strconv.ParseFloat(tok, 64)
Expand Down Expand Up @@ -531,7 +543,7 @@ func (e *Eval) compileToken(token lexer.Token) error {

}

// output a string, in compiled form
// output a string-print operation, in compiled form
if token.Name == ".\"" {
e.strings = append(e.strings, token.Value)
e.tmp.Words = append(e.tmp.Words, -5)
Expand Down Expand Up @@ -664,6 +676,14 @@ func (e *Eval) compileToken(token lexer.Token) error {
return nil
}

// save a string, in compiled form
if token.Name == "\"" {
e.strings = append(e.strings, token.Value)
e.tmp.Words = append(e.tmp.Words, -1)
e.tmp.Words = append(e.tmp.Words, float64(len(e.strings))-1)
return nil
}

// Convert to float
val, err := strconv.ParseFloat(tok, 64)
if err != nil {
Expand Down
Loading

0 comments on commit 9e9279c

Please sign in to comment.