diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..9d14f28
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+github: skx
+custom: https://steve.fi/donate/
diff --git a/.github/build b/.github/build
index cf8bd4e..d9b2710 100755
--- a/.github/build
+++ b/.github/build
@@ -3,6 +3,9 @@
# The basename of our binary
BASE="gobasic"
+# I don't even ..
+go env -w GOFLAGS="-buildvcs=false"
+
# Get the dependencies
go mod init
diff --git a/.github/main.workflow b/.github/main.workflow
deleted file mode 100644
index 849a5d6..0000000
--- a/.github/main.workflow
+++ /dev/null
@@ -1,38 +0,0 @@
-# pushes trigger the testsuite
-workflow "Push Event" {
- on = "push"
- resolves = ["Test"]
-}
-
-# pull-requests trigger the testsuite
-workflow "Pull Request" {
- on = "pull_request"
- resolves = ["Test"]
-}
-
-# releases trigger new binary artifacts
-workflow "Handle Release" {
- on = "release"
- resolves = ["Upload"]
-}
-
-##
-## The actions
-##
-
-
-##
-## Run the test-cases, via .github/run-tests.sh
-##
-action "Test" {
- uses = "skx/github-action-tester@master"
-}
-
-##
-## Build the binaries, via .github/build, then upload them.
-##
-action "Upload" {
- uses = "skx/github-action-publish-binaries@master"
- args = "go*-*"
- secrets = ["GITHUB_TOKEN"]
-}
diff --git a/.github/run-tests.sh b/.github/run-tests.sh
index 916d1b6..5c2761e 100755
--- a/.github/run-tests.sh
+++ b/.github/run-tests.sh
@@ -1,8 +1,46 @@
-#!/bin/sh
+#!/bin/bash
-# init modules
-go mod init
+
+# I don't even ..
+go env -w GOFLAGS="-buildvcs=false"
+
+
+# Install the tools we use to test our code-quality.
+#
+# Here we setup the tools to install only if the "CI" environmental variable
+# is not empty. This is because locally I have them installed.
+#
+# NOTE: Github Actions always set CI=true
+#
+if [ ! -z "${CI}" ] ; then
+ go install golang.org/x/lint/golint@latest
+ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
+ go install honnef.co/go/tools/cmd/staticcheck@latest
+fi
+
+# Run the static-check tool - we ignore errors in goserver/static.go
+t=$(mktemp)
+staticcheck -checks all ./... | grep -v "is deprecated"> $t
+if [ -s $t ]; then
+ echo "Found errors via 'staticcheck'"
+ cat $t
+ rm $t
+ exit 1
+fi
+rm $t
+
+# At this point failures cause aborts
+set -e
+
+# Run the linter
+echo "Launching linter .."
+golint -set_exit_status ./...
+echo "Completed linter .."
+
+# Run the shadow-checker
+echo "Launching shadowed-variable check .."
+go vet -vettool=$(which shadow) ./...
+echo "Completed shadowed-variable check .."
# Run golang tests
go test ./...
-
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..cad50f7
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,71 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+name: "CodeQL"
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [master]
+ schedule:
+ - cron: '0 1 * * 3'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ # Override automatic language detection by changing the below list
+ # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
+ language: ['go']
+ # Learn more...
+ # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ # We must fetch at least the immediate parents so that if this is
+ # a pull request then we can checkout the head.
+ fetch-depth: 2
+
+ # If this run was triggered by a pull request event, then checkout
+ # the head of the pull request instead of the merge commit.
+ - run: git checkout HEAD^2
+ if: ${{ github.event_name == 'pull_request' }}
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ # âšī¸ Command-line programs to run using the OS shell.
+ # đ https://git.io/JvXDl
+
+ # âī¸ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
new file mode 100644
index 0000000..691f8f8
--- /dev/null
+++ b/.github/workflows/pull_request.yml
@@ -0,0 +1,10 @@
+on: pull_request
+name: Pull Request
+jobs:
+ test:
+ name: Run tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Test
+ uses: skx/github-action-tester@master
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
new file mode 100644
index 0000000..af6a41a
--- /dev/null
+++ b/.github/workflows/push.yml
@@ -0,0 +1,13 @@
+on:
+ push:
+ branches:
+ - master
+name: Push Event
+jobs:
+ test:
+ name: Run tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Test
+ uses: skx/github-action-tester@master
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..2b384f8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,16 @@
+on: release
+name: Handle Release
+jobs:
+ upload:
+ name: Upload release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: Generate the artifacts
+ uses: skx/github-action-build@master
+ - name: Upload
+ uses: skx/github-action-publish-binaries@master
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ args: go*-*
diff --git a/FUZZING.md b/FUZZING.md
index 0bf9b78..c7d682f 100644
--- a/FUZZING.md
+++ b/FUZZING.md
@@ -1,22 +1,37 @@
# Fuzz-Testing
-If you don't have the appropriate tools installed you can fetch them via:
+The 1.18 release of the golang compiler/toolset has integrated support for
+fuzz-testing.
- $ go get github.com/dvyukov/go-fuzz/go-fuzz
- $ go get github.com/dvyukov/go-fuzz/go-fuzz-build
+Fuzz-testing is basically magical and involves generating new inputs "randomly"
+and running test-cases with those inputs.
-Now you can build the `eval` package with fuzzing enabled:
+## Running
- $ go-fuzz-build github.com/skx/gobasic/eval
+Assuming you have go 1.18 or higher you can run the fuzz-testing of the
+`eval` package like so:
-Create a location to hold the work, and give it copies of some sample-programs:
+ $ cd eval/
+ $ go test -fuzztime=60s -parallel=1 -fuzz=FuzzEval -v
- $ mkdir -p workdir/corpus
- $ cp examples/*.bas workdir/corpus
+Output will look something like this:
-Now you can actually launch the fuzzer - here I use `-procs 1` so that
-my desktop system isn't complete overloaded:
+ === FUZZ FuzzEval
+ fuzz: elapsed: 0s, gathering baseline coverage: 0/1084 completed
+ fuzz: elapsed: 3s, gathering baseline coverage: 108/1084 completed
+ fuzz: elapsed: 6s, gathering baseline coverage: 338/1084 completed
+ fuzz: elapsed: 9s, gathering baseline coverage: 637/1084 completed
+ fuzz: elapsed: 12s, gathering baseline coverage: 916/1084 completed
+ fuzz: elapsed: 15s, gathering baseline coverage: 1067/1084 completed
+ fuzz: elapsed: 15s, gathering baseline coverage: 1084/1084 completed, now fuzzing with 1 workers
+ fuzz: elapsed: 18s, execs: 9791 (2908/sec), new interesting: 0 (total: 1084)
+ fuzz: elapsed: 21s, execs: 31008 (7072/sec), new interesting: 1 (total: 1085)
+ fuzz: elapsed: 24s, execs: 59590 (9529/sec), new interesting: 1 (total: 1085)
- $ go-fuzz -procs 1 -bin=eval-fuzz.zip -workdir workdir/
+If the fuzzer terminates with a `panic` then you've found a new failure, and you should examine the contents of the file it generates and displays. There _are_ some error-cases which are expected:
-Now take a look at `workdir/crashers` to see the findings.
+* If the fuzzer generates bogus BASIC
+* If the fuzzer generates an infinite loop which is terminated via a timeout
+* etc.
+
+If you find a panic which looks like it is caused by bogus BASIC then update `fuzz_test.go` to add that to the "known failure" list. Otherwise leave it running (perhaps overnight, removing the `-fuzztime=60s` parameter), and ideally it will keep going and not crash.
diff --git a/README.md b/README.md
index 308cdcf..fc796e5 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-[![Coverage Status](https://coveralls.io/repos/github/skx/gobasic/badge.svg?branch=master)](https://coveralls.io/github/skx/gobasic?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/skx/gobasic)](https://goreportcard.com/report/github.com/skx/gobasic)
[![license](https://img.shields.io/github/license/skx/gobasic.svg)](https://github.com/skx/gobasic/blob/master/LICENSE)
[![Release](https://img.shields.io/github/release/skx/gobasic.svg)](https://github.com/skx/gobasic/releases/latest)
@@ -21,6 +20,7 @@
* [50 PRINT "Implementation"](#50-print-implementation)
* [60 PRINT "Sample Code"](#60-print-sample-code)
* [70 PRINT "Embedding"](#70-print-embedding)
+* [75 PRINT "DoS"](#75-print-dos)
* [80 PRINT "Visual BASIC!"](#80-print-visual-basic)
* [90 PRINT "Bugs?"](#90-print-bugs)
* [100 PRINT "Project Goals / Links"](#100-print-project-goals--links)
@@ -49,7 +49,7 @@ The following obvious primitives work as you'd expect:
* `DIM`
* Create an array. Note that only one and two-dimensional arrays are supported.
- * See [examples/95-arrays.bas](examples/95-arrays) and [examples/100-array-sort.bas](examples/100-array-sort.bas) for quick samples.
+ * See [examples/95-arrays.bas](examples/95-arrays) and [examples/40-array-sort.bas](examples/40-array-sort.bas) for quick samples.
* `END`
* Exit the program.
* `GOTO`
@@ -174,7 +174,8 @@ The set of comparison functions _probably_ includes everything you need:
* This passes if `a` is a number which is not zero.
* This passes if `a` is a string which is non-empty.
-If you're missing something from that list please let me know by filing an issue.
+You can see several examples of the IF statement in use in the example [examples/70-if.bas](examples/70-if.bas).
+
### `DATA` / `READ` Statements
@@ -229,21 +230,18 @@ This seemed better than trying to return a string, unless the input looked like
## 30 PRINT "Installation"
-### Build without Go Modules (Go before 1.11)
-
-Providing you have a working [go-installation](https://golang.org/) you should be able to install this software by running:
-
- go get -u github.com/skx/gobasic
+We don't pull in any external dependencies, except for the embedded examples,
+so installation is simple.
-**NOTE** This will only install the command-line driver, rather than the HTTP-server, or the embedded example code.
-
-### Build with Go Modules (Go 1.11 or higher)
-
- git clone https://github.com/skx/gobasic ;# make sure to clone outside of GOPATH
+ git clone https://github.com/skx/gobasic
cd gobasic
go install
-If you don't have a golang environment setup you should be able to download various binaries from the github release page:
+You can also install directly via:
+
+ go install github.com/skx/gobasic@latest
+
+If you don't have a golang environment setup you should be able to download a binary release from our release page:
* [Binary Releases](https://github.com/skx/gobasic/releases)
@@ -323,7 +321,7 @@ Perhaps the best demonstration of the code are the following two samples:
* [examples/90-stars.bas](examples/90-stars.bas)
* Prompt the user for their name and the number of stars to print.
* Then print them. Riveting! Fascinating! A program for the whole family!
-* [examples/99-game.bas](examples/99-game.bas)
+* [examples/55-game.bas](examples/55-game.bas)
* A classic game where you guess the random number the computer has thought of.
@@ -366,6 +364,52 @@ in the standalone interpreter.)
+
+## 75 PRINT "DoS"
+
+When it comes to security problems the most obvious issue we might suffer from is denial-of-service attacks; it is certainly possible for this library to be given faulty programs, for example invalid syntax, or references to undefined functions. Failures such as those would be detected at parse/run time, as appropriate.
+
+In short running user-supplied scripts should be safe, but there is one obvious exception, the following program is valid:
+
+```
+10 PRINT "STEVE ROCKS!"
+20 GOTO 10
+```
+
+This program will __never__ terminate! If you're handling untrusted user-scripts, you'll want to ensure that you explicitly setup a timeout period.
+
+The following will do what you expect:
+
+```
+// Setup a timeout period of five seconds
+ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+defer cancel()
+
+// Now create the interpreter, via the tokenizer
+t := tokenizer.New(string(`10 GOTO 10`))
+
+// Ensure we pass the context over
+e, err := eval.NewWithContext(ctx,t)
+if err != nil {
+ fmt.Printf("error creating interpreter: %s\n", err.Error())
+ panic(err) // proper handling here
+}
+
+// Now run the program
+err = e.Run()
+if err != nil {
+ fmt.Printf("error running: %s\n", err.Error())
+ panic(err) // proper handling here
+}
+
+// Here we'll see a timeout eror
+
+```
+
+The program will be terminated with an error after five seconds, which means that your host application will continue to run rather than being blocked forever!
+
+
+
## 80 PRINT "Visual BASIC!"
Building upon the code in the embedded-example I've also implemented a simple
diff --git a/builtin/builtin.go b/builtin/builtin.go
index addcba0..f9bded6 100644
--- a/builtin/builtin.go
+++ b/builtin/builtin.go
@@ -1,4 +1,4 @@
-// The builtin package contains the implementation for the core functions
+// Package builtin contains the implementation for the core functions
// implemented for BASIC, such as SIN, COS, RND, PRINT, etc.
//
// The builtin package also provides an interface which with you can
@@ -11,6 +11,7 @@
package builtin
import (
+ "bufio"
"sync"
"github.com/skx/gobasic/object"
@@ -22,7 +23,7 @@ import (
// single object back to the caller.
//
// In the case of an error then the object will be an error-object.
-type Signature func(env interface{}, args []object.Object) object.Object
+type Signature func(env Environment, args []object.Object) object.Object
// Builtins holds our state.
type Builtins struct {
@@ -69,3 +70,25 @@ func (b *Builtins) Get(name string) (int, Signature) {
return b.argRegistry[name], b.fnRegistry[name]
}
+
+// Environment is an interface which is passed to all built-in functions.
+type Environment interface {
+ // StdInput is a handle to a reader-object, allowing input to
+ // be processed by the built-ins.
+ StdInput() *bufio.Reader
+
+ // StdOutput is a handle to a writer-object, allowing output to
+ // be generated by the built-ins.
+ StdOutput() *bufio.Writer
+
+ // StdError is a handle to a writer-object, allowing user errors to
+ // be communicated by the built-ins.
+ StdError() *bufio.Writer
+
+ // LineEnding specifies any additional characters that should be
+ // appended to PRINT commands - for example '\n'
+ LineEnding() string
+
+ // Data allows the builtins to get a reference to the intepreter.
+ Data() interface{}
+}
diff --git a/builtin/maths.go b/builtin/maths.go
index c4a2286..1469960 100644
--- a/builtin/maths.go
+++ b/builtin/maths.go
@@ -20,7 +20,7 @@ func init() {
}
// ABS implements ABS
-func ABS(env interface{}, args []object.Object) object.Object {
+func ABS(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -38,7 +38,7 @@ func ABS(env interface{}, args []object.Object) object.Object {
}
// ACS (arccosine)
-func ACS(env interface{}, args []object.Object) object.Object {
+func ACS(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -50,7 +50,7 @@ func ACS(env interface{}, args []object.Object) object.Object {
}
// ASN (arcsine)
-func ASN(env interface{}, args []object.Object) object.Object {
+func ASN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -62,7 +62,7 @@ func ASN(env interface{}, args []object.Object) object.Object {
}
// ATN (arctan)
-func ATN(env interface{}, args []object.Object) object.Object {
+func ATN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -74,7 +74,7 @@ func ATN(env interface{}, args []object.Object) object.Object {
}
// BIN converts a number from binary.
-func BIN(env interface{}, args []object.Object) object.Object {
+func BIN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -94,7 +94,7 @@ func BIN(env interface{}, args []object.Object) object.Object {
}
// COS implements the COS function..
-func COS(env interface{}, args []object.Object) object.Object {
+func COS(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -106,7 +106,7 @@ func COS(env interface{}, args []object.Object) object.Object {
}
// EXP x=e^x EXP
-func EXP(env interface{}, args []object.Object) object.Object {
+func EXP(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
return object.Error("Wrong type")
@@ -117,7 +117,7 @@ func EXP(env interface{}, args []object.Object) object.Object {
}
// INT implements INT
-func INT(env interface{}, args []object.Object) object.Object {
+func INT(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -130,7 +130,7 @@ func INT(env interface{}, args []object.Object) object.Object {
}
// LN calculates logarithms to the base e - LN
-func LN(env interface{}, args []object.Object) object.Object {
+func LN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -142,12 +142,12 @@ func LN(env interface{}, args []object.Object) object.Object {
}
// PI returns the value of PI
-func PI(env interface{}, args []object.Object) object.Object {
+func PI(env Environment, args []object.Object) object.Object {
return &object.NumberObject{Value: math.Pi}
}
// RND implements RND
-func RND(env interface{}, args []object.Object) object.Object {
+func RND(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -168,7 +168,7 @@ func RND(env interface{}, args []object.Object) object.Object {
}
// SGN is the sign function (sometimes called signum).
-func SGN(env interface{}, args []object.Object) object.Object {
+func SGN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -187,7 +187,7 @@ func SGN(env interface{}, args []object.Object) object.Object {
}
// SIN operats the sin function.
-func SIN(env interface{}, args []object.Object) object.Object {
+func SIN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -199,7 +199,7 @@ func SIN(env interface{}, args []object.Object) object.Object {
}
// SQR implements square root.
-func SQR(env interface{}, args []object.Object) object.Object {
+func SQR(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -215,7 +215,7 @@ func SQR(env interface{}, args []object.Object) object.Object {
}
// TAN implements the tan function.
-func TAN(env interface{}, args []object.Object) object.Object {
+func TAN(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
diff --git a/builtin/misc.go b/builtin/misc.go
index 98aed8b..c5a6904 100644
--- a/builtin/misc.go
+++ b/builtin/misc.go
@@ -5,49 +5,68 @@
package builtin
import (
+ "bufio"
"fmt"
+ "os"
"github.com/skx/gobasic/object"
)
// DUMP just displays the only argument it received.
-func DUMP(env interface{}, args []object.Object) object.Object {
+func DUMP(env Environment, args []object.Object) object.Object {
+ var out *bufio.Writer
+ if env == nil {
+ out = bufio.NewWriter(os.Stdout)
+ } else {
+ out = env.StdOutput()
+ }
// Get the (float) argument.
if args[0].Type() == object.NUMBER {
i := args[0].(*object.NumberObject).Value
- fmt.Printf("NUMBER: %f\n", i)
+ out.WriteString(fmt.Sprintf("NUMBER: %f\n", i))
}
if args[0].Type() == object.STRING {
s := args[0].(*object.StringObject).Value
- fmt.Printf("STRING: %s\n", s)
+ out.WriteString(fmt.Sprintf("STRING: %s\n", s))
}
if args[0].Type() == object.ERROR {
s := args[0].(*object.ErrorObject).Value
- fmt.Printf("Error: %s\n", s)
+ out.WriteString(fmt.Sprintf("Error: %s\n", s))
}
+ out.Flush()
// Otherwise return as-is.
return &object.NumberObject{Value: 0}
}
// PRINT handles displaying strings, integers, and errors.
-func PRINT(env interface{}, args []object.Object) object.Object {
+func PRINT(env Environment, args []object.Object) object.Object {
+ var out *bufio.Writer
+ if env == nil {
+ out = bufio.NewWriter(os.Stdout)
+ } else {
+ out = env.StdOutput()
+ }
for _, ent := range args {
switch ent.Type() {
case object.NUMBER:
n := ent.(*object.NumberObject).Value
if n == float64(int(n)) {
- fmt.Printf("%d", int(n))
+ out.WriteString(fmt.Sprintf("%d", int(n)))
} else {
- fmt.Printf("%f", n)
+ out.WriteString(fmt.Sprintf("%f", n))
}
case object.STRING:
- fmt.Printf("%s", ent.(*object.StringObject).Value)
+ out.WriteString(ent.(*object.StringObject).Value)
case object.ERROR:
- fmt.Printf("%s", ent.(*object.ErrorObject).Value)
+ out.WriteString(ent.(*object.ErrorObject).Value)
}
}
+ if env != nil {
+ out.WriteString(env.LineEnding())
+ }
+ out.Flush()
// Return the count of values we printed.
return &object.NumberObject{Value: float64(len(args))}
diff --git a/builtin/misc_test.go b/builtin/misc_test.go
index e7deb04..40379fa 100644
--- a/builtin/misc_test.go
+++ b/builtin/misc_test.go
@@ -3,52 +3,98 @@
package builtin
import (
+ "bufio"
+ "bytes"
"testing"
"github.com/skx/gobasic/object"
)
+type bufferEnv struct {
+ writer *bufio.Writer
+}
+
+func (b *bufferEnv) StdInput() *bufio.Reader {
+ return nil
+}
+
+func (b *bufferEnv) LineEnding() string {
+ return "\n"
+}
+
+func (b *bufferEnv) StdOutput() *bufio.Writer {
+ return b.writer
+}
+
+func (b *bufferEnv) StdError() *bufio.Writer {
+ return nil
+}
+
+func (b *bufferEnv) Data() interface{} {
+ return nil
+}
+
func TestDump(t *testing.T) {
+ buf := bytes.NewBuffer([]byte{})
+ env := &bufferEnv{}
+ env.writer = bufio.NewWriter(buf)
//
// Number
//
var in1 []object.Object
in1 = append(in1, object.Number(1))
- out1 := DUMP(nil, in1)
+ out1 := DUMP(env, in1)
if out1.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
+ str := buf.String()
+ if str != "NUMBER: 1.000000\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
+ buf.Reset()
//
// String
//
var in2 []object.Object
in2 = append(in2, object.String("Stve"))
- out2 := DUMP(nil, in2)
+ out2 := DUMP(env, in2)
if out2.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
+ str = buf.String()
+ if str != "STRING: Stve\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
+ buf.Reset()
//
// Error
//
var in3 []object.Object
in3 = append(in3, object.Error("Stve"))
- out3 := DUMP(nil, in3)
+ out3 := DUMP(env, in3)
if out3.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
+ str = buf.String()
+ if str != "Error: Stve\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
}
func TestPrint(t *testing.T) {
+ buf := bytes.NewBuffer([]byte{})
+ env := &bufferEnv{}
+ env.writer = bufio.NewWriter(buf)
//
// Number
//
var in1 []object.Object
in1 = append(in1, object.Number(1))
- out1 := PRINT(nil, in1)
+ out1 := PRINT(env, in1)
if out1.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
@@ -56,13 +102,18 @@ func TestPrint(t *testing.T) {
t.Errorf("We didn't print one item: %f",
out1.(*object.NumberObject).Value)
}
+ str := buf.String()
+ if str != "1\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
+ buf.Reset()
//
// String
//
var in2 []object.Object
in2 = append(in2, object.String("Stve"))
- out2 := PRINT(nil, in2)
+ out2 := PRINT(env, in2)
if out2.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
@@ -70,13 +121,18 @@ func TestPrint(t *testing.T) {
t.Errorf("We didn't print one item: %f",
out2.(*object.NumberObject).Value)
}
+ str = buf.String()
+ if str != "Stve\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
+ buf.Reset()
//
// Error
//
var in3 []object.Object
in3 = append(in3, object.Error("Stve"))
- out3 := PRINT(nil, in3)
+ out3 := PRINT(env, in3)
if out3.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
@@ -84,6 +140,11 @@ func TestPrint(t *testing.T) {
t.Errorf("We didn't print one item:%f",
out3.(*object.NumberObject).Value)
}
+ str = buf.String()
+ if str != "Stve\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
+ buf.Reset()
//
// Now a bunch of things
@@ -93,7 +154,7 @@ func TestPrint(t *testing.T) {
in4 = append(in4, object.String("Stve"))
in4 = append(in4, object.Number(3))
in4 = append(in4, object.Number(4.3))
- out4 := PRINT(nil, in4)
+ out4 := PRINT(env, in4)
if out4.Type() != object.NUMBER {
t.Errorf("We didn't receive a number in response")
}
@@ -101,5 +162,8 @@ func TestPrint(t *testing.T) {
t.Errorf("We didn't print the expected count of items:%f",
out4.(*object.NumberObject).Value)
}
-
+ str = buf.String()
+ if str != "StveStve34.300000\n" {
+ t.Errorf("We didn't print the correct string: %s", str)
+ }
}
diff --git a/builtin/string.go b/builtin/string.go
index b6da703..a42d33b 100644
--- a/builtin/string.go
+++ b/builtin/string.go
@@ -13,7 +13,7 @@ import (
)
// CHR returns the character specified by the given ASCII code.
-func CHR(env interface{}, args []object.Object) object.Object {
+func CHR(env Environment, args []object.Object) object.Object {
// Get the (float) argument.
if args[0].Type() != object.NUMBER {
@@ -33,7 +33,7 @@ func CHR(env interface{}, args []object.Object) object.Object {
}
// CODE returns the integer value of the specified character.
-func CODE(env interface{}, args []object.Object) object.Object {
+func CODE(env Environment, args []object.Object) object.Object {
// Get the (string) argument.
if args[0].Type() != object.STRING {
@@ -53,7 +53,7 @@ func CODE(env interface{}, args []object.Object) object.Object {
}
// LEFT returns the N left-most characters of the string.
-func LEFT(env interface{}, args []object.Object) object.Object {
+func LEFT(env Environment, args []object.Object) object.Object {
// Get the (string) argument.
if args[0].Type() != object.STRING {
@@ -85,7 +85,7 @@ func LEFT(env interface{}, args []object.Object) object.Object {
}
// LEN returns the length of the given string
-func LEN(env interface{}, args []object.Object) object.Object {
+func LEN(env Environment, args []object.Object) object.Object {
// Get the (string) argument.
if args[0].Type() != object.STRING {
@@ -100,7 +100,7 @@ func LEN(env interface{}, args []object.Object) object.Object {
}
// MID returns the N characters from the given offset
-func MID(env interface{}, args []object.Object) object.Object {
+func MID(env Environment, args []object.Object) object.Object {
// Get the (string) argument.
if args[0].Type() != object.STRING {
@@ -147,7 +147,7 @@ func MID(env interface{}, args []object.Object) object.Object {
}
// RIGHT returns the N right-most characters of the string.
-func RIGHT(env interface{}, args []object.Object) object.Object {
+func RIGHT(env Environment, args []object.Object) object.Object {
// Get the (string) argument.
if args[0].Type() != object.STRING {
@@ -177,8 +177,33 @@ func RIGHT(env interface{}, args []object.Object) object.Object {
return &object.StringObject{Value: string(right)}
}
+// SPC returns a string containing the given number of spaces
+func SPC(env Environment, args []object.Object) object.Object {
+
+ // Get the (float) argument.
+ if args[0].Type() != object.NUMBER {
+ return object.Error("Wrong type")
+ }
+ n := int(args[0].(*object.NumberObject).Value)
+
+ // ensure it is positive
+ if n < 0 {
+ return object.Error("Positive argument only")
+ }
+ if n > 65535 {
+ return object.Error("length of strings cannot exceed 65535 characters")
+ }
+
+ s := ""
+ for i := 0; i < n; i++ {
+ s += " "
+ }
+
+ return &object.StringObject{Value: s}
+}
+
// STR converts a number to a string
-func STR(env interface{}, args []object.Object) object.Object {
+func STR(env Environment, args []object.Object) object.Object {
// Error?
if args[0].Type() == object.ERROR {
@@ -191,8 +216,7 @@ func STR(env interface{}, args []object.Object) object.Object {
}
// Get the value
- var i float64
- i = args[0].(*object.NumberObject).Value
+ i := args[0].(*object.NumberObject).Value
s := ""
if i == float64(int(i)) {
@@ -204,7 +228,7 @@ func STR(env interface{}, args []object.Object) object.Object {
}
// TL returns a string, minus the first character.
-func TL(env interface{}, args []object.Object) object.Object {
+func TL(env Environment, args []object.Object) object.Object {
// Get the (string) argument.
if args[0].Type() != object.STRING {
@@ -224,7 +248,7 @@ func TL(env interface{}, args []object.Object) object.Object {
}
// VAL converts a string to a number
-func VAL(env interface{}, args []object.Object) object.Object {
+func VAL(env Environment, args []object.Object) object.Object {
// Error?
if args[0].Type() == object.ERROR {
diff --git a/builtin/string_test.go b/builtin/string_test.go
index cef72b5..4606c8f 100644
--- a/builtin/string_test.go
+++ b/builtin/string_test.go
@@ -423,6 +423,73 @@ func TestRight(t *testing.T) {
}
+func TestSpc(t *testing.T) {
+
+ //
+ // Call with a non-number argument.
+ //
+ var failArgs []object.Object
+ failArgs = append(failArgs, object.Error("Bogus type"))
+ out := SPC(nil, failArgs)
+ if out.Type() != object.ERROR {
+ t.Errorf("We expected a type-error, but didn't receive one")
+ }
+
+ //
+ // Now a string which will also fail.
+ //
+ var nArgs []object.Object
+ nArgs = append(nArgs, object.String("steve"))
+ out = SPC(nil, nArgs)
+ if out.Type() != object.ERROR {
+ t.Errorf("We expected a type-error, but didn't receive one")
+ }
+
+ //
+ // Call with a negative argument.
+ //
+ var failArgs2 []object.Object
+ failArgs2 = append(failArgs2, object.Number(-33))
+ out2 := SPC(nil, failArgs2)
+ if out2.Type() != object.ERROR {
+ t.Errorf("We expected a type-error, but didn't receive one")
+ }
+ if !strings.Contains(out2.String(), "Positive") {
+ t.Errorf("Our error message wasn't what we expected")
+ }
+
+ //
+ // Now do it properly.
+ //
+ var fArgs []object.Object
+ fArgs = append(fArgs, object.Number(17))
+ fOut := SPC(nil, fArgs)
+ if fOut.Type() != object.STRING {
+ t.Errorf("We expected a string return, but didn't get one: %s", fOut.String())
+ }
+ if len(fOut.(*object.StringObject).Value) != 17 {
+ t.Errorf("Function returned the wrong length: '%s' - %d", fOut.String(), len(fOut.String()))
+ }
+
+ //
+ // Now do it properly - int
+ //
+ var iArgs []object.Object
+ iArgs = append(iArgs, object.Number(3))
+ iOut := SPC(nil, iArgs)
+ if iOut.Type() != object.STRING {
+ t.Errorf("We expected a string return, but didn't get one: %s", iOut.String())
+ }
+ if len(iOut.(*object.StringObject).Value) != 3 {
+ t.Errorf("Function returned the wrong length: '%s'", fOut.String())
+ }
+
+ if (iOut.(*object.StringObject).Value) != " " {
+ t.Errorf("Function returned a surprising result '%s'", iOut.String())
+ }
+
+}
+
func TestStr(t *testing.T) {
//
diff --git a/embed/main.go b/embed/main.go
index 25fac1f..790df76 100644
--- a/embed/main.go
+++ b/embed/main.go
@@ -22,6 +22,7 @@ import (
"image/png"
"os"
+ "github.com/skx/gobasic/builtin"
"github.com/skx/gobasic/eval"
"github.com/skx/gobasic/object"
"github.com/skx/gobasic/tokenizer"
@@ -36,45 +37,61 @@ var img *image.RGBA
// peekFunction is the golang implementation of the PEEK primitive,
// which is made available to BASIC.
-// We just log that we've been invoked here.
-func peekFunction(env interface{}, args []object.Object) object.Object {
- fmt.Printf("PEEK called with %v\n", args[0])
+//
+// We just log that we've been invoked here, along with any supplied arguments.
+func peekFunction(env builtin.Environment, args []object.Object) object.Object {
+ fmt.Printf("PEEK called.\n")
+
+ for i, e := range args {
+ fmt.Printf(" Arg %d -> %v\n", i, e)
+ }
+
return &object.NumberObject{Value: 0.0}
}
-// pokeFunction is the golang implementation of the PEEK primitive,
+// pokeFunction is the golang implementation of the POKE primitive,
// which is made available to BASIC.
-// We just log that we've been invoked here, along with the (three) args.
-func pokeFunction(env interface{}, args []object.Object) object.Object {
+//
+// We just log that we've been invoked here, along with any supplied arguments.
+func pokeFunction(env builtin.Environment, args []object.Object) object.Object {
fmt.Printf("POKE called.\n")
+
for i, e := range args {
fmt.Printf(" Arg %d -> %v\n", i, e)
}
+
return &object.NumberObject{Value: 0.0}
}
// circleFunction allows drawing a circle upon our image.
-func circleFunction(env interface{}, args []object.Object) object.Object {
+func circleFunction(env builtin.Environment, args []object.Object) object.Object {
+
+ // Ensure we have three arguments
+ if len(args) != 3 {
+ return object.Error("CIRCLE requires three arguments: X, Y, R")
+ }
var xx, yy, rr float64
if args[0].Type() == object.NUMBER {
xx = args[0].(*object.NumberObject).Value
+ } else {
+ return object.Error("Wrong type for X")
}
+
if args[1].Type() == object.NUMBER {
yy = args[1].(*object.NumberObject).Value
} else {
return object.Error("Wrong type for Y")
}
+
if args[2].Type() == object.NUMBER {
rr = args[2].(*object.NumberObject).Value
} else {
return object.Error("Wrong type for R")
}
- //
- // They need to be ints.
- //
+ // We want the parameters as integers, rather than float64
x0 := int(xx)
y0 := int(yy)
r := int(rr)
@@ -83,7 +100,7 @@ func circleFunction(env interface{}, args []object.Object) object.Object {
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
black := color.RGBA{0, 0, 0, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{black}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{black}, image.Point{}, draw.Src)
}
// Create the colour
@@ -120,15 +137,23 @@ func circleFunction(env interface{}, args []object.Object) object.Object {
}
// plotFunction is the golang implementation of the PLOT primitive.
-func plotFunction(env interface{}, args []object.Object) object.Object {
+func plotFunction(env builtin.Environment, args []object.Object) object.Object {
var x, y float64
+ // Ensure we have two arguments
+ if len(args) != 2 {
+ return object.Error("PLOT requires two arguments: X, Y")
+ }
+
+ // Get X
if args[0].Type() == object.NUMBER {
x = args[0].(*object.NumberObject).Value
} else {
return object.Error("Wrong type for X")
}
+
+ // Get Y
if args[1].Type() == object.NUMBER {
y = args[1].(*object.NumberObject).Value
} else {
@@ -139,10 +164,10 @@ func plotFunction(env interface{}, args []object.Object) object.Object {
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
black := color.RGBA{0, 0, 0, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{black}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{black}, image.Point{}, draw.Src)
}
- // Draw the dot
+ // Plot the pixel
img.Set(int(x), int(y), color.RGBA{255, 0, 0, 255})
return &object.NumberObject{Value: 0.0}
@@ -151,13 +176,13 @@ func plotFunction(env interface{}, args []object.Object) object.Object {
// saveFunction is the golang implementation of the SAVE primitive,
// which is made available to BASIC.
// We save the image-canvas to the file `out.png`.
-func saveFunction(env interface{}, args []object.Object) object.Object {
+func saveFunction(env builtin.Environment, args []object.Object) object.Object {
// If we have no image, create it.
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
black := color.RGBA{0, 0, 0, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{black}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{black}, image.Point{}, draw.Src)
}
// Now write out the image.
diff --git a/eval/Fuzz.go b/eval/Fuzz.go
deleted file mode 100644
index 345ca07..0000000
--- a/eval/Fuzz.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package eval
-
-import "github.com/skx/gobasic/tokenizer"
-
-// Fuzz is the function that our fuzzer-application uses.
-// See `FUZZING.md` in our distribution for how to invoke it.
-func Fuzz(data []byte) int {
-
- tokener := tokenizer.New(string(data))
- e, err := New(tokener)
- if err == nil {
- e.Run()
- }
- return 1
-
-}
diff --git a/eval/eval.go b/eval/eval.go
index 4175b8f..d27fc76 100644
--- a/eval/eval.go
+++ b/eval/eval.go
@@ -10,12 +10,11 @@
// as REM, DATA, READ, etc. Things that could be pushed outside the core,
// such as the maths-primitives (SIN, COS, TAN, etc) have been moved into
// their own package to keep this as simple and readable as possible.
-//
package eval
import (
"bufio"
- "flag"
+ "context"
"fmt"
"math"
"os"
@@ -78,6 +77,16 @@ type Interpreter struct {
// STDIN is an input-reader used for the INPUT statement
STDIN *bufio.Reader
+ // STDOUT is the writer used for PRINT and DUMP statements
+ STDOUT *bufio.Writer
+
+ // STDERR is the writer used for user facing errors during program execution
+ STDERR *bufio.Writer
+
+ // LINEEND defines any additional characters to output when printing
+ // to the output or error streams.
+ LINEEND string
+
// Hack: Was the previous statement a GOTO/GOSUB?
jump bool
@@ -102,6 +111,46 @@ type Interpreter struct {
// fns contains a map of user-defined functions.
fns map[string]userFunction
+
+ // context for handling timeout
+ context context.Context
+}
+
+// StdInput allows access to the input-reading object.
+func (e *Interpreter) StdInput() *bufio.Reader {
+ if e.STDIN == nil {
+ e.STDIN = bufio.NewReader(os.Stdin)
+ }
+
+ return e.STDIN
+}
+
+// StdOutput allows access to the output-writing object.
+func (e *Interpreter) StdOutput() *bufio.Writer {
+ if e.STDOUT == nil {
+ e.STDOUT = bufio.NewWriter(os.Stdout)
+ }
+
+ return e.STDOUT
+}
+
+// StdError allows access to the error-writing object.
+func (e *Interpreter) StdError() *bufio.Writer {
+ if e.STDERR == nil {
+ e.STDERR = bufio.NewWriter(os.Stderr)
+ }
+
+ return e.STDERR
+}
+
+// Data returns a reference to this underlying Interpreter
+func (e *Interpreter) Data() interface{} {
+ return e
+}
+
+// LineEnding defines an additional characters to write after PRINT commands
+func (e *Interpreter) LineEnding() string {
+ return e.LINEEND
}
// New is our constructor.
@@ -126,6 +175,9 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
// allow reading from STDIN
t.STDIN = bufio.NewReader(os.Stdin)
+ // set standard output for STDOUT
+ t.STDOUT = bufio.NewWriter(os.Stdout)
+
//
// Setup a map to hold our jump-targets
//
@@ -136,6 +188,11 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
//
t.fns = make(map[string]userFunction)
+ //
+ // No context by default
+ //
+ t.context = context.Background()
+
//
// The previous token we've seen, if any.
//
@@ -233,7 +290,8 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
// If so that means we have duplicate line-numbers
//
if t.lines[line] != 0 {
- fmt.Printf("WARN: Line %s is duplicated - GOTO/GOSUB behaviour is undefined\n", line)
+ err := fmt.Sprintf("WARN: Line %s is duplicated - GOTO/GOSUB behaviour is undefined", line)
+ t.StdError().WriteString(err + t.LineEnding())
}
t.lines[line] = offset
}
@@ -279,7 +337,6 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
case token.NEWLINE:
run = false
- break
case token.COMMA:
// NOP
case token.STRING:
@@ -288,7 +345,7 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
i, _ := strconv.ParseFloat(tk.Literal, 64)
t.data = append(t.data, &object.NumberObject{Value: i})
default:
- return nil, fmt.Errorf("Error reading DATA - Unhandled token: %s", tk.String())
+ return nil, fmt.Errorf("error reading DATA - Unhandled token: %s", tk.String())
}
start++
}
@@ -304,7 +361,7 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
//
err := t.parseDefFN(offset)
if err != nil {
- return nil, fmt.Errorf("Error in DEF FN: %s", err.Error())
+ return nil, fmt.Errorf("error in DEF FN: %s", err.Error())
}
}
@@ -336,6 +393,7 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
t.RegisterBuiltin("EXP", 1, builtin.EXP)
t.RegisterBuiltin("INT", 1, builtin.INT)
t.RegisterBuiltin("LN", 1, builtin.LN)
+ t.RegisterBuiltin("LOG", 1, builtin.LN)
t.RegisterBuiltin("PI", 0, builtin.PI)
t.RegisterBuiltin("RND", 1, builtin.RND)
t.RegisterBuiltin("SGN", 1, builtin.SGN)
@@ -352,6 +410,7 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
t.RegisterBuiltin("LEN", 1, builtin.LEN)
t.RegisterBuiltin("MID$", 3, builtin.MID)
t.RegisterBuiltin("RIGHT$", 2, builtin.RIGHT)
+ t.RegisterBuiltin("SPC", 1, builtin.SPC)
t.RegisterBuiltin("STR$", 1, builtin.STR)
t.RegisterBuiltin("TL$", 1, builtin.TL)
@@ -365,6 +424,22 @@ func New(stream *tokenizer.Tokenizer) (*Interpreter, error) {
return t, nil
}
+// NewWithContext is a constructor which allows a context to be specified.
+//
+// It will defer to New for the basic constructor behaviour.
+func NewWithContext(ctx context.Context, stream *tokenizer.Tokenizer) (*Interpreter, error) {
+
+ // Create
+ i, err := New(stream)
+ if err != nil {
+ return nil, err
+ }
+
+ i.context = ctx
+
+ return i, nil
+}
+
// FromString is a constructor which takes a string, and constructs
// an Interpreter from it - rather than requiring the use of the tokenizer.
func FromString(input string) (*Interpreter, error) {
@@ -521,7 +596,8 @@ func (e *Interpreter) factor() object.Object {
}
// terminal - handles parsing of the form
-// ARG1 OP ARG2
+//
+// ARG1 OP ARG2
//
// See also expr() which is similar.
func (e *Interpreter) term() object.Object {
@@ -529,6 +605,14 @@ func (e *Interpreter) term() object.Object {
// First argument
f1 := e.factor()
+ if f1 == nil {
+ return object.Error("term() - received a nil value from factor() - f1")
+ }
+
+ if f1.Type() == object.ERROR {
+ return f1
+ }
+
if e.offset >= len(e.program) {
return f1
}
@@ -552,55 +636,133 @@ func (e *Interpreter) term() object.Object {
// get the second argument
f2 := e.factor()
+ if f2 == nil {
+ return object.Error("term() - received a nil value from factor() - f2")
+ }
+
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing term()")
}
+
//
- // We allow operations of the form:
+ // Have we handled this operation?
//
- // NUMBER OP NUMBER
+ handled := false
+
//
- // We can error on strings.
+ // We allow string "multiplication"
//
- if f1.Type() != object.NUMBER ||
- f2.Type() != object.NUMBER {
- return object.Error("term() only handles integers")
- }
-
+ // STRING * NUMBER
//
- // Get the values.
+ // "STEVE " * 4 => "STEVE STEVE STEVE STEVE "
//
- v1 := f1.(*object.NumberObject).Value
- v2 := f2.(*object.NumberObject).Value
+ if f1.Type() == object.STRING &&
+ f2.Type() == object.NUMBER &&
+ tok.Type == token.ASTERISK {
+
+ // original value
+ val := f1.(*object.StringObject).Value
+ orig := val
+ // repeat
+ rep := f2.(*object.NumberObject).Value
+
+ // The string repetition won't work if
+ // the input string is empty.
+ //
+ // For example:
+ //
+ // "" * 55
+ //
+ // Will generate the output: ""
+ //
+ // Catch that in advance of the loop to avoid
+ // wasting time - we also cap the maximum length
+ // of our string here.
+ if len(orig) > 1 {
+
+ // while there are more repetitions
+ for rep > 0 {
+
+ // append
+ orig = orig + val
+
+ // reduce by one
+ rep--
+
+ // ensure we terminate if the string is too long
+ if len(orig) > 65535 {
+ fmt.Printf("WARNING: string too long, max length is 65535: %d currently\n", len(orig))
+
+ // Return early
+ // even with less than expected
+ // repetitions
+ f1 = &object.StringObject{Value: orig}
+ return f1
+ }
+ }
+ }
+
+ // Save the updated value
+ f1 = &object.StringObject{Value: orig}
+
+ // We've handled this
+ handled = true
+ }
//
- // Handle the operator.
+ // We allow operations of the form:
//
- if tok.Type == token.ASTERISK {
- f1 = &object.NumberObject{Value: v1 * v2}
- }
- if tok.Type == token.POW {
- f1 = &object.NumberObject{Value: math.Pow(v1, v2)}
- }
- if tok.Type == token.SLASH {
- if v2 == 0 {
- return object.Error("Division by zero")
+ // NUMBER OP NUMBER
+ //
+ if f1.Type() == object.NUMBER &&
+ f2.Type() == object.NUMBER {
+
+ //
+ // Get the values.
+ //
+ v1 := f1.(*object.NumberObject).Value
+ v2 := f2.(*object.NumberObject).Value
+
+ //
+ // Handle the operator.
+ //
+ if tok.Type == token.ASTERISK {
+ f1 = &object.NumberObject{Value: v1 * v2}
}
- f1 = &object.NumberObject{Value: v1 / v2}
- }
- if tok.Type == token.MOD {
+ if tok.Type == token.POW {
+ f1 = &object.NumberObject{Value: math.Pow(v1, v2)}
+ }
+ if tok.Type == token.SLASH {
+ if v2 == 0 {
+ return object.Error("Division by zero")
+ }
+ f1 = &object.NumberObject{Value: v1 / v2}
+ }
+ if tok.Type == token.MOD {
- d1 := int(v1)
- d2 := int(v2)
+ d1 := int(v1)
+ d2 := int(v2)
- if d2 == 0 {
- return object.Error("MOD 0 is an error")
+ if d2 == 0 {
+ return object.Error("MOD 0 is an error")
+ }
+ f1 = &object.NumberObject{Value: float64(d1 % d2)}
+ }
+
+ if e.offset >= len(e.program) {
+ return object.Error("Hit end of program processing term()")
}
- f1 = &object.NumberObject{Value: float64(d1 % d2)}
+
+ // we've handled the operation now
+ handled = true
}
- if e.offset >= len(e.program) {
- return object.Error("Hit end of program processing term()")
+ //
+ // If we didn't handle the operation then the types
+ // were invalid, so report that.
+ //
+ if !handled {
+ return object.Error("term() only handles string-multiplication and integer-operations")
}
// repeat?
@@ -611,7 +773,9 @@ func (e *Interpreter) term() object.Object {
}
// expression - handles parsing of the form
-// ARG1 OP ARG2
+//
+// ARG1 OP ARG2
+//
// See also term() which is similar.
func (e *Interpreter) expr(allowBinOp bool) object.Object {
@@ -648,7 +812,7 @@ func (e *Interpreter) expr(allowBinOp bool) object.Object {
// This is mostly due to our naive parser, because
// it gets confused handling "IF BLAH AND BLAH .."
//
- if allowBinOp == false {
+ if !allowBinOp {
if tok.Type == token.AND ||
tok.Type == token.OR ||
tok.Type == token.XOR {
@@ -775,11 +939,11 @@ func (e *Interpreter) compare(allowBinOp bool) object.Object {
switch t1.Type() {
case object.STRING:
- if "" != t1.(*object.StringObject).Value {
+ if t1.(*object.StringObject).Value != "" {
return &object.NumberObject{Value: 1}
}
case object.NUMBER:
- if 0 != t1.(*object.NumberObject).Value {
+ if t1.(*object.NumberObject).Value != 0 {
return &object.NumberObject{Value: 1}
}
}
@@ -905,37 +1069,37 @@ func (e *Interpreter) parseDefFN(offset int) error {
// skip past the DEF
offset++
if offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DEF FN")
+ return fmt.Errorf("hit end of program processing DEF FN")
}
// Next token should be "FN"
fn := e.program[offset]
if fn.Type != token.FN {
- return (fmt.Errorf("Expected FN after DEF"))
+ return (fmt.Errorf("expected FN after DEF"))
}
offset++
if offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DEF FN")
+ return fmt.Errorf("hit end of program processing DEF FN")
}
// Now a name
name := e.program[offset]
if name.Type != token.IDENT {
- return (fmt.Errorf("Expected function-name after 'DEF FN', got %s", name.String()))
+ return (fmt.Errorf("expected function-name after 'DEF FN', got %s", name.String()))
}
offset++
if offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DEF FN")
+ return fmt.Errorf("hit end of program processing DEF FN")
}
// Now an opening parenthesis.
open := e.program[offset]
if open.Type != token.LBRACKET {
- return (fmt.Errorf("Expected ( after 'DEF FN %s'", name))
+ return (fmt.Errorf("expected ( after 'DEF FN %s'", name))
}
offset++
if offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DEF FN")
+ return fmt.Errorf("hit end of program processing DEF FN")
}
//
@@ -969,7 +1133,7 @@ func (e *Interpreter) parseDefFN(offset int) error {
// Otherwise we'll assume we have an ID.
// Anything else is an error.
if tt.Type != token.IDENT {
- return (fmt.Errorf("Unexpected token %s in DEF FN %s", tt.String(), name))
+ return (fmt.Errorf("unexpected token %s in DEF FN %s", tt.String(), name))
}
//
@@ -984,7 +1148,7 @@ func (e *Interpreter) parseDefFN(offset int) error {
// Ensure we've still got tokens.
//
if offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DEF FN")
+ return fmt.Errorf("hit end of program processing DEF FN")
}
//
@@ -993,7 +1157,7 @@ func (e *Interpreter) parseDefFN(offset int) error {
eq := e.program[offset]
offset++
if eq.Type != token.ASSIGN {
- return (fmt.Errorf("Expected = after 'DEF FN %s(%s) - Got %s", name, strings.Join(args, ","), eq.String()))
+ return (fmt.Errorf("expected = after 'DEF FN %s(%s) - Got %s", name, strings.Join(args, ","), eq.String()))
}
//
@@ -1023,7 +1187,7 @@ func (e *Interpreter) parseDefFN(offset int) error {
// Empty body?
//
if body == "" {
- return fmt.Errorf("Hit end of program processing DEF FN")
+ return fmt.Errorf("hit end of program processing DEF FN")
}
//
@@ -1255,11 +1419,11 @@ func (e *Interpreter) runDIM() error {
// 1. We now expect a variable name.
//
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DIM")
+ return fmt.Errorf("hit end of program processing DIM")
}
target := e.program[e.offset]
if target.Type != token.IDENT {
- return fmt.Errorf("Expected IDENT after DIM, got %v", target)
+ return fmt.Errorf("expected IDENT after DIM, got %v", target)
}
e.offset++
@@ -1267,11 +1431,11 @@ func (e *Interpreter) runDIM() error {
// 2. Now we expect "("
//
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DIM")
+ return fmt.Errorf("hit end of program processing DIM")
}
open := e.program[e.offset]
if open.Type != token.LBRACKET {
- return fmt.Errorf("Expected '(' after 'DIM' , got %v", open)
+ return fmt.Errorf("expected '(' after 'DIM' , got %v", open)
}
e.offset++
@@ -1279,11 +1443,11 @@ func (e *Interpreter) runDIM() error {
// 3. Now we expect a dimension.
//
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DIM")
+ return fmt.Errorf("hit end of program processing DIM")
}
first := e.program[e.offset]
if first.Type != token.INT {
- return fmt.Errorf("Expected 'INT' after 'DIM(' , got %v", first)
+ return fmt.Errorf("expected 'INT' after 'DIM(' , got %v", first)
}
e.offset++
@@ -1296,7 +1460,7 @@ func (e *Interpreter) runDIM() error {
// 4. Now we either expect a "," or a ")"
//
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DIM")
+ return fmt.Errorf("hit end of program processing DIM")
}
tok := e.program[e.offset]
e.offset++
@@ -1307,7 +1471,7 @@ func (e *Interpreter) runDIM() error {
// Get the next factor
//
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DIM")
+ return fmt.Errorf("hit end of program processing DIM")
}
//
@@ -1320,11 +1484,11 @@ func (e *Interpreter) runDIM() error {
}
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DIM")
+ return fmt.Errorf("hit end of program processing DIM")
}
close := e.program[e.offset]
if close.Type != token.RBRACKET {
- return fmt.Errorf("Expected ')' after 'DIM %s(%s' , got %v", target.Literal, first, tok)
+ return fmt.Errorf("expected ')' after 'DIM %s(%s' , got %v", target.Literal, first, tok)
}
e.offset++
@@ -1332,7 +1496,7 @@ func (e *Interpreter) runDIM() error {
//
// Get the next factor
//
- return fmt.Errorf("Expected ')' after 'DIM %s(%s' , got %v", target.Literal, first, tok)
+ return fmt.Errorf("expected ')' after 'DIM %s(%s' , got %v", target.Literal, first, tok)
}
//
@@ -1345,12 +1509,12 @@ func (e *Interpreter) runDIM() error {
// 2D array
a, _ := strconv.ParseFloat(first.Literal, 64)
if a > 1024 {
- return (fmt.Errorf("Dimension too large! %f > 1024", a))
+ return (fmt.Errorf("dimension too large! %f > 1024", a))
}
b, _ := strconv.ParseFloat(sec.Literal, 64)
if b > 1024 {
- return (fmt.Errorf("Dimension too large! %f > 1024", b))
+ return (fmt.Errorf("dimension too large! %f > 1024", b))
}
x = object.Array(int(a), int(b))
@@ -1359,7 +1523,7 @@ func (e *Interpreter) runDIM() error {
// 1D array
a, _ := strconv.ParseFloat(first.Literal, 64)
if a > 1024 {
- return (fmt.Errorf("Dimension too large! %f > 1024", a))
+ return (fmt.Errorf("dimension too large! %f > 1024", a))
}
x = object.Array(0, int(a))
@@ -1379,34 +1543,34 @@ func (e *Interpreter) runForLoop() error {
// Ensure we've not walked off the end of the program.
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
+ return fmt.Errorf("hit end of program processing FOR")
}
// We now expect a variable name.
target := e.program[e.offset]
if target.Type != token.IDENT {
- return fmt.Errorf("Expected IDENT after FOR, got %v", target)
+ return fmt.Errorf("expected IDENT after FOR, got %v", target)
}
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
+ return fmt.Errorf("hit end of program processing FOR")
}
// Now an EQUALS
eq := e.program[e.offset]
if eq.Type != token.ASSIGN {
- return fmt.Errorf("Expected = after 'FOR %s' , got %v", target.Literal, eq)
+ return fmt.Errorf("expected = after 'FOR %s' , got %v", target.Literal, eq)
}
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
+ return fmt.Errorf("hit end of program processing FOR")
}
// Now an integer/variable
startI := e.program[e.offset]
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
+ return fmt.Errorf("hit end of program processing FOR")
}
var start float64
@@ -1421,45 +1585,82 @@ func (e *Interpreter) runForLoop() error {
}
start = x.(*object.NumberObject).Value
} else {
- return fmt.Errorf("Expected INT/VARIABLE after 'FOR %s=', got %v", target.Literal, startI)
+ return fmt.Errorf("expected INT/VARIABLE after 'FOR %s=', got %v", target.Literal, startI)
}
// Now TO
to := e.program[e.offset]
if to.Type != token.TO {
- return fmt.Errorf("Expected TO after 'FOR %s=%s', got %v", target.Literal, startI, to)
+ return fmt.Errorf("expected TO after 'FOR %s=%s', got %v", target.Literal, startI, to)
}
-
e.offset++
- if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
- }
- // Now an integer/variable
- endI := e.program[e.offset]
+ // The terminal value.
+ //
+ // Here we're lookin for either a literal, or falling back to
+ // an expression.
+ //
+ if (e.offset) >= len(e.program) {
+ return fmt.Errorf("hit end of program processing FOR")
+ }
+ //
+ // End value we'll populate.
+ //
var end float64
- if endI.Type == token.INT {
- v, _ := strconv.ParseFloat(endI.Literal, 64)
- end = v
- } else if endI.Type == token.IDENT {
+ //
+ // Get the current/next token.
+ //
+ endI := e.program[e.offset]
+ //
+ // If it is a variable then use the value - or return the error
+ //
+ if endI.Type == token.IDENT {
x := e.GetVariable(endI.Literal)
if x.Type() != object.NUMBER {
return fmt.Errorf("FOR: end-variable must be an integer")
}
end = x.(*object.NumberObject).Value
+
+ //
+ // Step past the variable-name.
+ //
+ e.offset++
} else {
- return fmt.Errorf("Expected INT/VARIABLE after 'FOR %s=%s TO', got %v", target.Literal, startI, endI)
+
+ //
+ // if it wasn't a variable then it's either a literal number
+ // or an expression.
+ //
+ // This will handle both cases.
+ //
+ tmp := e.expr(true)
+ if tmp.Type() != object.NUMBER {
+ return fmt.Errorf("FOR loops expect an integer STEP, got %s", tmp.String())
+ }
+ end = tmp.(*object.NumberObject).Value
+
+ //
+ // NOTE: Here we move past the value/expression.
+ //
+
+ //
+ // Hence why we bumped in the previous case
+ //
}
- // Default step is 1.
+ //
+ // The default step-increment is 1.
+ //
step := 1.0
- e.offset++
+ //
+ // Make sure we're still within our program.
+ //
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
+ return fmt.Errorf("hit end of program processing FOR")
}
// Is the next token a step?
@@ -1469,7 +1670,7 @@ func (e *Interpreter) runForLoop() error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing FOR")
+ return fmt.Errorf("hit end of program processing FOR")
}
// Parse the STEP-expression.
@@ -1530,7 +1731,7 @@ func (e *Interpreter) runGOSUB() error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing GOSUB")
+ return fmt.Errorf("hit end of program processing GOSUB")
}
// Get the target
@@ -1569,7 +1770,7 @@ func (e *Interpreter) runGOSUB() error {
//
// Otherwise we have an error.
//
- return fmt.Errorf("GOSUB: Line %s does not exist!", target.Literal)
+ return fmt.Errorf("GOSUB: Line %s does not exist", target.Literal)
}
// runGOTO handles a control-flow change
@@ -1579,7 +1780,7 @@ func (e *Interpreter) runGOTO() error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing GOTO")
+ return fmt.Errorf("hit end of program processing GOTO")
}
// Get the GOTO-target
@@ -1606,21 +1807,22 @@ func (e *Interpreter) runGOTO() error {
//
// Otherwise we have an error.
//
- return fmt.Errorf("GOTO: Line %s does not exist!", target.Literal)
+ return fmt.Errorf("GOTO: Line %s does not exist", target.Literal)
}
// runINPUT handles input of numbers from the user.
//
// NOTE:
-// INPUT "Foo", a -> Reads an integer
-// INPUT "Foo", a$ -> Reads a string
+//
+// INPUT "Foo", a -> Reads an integer
+// INPUT "Foo", a$ -> Reads a string
func (e *Interpreter) runINPUT() error {
// Skip the INPUT-instruction
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing INPUT")
+ return fmt.Errorf("hit end of program processing INPUT")
}
// Get the prompt
@@ -1628,7 +1830,7 @@ func (e *Interpreter) runINPUT() error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing INPUT")
+ return fmt.Errorf("hit end of program processing INPUT")
}
// We expect a comma
@@ -1639,7 +1841,7 @@ func (e *Interpreter) runINPUT() error {
}
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing INPUT")
+ return fmt.Errorf("hit end of program processing INPUT")
}
// Now the ID
@@ -1668,31 +1870,18 @@ func (e *Interpreter) runINPUT() error {
default:
return fmt.Errorf("INPUT invalid prompt-type %s", prompt.String())
}
- fmt.Printf("%s", p)
+
+ e.StdOutput().WriteString(p)
+ e.StdOutput().Flush()
//
// Read the input from the user.
//
- var input string
+ input, _ := e.StdInput().ReadString('\n')
- if flag.Lookup("test.v") == nil {
- input, _ = e.STDIN.ReadString('\n')
- } else {
- //
- // This is horrid
- //
- // If prompt contains "string" we return a string
- //
- // If prompt contains "number" we return a number
- //
- if strings.Contains(p, "string") {
- input = "steve"
- }
- if strings.Contains(p, "number") {
- input = "3.21"
- }
-
- }
+ //
+ // Remove the newline(s).
+ //
input = strings.TrimRight(input, "\n")
//
@@ -1723,10 +1912,9 @@ func (e *Interpreter) runINPUT() error {
//
// Here we _only_ allow:
//
-// IF $EXPR THEN $STATEMENT ELSE $STATEMENT NEWLINE
+// IF $EXPR THEN $STATEMENT ELSE $STATEMENT NEWLINE
//
// $STATEMENT will only be a single expression
-//
func (e *Interpreter) runIF() error {
// Bump past the IF token
@@ -1818,7 +2006,7 @@ func (e *Interpreter) runIF() error {
// Now we're in the THEN section.
//
if target.Type != token.THEN {
- return fmt.Errorf("Expected THEN after IF EXPR, got %v", target)
+ return fmt.Errorf("expected THEN after IF EXPR, got %v", target)
}
//
@@ -1919,7 +2107,7 @@ func (e *Interpreter) runLET(skipLet bool) error {
}
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing LET")
+ return fmt.Errorf("hit end of program processing LET")
}
// We now expect an ID
@@ -1927,10 +2115,10 @@ func (e *Interpreter) runLET(skipLet bool) error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing LET")
+ return fmt.Errorf("hit end of program processing LET")
}
if target.Type != token.IDENT {
- return fmt.Errorf("Expected IDENT after LET, got %v", target)
+ return fmt.Errorf("expected IDENT after LET, got %v", target)
}
//
@@ -1952,18 +2140,18 @@ func (e *Interpreter) runLET(skipLet bool) error {
}
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing LET")
+ return fmt.Errorf("hit end of program processing LET")
}
// Now "="
assign := e.program[e.offset]
if assign.Type != token.ASSIGN {
- return fmt.Errorf("Expected assignment after LET x, got %v", assign)
+ return fmt.Errorf("expected assignment after LET x, got %v", assign)
}
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing LET")
+ return fmt.Errorf("hit end of program processing LET")
}
// now we're at the expression/value/whatever
res := e.expr(true)
@@ -1991,14 +2179,14 @@ func (e *Interpreter) runNEXT() error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing NEXT")
+ return fmt.Errorf("hit end of program processing NEXT")
}
// Get the identifier
target := e.program[e.offset]
e.offset++
if target.Type != token.IDENT {
- return fmt.Errorf("Expected IDENT after NEXT in FOR loop, got %v", target)
+ return fmt.Errorf("expected IDENT after NEXT in FOR loop, got %v", target)
}
// OK we've found the tail of a loop
@@ -2105,19 +2293,21 @@ func (e *Interpreter) runNEXT() error {
//
// This is used by:
//
-// REM
-// DATA
-// DEF FN
-//
+// REM
+// DATA
+// DEF FN
func (e *Interpreter) swallowLine() error {
- run := true
+ // Look forwards
+ for e.offset < len(e.program) {
- for e.offset < len(e.program) && run {
+ // If the token is a newline, or EOF we're done
tok := e.program[e.offset]
if tok.Type == token.NEWLINE || tok.Type == token.EOF {
- run = false
+ return nil
}
+
+ // Otherwise keep going.
e.offset++
}
@@ -2137,7 +2327,7 @@ func (e *Interpreter) runREAD() error {
// Ensure we don't walk off the end of our program.
//
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing DATA")
+ return fmt.Errorf("hit end of program processing DATA")
}
//
@@ -2170,7 +2360,7 @@ func (e *Interpreter) runREAD() error {
// OK that just leaves IDENT
if tok.Type != token.IDENT {
- return (fmt.Errorf("Expected identifier after DATA - found %s", tok.String()))
+ return (fmt.Errorf("expected identifier after DATA - found %s", tok.String()))
}
//
@@ -2179,7 +2369,7 @@ func (e *Interpreter) runREAD() error {
// not read too much.
//
if e.dataOffset >= len(e.data) {
- return fmt.Errorf("Read past the end of our DATA storage - length %d", len(e.data))
+ return fmt.Errorf("read past the end of our DATA storage - length %d", len(e.data))
}
//
@@ -2231,7 +2421,7 @@ func (e *Interpreter) runSWAP() error {
// Skip past the SWAP token
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing SWAP")
+ return fmt.Errorf("hit end of program processing SWAP")
}
//
@@ -2241,10 +2431,10 @@ func (e *Interpreter) runSWAP() error {
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing SWAP")
+ return fmt.Errorf("hit end of program processing SWAP")
}
if a.Type != token.IDENT {
- return fmt.Errorf("Expected IDENT after SWAP, got %v", a)
+ return fmt.Errorf("expected IDENT after SWAP, got %v", a)
}
//
@@ -2262,16 +2452,21 @@ func (e *Interpreter) runSWAP() error {
return aErr
}
+ // Ensure the index-finding didn't walk us off the end of the program
+ if e.offset >= len(e.program) {
+ return fmt.Errorf("hit end of program processing SWAP")
+ }
+
//
// Now we expect a ","
//
comma := e.program[e.offset]
if comma.Type != token.COMMA {
- return fmt.Errorf("Expected comma after SWAP a, got %v", comma)
+ return fmt.Errorf("expected comma after SWAP a, got %v", comma)
}
e.offset++
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing SWAP")
+ return fmt.Errorf("hit end of program processing SWAP")
}
//
@@ -2281,7 +2476,7 @@ func (e *Interpreter) runSWAP() error {
e.offset++
if b.Type != token.IDENT {
- return fmt.Errorf("Expected IDENT after SWAP a, got %v", b)
+ return fmt.Errorf("expected IDENT after SWAP a, got %v", b)
}
//
@@ -2367,7 +2562,7 @@ func (e *Interpreter) runRETURN() error {
func (e *Interpreter) RunOnce() error {
if e.offset >= len(e.program) {
- return fmt.Errorf("Hit end of program processing RunOnce()")
+ return fmt.Errorf("hit end of program processing RunOnce()")
}
//
@@ -2488,10 +2683,24 @@ func (e *Interpreter) Run() error {
//
for e.offset < len(e.program) && !e.finished {
+ //
+ // We've been given a context, which we'll test at every
+ // iteration of our main-loop.
+ //
+ // This is a little slow and inefficient, but we need
+ // to allow our execution to be time-limited.
+ //
+ select {
+ case <-e.context.Done():
+ return fmt.Errorf("timeout during execution")
+ default:
+ // nop
+ }
+
err := e.RunOnce()
if err != nil {
- return fmt.Errorf("Line %s : %s", e.lineno, err.Error())
+ return fmt.Errorf("line %s : %s", e.lineno, err.Error())
}
}
@@ -2500,7 +2709,7 @@ func (e *Interpreter) Run() error {
// alert on unclosed FOR-loops.
//
if !e.loops.Empty() {
- return fmt.Errorf("Unclosed FOR loop")
+ return fmt.Errorf("unclosed FOR loop")
}
return nil
@@ -2548,7 +2757,7 @@ func (e *Interpreter) findIndex() ([]int, error) {
if x.Type() == object.NUMBER {
indexes = append(indexes, int(x.(*object.NumberObject).Value))
} else {
- return indexes, fmt.Errorf("Array indexes must be numbers!")
+ return indexes, fmt.Errorf("array indexes must be numbers")
}
} else {
@@ -2556,7 +2765,7 @@ func (e *Interpreter) findIndex() ([]int, error) {
// then that's an error.
if e.program[e.offset].Type != token.COMMA {
- return indexes, fmt.Errorf("Unexpected value found when looking for index: %s", e.program[e.offset].String())
+ return indexes, fmt.Errorf("unexpected value found when looking for index: %s", e.program[e.offset].String())
}
}
e.offset++
@@ -2581,7 +2790,6 @@ func (e *Interpreter) GetTrace() bool {
// SetVariable sets the contents of a variable in the interpreter environment.
//
// Useful for testing/embedding.
-//
func (e *Interpreter) SetVariable(id string, val object.Object) {
e.vars.Set(id, val)
}
@@ -2591,17 +2799,25 @@ func (e *Interpreter) SetVariable(id string, val object.Object) {
// Useful for testing/embedding
func (e *Interpreter) SetArrayVariable(id string, index []int, val object.Object) error {
+ // Is the value we're setting nil, or an error?
+ if val == nil {
+ return fmt.Errorf("SetArrayVariable - Setting a nil value is a bug")
+ }
+ if val.Type() == object.ERROR {
+ return fmt.Errorf("SetArrayVariable - Setting an error inside an array is a bug: %s", val.String())
+ }
+
// get the current variable - i.e. the parent array
x := e.GetVariable(id)
// If there was an error, then return it.
if x.Type() == object.ERROR {
- return fmt.Errorf("Error handling %s - %s", id, x.(*object.ErrorObject).Value)
+ return fmt.Errorf("error handling %s - %s", id, x.(*object.ErrorObject).Value)
}
// Ensure we've got an index.
if x.Type() != object.ARRAY {
- return (fmt.Errorf("Object is not an array, it is %s", x.String()))
+ return (fmt.Errorf("object is not an array, it is %s", x.String()))
}
// Otherwise assume we can index appropriately.
@@ -2631,7 +2847,6 @@ func (e *Interpreter) SetArrayVariable(id string, index []int, val object.Object
// GetVariable returns the contents of the given variable.
//
// Useful for testing/embedding.
-//
func (e *Interpreter) GetVariable(id string) object.Object {
val := e.vars.Get(id)
@@ -2675,7 +2890,6 @@ func (e *Interpreter) GetArrayVariable(id string, index []int) object.Object {
// be called from the users' BASIC program.
//
// Useful for embedding.
-//
func (e *Interpreter) RegisterBuiltin(name string, nArgs int, ft builtin.Signature) {
//
diff --git a/eval/eval_test.go b/eval/eval_test.go
index 9f40f90..76aded2 100644
--- a/eval/eval_test.go
+++ b/eval/eval_test.go
@@ -3,10 +3,12 @@
package eval
import (
+ "bufio"
"strings"
"testing"
"github.com/skx/gobasic/object"
+ "github.com/skx/gobasic/token"
"github.com/skx/gobasic/tokenizer"
)
@@ -234,8 +236,8 @@ func TestDim(t *testing.T) {
for _, test := range invalid {
- tokener := tokenizer.New(test)
- e, err := New(tokener)
+ tokener = tokenizer.New(test)
+ e, _ = New(tokener)
err = e.Run()
if err == nil {
@@ -325,7 +327,7 @@ func TestDim(t *testing.T) {
if err == nil {
t.Errorf("Expected error running '%s', got none", test)
}
- if !strings.Contains(err.Error(), "Dimension too large") {
+ if !strings.Contains(err.Error(), "dimension too large") {
t.Errorf("Error '%s' wasn't the expected error!", err.Error())
}
}
@@ -861,7 +863,7 @@ func TestIF(t *testing.T) {
if err == nil {
t.Errorf("Expected runtime-error, received none")
}
- if !strings.Contains(err.Error(), "Expected THEN after IF") {
+ if !strings.Contains(err.Error(), "expected THEN after IF") {
t.Errorf("The error we found was not what we expected: %s", err.Error())
}
@@ -963,9 +965,17 @@ func TestINPUT(t *testing.T) {
}
//
- // Read a string
+ // Fake buffer for reading a string from.
+ //
+ strBuf := strings.NewReader("STEVE\n")
+
//
- // NOTE: This requires (hacked) support in eval.go
+ // Fake buffer for reading a number from.
+ //
+ numBuf := strings.NewReader("3.13\n")
+
+ //
+ // Read a string
//
ok1 := `
10 INPUT "give me a string", a$
@@ -974,10 +984,16 @@ func TestINPUT(t *testing.T) {
if err != nil {
t.Errorf("Error parsing %s - %s", ok1, err.Error())
}
+
+ // Fake input
+ e.STDIN = bufio.NewReader(strBuf)
err = e.Run()
if err != nil {
t.Errorf("Unexpected error, reading input %s", err.Error())
}
+
+ //
+ //
//
// Now a$ should be a string
//
@@ -986,15 +1002,13 @@ func TestINPUT(t *testing.T) {
t.Errorf("Variable a$ had wrong type: %s", cur.String())
}
out := cur.(*object.StringObject).Value
- if out != "steve" {
+ if out != "STEVE" {
t.Errorf("Reading INPUT returned the wrong string: %s", out)
}
//
// Read a number
//
- // NOTE: This requires (hacked) support in eval.go
- //
ok2 := `
10 LET p="Give me a number"
20 INPUT p,b
@@ -1003,6 +1017,8 @@ func TestINPUT(t *testing.T) {
if err != nil {
t.Errorf("Error parsing %s - %s", ok2, err.Error())
}
+ // Fake input
+ e.STDIN = bufio.NewReader(numBuf)
err = e.Run()
if err != nil {
t.Errorf("Unexpected error, reading input %s", err.Error())
@@ -1015,7 +1031,7 @@ func TestINPUT(t *testing.T) {
t.Errorf("Variable b had wrong type: %s", cur.String())
}
out2 := cur.(*object.NumberObject).Value
- if out2 != 3.21 {
+ if out2 != 3.130000 {
t.Errorf("Reading INPUT returned the wrong number: %f", out2)
}
}
@@ -1142,7 +1158,7 @@ func TestMaths(t *testing.T) {
// TestMismatchedTypes tests that expr() errors on mismatched types.
func TestMismatchedTypes(t *testing.T) {
input := `10 LET a=3
-20 LET b="steve"
+20 LET b = "steve"
30 LET c = a + b
`
tokener := tokenizer.New(input)
@@ -1164,7 +1180,7 @@ func TestMismatchedTypes(t *testing.T) {
// TestMismatchedTypesTerm tests that term() errors on mismatched types.
func TestMismatchedTypesTerm(t *testing.T) {
input := `10 LET a="steve"
-20 LET b = ( a * 2 ) + ( a * 33 )
+20 LET b = ( a + 2 ) + ( a + 33 )
`
tokener := tokenizer.New(input)
e, err := New(tokener)
@@ -1177,8 +1193,8 @@ func TestMismatchedTypesTerm(t *testing.T) {
if err == nil {
t.Errorf("Expected to see an error, but didn't.")
}
- if !strings.Contains(err.Error(), "handles integers") {
- t.Errorf("Our error-message wasn't what we expected")
+ if !strings.Contains(err.Error(), "type mismatch") {
+ t.Errorf("Our error-message wasn't what we expected: %s", err.Error())
}
}
@@ -1223,7 +1239,7 @@ func TestNext(t *testing.T) {
if err == nil {
t.Errorf("Expected to see an error, but didn't.")
}
- if !strings.Contains(err.Error(), "Expected IDENT after NEXT in FOR loop") {
+ if !strings.Contains(err.Error(), "expected IDENT after NEXT in FOR loop") {
t.Errorf("Our error-message wasn't what we expected")
}
@@ -1304,7 +1320,7 @@ func TestRead(t *testing.T) {
if err == nil {
t.Errorf("Expected to see an error, but didn't.")
}
- if !strings.Contains(err.Error(), "Expected identifier") {
+ if !strings.Contains(err.Error(), "expected identifier") {
t.Errorf("Our error-message wasn't what we expected")
}
@@ -1323,7 +1339,7 @@ func TestRead(t *testing.T) {
if err == nil {
t.Errorf("Expected to see an error, but didn't.")
}
- if !strings.Contains(err.Error(), "Read past the end of our DATA storage") {
+ if !strings.Contains(err.Error(), "read past the end of our DATA storage") {
t.Errorf("Our error-message wasn't what we expected")
}
@@ -1385,6 +1401,9 @@ func TestRem(t *testing.T) {
tokener := tokenizer.New(test)
e, err := New(tokener)
+ if err != nil {
+ t.Errorf("unexpected error parsing '%s' - %s", test, err.Error())
+ }
err = e.Run()
if err != nil {
@@ -1449,7 +1468,7 @@ func TestRun(t *testing.T) {
if err == nil {
t.Errorf("Expected to see an error, but didn't.")
}
- if !strings.Contains(err.Error(), "Unclosed FOR loop") {
+ if !strings.Contains(err.Error(), "unclosed FOR loop") {
t.Errorf("Our error-message wasn't what we expected")
}
}
@@ -1541,8 +1560,7 @@ func TestSwap(t *testing.T) {
t.Errorf("Failed to swap array")
}
- var b []int
- b = append(a, 2)
+ b := append(a, 2)
B = e.GetArrayVariable("A", b)
if B.Type() != object.STRING {
t.Errorf("Array variable has the wrong type")
@@ -1713,3 +1731,38 @@ func TestZero(t *testing.T) {
}
}
+
+// TestSwallowLine tests we don't eat too many tokens in the processing
+// of newlines.
+func TestSwallowLine(t *testing.T) {
+
+ input := `10 REM "This is a test" So is this
+20 PRINT "OK"
+`
+
+ tokener := tokenizer.New(input)
+ e, err := New(tokener)
+ if err != nil {
+ t.Errorf("Error parsing %s - %s", input, err.Error())
+ }
+
+ // We start at offset 0
+ if e.offset != 0 {
+ t.Fatalf("we didn't start at the beginning")
+ }
+
+ err = e.swallowLine()
+ if err != nil {
+ t.Fatalf("error eating line")
+ }
+
+ // offset should now be bigger
+ if e.offset != 6 {
+ t.Fatalf("our offset was %d not %d", e.offset, 6)
+ }
+
+ // And we should have a newline as the next token
+ if e.program[e.offset].Type != token.NEWLINE {
+ t.Fatalf("did not get a line number got %v", e.program[e.offset])
+ }
+}
diff --git a/eval/fuzz_test.go b/eval/fuzz_test.go
index caf5497..1936dbc 100644
--- a/eval/fuzz_test.go
+++ b/eval/fuzz_test.go
@@ -1,22 +1,204 @@
-// vars_test.go - Simple test-cases for our variable-store
+//go:build go1.18
+// +build go1.18
package eval
import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
"testing"
+ "time"
+
+ "github.com/skx/gobasic/tokenizer"
)
-// TestFuzz is just a simple wrapper that pretends we cover the fuzzer.
-func TestFuzz(t *testing.T) {
+func FuzzEval(f *testing.F) {
+ f.Add([]byte(""))
+
+ // Simple
+ f.Add([]byte("10 REM OK"))
+ f.Add([]byte("10 PRINT \"foo\"\r"))
+ f.Add([]byte("10 LET a = 3 + 4 * 5\r\n"))
+
+ // Broken
+ f.Add([]byte("20 PRINT \"incomplete\n"))
+ f.Add([]byte("10 GOTO 100\n"))
+ f.Add([]byte("10 GOTO 10\xbc\n"))
+
+ // Bigger
+ f.Add([]byte(`
+ 00 REM This program tests GOTO-handling.
+ 10 GOTO 80
+ 20 GOTO 70
+ 30 GOTO 60
+ 40 PRINT "Hello-GOTO!\n"
+ 50 END
+ 60 GOTO 40
+ 70 GOTO 30
+ 80 GOTO 20
+ `))
+
+ f.Add([]byte(`
+ 50 DEF FN double(x) = x + x
+ 60 DEF FN square(x) = x * x
+ 70 DEF FN cube(x) = x * x * x
+ 80 DEF FN quad(x) = x * x * x * x
+ 90 PRINT "N\tDoubled\tSquared\tCubed\tQuadded (?)\n"
+100 FOR I = 1 TO 10
+110 PRINT I, "\t", FN double(I), "\t", FN square(I), "\t", FN cube(I), "\t", FN quad(I), "\n"
+120 NEXT I
+`))
- var data []byte
- data = []byte(`
-10 REM
-`)
+ //
+ // Load each of our examples as a seed too.
+ //
+ files, err := filepath.Glob("../examples/*.bas")
+ if err == nil {
- out := Fuzz(data)
- if out != 1 {
- t.Errorf("We found an unexpected result in Fuzz!")
+ // For each example
+ for _, name := range files {
+ var data []byte
+
+ // Read the contents
+ data, err = os.ReadFile(name)
+
+ if err == nil {
+ // If no error then seed.
+ fmt.Printf("Seeded with %s\n", name)
+ f.Add(data)
+ }
+ }
}
+ f.Fuzz(func(t *testing.T, input []byte) {
+
+ //
+ // Expected errors, caused by bad syntax,
+ // invalid types, etc.
+ //
+ // We hope that the fuzz-tester will result
+ // in a panic, or error, but we know that
+ // some programs that are malformed aren't
+ // actually worth aborting for.
+ //
+ // For example this program:
+ //
+ // 10 PRINT "STEVE"
+ // 20 GOTO 100
+ //
+ // Is invalid, as there is no line 100. That's
+ // not something the fuzz-tester should regard as
+ // an interesting result.
+ //
+ // Similarly this is gonna cause an error:
+ //
+ // 10 GOTO 10
+ //
+ // Because it'll get caught by the timeout we've defined,
+ // but that's not something we regard as interesting either.
+ //
+ expected := []string{
+ "expect an integer",
+ "got token",
+ "access out of bounds",
+ "argument count mis-match",
+ "def fn: expected ",
+ "dimension too large",
+ "division by zero",
+ "don't support operations",
+ "mod 0 is an error",
+ "object is not an array",
+ "only handles string-prompts",
+ "unclosed bracket around",
+ "wrong type",
+ "array indexes must be",
+ "does not exist",
+ "doesn't exist",
+ "end of program processing",
+ "expected ident after ",
+ "expected assignment",
+ "expected identifier",
+ "index out of range",
+ "input should be",
+ "invalid prompt-type",
+ "length of strings cannot exceed",
+ "must be an integer",
+ "must be >0",
+ "next variable",
+ "nil terminal",
+ "not supported for strings",
+ "only handles string-multiplication and integer-operations",
+ "only integers are used for dimensions",
+ "positive argument only",
+ "read past the end of our data storage",
+ "received a nil value",
+ "return without gosub",
+ "setarrayvariable",
+ "should be followed by an integer",
+ "strconv.parse",
+ "the variable",
+ "timeout during execution",
+ "type mismatch between",
+ "unclosed for loop",
+ "unexpected token",
+ "unexpected value found when looking for index",
+ "unhandled token",
+ "while searching for argument",
+ "without opening for",
+ }
+
+ //
+ // Setup a timeout period to avoid infinite loops.
+ //
+ ctx, cancel := context.WithTimeout(
+ context.Background(),
+ 500*time.Millisecond,
+ )
+ defer cancel()
+
+ // Tokenize
+ toker := tokenizer.New(string(input))
+
+ // Prepare to run
+ e, err := NewWithContext(ctx, toker)
+ if err != nil {
+
+ // Lower case the error
+ fail := strings.ToLower(err.Error())
+
+ // Is this failure a known one? Then return
+ for _, txt := range expected {
+ if strings.Contains(fail, txt) {
+ return
+ }
+ }
+
+ // This is a panic caused by the fuzzer.
+ // Report it.
+ panic(fmt.Sprintf("input %s gave error %s", input, err))
+ }
+
+ // Run
+ err = e.Run()
+
+ if err != nil {
+
+ // Lower case the error
+ fail := strings.ToLower(err.Error())
+
+ // Is this failure a known one? Then return
+ for _, txt := range expected {
+ if strings.Contains(fail, txt) {
+ return
+ }
+ }
+
+ // This is a panic caused by the fuzzer.
+ // Report it.
+ panic(fmt.Sprintf("input %s gave error %s", input, err))
+ }
+ })
}
diff --git a/eval/testdata/fuzz/FuzzEval/08df59593afc1d599e6316bb2eed0af829803628c227cbd3c091f06debd22a77 b/eval/testdata/fuzz/FuzzEval/08df59593afc1d599e6316bb2eed0af829803628c227cbd3c091f06debd22a77
new file mode 100644
index 0000000..d410f27
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/08df59593afc1d599e6316bb2eed0af829803628c227cbd3c091f06debd22a77
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("NEXT A")
diff --git a/eval/testdata/fuzz/FuzzEval/0c73dd167de6594383e83c9c7b86f3a3ff99f33ec22bcc01a5d60e7a671aefe3 b/eval/testdata/fuzz/FuzzEval/0c73dd167de6594383e83c9c7b86f3a3ff99f33ec22bcc01a5d60e7a671aefe3
new file mode 100644
index 0000000..9b86e45
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/0c73dd167de6594383e83c9c7b86f3a3ff99f33ec22bcc01a5d60e7a671aefe3
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("\"00\"*1008888888A")
diff --git a/eval/testdata/fuzz/FuzzEval/2d15912b1b7f60074191ddb832e50fdecc740a9b139a15cf7f7785fd62139a58 b/eval/testdata/fuzz/FuzzEval/2d15912b1b7f60074191ddb832e50fdecc740a9b139a15cf7f7785fd62139a58
new file mode 100644
index 0000000..3ecd6b1
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/2d15912b1b7f60074191ddb832e50fdecc740a9b139a15cf7f7785fd62139a58
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("00DEF FN A00000(A)=0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"00\"\n00FOR A0=000TO\"")
diff --git a/eval/testdata/fuzz/FuzzEval/2dbd8156f1dc62d2da0ca4654064992e22e6f68d09a48e4636543eef80f67494 b/eval/testdata/fuzz/FuzzEval/2dbd8156f1dc62d2da0ca4654064992e22e6f68d09a48e4636543eef80f67494
new file mode 100644
index 0000000..9e40300
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/2dbd8156f1dc62d2da0ca4654064992e22e6f68d09a48e4636543eef80f67494
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("A=0A(A[0")
diff --git a/eval/testdata/fuzz/FuzzEval/2f049c13f886b83b934a9e532e0753ad08ca8011de55fe3a9d5d448e1f08c5cc b/eval/testdata/fuzz/FuzzEval/2f049c13f886b83b934a9e532e0753ad08ca8011de55fe3a9d5d448e1f08c5cc
new file mode 100644
index 0000000..2045093
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/2f049c13f886b83b934a9e532e0753ad08ca8011de55fe3a9d5d448e1f08c5cc
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(0)00J=0A B=0 0IF A[0 0 0")
diff --git a/eval/testdata/fuzz/FuzzEval/323ede30ab52badbafa9c69d88d512c0eb1ed1f215c15d538d5e5799d7d62350 b/eval/testdata/fuzz/FuzzEval/323ede30ab52badbafa9c69d88d512c0eb1ed1f215c15d538d5e5799d7d62350
new file mode 100644
index 0000000..eedaaad
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/323ede30ab52badbafa9c69d88d512c0eb1ed1f215c15d538d5e5799d7d62350
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(0,A")
diff --git a/eval/testdata/fuzz/FuzzEval/59d333f27ccaec38c9d0f83647b5cd08deab581203cf50c5d31ac28acdcbbd6d b/eval/testdata/fuzz/FuzzEval/59d333f27ccaec38c9d0f83647b5cd08deab581203cf50c5d31ac28acdcbbd6d
new file mode 100644
index 0000000..d992613
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/59d333f27ccaec38c9d0f83647b5cd08deab581203cf50c5d31ac28acdcbbd6d
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(10)00SWAP A[0 0 0],A[0")
diff --git a/eval/testdata/fuzz/FuzzEval/67c262c5a376da64175565df1e908d11e207aab88163158532613f178fedabeb b/eval/testdata/fuzz/FuzzEval/67c262c5a376da64175565df1e908d11e207aab88163158532613f178fedabeb
new file mode 100644
index 0000000..1e7ff33
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/67c262c5a376da64175565df1e908d11e207aab88163158532613f178fedabeb
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("A=\"000000000000000000\"00L=(LEN A)0FOR I=0TO 0000A A0=MID$ A 0 0(I=A(NEXT I")
diff --git a/eval/testdata/fuzz/FuzzEval/7254fe1fe3d88bb3c34a1ae1687a8bf28ca888a6d660345adccb6dca60f3bcdf b/eval/testdata/fuzz/FuzzEval/7254fe1fe3d88bb3c34a1ae1687a8bf28ca888a6d660345adccb6dca60f3bcdf
new file mode 100644
index 0000000..27a314a
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/7254fe1fe3d88bb3c34a1ae1687a8bf28ca888a6d660345adccb6dca60f3bcdf
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("SPC 77700700")
diff --git a/eval/testdata/fuzz/FuzzEval/7b751db98fa6219561d2f9986374d9d067ba2c32586a1d3e4c83617d3360a4da b/eval/testdata/fuzz/FuzzEval/7b751db98fa6219561d2f9986374d9d067ba2c32586a1d3e4c83617d3360a4da
new file mode 100644
index 0000000..3cf34e7
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/7b751db98fa6219561d2f9986374d9d067ba2c32586a1d3e4c83617d3360a4da
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("INPUT(,A")
diff --git a/eval/testdata/fuzz/FuzzEval/852c38ab356a99490a0c2bdfcf10beee7586527ff62e820a33ac5b85299fac63 b/eval/testdata/fuzz/FuzzEval/852c38ab356a99490a0c2bdfcf10beee7586527ff62e820a33ac5b85299fac63
new file mode 100644
index 0000000..75c058c
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/852c38ab356a99490a0c2bdfcf10beee7586527ff62e820a33ac5b85299fac63
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("dATA!")
diff --git a/eval/testdata/fuzz/FuzzEval/8c78f7663b810718f4bb9025800b39bcc4961180eb481ff559723b559c0ede81 b/eval/testdata/fuzz/FuzzEval/8c78f7663b810718f4bb9025800b39bcc4961180eb481ff559723b559c0ede81
new file mode 100644
index 0000000..3418c66
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/8c78f7663b810718f4bb9025800b39bcc4961180eb481ff559723b559c0ede81
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0DEF FN A00000(A)=000000000000000000000000000000000000000000000000\n0PRINT\"\"\nFOR A=0TO 0A")
diff --git a/eval/testdata/fuzz/FuzzEval/8deefb20b964592d87556c1377e8f4d07c9cb5911c14f93c52eb4ee9f89920d2 b/eval/testdata/fuzz/FuzzEval/8deefb20b964592d87556c1377e8f4d07c9cb5911c14f93c52eb4ee9f89920d2
new file mode 100644
index 0000000..7101faa
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/8deefb20b964592d87556c1377e8f4d07c9cb5911c14f93c52eb4ee9f89920d2
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("SWAP A[0")
diff --git a/eval/testdata/fuzz/FuzzEval/967b15fd430632f501b8e67864bafc78eda6f3a123aa44ece28643a68c5ff513 b/eval/testdata/fuzz/FuzzEval/967b15fd430632f501b8e67864bafc78eda6f3a123aa44ece28643a68c5ff513
new file mode 100644
index 0000000..4c68aac
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/967b15fd430632f501b8e67864bafc78eda6f3a123aa44ece28643a68c5ff513
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("READ 0")
diff --git a/eval/testdata/fuzz/FuzzEval/9a7873421031195dbfe7c1770c751d2b1e7013fec7e1356df7fa7e2e18101f70 b/eval/testdata/fuzz/FuzzEval/9a7873421031195dbfe7c1770c751d2b1e7013fec7e1356df7fa7e2e18101f70
new file mode 100644
index 0000000..378ace7
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/9a7873421031195dbfe7c1770c751d2b1e7013fec7e1356df7fa7e2e18101f70
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("VAL\"")
diff --git a/eval/testdata/fuzz/FuzzEval/9b475f0acf3a636cce37b3518d0444ac1bb82cb165e2514a9768c3b0c5f38540 b/eval/testdata/fuzz/FuzzEval/9b475f0acf3a636cce37b3518d0444ac1bb82cb165e2514a9768c3b0c5f38540
new file mode 100644
index 0000000..05f8809
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/9b475f0acf3a636cce37b3518d0444ac1bb82cb165e2514a9768c3b0c5f38540
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("\"00\"*50000*50000A")
diff --git a/eval/testdata/fuzz/FuzzEval/a0e3b497a8745f226673f84d5189d15dff3faa03c07a373650653dbea645f5fd b/eval/testdata/fuzz/FuzzEval/a0e3b497a8745f226673f84d5189d15dff3faa03c07a373650653dbea645f5fd
new file mode 100644
index 0000000..7371c38
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/a0e3b497a8745f226673f84d5189d15dff3faa03c07a373650653dbea645f5fd
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(0)00A=0%A[0 0 0]0")
diff --git a/eval/testdata/fuzz/FuzzEval/a50f6e8e6ba6db4b7510afc9feb797e15aad179c1d2e6dde909327f22b6aa954 b/eval/testdata/fuzz/FuzzEval/a50f6e8e6ba6db4b7510afc9feb797e15aad179c1d2e6dde909327f22b6aa954
new file mode 100644
index 0000000..e8b69d3
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/a50f6e8e6ba6db4b7510afc9feb797e15aad179c1d2e6dde909327f22b6aa954
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("SPC-1")
diff --git a/eval/testdata/fuzz/FuzzEval/ad5f8840c34b6cff6418f3fece6311f00e1e6989c7df065e5e0745ad40648252 b/eval/testdata/fuzz/FuzzEval/ad5f8840c34b6cff6418f3fece6311f00e1e6989c7df065e5e0745ad40648252
new file mode 100644
index 0000000..8000b3c
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/ad5f8840c34b6cff6418f3fece6311f00e1e6989c7df065e5e0745ad40648252
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(0)0(A+A")
diff --git a/eval/testdata/fuzz/FuzzEval/b24ea2e26240d323fc3b94f3eb202ab5b0de8442d8a550c2b8d0d5c476c5c25f b/eval/testdata/fuzz/FuzzEval/b24ea2e26240d323fc3b94f3eb202ab5b0de8442d8a550c2b8d0d5c476c5c25f
new file mode 100644
index 0000000..ced5e74
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/b24ea2e26240d323fc3b94f3eb202ab5b0de8442d8a550c2b8d0d5c476c5c25f
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("RETURN")
diff --git a/eval/testdata/fuzz/FuzzEval/b3d24fc08745e9572324aec7cd63cbfa4e72f240248cdc0b5a58e1ab74f6d078 b/eval/testdata/fuzz/FuzzEval/b3d24fc08745e9572324aec7cd63cbfa4e72f240248cdc0b5a58e1ab74f6d078
new file mode 100644
index 0000000..5f395dc
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/b3d24fc08745e9572324aec7cd63cbfa4e72f240248cdc0b5a58e1ab74f6d078
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("INPUT!,A")
diff --git a/eval/testdata/fuzz/FuzzEval/cf3251baf371fb890f768c0063333d6eff4b6d6d0e1daf91954f3185d4c70451 b/eval/testdata/fuzz/FuzzEval/cf3251baf371fb890f768c0063333d6eff4b6d6d0e1daf91954f3185d4c70451
new file mode 100644
index 0000000..4ef1bb2
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/cf3251baf371fb890f768c0063333d6eff4b6d6d0e1daf91954f3185d4c70451
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0DEF")
diff --git a/eval/testdata/fuzz/FuzzEval/d0d5fd614af11e360b7f76911ff3b2aca68f08d597c160200aef5a2b4bc6dfce b/eval/testdata/fuzz/FuzzEval/d0d5fd614af11e360b7f76911ff3b2aca68f08d597c160200aef5a2b4bc6dfce
new file mode 100644
index 0000000..37b3d21
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/d0d5fd614af11e360b7f76911ff3b2aca68f08d597c160200aef5a2b4bc6dfce
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("\"\"OR\"")
diff --git a/eval/testdata/fuzz/FuzzEval/d6e65695da6aa3a70dcc3be49f62d6f157a191c45b239eb740d06ec882ea44c6 b/eval/testdata/fuzz/FuzzEval/d6e65695da6aa3a70dcc3be49f62d6f157a191c45b239eb740d06ec882ea44c6
new file mode 100644
index 0000000..6504a65
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/d6e65695da6aa3a70dcc3be49f62d6f157a191c45b239eb740d06ec882ea44c6
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(1070)")
diff --git a/eval/testdata/fuzz/FuzzEval/de2307442f0d074484aac037a6d72d02eec1f6f52e44bbb121109de09d8b13e1 b/eval/testdata/fuzz/FuzzEval/de2307442f0d074484aac037a6d72d02eec1f6f52e44bbb121109de09d8b13e1
new file mode 100644
index 0000000..000fa60
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/de2307442f0d074484aac037a6d72d02eec1f6f52e44bbb121109de09d8b13e1
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("READ A")
diff --git a/eval/testdata/fuzz/FuzzEval/de57ebc50a0839af9c4959fe8ff09148bdb90ddd3da6b251acf800bacf15e4fd b/eval/testdata/fuzz/FuzzEval/de57ebc50a0839af9c4959fe8ff09148bdb90ddd3da6b251acf800bacf15e4fd
new file mode 100644
index 0000000..151d569
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/de57ebc50a0839af9c4959fe8ff09148bdb90ddd3da6b251acf800bacf15e4fd
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(0)00I=0 0IF A[0 0I]%0A0")
diff --git a/eval/testdata/fuzz/FuzzEval/df0497a7a3b9ae507a4c1c3277126fc71605d40cc9bf6364a4c1b30bae016f3a b/eval/testdata/fuzz/FuzzEval/df0497a7a3b9ae507a4c1c3277126fc71605d40cc9bf6364a4c1b30bae016f3a
new file mode 100644
index 0000000..62599e5
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/df0497a7a3b9ae507a4c1c3277126fc71605d40cc9bf6364a4c1b30bae016f3a
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("DIM A(0)0FOR00=00000(INT A[0")
diff --git a/eval/testdata/fuzz/FuzzEval/e4926b097957868f74bc4fc2528f6df365f83255752b4de4a89fb525e6eb7404 b/eval/testdata/fuzz/FuzzEval/e4926b097957868f74bc4fc2528f6df365f83255752b4de4a89fb525e6eb7404
new file mode 100644
index 0000000..df340dd
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/e4926b097957868f74bc4fc2528f6df365f83255752b4de4a89fb525e6eb7404
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("INPUT! 0")
diff --git a/eval/testdata/fuzz/FuzzEval/e682019c8a5fcba3f0ee24fc3c6cc3331df43f5a610ae6739a4f7e7f1ea44f3d b/eval/testdata/fuzz/FuzzEval/e682019c8a5fcba3f0ee24fc3c6cc3331df43f5a610ae6739a4f7e7f1ea44f3d
new file mode 100644
index 0000000..5006dfc
--- /dev/null
+++ b/eval/testdata/fuzz/FuzzEval/e682019c8a5fcba3f0ee24fc3c6cc3331df43f5a610ae6739a4f7e7f1ea44f3d
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("\"\"*50555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555A")
diff --git a/examples/10-goto.bas b/examples/10-goto.bas
index fcfb0f8..c9a98b1 100644
--- a/examples/10-goto.bas
+++ b/examples/10-goto.bas
@@ -1,5 +1,4 @@
00 REM This program tests GOTO-handling.
-05 REM And I guess the REM statement.
10 GOTO 80
20 GOTO 70
30 GOTO 60
diff --git a/examples/100-fibonacci.bas b/examples/100-fibonacci.bas
new file mode 100644
index 0000000..38b6c76
--- /dev/null
+++ b/examples/100-fibonacci.bas
@@ -0,0 +1,10 @@
+10 LET MAX = 50000
+20 LET X = 1 : LET Y = 1
+30 IF X > MAX THEN GOTO 100
+40 PRINT X, "\n"
+50 X = X + Y
+60 IF Y > MAX THEN GOTO 100
+70 PRINT Y, "\n"
+80 Y = X + Y
+90 GOTO 30
+100 END
\ No newline at end of file
diff --git a/examples/20-endless-loop.bas b/examples/20-endless-loop.bas
index 03c53ab..f451939 100644
--- a/examples/20-endless-loop.bas
+++ b/examples/20-endless-loop.bas
@@ -1,2 +1,6 @@
-10 print "STEVE IS AWESOME"
-20 goto 10
+10 REM
+20 REM This program is a callback to the very first "programs"
+30 REM I would have written as a child.
+40 REM
+50 print "STEVE IS AWESOME"
+60 goto 50
diff --git a/examples/30-gosub.bas b/examples/30-gosub.bas
deleted file mode 100644
index 8d117bc..0000000
--- a/examples/30-gosub.bas
+++ /dev/null
@@ -1,9 +0,0 @@
-10 REM This program tests that GOSUB+RETURN works
-
-20 GOSUB 100
-30 GOSUB 100
-40 GOSUB 100
-50 END
-
-100 PRINT "SUBROUTINE WAS CALLED!\n"
-110 RETURN
diff --git a/examples/30-sine-wave.bas b/examples/30-sine-wave.bas
new file mode 100644
index 0000000..61cbd04
--- /dev/null
+++ b/examples/30-sine-wave.bas
@@ -0,0 +1,15 @@
+100 PRINT SPC(20);"SINE WAVE\n"
+110 PRINT SPC(10);"CREATIVE BASIC - JOHAN VDB\n"
+120 PRINT "\n\n\n\n"
+200 B=0
+210 FOR I=0 TO 30 STEP 0.14
+220 A=INT(40+40*SIN(I))
+230 PRINT SPC(A);
+240 IF B=1 THEN GOTO 280
+250 PRINT "GOLANG\n"
+260 B=1
+270 GOTO 300
+280 PRINT "BASIC\n"
+290 B=0
+300 NEXT I
+310 END
diff --git a/examples/35-read-data.bas b/examples/35-read-data.bas
index 4dfe61e..6bbcb5a 100644
--- a/examples/35-read-data.bas
+++ b/examples/35-read-data.bas
@@ -1,6 +1,6 @@
10 REM
20 REM This program prints the output of reading from DATA
-30
+30 REM
40 FOR n=1 TO 6
50 READ D
60 DATA 2,4,"Six"
diff --git a/examples/100-array-sort.bas b/examples/40-array-sort.bas
similarity index 100%
rename from examples/100-array-sort.bas
rename to examples/40-array-sort.bas
diff --git a/examples/40-gosub-error.bas b/examples/40-gosub-error.bas
deleted file mode 100644
index f6ce9ee..0000000
--- a/examples/40-gosub-error.bas
+++ /dev/null
@@ -1,3 +0,0 @@
-10 REM This program tests that RETURN errors
-
-20 RETURN
diff --git a/examples/45-case-conversion.bas b/examples/45-case-conversion.bas
index 452cc2b..6d4b943 100644
--- a/examples/45-case-conversion.bas
+++ b/examples/45-case-conversion.bas
@@ -1,5 +1,6 @@
10 REM
-20 REM This is a horrid script which converts a string to lower-case
+20 REM This is a horrid script which converts the case of strings.
+30 REM First upper->lower, then the reverse.
40 REM
100 LET A="STEVE IS LOWER-CASE"
diff --git a/examples/50-expr.bas b/examples/50-expr.bas
deleted file mode 100644
index c94642e..0000000
--- a/examples/50-expr.bas
+++ /dev/null
@@ -1,6 +0,0 @@
-10 REM This program tests basic expressions
-20 REM
-30 LET a = 3
-40 LET b = 7
-50 LET c = a * 2 + b
-60 PRINT c ,"\n"
diff --git a/examples/50-misc-maths.bas b/examples/50-misc-maths.bas
new file mode 100644
index 0000000..b31f2cd
--- /dev/null
+++ b/examples/50-misc-maths.bas
@@ -0,0 +1,16 @@
+ 10 REM
+ 20 REM This demonstrates some simple maths
+ 30 REM primitives.
+ 40 REM
+
+100 INPUT "Please enter a number: ",N
+110 PRINT "ABS(N)=", ABS(N), "\n"
+120 PRINT "ATN(N)=", ATN(N), "\n"
+130 PRINT "COS(N)=", COS(N), "\n"
+140 PRINT "EXP(N)=", EXP(N), "\n"
+150 PRINT "INT(N)=", INT(N), "\n"
+160 PRINT "LOG(N)=", LN(N), "\n"
+170 PRINT "SGN(N)=", SGN(N), "\n"
+180 PRINT "SQR(N)=", SQR(N), "\n"
+190 PRINT "TAN(N)=", TAN(N), "\n"
+200 END
diff --git a/examples/55-binary.bas b/examples/55-binary.bas
deleted file mode 100644
index ad870cd..0000000
--- a/examples/55-binary.bas
+++ /dev/null
@@ -1,15 +0,0 @@
-10 REM
-20 REM AND + OR test
-30 REM
-
-
-110 LET A = BIN 00001111
-120 LET B = BIN 11110000
-130 LET C = A OR B
-140 IF C = BIN 11111111 THEN PRINT "OR worked\n"
-
-
-200 LET A = BIN 10000001
-210 LET B = BIN 10000011
-220 LET C = A AND B
-230 IF C = 129 THEN PRINT "AND worked\n"
diff --git a/examples/99-game.bas b/examples/55-game.bas
similarity index 55%
rename from examples/99-game.bas
rename to examples/55-game.bas
index a203ec3..52a5d2f 100644
--- a/examples/99-game.bas
+++ b/examples/55-game.bas
@@ -10,16 +10,15 @@
10 LET b=RND 100
20 LET count=1
- 30 PRINT "I have picked a random number, please guess it!!\n"
+ 30 PRINT "I have picked a random number (1-100), please guess it!!\n"
40 INPUT "Enter your choice:", a
- 50 PRINT "\n"
- 60 IF b = a THEN GOTO 2000 ELSE PRINT "You were wrong: ":
- 70 IF a < b THEN PRINT "too low\n":
- 80 IF a > b THEN PRINT "too high\n":
+ 60 IF b = a THEN GOTO 2000 ELSE PRINT "Your choice was ":
+ 70 IF a < b THEN PRINT "too low!\n\n":
+ 80 IF a > b THEN PRINT "too high!\n\n":
90 LET count = count + 1
100 GOTO 40
-2000 PRINT "You guessed my number!\n"
-2010 PRINT "You took", count, "attepts\n"
+2000 PRINT "\n\nYou guessed my number!\n"
+2010 PRINT "You took", count, "attempts.\n"
2020 END
diff --git a/examples/60-for-loop.bas b/examples/60-for-loop.bas
index 241bb72..1fe3ccf 100644
--- a/examples/60-for-loop.bas
+++ b/examples/60-for-loop.bas
@@ -1,24 +1,29 @@
-10 REM This program tests for-loops a little.
-15 REM
+10 REM
+20 REM This program demonstrates the use of FOR-loops.
+30 REM
-20 PRINT "IN ONES\n"
-30 FOR I = 1 to 10 STEP 1
-40 PRINT "",I, "\n"
-50 NEXT I
-
-100 PRINT "IN TWOS\n"
-110 FOR I = 0 to 10 STEP 2
-120 PRINT "",I,"\n"
+100 PRINT "IN ONES\n"
+110 FOR I = 1 to 10 STEP 1
+120 PRINT "\t",I, "\n"
130 NEXT I
+200 PRINT "IN TWOS\n"
+210 FOR I = 0 to 10 STEP 2
+220 PRINT "\t",I,"\n"
+230 NEXT I
-500 PRINT "Backwards\n"
-510 FOR I = 10 to 0 STEP -1
-520 PRINT "",I,"\n"
-530 NEXT I
+300 PRINT "Backwards\n"
+310 FOR I = 10 to 0 STEP -1
+320 PRINT "\t",I,"\n"
+330 NEXT I
-1000 PRINT "With a variable\n"
-1010 LET term=4
-1020 FOR I = 1 TO term STEP 1
-1030 PRINT "", I, "\n"
-1040 NEXT I
+400 PRINT "With a variable\n"
+410 LET term=4
+420 FOR I = 1 TO term STEP 1
+430 PRINT "\t", I, "\n"
+440 NEXT I
+
+500 PRINT "With an expression\n"
+510 FOR I = 1 TO 3 * 4 + 5
+520 PRINT "\t", I, "\n"
+530 NEXT I
diff --git a/examples/70-if.bas b/examples/70-if.bas
index c5ee00c..3fc494b 100644
--- a/examples/70-if.bas
+++ b/examples/70-if.bas
@@ -1,15 +1,29 @@
-10 REM This program demonstrates our in-progress IF support
-20 REM
-30 REM For the moment we skip a single token, and allow a single
-40 REM expression between THEN+ELSE, or ELSE+NEWLINE
-50 REM
+ 10 REM
+ 20 REM This program demonstrates our IF support.
+ 50 REM
-100 IF 1 < 10 THEN PRINT "OK1\n" : ELSE PRINT "FAIL1\n"
-110 IF 1 > 0 THEN PRINT "OK2\n" : ELSE PRINT "FAIL2\n"
-120 REM
-130 REM Prove execution keeps going.
-140 REM
+100 FOR A=1 TO 2
+110 FOR B=1 TO 2
+120 PRINT "A=";A;" B=";B;"\n"
+130 IF A=1 AND B=2 THEN PRINT " --> A=1 AND B=2\n"
+140 IF A=2 OR B=2 THEN PRINT " --> A=2 OR B=2\n"
+150 IF A=2 AND B=2 THEN PRINT " --> A AND B ARE BOTH 2\n"
+160 NEXT B
+170 NEXT A
-150 LET a = 3
-160 PRINT "A is", a, "\n"
+200 IF 1 < 10 THEN PRINT "OK1\n" : ELSE PRINT "FAIL1\n"
+210 IF 1 > 0 THEN PRINT "OK2\n" : ELSE PRINT "FAIL2\n"
+
+300 LET a = 3
+310 IF A THEN PRINT "OK3\n" : ELSE PRINT "FAIL3\n"
+
+400 LET A = BIN 00001111
+410 LET B = BIN 11110000
+420 LET C = A OR B
+430 IF C = BIN 11111111 THEN PRINT "OR worked\n"
+
+500 LET A = BIN 10000001
+510 LET B = BIN 10000011
+520 LET C = A AND B
+530 IF C = 129 THEN PRINT "AND worked\n"
diff --git a/examples/80-cos.bas b/examples/80-cos.bas
deleted file mode 100644
index 65c2da4..0000000
--- a/examples/80-cos.bas
+++ /dev/null
@@ -1,6 +0,0 @@
-10 LET A = PI
-15 PRINT "PI\t\t", A, "\n"
-20 LET A = A / 2
-25 PRINT "PI/2\t\t", A, "\n"
-30 LET A = COS A
-35 PRINT "COS(PI/2)\t", A, "\n"
diff --git a/examples/95-arrays.bas b/examples/95-arrays.bas
index be679ad..8319fa8 100644
--- a/examples/95-arrays.bas
+++ b/examples/95-arrays.bas
@@ -1,13 +1,58 @@
- 10 REM THis program demonstrates the use of arrays
- 20 REM
- 30 DIM a(10,10)
- 40 FOR X = 0 TO 10
- 50 FOR Y = 0 TO 10
- 60 LET a[X,Y] = X * Y
- 70 NEXT Y
- 80 NEXT X
-100 FOR X = 0 TO 10
-110 FOR Y = 0 TO 10
-120 PRINT X, "*", Y, "=", a[X,Y], "\n"
-130 NEXT Y
-140 NEXT X
+ 10 REM
+ 20 REM This program demonstrates the use of arrays
+ 30 REM It creates a 10x10 array, full of random numbers,
+ 40 REM then prints it out - as hex
+
+ 50 REM Setup hex-table
+ 60 GOSUB 2000
+
+100 REM
+110 REM Generate a 10x10 array and populate it with random numbers
+120 REM
+130 DIM a(10,10)
+140 FOR X = 0 TO 10
+150 FOR Y = 0 TO 10
+160 LET a[X,Y] = RND 255
+170 NEXT Y
+180 NEXT X
+
+200 REM
+210 REM Now print the contents - as hex values
+220 REM
+230 FOR X = 0 TO 10
+240 FOR Y = 0 TO 10
+250 LET v = a[X,Y]
+260 GOSUB 1000
+270 PRINT " "
+280 NEXT Y
+290 PRINT "\n"
+300 NEXT X
+
+400 END
+
+
+1000 REM
+1010 REM Print the value in "v" as a two-digit Hex number
+1020 REM
+1030 LET a1 = INT(v / 16)
+1040 LET b1 = v - ( a1 * 16 )
+1050 LET x = hex[a1] + hex[b1]
+1060 PRINT x
+1070 RETURN
+
+
+2000 REM
+2010 REM Setup a hex-table, via the DATA statements later.
+2020 REM
+2030 DIM hex(16)
+2040 FOR I = 0 TO 15
+2050 READ x
+2060 hex[I] = CHR$ x
+2070 NEXT I
+2080 RETURN
+
+
+10000 REM ASCII-codes of the digits 0-9
+10010 DATA 48, 49, 50, 51, 52, 53, 55, 55, 56, 57
+10000 REM ASCII-codes of the letters A-F
+10030 DATA 65, 66, 67, 68, 69, 70
diff --git a/go.mod b/go.mod
index 8a6f1fc..ae98ff6 100644
--- a/go.mod
+++ b/go.mod
@@ -1 +1,3 @@
module github.com/skx/gobasic
+
+go 1.17
diff --git a/goserver/main.go b/goserver/main.go
index f73a099..34ea74f 100644
--- a/goserver/main.go
+++ b/goserver/main.go
@@ -9,7 +9,6 @@
// points, lines, circles, and view a rendered image containing the output.
//
// Graphing SIN and similar functions becomes very simple and natural.
-//
package main
import (
@@ -20,16 +19,21 @@ import (
"image/color"
"image/draw"
"image/png"
- "io/ioutil"
"log"
"net/http"
"os"
+ "github.com/skx/gobasic/builtin"
"github.com/skx/gobasic/eval"
"github.com/skx/gobasic/object"
"github.com/skx/gobasic/tokenizer"
+
+ _ "embed" // embedded-resource magic
)
+//go:embed data/index.html
+var indexResource string
+
// img holds the canvas we draw into.
var img *image.RGBA
@@ -45,7 +49,7 @@ func init() {
//
// It is invoked with two arguments (NUMBER NUMBER) and sets
// the corresponding pixel in our canvas to be Red.
-func plotFunction(env interface{}, args []object.Object) object.Object {
+func plotFunction(env builtin.Environment, args []object.Object) object.Object {
var x, y float64
@@ -67,7 +71,7 @@ func plotFunction(env interface{}, args []object.Object) object.Object {
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
c := color.RGBA{255, 255, 255, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, draw.Src)
}
// Draw the dot
@@ -81,17 +85,17 @@ func plotFunction(env interface{}, args []object.Object) object.Object {
//
// We save the image-canvas to a temporary file, and set that filename
// within the BASIC environment.
-func saveFunction(env interface{}, args []object.Object) object.Object {
+func saveFunction(env builtin.Environment, args []object.Object) object.Object {
// If we have no image, create it.
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
c := color.RGBA{255, 255, 255, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, draw.Src)
}
// Generate a temporary filename
- tmpfile, _ := ioutil.TempFile("", "goserver")
+ tmpfile, _ := os.CreateTemp("", "goserver")
// Now write out the image.
f, _ := os.OpenFile(tmpfile.Name(), os.O_WRONLY|os.O_CREATE, 0600)
@@ -100,7 +104,7 @@ func saveFunction(env interface{}, args []object.Object) object.Object {
// And save the temporary filename in a variable
- env.(*eval.Interpreter).SetVariable("file.name", &object.StringObject{Value: tmpfile.Name()})
+ env.Data().(*eval.Interpreter).SetVariable("file.name", &object.StringObject{Value: tmpfile.Name()})
// Finally we can nuke the image
img = nil
@@ -109,7 +113,7 @@ func saveFunction(env interface{}, args []object.Object) object.Object {
}
// colorFunction allows the user to change the current colour.
-func colorFunction(env interface{}, args []object.Object) object.Object {
+func colorFunction(env builtin.Environment, args []object.Object) object.Object {
var r, g, b float64
@@ -141,7 +145,7 @@ func colorFunction(env interface{}, args []object.Object) object.Object {
}
// circleFunction allows drawing a circle upon our image.
-func circleFunction(env interface{}, args []object.Object) object.Object {
+func circleFunction(env builtin.Environment, args []object.Object) object.Object {
var xx, yy, rr float64
@@ -177,7 +181,7 @@ func circleFunction(env interface{}, args []object.Object) object.Object {
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
c := color.RGBA{255, 255, 255, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, draw.Src)
}
// Now circle-magic happens.
@@ -211,7 +215,7 @@ func circleFunction(env interface{}, args []object.Object) object.Object {
}
// lineFunction draws a line.
-func lineFunction(env interface{}, args []object.Object) object.Object {
+func lineFunction(env builtin.Environment, args []object.Object) object.Object {
var xx1, yy1, xx2, yy2 float64
@@ -248,7 +252,7 @@ func lineFunction(env interface{}, args []object.Object) object.Object {
if img == nil {
img = image.NewRGBA(image.Rect(0, 0, 600, 400))
c := color.RGBA{255, 255, 255, 255}
- draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.ZP, draw.Src)
+ draw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, draw.Src)
}
var dx, dy, e, slope int
@@ -370,13 +374,11 @@ func lineFunction(env interface{}, args []object.Object) object.Object {
return &object.NumberObject{Value: 0.0}
}
-//
// Runs the script the user submitted.
//
// Returns the base64-encoded version of the output image.
//
// More reliable than it has any reason to be.
-//
func runScript(code string) (string, error) {
t := tokenizer.New(code)
@@ -399,15 +401,15 @@ func runScript(code string) (string, error) {
// Get the name of the file the SAVE function wrote to
pathObj := e.GetVariable("file.name")
if pathObj == nil {
- return "", fmt.Errorf("Your script did not include a 'SAVE' statement")
+ return "", fmt.Errorf("your script did not include a 'SAVE' statement")
}
if pathObj.Type() == object.ERROR {
- return "", fmt.Errorf("Your script did not include a 'SAVE' statement: %s", pathObj.(*object.ErrorObject).Value)
+ return "", fmt.Errorf("your script did not include a 'SAVE' statement: %s", pathObj.(*object.ErrorObject).Value)
}
path := pathObj.(*object.StringObject).Value
// Read the file
- b, err := ioutil.ReadFile(path)
+ b, err := os.ReadFile(path)
if err != nil {
return "", err
}
@@ -422,13 +424,11 @@ func runScript(code string) (string, error) {
return encoded, nil
}
-//
// Called via a HTTP-request.
//
// If GET serve `index.html`.
//
// If POST serve a PNG created by executing the user-submitted code.
-//
func handler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.Error(w, "404 not found.", http.StatusNotFound)
@@ -437,12 +437,8 @@ func handler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
- tmpl, err := getResource("data/index.html")
- if err == nil {
- fmt.Fprintf(w, "%s\n", string(tmpl))
- } else {
- http.Error(w, "404 not found.", http.StatusNotFound)
- }
+ tmpl := []byte(indexResource)
+ fmt.Fprintf(w, "%s\n", string(tmpl))
case "POST":
if err := r.ParseForm(); err != nil {
fmt.Fprintf(w, "ParseForm() err: %v", err)
@@ -484,13 +480,19 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
}
-//
// Entry-point.
-//
func main() {
+ //
+ // We'll bind a handler.
+ //
http.HandleFunc("/", handler)
- fmt.Printf("goserver running on http://localhost:8080/\n")
+
+ fmt.Printf("Listening on http://localhost:8080/\n")
+
+ //
+ // Launch the server.
+ //
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
diff --git a/goserver/static.go b/goserver/static.go
deleted file mode 100644
index 957cf05..0000000
--- a/goserver/static.go
+++ /dev/null
@@ -1,97 +0,0 @@
-//
-// This file was generated via github.com/skx/implant/
-//
-// Local edits will be lost.
-//
-package main
-
-import (
- "bytes"
- "compress/gzip"
- "encoding/hex"
- "errors"
- "io/ioutil"
-)
-
-//
-// EmbeddedResource is the structure which is used to record details of
-// each embedded resource in your binary.
-//
-// The resource contains the (original) filename, relative to the input
-// directory `implant` was generated with, along with the original size
-// and the compressed/encoded data.
-//
-type EmbeddedResource struct {
- Filename string
- Contents string
- Length int
-}
-
-//
-// RESOURCES is a simple array containing one entry for each embedded
-// resource.
-//
-// It is exposed to callers via the `getResources()` function.
-//
-var RESOURCES []EmbeddedResource
-
-//
-// Populate our resources
-//
-func init() {
-
- var tmp EmbeddedResource
-
- tmp.Filename = "data/index.html"
- tmp.Contents = "1f8b08000000000004ffecddfdef27b79127f6dfe7afe81848707b6e4bfdf079ea5d6b01db926f95f34abeb1f66e3741008fa5b134ebd1ccdcccc89671d8ff3d7815bb8a947773072408f24b244cd787ef2f9b0fc522592c16d93ffd9f3efefc175ffcd3af3f99be79ffedcbbf7df2d346a6e9a7df3c7ff6d5df3e99a669fae9fb17ef5f3effdbfffce2dd77cf5e4e3f7ff6eec597d3dffcd54f3f6c708bf2edf3f7cfa62fbf79f6f6ddf3f71ffde81fbef8e54f1e3f9a3e3cfff8eecbb72fdebc9fdebdfdf2a31f7df3fefd9b777ffde1875fbefeeaf907fffc5fbf7bfef6cf1f7cf9fadb0fff397efe64ff60ff60fde0db17af3ef8e7773f9a5ebc7afffcebb72fdefff9a31fbdfbe6d976bdfde4975fbff9c5ef3efc8fffeb7f7af9abcf7efff977c7fafed9bebdfef0b3bfffdfbe7ff9fe4f4f5f3ffed3fb6ffff0f74fbffad9778f8f7e347df9f6f5bb77afdfbef8fac5ab8f7ef4ecd5eb577ffef6f577ef7ef4b73ffdf05d14e907e5fbdb275195e9c30fa7df3c7fffdd9be9f9f7cfbe7df3f2f9bb06fff1d9db42a68fa6fffdfff89b8667ac0fde7cf7ee9b7f37fdb7e9c5577f3dadf314dcf9ebe9471fbf7df6a7e9f51f9fbf7df9eccd9b17afbe9ebe7cf1f6cb97cfdffd689ef0e0afa7dfaecbf4f493bf7fb20599bef8e6c5bbe9e4d89f5ebc7c39fdeef9f4fcfbe75f7ef7fef957d39f5ebcff667af66a7a2ccbf7b765995e7cfbecebe74ff62512b80c099ca59abe7afbec4feffeadec3f78728de84f6ecbf4cbcf9f4e9f4e1f4dcbf4c5e7d3b62ccbf49b2f3ef9f5b42d4feecb34fde2d3a7bff8d527d3a7f3f4e93c7dfae4b14c9f7df28f5f4c9f3e3996e9373ffbcf9f3c9908c96f1b37fe65faabff1e5fb6bfe0cbbb17afa6afdf3e7bf34de7c674b263fabfcf8fe964c8f47fcd9167d3bb171a57099e4f7f7af6c7e71f3c994e8e4c274bbeff2818f25896271346fcfa579f7f317d3fe3d0f4e369bd2ed3bf9f7ef3e967d3f71faedb63faf7d3af3f7d322577be7f32257b7efb3f62cafe174cf9f2f5cbd7dfbd7dfed5f4eefddb176f4649998a39ff4fb853ecf9eff0e7772f5f7ff98777d3ebdf9fa579f7c193a9f883414f3ff9fbe9bfbcf8eafd37e29c052d795d97e927d3ba2e4f268cfbd5275f4c7f8aa81f4d4f3ffb785a3114a7c63fb4083f9ee2a5a3c9e49f5b03dcc45f97659a5efc7efaf3f43f9f71435effee93cfa6fff0f96ffee1e7222c4f643a853c8f8db76ecb94adf7e727d3ba2f5393e1ef9f4ceb6569bffde1ba9c12bdde96e993cf3e7eb2ea0c3ae7ba34b134344ccfa6b7cf5e7df5fadbe0cddb27ebd244f5c9baec4b54eaed59d3ed7a7db22e97067efd03f0dac0dffd00bc2dd32f3effd5e74fa7b7f3f4f53cfdeec9badcf5d22ffee1e967ad7f8530b59f6717ffe1c073f90b591a079e972f5efd40985a8dfe5fee68ffaa003fec67adbadbf53a930cb475b6bf1c92da8844381ecb344dbffaf4b34fb47876c64fe7e9ba9ca19f7c1a7defb33648919b96c9525944b3ac4dc6c671af320981f98b4c7e327d3a0db9fc78faf40941ca6c2ea7e8fc76fa97bf3a2792f8efdf6ca56b6fa55fbe78f5eac5bb6fa65fbe7cf6f53008fe65e3c42cf2fe9be753c6fffdcb675fcf7d187bfdca00368c7badcedb65be3de675bbe5e036fdc39b1adffe71fa28463183feb5b1bdf59d7f6a7debf3e9a24b05c3a718fcfe71fea7e0edd97dfe29b81b1cf8c727186a50f8d9cbd7afbe0e0e6a4379d4288a5f3d07bdedcc1af7fc5799f85bcfe47676d17f7cb2de47367ba5fdf76f72f9d6b9fcf3d76fbf7afe76fac5eb57efdfbe7ef9aff9dcf8fb6cfafd8b972f9f7f35fd2ea29b911b1b5336ffd58f276787ff6cfa68ba2e66e06130cad1e41c4b9ee096de6ca06823c6347dfcf467ff65fa5964f30f4f3ff978faf9e74f3ffee4691b49dab873ce61e760fbe2ddf4fcfb37cfbfa40cbc7f4d3778f7fcfdf4e2d5f4d993d598e29d27ab81445b7cf1f9afa71f4f3ffffc8b2f3e97e9fd14f936ac7a43dbfeeb665a0e7dec6c8c4f9faccb719d4a020885cea0c1731cd5faebd948d12da29ad3af3ef9e517d38fa7a79ffe87bffbe2c9baafe7d05c5d8e78adbbc1390af134c5eeb327eb6e8c3ef37caa0cfb4599cc184fc5a3fcfce4e99375bf825b599f3ea59facfb0d145229c2bdca159acdd33698fef65ffeea6fce6efa6fcacebdcbce6ffef0ddcb97d3fff2ecdb377f33fdfcf5ff78083dd33b55af77afbf7dfefe1b7adfeffe3cbd79f9fafd7bbf9f4def9ebf7d41b56c5dd6241ac3f307a9b45487fdf4bd46ffddb377cfbf9abe7bf3fad5f46cfaafdfbdf8f20fd337cfbefcc3f4e9f49d3ffcfef5dbe9ab17efdebc7cf6e717afbe8e1eaefd433d7c77bef66a7afeeb676f9ebf9dbe7afec7175f3e9fbe7cfdea5509d2b357d327bff9f563bbdd42d7f1f219ed27d337a7cafeeefdf33f3effe0f72f3efcbb676fbffad3b3b7cf3ffc6afdc9f337d2fc30341fc267bc7dfac9cf3e9e9e1190e9d35f4ecfa68fa6e338a673aefee2733d3f068816f177f397f3576d6cc8617dfaf1f43b63edf4e3e9d92cc51f4f5f09ff78fa32c68aff10a92c4b8c13bfa1831a234cd84f3efed9173f9b96d9ff976569c1755ee6b507b77999b71edce765de7bf0322ff3a507aff3325f7bf0362ff3ad07eff332df7bf0312ff3a3078f79998f797d6cad187e5e02b874e016c0ad038f001e096ceb753e3c3b700fe0de812380a3806d9d8f5e8a152fd6655eef6739e2f7b54167b2eb326fdb1db46d67d6a07d01753e0623d779bd9d1558d779dbaff33a3077c5dd759bd7eb59c0f87d34e82ce2bacddb6505f5a4b5c2bacfeb756fd558f779bbdc40dbe5e44e400f507f4d6bad9779ad387e1f0dcadc2ef3765d41fd35adba5ee7f5922cb9cedbf506ea7134f5aa0499ce6dde5424c859c8dbbcddf063788d48acf779dd93b5f779bb5d403d6972b2aa4966ff9837f3f4203b2be159f12df33ae6edbe822a9d4dcb6e4bccef21f7db326ff70ba8c7d1661b669f6db6adf376bf817a1c6db66df3baad673adbbcdd0f508fa381b67d5eb7b3536dfbbc3db4ff10476b6c97794de1dc2ef3f6d090431cacdfc8ccc99fed3a6f0fec1fe260fd769bd7f51486ed366f0fbc1ee2e033915db3ccf7797b3c40bdccf8bc3d662b91c69fc7bc1d0ba8c7c1e7ed98d7e594fbed98b76305559c1d9fa32f9c32bd2ff3766ca01e079ff7755e97b35ebb342ea01e079ff76d5e97b32df66dde8e2ba8c7c1e77d9fd7e5acfbbecfdb7103f538f8bc5fe67539e567bfccdbf100f538f8ac6fe620b85fe7ed38403d0e3eefb7f938456cbfcdfbb2407a146cdeeff3717267bfcffbb2427a145cde1ff391cc79ccfbb2417a144cde8ff9387bf27eccfbb2432aca058f2fcb7c9cecbb2cf3be909b210a165fd6f938b97759e77db9427a2a387cd9e6e364de4523dd203d0a065ff61e659ff7e50ee951f0f772998f53b42e97795f1e901e057b2fd71ee5daa20c527cc1ddcb6d3ece0e73c1dd03d253c1ddcb7d7e9c0d70b9cffbba407a14dcbd3ce69c112e8f7937de0e43c505772f478f72b428c31c70c5ddeb323fce36ba2ef3be6e90cae88abbd7b54759e77ddd213d0aee5eb7f971b6d1756b5186c9f48abbd7bd47d9e77dbd407a2ab87bbdf4283ac915d2a3e0eef53ae74479bdcebb280377afb87bbdcd8fb3a5afb716651822aeb87bbdf728b87b83f48c70f7fae85170f70ee95170f77acc398b5f71f70ea92837dcbd2d15e586bb77488f82bbb7757e9c2275c3dd07a447c1dddbd6a3e0ee03d2a3e0ee6d9f1fa748dd70f780f428b87bbbf428b87b407a14dcbd5d7b14dc3d203d0aeede6e3dca6d3eee801c076e847983ec3950812433b4c04d0bdceef3fd94efdb3d92b9d788736bc34990b357fbbd2da05e1aad747bf4641ef37105ac4bb2d398b380f61c7f41d2193aca4d53de8e9ece11e918fc339d2306d29bf1e91cabfc96ced09beedafbbe543af7653e2e801ab4ef26883b68cf39e3beccfbb682aa5e7742715fe7fb39b8de8d6d809a20ee26913ba8a7b3b674069deb4e72ee5b4fc70008e8e531d13c403d9dada53374de3bf1baef3d1da324a0269abbc9e800ed395fdef796cea007dcc9e0fdd2d3b9ccc706a8f6a2a4482706d63637fbbd45acce1f827abff674ae910ed5f56caf7b9bd4ee31fa9ee9984f57504f8734df6f3d9d5ba4731bca439c1771f6e5945590740671be13e7fbbda7738f74e8cd599e7b4cb277629de5b9cf51af61997027cff7474fe711e93c2c75ce4a34790e92e93c5a3a833cdfc9f3fd98efe7a87e3f229d419eef0499a00ef53a5a3a833c3fc8f363a9f23c16e93c28ff67bd1ee4f900157f1ee47903159f1fe4f9d1e5f9b1463a94a2b35e0f53f6224ec90f483a833c3fc8f363ab7a3db64887e294e531af1b7087f290e7cd6bbd3ce4f9d1e5f9b1473a94ab4ca7c9f3830e70f2d96fe90cf2fc20cf8f2ecf56711ba0e4f941901750afd725f8fca0c364edc9f3a3cbf323e4f931c83345189f0779f65b7986b9ef419e1f5d9e2d2277402fcf2de4f041acb35eb7569e419e1fe4f9d1e5f911f2fc18e4f9d1e4f931c8b3dfca33c8f3833c3fba3c5bc3ee805e1e8a370d65e04f93e7c720cf0ff2fce8e3f323e4f9419e4f45f7617c3e409dcf4d9e1f833c1fe4f9e8f27c2cf3b1036a1cb308381ea01a0f0ff2bc824a7e0ef27cac35ce1f6ba4439ecff21cc6e787383d1de3f306eae9906723e039b41c313e1fe439d369e373ac29ceceecb7f20ce3f3419e8fbd976737ef1ce4f9d4820ef27c07f5f2b4f1f918e4f920cf47d7118e8bd0614191e95850dc417bce83d632ca33c8f3419e8f6b2fcf55e820cfe7647e189f6fa09e4e1b9f8f419e0ff27c746da3bd71189f331df27c03d5fc6e4da43c833c1fe4f9b897d6a20657402dca0ef21cba4c4fc7f8bc88d5db8b3c1f8fd2c4b4f00dd0d321cf57d05e4524cf0ba8a7439e89eb39da1c564080b5984a9eafa09e0e795e4095ceba1068cf4cc9ef433f35469f8a0a6c8b465c7a6a40ea0b3aa447b0d7a5aba37e477a64bbd223dc84c2a07d0a8688b43c74488f80af8bd1b7cd5f7e1fe633327e0a346c0b61356a577ab4dce8e1637a047d5df6d2a8fd8ef4c87aa547d80146efcab8a9c45e18ca47e0d7e532a477c161502dfc055af90ce6951efd594d06b95f1782ef99ab0fbf0fb520fbe7800b632540f79e1e655b7a83fcaf8b0ee0990b1ebf233d7da0d2d30930d4a87e8a94883b7393556ece33eba22378f6f4e8dc010df5d519a46774aff42c9248d630beaf8b0ee1d9d37bb4f2a9c939aaf97babaf51bed2b3a28a5a8ceda163accb516b40bf8968a35561bd4301a92f95a0f5970487017f6d36ce75e9093272ea1d413341364d09ae7ac8392947408216de1d8c1eb2ae739a66fdd622cc9f5563f6cf484f0fc9020a30435aa5f7f4a287acdb909e8a2d336c48709b5706c546cfbe1481bdc5bc25b381cc814153581956c3deb50d765c01c6aba0c50926d896e676c9be0834bbad3ae8907babced841d7e8a0eb5ed602bf1bbf75c82a2483ae3aa295a080acf779bd9fe3bbd7b766f19bb74b76f030f81ecc80b30dca30e105183cd7ebab0df57a86c761965b9b7178bd9461c4ef5648a35a1592c9582183662e02db1c6f94913840b930e7e628b95ee62d6c9ecccc557201250fdad36c2537be14378c2fb8318e2fcd3ebd5e87921b581412ad46bcce2b53219a6b00818d9d9649fb761a6a5ae03e373abcde9a9bb65915373ea9f8383e355bf87a1bcaa3072a0f5a15bccd2b532a9a6b5a818d6d99adfc5eb53694ad3370ef5933e1c87a98d2d7664f5fbb8dccefd68846a9cafa3eaf610dbecf6baa5962da0b09da25ed1cf556a35ed5daa8a73ce3a8d7ccf42b35315bd070a7d668b1d1063b83f6635ed330e3ad8d659e65bf365904a2d606c8214d363f7f1c868b18206dec5434a3b7acd1aab5fd0102696b6988d9b2b6615002790ea5eb38944680948e43696c2a78a6f5d3ef60b8fa54d602c40c2dadc4fe03002d0110d061b771d08d00c60df6dd35f62a3c8f1cd06c5ca875d0acb540feeb59aff3860d68098040643d8ecf11202ec38a738d2d10cfb41cc76fec354c67530b44fdb679c8793b73dee6ad1a4120721e87d208e823e3501a1b2b9e69d68edf72369256ceb1f312f16a3f51c48dd4b5ad985346052267e3e300ee91f3383ec6768d67cfb9694544aae77cca179a36508196b30d9ecae4d2c6b76d1cdf22708d6cba78c72690675afbe3b73a8fc39b4034e975acf3f5ccd9b651898965060e19c9aa38020d1c720e4d6bb394487e358ddce4d3eb6cef8974dec69c6d9a3570eb391bc8ec9c8d6356ec4d790e291a7cbd7c1f53bc9f9263ebaaea627c52ec71288a9d2ccfb5b4df98e7a568d8a9971f27771e36deb286162b521c55b0d8f7f21cca682890a2d1a4b868f359cb1c63ad8d26521c078ed825f3ec650cb5c33ee732d4dae69914d1d29f05a4b88fe3c11e4a586caae53826608c47ab8cb6da22c575a8b580d5cb3e76f3d881f31cca68c651467d3ac7501b733a3a5acc158814c7ee1bfb759eb5a68b40a4b80f13826dbc48d1b669369740a43876cbd8ddf31c523cfb205a83db4ef15823e650467df0126017fad80bf41c5234611d0dec295edb548d76e6ea5b148d5121889d43cf214503dbd2c09a76f65b5346d09ea23e23c5b1cfc446a367adb12310299ad34b00ee678afa4c35d73df651bd31d43a162dbba93ad5208160d9635e4b81db1f4d81437b8afa8c328e7d26b6313d8732ea1a1ae11853b475ee653370ce5a0291e2d86762d7d3b3a7481535bfa0a5395cf867f057b0c19f290a30925cc63e139ba49eb587108148711d14eecbda146eb4a66a814871ec33b1a7ea39a468c4db1b78cf61e662d1710f70abe602468a639f892d58cf2145a389142d0ab27b5c76cb988839a468d52f9bb1cfc48ead67ed7844c09c45612fbdf3a2cf700fb9708238c74781487154c16383d77348d134a611aef37a4fd1bb5ce7585da125520291e2d867623fd87348519f91e26d4cf176a6789bbbea2810298e7d26b68f3d8714293652bccf6b29fa97fbbc19ded02e52fa0c3edee7fd56b5116860ef48b105ed59ee2a36a17151dfaae63773dc22de90f363de0c836869e00291f363deafa9e00844ce63878b9d6dcf9ef361bf1f34d4591f54e8635e6fd5a8c7bc192ed19eb3798b40f183c8114580f38da44b2b8f0d73cf3d27c2ebc24700292b7204ccbfd6b895f375694e516889b2809c6dbc5f73381290b3a47bce31e9c5eefb299fd795eb01d2ebcca18920a3e58d256088099ad5bbaef3460d0a5a3973dce1efc41928bb9b4094719df76b2ecc05ec902a442f6358336cffa7e0f001901e6d392542c0cc8fde46d0440f4c77108156c66ddecafc2860e847abbb094419b779bf0e69b6328e834c782278f632ee9c2f40430bee330d21c05b31629f574a5dd0e2e37e96114da9153099a0659a118832eef37e1dd26c651c87ad7085f0ec65bc70f6000d6d6d3e6734b9300ea5505ce6d55a32680afd95ef578bb9d57209a8b7a235240844192ff35e961981fb12b90f6d1db6ce2b2d20b3beb632d2d5ab7aa6bbeb2cde5aa309d0ee0ddaf9783df948912ff1b9ce9b090fed1d499e8f487328e3756e651c87d66b68f5577a4596f1c6230634f091339f3212d46a412abe4c6ef3208fb7b38c54fee2f86dde4c7c68ef72f2bc4746fbb56a733bcb380ed6e18de2590e7dd77b2b233b468932cfc123e2add76a568b0699dce7818ff7b38c1611c3eb9b090fd8c5f91ce9f9bf9453a400fba142f47e1d2ad395ee937c7c7012020d7c3482f2f0c4b61a272c4382b163191f67192d4baa8c0f2e9d91e6d69b419eaac88b6dc8bcb5f53827843f8e67e7e3c14b093494d1f42493a0591901cc0d3a80b66081bd231d67c1ad7e6ad03c9a2369a3fdf5cd38116055f1986386e32bd46b73cc8de3a30218ae439e559bdb12b5b9d9d9c9f4041e7aff3208e96d696544ab8c02f8c805e992322ea0ad6fa3a2181e499e3de7957317a8eb658cd136ccd1eac2b7734a41d764ae40e4bcce7b59700522e751a10c4727cf7235be6d3cc4902167ebb1bd813d67134403b77432f55ae8981ca6ca0f5820721ee784f09ff2ec75de5bce6c2d29ca029c0dd092889b615fcebb15f6d9f802b45b7e583de7bdb5f36d1ce96fb10375b33acb772f5cde405df7bf5dac931bd8eb7c0eeae85a394be91631f74b0a285fafa8f3a8c886b79767e7f6956f1d68c8998d8527f37594b073a846d3bd3c02940c5e6397629951b9bdde47937022f3ec39dfb8ec81869c0dc04b037b9dcf0118ed391b6baf1173af3d0b5e6791f338d686df9967cff9ceef0fd457acd4cdc8d9b05a627c0eab68ba3a47c0aa9a6f5aed0608dc35d668a8095735cf2df586db83af20d26d0e943e6e3a68cff91c2cd19eb37151cf372ee6fc2270278be3b818ce6d9e5b55e5e082081a72b64ea5271fc330cdeb2d64dbba350736018a1affb7ee026f0cd304e31816ee709ee962eeb7ce439728554940ce68b5333fb9c56e8af56de62c40fde231b7271b05786cddc7312c1ce83cb71c29efeb4c8449442940029cabd0e236cf3a75467bce76bcad92d779dfb357099caf77d90e973bcffeeec6671154eee6118857b7a157f1c58b9cad97537f1238e28cc6bcd792838b5eb06c1cc3c249cfb36484c79e57ed22557ad6d1ac1c54d9e2e13986a13d6706a916732f3594539fae317af5ade1d6e759a3101fbfad4135bddf35278b0d05b5723ec730b4e7cc786ce5cb536a88b91b88eee318760f1d94e055b42b0fbf806a068abfabc93886f1100c6e876deb1c7b05cc2de12b9863988061ff3e6a96e13ae8d973b971ef0ba8f3412dd4991259453cc730b4d7d949061216de58599cdbbc9bde475fc3359c0d3dd72a20cf9606f5b6b7ded70441333d45c1887160e39a188c60401b626e627152ac7156804634ba2c12637953fd32176ed3b33fac5bf515468033f7dea46751d0ce9d73b4437bef7dcc1b89e5ebd819fe98f7109371b40bd747cf2e77bc091a54b37b741ec531dae5b0783f753bb43382a94e057917540d8f79a777dfc7d12e9c253dabaf3df8d53768cf71283acf0956cebc288d7668d559c00cc49fb2d61a023ae5631cedc2bdd2b3e71cfe6a01ed59bd902e393b2b94b2e8a08ec647abce02a171d81d4bc54220721e35b670c8f4ec050c17b6806a5a8ace2367a35de57c6a6c68aff3d6ce4df1d9acf5be40e43c8e76e1c2e9d973def98e0574c91923fab29c8d76c58873b4437b9df779a345f3f22c854f403b8f3e9ff46dbb16d6dea7c073fbb4cae11a3060fa89bf75d97e9ca31dda73e60aa08c76c7722617b03a6eb4e7b367ccb252891105bf0ce63a015d637432b59851707d2113bc3af6e30f35a98114e63ad7aad51bfcbfd0522905f45bb473f6d40bd15e3f67b8689156eda9e4ca86710cad35b3001969742823ce8e7eae6b38ba7af65c2cc21b94bd144487bc75cdc76f2d65b0cdf4394dc58bb564c5b9e814ac96c52b43797b77addd4909d9c4447f5014d617e01873b7ee945d19ad226039c32e501516c0af51710d8f5ccf5afb934bbbd5686976022c6b68ad2f05a2e446fd620e751677c6515f80fd0ded85bcb7b117ad4d70cebeec09a3b7ef1aeebe9e759e53b7e54880d6b02f10db74cc0425f0e602fb8bcc04d589cc050a6927a6faf483d34fe4d3cd564013045aae113c89ed6d8caec4ec4e5e3ee63283b044f1f1407b21ed81ad738065070346214d1bd55ea60d82312ac9022c0ae8ad44c85ca2b96d990ee0ce5238fa29afe1a8ec597d830ec8bd08adc58b00fb7dd04c30020ac8609785145072b4f8c3d959eba0a5380830d9a35572012547cbf18d6334b3d931ce45e11aedd9df6dee33b0a1e4b6c60cca6895dc615c968c95ddf5ec9f0226cda0397c1c9c3acc6336ab5284046c6fa0652b1408fdde167071831adf72ef9a7b38637b96e45b6bd80b454b7510887737db8a59c82db6b7e28f358c0063b2d5e443cc8d2f13c7ee52ac040c0e6859be04c2ba624ecc2940803a82d69e248f70bd7e7409674bc74ac77db290b6d21a3654879d9b468ee69828a077a3b5292110f3a8ddeb1c46f895f3320b9a13ac80bd30b4e62981a88e89b6aab3cfad3ad61ad58e1cd38d55a39d245cd33d6b9fcaea37aa6326ae770554072d693131d3fd2f83ed9427fb096e35a8024da641ab90f604f10d1dd2dced228e6eeff658c4eb073920b1b1cc3cd10b79dac483568236e72c13c2109d4dc6fcad3ae6d0e2f9b5a92a4147300a797542bbbfbedb846eaef52348f8c7e54c78da7bd6663c2352bccb385eef0ae0a499b1782ea0bd98554bcecdc772712abefab2a91887d02ab980f1cab666492097bf060e3d34bc97f9f15782b1baa161cd6b2fa48042dafaab420a985bd892ab90774eaef1fad60b796fc7f9d1da9d8a407bbdef8a3b21c0a5f1180d3d7146c0b38f93d63e0ac999a43829a0b92d9caa9002723133162b98c5dbeb5b1fcf4d82f8e38280e286407bbd6f8b3b7e1013c7b84a8a03089e7d44b5578a15e6bd1a3104b42cbb78955c40d60ce655c8638e3d10b4a6cb0828cf31f74d87089c60790338db10e3f9b0a0dae27083673a1cfa1d6e6c3cffb2902d409b59b83734210fd0a62e30b51101850c9a856c017ac6d20b19200b1b300b29e0b4375a32b92d76653d53b3f2bb15d2d4767232c028a4296e04edff027b21d7d91d18c0f26d6e01755cc7420a9c607a1888c9a10f1d0a695b765bb63a8aef372788a0b9366d01ac88d92c396943c78e027a0e2c62b6426e63210594c784550d217082bd909c1fadff86159d65a86cf63a030c698534379d421e601492534815d2dee6b9f9d00bc965bc810327b98c2b8f69a80a29d0ac32e5072123971ea0032799eb63ad9e3b9c11e0e16801df4b19f34f8b996bc28811ac340f152b2f272b2d03cf81404cd78e04cd5dd316504af3d0393905a8df8fc7502c49b0d23e6cc56b3e2afeb4eee7d0db024a69ee29069b80b08d39b70a64226ae0c04bb39202c58494f2227082bd94d67e1687c39cb3c5e116cfb2424780cf26a5bf97d2a4a394e6992abac94681d0e2a549a7815b2e7fa4e5fab8a0bd3e261da534e9148fec2310cb61e5b5c59119cfb2de46204a69a2a9029960701d1d41bbfec02e6d5c70d8208226dbb8e22850d011d421cd53e7a425ad569f98a78698766380e9d512016b8df1c8ce1647763cd7b4164520ea634e2a66988bd407adfa989814fd619b28f316d0a5827670e369b998bdaa3e662d628056d1cd5a27d88bce4fb5a539743eceefb162c96dcd0870b5b58ce94d618622066815dd74c5fe62a3a3b82e206fb44a2940de82667d6cf86a8aa003c8951c98ebd316c02353628d3002090e19b596b47f3c6614ecb0581c0abfb1132de3fc18679b3ccbde1c01ec70b8a95a52003b8266a3096047d02c90809d54b4e6b308f077a0b865291d9832ffa069616c013bfa1c9d9247026a1e74c8888924c0d42122a0e66256cd055842c743585b1cc2f2ec1713d11ea2e666dd649b40d4dcac5b35b7babcb4d753138c98b64e1ddccaa55e04c830b073d3fc6caf17adfaac677dd0aab9f999330b5faaa1487183d27808cc827069cbc2b484825a7d2c33abe826664b5f7404f54960af8ffd479544ab40f6034eb01708a8799d56c8de1b01926e2b2a3b4b04389500c798bc958163395b9b3b4c563ca21eb4dc8736a71ee0918ba58a4702ad48bdf3c7c932cf41b043056860cf862a201bb4f2b6297a82699888b4229bd8ac38bbb48081074d43490b106c3d38750e01d219b478c433555ba223c8471cd879443b2147e8504e4e2311b3b725ed048f86f5f01667d83cfbb047cbe7561bb452a46ba8393a82c10e1bc8d592f6e7701dad7ecea6aacd2d42738a8f00e9309c163b64a2e668d59c5b2d81454730d871e10f997ca70661073a94b3b1836e3494b30d13c346201b84bc2dac4b8a2835a403ad142937b2412b459a0e06a32318ec602f2f7634bbacecd65c3a4520d86122c9015f20d821ddaab9bdc656cedaf38898d183380c173be85bad9c75cd5dc43c0b3f48c7a96f8de7f2b63897e7394807ed498a685592f6a4c1d0e211f711c546c798c10e66f06207db2ce9b0902de910201db4996207c15273b4d8e11000e940abd97839b7720e9d85627782bd2dc3b410d52c0b8e3a6f14bbf1ace01667053d077650d3bc8c56cd1907b003ad9a53d34eb00fa5360fd5052d76d838c10e740463eca0cd5467110b3bd0620753fc09f6ce421f24c56849077db0157e60077df08c3916beb163305138a0251bda5e719d962745b46a7e6a7b414730d8316a7b02c18eb0e267976614c7237404433ae852251dc6d656a4adb3238cfe9caac6cec26f5c25d162071db3157e60071df38cd9459baed98a34cc2ca156b2367576d01ca58856cd4f0d32e8084667e13b3382c10e1a64cd2c7637b437db49751601b5a76f9674e83dd8819674d85e38c1de5938b3b7720e6a185d53cdd1126d3ae609767630cc60c7a856c6394fcfbe9a6566c58ea05949019d05ad7555040cf61cb7c698d801acce22403ad0624704b4b78547b243003b82263b04eadf08920e7f28e988c0c9bc6207103b82a61c0960c778f6947e1f3e48233ba8932a8966f30a189cd15ef3e6d11160afb96d257d1dcd7e21a05f180fab5f0844cd7583aaa43d8ffc97fd1710351fb55281a824ad340541202a39ece33b87ad9274cdca868ea99268559211e8047b254f0d3268d527363de6007b25993135854576767e81a824e91c72d70cf1c7debc5b1c1a09b0b724b5123bd0aa24b5b2e5defb791ca8f51c049b06a93e685592e618fa1b77eb733003f264457383a805cce934c8aab9fd1179b3a6663f8f00c11e354881a8b91e5c3597b3faa03f00310e386414cb13606e234420baeaa8560a8420307e95b4532bcdbca35a19477f3d071ed93dc123b478449d5418b478449db438a04156d105648356d14fb512ed3ca263aae4a8560a048fccc0439a4d3a6890d5e634c756a4413a688eb88956cd2f6d52dc460d328e1e7b0e35a739aa395a35a7399e6097379ae309f64a5216e51d34e548003bd06287dd0a351f354881a83955ac6a2ed05e2f0feb169370f9e350cec6a351ad14081e512f8b71cc78ad9c3e80f0f1cfbef8594b337ad068b18b43d29e7da6e461e1545bd0ca9b3a891d68490775526741c798dc3a81b9a3de02e7eb658688186a4ebdaca20ba8395db3e60b81f67a5fcb03ade5830eaf3776502f4730d841bd2c9139ed85e3716e17631b355901ab7da893f246ab79a9932413ad6c4eb53268f1883ad95eef93bc35b7493e6815e85401830e696e0cccc0dc2e8c40f4f351058c73e39e83b453fdb40fadafc62d0105422b1baadf09f6a253fdb40f6dafa45d40cda9385574010364d09437015d406b163769790614747c3d6498d63714a9d59cd65793e2a9ed8de7dbb738dfee39c8302d4fd1297855740145472b1b5a1e1ea155745adef97a697bc0e8e714bce2a6c09966176ce0f97a176c20568c7aa1408c08a35e2810f305e655cde985188716e3e8839a0d1d6ac4dd2392e98c3bf5c2f118bfc3acec7a4e726736021887563f17201d4193470266caa0d92f04cc9468f128028a4d05ccc61050f3a0291d0254b6a023a8b300ab9f0b101934f77923a05f8c370b18225492b657950c2d2fa6da3ee003a392ce3654254f6d2f68559296674d8b8ea0e63522d5b82510951cb53d01cd1b742852687bc06a5e81a8246daf18c706d972ef8a505c76e0d9bb002f22fba56849bb40549211b12a796a7b41ab3eb43dd9a023182d396a7b0251c951db13884a12d96a49da5efe4b19064425697b5549da5ecb7da8a49ba888c250490a9d03ae68ce4002514926c2aa24854e8ae81833c47554ec22405c47c54e202a392a760221ae0680aae41e0d117fece24a775373b4b8495d33978fea5a5c09e139549246e665ca580e3c11a02c01b3eb47806003ab92d43435472b6feada09f6e6a5aeb5d7fb8ac5351351f3515d1388e635f8940c2b8edaa3d5bc74381d101d8a1406bf7d54d7e2ea0acf6e32e7f3619e467b8aa7ba86a6c75f0b44be4efe9c33901831448d063f8168731a5a7153409b5f6da0f7d75bcd2961d5bce60a12878e2091058ee574a839c01a9cc58836e7d4538c13086bd028ed0eba6e1c4ad21dae05349aedd362308dec04bbb4b3e99d609776b6bd13ec353ff5add8a9aceec782a0d1d0aaa4194dcdd12abaf4c8267d6b04a3316cc58ea03db8f12a10fc914ddc98935ce70024554a5855929e4532a95863cca82415ab049ba50e2fd1118ce61d2d760236728256256931ad487d950a8c4ad2d04a3a7cec43ef0d67da5ef88dc7fb3e6a66713b8967dfe4e41e1a9564b1abde4b099337bdabb211c00eb4ea43093bc15e492e3878c40e37bcde64983256a5a4866849b4da4700e32861d52fe85bbad583c77056d24eae9a0ffe478e9d2bba7b852a6f012f07cd977925c9862352d59cf265e4a07755f39ed635b48b2b2d4a47a54595b8d29e5aeeddd8088c96147bc8a849a6add8aa0fe75d8c1b6d66715d8b677712e0c8aae8687ab846801800d389ad052cc86cb0262f2340511bd52001450f9a058a407bbd8efb369000aa68725300e3d01219016ebde365317c7b96704ba9637a20575a04edf5a1fe2825f9a8a2d3394c5df63db37905a8bee30d329cb164e36e88ecbb02f476b46743e79022fd205b4780b501edd9b82fd9e8327a16c5b5329e750c3c02910dd7a2942101278d1a3d4550c00674a3236873d35c9e82251005e2d43a82d18ee3b41db7d278d647582240ede6edd90b64ded58a41336f013c0f3a805c9c81b58e8e8099d4645c0512c04bb424834584e9906bd110d3275d24d3d5aab8fec6333ff8e0b733078d5679aee1a30aac9304023edd820e09c6cce573282502262c7261a7aa386e01dec0ba50c35b3ed1820e09f212e508def96072503b8bf2aa9ded12a39791b98aedba47b98c23725c73e35983271fcd48d092b924dcd2538246c1921eb7dce2f638fac5ed359ee922ea3747fea03d41267bad6f3556093a87a6b78fc34f5c4ae3997ed97e4782ced8946717d3c0955583ab6326e8ae19c6a3f1ae19f68f25ee3448a76c48c8a72b646aeaa64cf3e741cb28ea6218f3c578310ceba3047d59e69458bf29f1fd367f500c998ea0947b434462bde31490639e5b5e58f1d0d29005942468b64a04daeb5d44e20a18cf12afebeee83b522a88cb1bc234c6369c9d8f4520ff95df98fb5c0cf56867b64b5ec858d0acb480661d4783b8ecc5b3c6bbebc5d1f046f2cd8b6fc2807acbc5274140d5651d3b62fb467b995bf581e5bb2ce0ccc0497b263bd5c57d2fe5ba1a01495ce6bda69208a81db06a27a0764187340d2d2eb0c9e36d8486385ce79a6b9d103301a3bd454fcb24da25d15bca33eabf71d18b674fb05dd5085bcb9c45fa7527b4e6dfeb6dde4dcad7d1ed2f6e65f1ec09d23b15887699a38816d6e1d1fcfa91404b701c45e20a15cf9ea00f90b1c8b3ad95641b41f4516a5331f631ef51e5711489fb4e3c7b8254277c65b2caa19db8198dd1d22e5c3912098ea3485c39e259f30f5f7d9e786899426314b618f5e98f2cb69b44ccf9e34d228e7b1869d6b9a60507407859a2b502271dd403b426016f01c60b42ccae1234b79fd2e53290f0de65a218c050e84cc679de54c03949b4cb61dcfbe15983aeeb0938b6a1b55e5433cb133778940c0b68a5f13a0feaa2125ee61a076e1717d42375139840acf8ddcb51cab70031f272cfc401776bc01a0f6f575f3b40eace6f01b63a742f4dcad51be4fc36f693b87ac3b3a777f3090850f709634c31f4b943236fd88c80a164bc50c31681fadee71a6d7cc489026845550d62dbdbb418349bee5c3cb92e23ef7b8a80915e8a9d096e1fe28f5f2681dbc3773d9035bf7f16018a6c1c2ec8e13f027411cb9d54302260da07661f8b40ccb40e466419ad8ed426e808ea00c0ea00022dcdadb30cd8d2ac6bb764142b497facd54f042861f485a1f031a18dd7796c719d87670d6ab7c3f75c90bd6ceceee388697eece0711f87670d5ff7f8340d52c7ea2360481b2fd47084ce0d932e023ab9705f7d5207d9cb6dd08d18f4a9f11b54ce0d7ad54d3ef9aa6f99cdfe50770b44205ed5b9536ee24a0bcf1a1eefbb6f04217b193adc4911af8e936adc49e1d95fbdcc770b39974a64530ac4aba369292e95f0ac31d4a5125ef58592aaff350ef4fa5b97d47b28b3f7dbf0aaaff1b85372decb00e45a87c8759c70e25a07cf9eebdd8512a0e1d5fb99eb38b5c467a33cfbab0f9fac02edb564775d829965bc2e818d45e31cc3abc7cc0a76f7b591ec3002f1ea284d71df8167cd0cbe14e5ea0e1fccc95705bc3a5e58e0f3a03eb6b9f65c7d1ccaabbe91930b3c817875f454881b073c7bae9bcf8e81f6f24a756540bc3a4a535c19e0d95f8d8f98818657f7c6e1f1cc3fdf6b05be0caf5e7c34cd1ff63c8e1b0153ed78eade274cbdea6aa553fc7de8c96954d294038d40bc3a0ed671caddb32cf28fdb6cb9ed88782db705e2d5519ae2bcb867afebbdbd7a9ff73c84e6efedd5519ae214b767cff5e1061bd05e1b9d71b65a8b8d2a497ca7c9b3bf7af81420a8eb8e3ecc14af8ed214279e3debd56371630fb29786e3c831dd633c72ece67caac83a974671acae1942ea9ea01670fd9ab3bfc9768130198c221607813d7bbf8e0f2c790e509c041a8f7fbafd4a41c6268c73959ec38bb13e1dcf0c3ac0e3c591957142cfb35e3c8ee3fc30f934fdf6c9bffc55fbcafdf951f1771fbcf9eedd37ff6efa6fd38baffe7a7af48f94ffe2f9cbf72fbe9cfee3abd7effff4faed1f862fdcaf4b7c977b3abf38ffc537f109f967dfbe79f9ff7fa2fcffc34f94db27b422714ae11c09e337c5c02d526d10d90d94be9034ef392305e4a321fd725190c518e85c0fd8a16343e0d575f602bf63d1eeaac43379eab4b4ddfa37404e66f7933f11cb57b2fa2e4a40a60ef7ffd48bbed1bafb0c5f56c86f9fc5ea83274839876baefcf69dde6dde533d8fdfc64de3564f9ed90634e4c8acb1cd7b1eba8adf51fa3d8fcbdab99396cdc0aca3ed3e03a40b6ccee4c3c2e72bcb5b2e7e771f508858a534817cbcc3d766b21096fa2d568e4b200ba57dde73ef257e1f92dfd308655f4db9b85b25bf98fa5c9fe8ae96b35ccc775410ee701df26d03d0106b5f7cff79cbf9c29f7183a97d7871bf44ac348808fa62963b69b242642bd2da535d075923b87aa61775a7c45ee63dad35b6cb54881755968bf7d379f94c261ffb66eea3a90b1c413e1acddc75ea8ebb6f372c62d52d3d208aa79b06537c590e99095cd47d32c776964bac5d4451106bfa3e6c5bf9edbbcf2cc85921870259081843ebc59d98b89d2b59c8b4a8042ef23e6339381d5fc45e7399cc62e2bb579641a78eeb777bb14e2c82dc81e076d69e969b717c64bbf8c5f678f535ed3afb02f2a2154c56db0a44391df93bcbc5c2e0022b06be0e995ae34ec182dc2084fd79fcda6e8a1cc35be68c6583e5ea7bdd75c99139d6b7d16cb9f458ac81a0d4c6fdd61c26e4e4c431eff8859c85b00912179bf73a8262eeecab343b18719278def2e62650d8d8ba471b2814338a4c2bbd4f89c79d19f39e0bc6802c06e63d5786f63814821bc85921bf1d5577c6f01400bf59a4385f9c4b4abf29a0a1eb9d39b285dcdd545ed3babd0dc91b9033790e138b8f936f953c53e93da03c2908a255fbba4c16629b775ac4a0f5dab690fc60c4a440e1178f85643433d4e11ef32d474c9055c67033879e21f9619d658b81f7787c87e2ac630c5c7133791a05687bcb0edad23c01622cb2e6cdd2db7cb846acdcd6f38175395ed4eb4cde072a1e3eb15e5bdd20167b034cf2ded6fc55ac5a8d83988eae0c3e95962520288db921e077d09e3b1cf6219c9f0883e2f9a2fbf997b8353cd792208530e6f4584e1d5b88e420ea3783d18db1a6d28a05f3b0fded8bf00a711b0ac1fac8cce2ab55e78bf6be571f855f7369095208c3508f65ccb1decc8b2ffd661562ae4891b32f1d1f98afbd3493aa42dc87aec062798b4bbe73cd0ff2d922775ca4301999ee11ab3811e30f68cb810fc49464717c8e00364b7cb6f331e4c8a4d9aeecceeb65403c776d0d67e90d498fb834bb726cbbbc4152be9a252648ca2a55e50eaa0560404740435a512e23d399a37d95d51d697d9b1eb43528d7a1a04bbb283bd362945957776797f107743b024a437e682f0ba8166ba07d6fd029be01dd1ad4cbb55fda8b39958b75b1f1d2d5a880a2a8759f3fe87a8fb472fc021d2d5655db5d4e97b8873a4d4da0ed1e502e8441aa6dc43cbb827d9975136bcb4d47d03deeb42e91a385fa103fcdf39c9ae2f7eec5328280ec1ed16acfa68ddf51ae3d471390d21b71cf9e665f49e97dd8e79455bfb7475c6f9d6b7190d21b904f31f1fb7e8958b93d14d0ad41c9fb507a1b94d5e60fb70494c650d6cd5688daad078569cd68d4fab66d2b6242c5efc9ef6e34a7d5f6e4a38e86fdaca3cb5edbddd55547f73e1d719d75aefc41ea6856c83ad27dd788951b1936ba1495175b16a2a9c38d9c4505458e5b09a61df00615bf28cd0f2feee9b90072e53475b8977e772333a8976b3f2ef162d5d16cb3c52dd99596fb36d680aa8ee621862d5f05398bcaa9adbd981e03a0c71e2fa666c5c8ad6fd3a3b3da7e3fc4da4ac829d04b40b95f02d2f96cb99f439adf24c7d4d6d3b20b6e5d96b382df3ba361377580dc104db5eea5df8f28ea9e0a92dd329c3027e6a0e00cec6a37b35cbb4138c1152ecbc5c96d8b58a9a6831e8f8052e3b3efa63f52cdcfe9c4effd2ed6968b32d09d7d8badf26434353d92affd08502bd73ebc1875a4a9a7445326632f764fa53936f2d6b81f3c477290b190a3407618f3ee11b1aa8edcdab680aab5ad021e01a5c6678b64bd83ea1275d02572dc72c102d2dd4dd259540b83c8714f2dcdba9d98dce63dd7567eb7b4ea3a761021b7a8e869d14441556d33fd1a9793a7ef0668bf0494be6ea0eb1250559b83db165035adcd98474025e456272d5609936549c4aa9be940260a5e0859549a428b95cb2dd011f7a8d7ee9895ef7693639947412dadfa0c00c8d5efd6652939d48d2857e9e4f647d7650e92630e4fb86b4035f3d9456db172390fc209ce70d93bf8c23de2c5e284e5d225a09ae6f8bf1da0ba171174b907542326d525d2aacd69504bab361eafec1c91fc5ea3893bdb23ad725e00e1848dde1c27a8418c9a0327e21b9f7c03cacf0c64e8e020972cb4fdbb44ace20457b93da0e24473980b924ddb5673417a5afce26c1ed78cccf7280a519768814c735ce3b285acecb678b1e48bb21585a84d09504bab3e9202322b1cf35e9ca07845856a2d6a477a69d7d8a7560bdaef71b37d0ec8a0eb1a50720274df034a4e8058c75d0b7a72c2efa3a575748813b35defe484df2df95ad7821e4bc4ca45594051d42dc71c46a06d13ab941ad0355eac4fda806e978895f323885ddcf2f41cc96da52f0fce219d138e2f34282ddd204565553c5bc86f9c60553c0751bf71c251855300fc3e5a5a79f98b6df96d9563dd790bba1c01a5a111f4d8024a4f8b80ee010da5f715fab0359eba9cdfada87b5a0b40b73d62e5b581205ca5de2527f8113ee646da3ce4f7eeaa57dfd2eb90e4593b33478adb1e2f1627286eb7808a13edf02aa782dca0f45b3b724b3c0718bf2f5188ba270bc47dc712feecdb7e3f22c7ce1cebf86b245f5248fb8bd2d76627e81a2f96e60eba6d5e2cd326c8a6cd36efe9b3c69f61b9db1629974ed0650928bde240d72da0620ebd700fa898c33e7a0b283dd878482c91569d05006df162f946832ef1e296d309e87a95565d5a00c21c9687532f64ba5d1f62d58e310857990c8758b7d8faa955803fe3c43e97e1cccde638e153df292674c4750e9232d18e5104c9a6a53d5e022a4eb0c8de022a31717a35d2da522d006df1621dea035d1a94561fd0f52eadba1e074426ecb3651d596123d69e96661016fa6257ca97df87b4ca020ed2f92e03275c1077b3cbb6a6cb02e8b205549ca03636a838417bbc44ace2048b6e4bab64820d263ebbd139e11c44bcb8a5891a74d9a5b5a55f21e8fa08287d7a4038e1e3be59471f24bf8b550e08a06b14a2be5c086ae5daab857c9afc665fb23ef902526d4a64b6b693127bc4aa6a53282f01a5b90824797a6596cbf1d34dac6de969b1ccfb4b559b89790d68c89149cc5fd208e7774b7e1b4a6f8dccec9e3ab9df24e7d63f9be8377e31fd66b97c4eec6a27b7579b9de81250f19e77db1e50559b42d96255b529942dad2179363e5e3d6955f47b8b585baa3e2003328532e72166a8355eccb53b88acfa946b76519eb991566dba80c8fdddadf8e7e07e8f0f6604c90ee35b94d73948728281ea1250eaf720d5a644e664486d6c2f56b5698fb778b1aacdb2b5837ab59d8d88585b1a824066e438377016953fd4162fe68e14c860c5f5afc7727bab1d9534b8f8ad3ffa4866afe3fe880aede901ec465dd5a631662c56b22b2f80da31001944698c596daa628b55d5a62a36a8aacdbc7f91565974415b7332a86ac7698a8895ca29c854ce16d773e41300aaa99c977324bfa715034441f285ceec567631362feea962b80279b9cc41ce6afb7db90594365110ef5937a40fb1b8f9f8d4c729267ef3d5a01776c8a5994eaca730f9bddd034ad300688f42d4993ed0ad41a9cb818e255ecc6afb96d1ba83aadaa04b245f7a2188bfcc624fa809931bce557b9deb3b697e6fe1215c9f0700498b5deeec307e5faf73233d2d9ca01766b52984b788559cb0f7112f764eb843e02156e78473215b40b95007e104afba9e6370223eb25485084eac48872e51a132ccfa334eac032728812e3d1f38d13e4a1ce414b9f87d6b5072828e780f28751310e65015b3a854c5f66231a7a98a8d9c45b58113b16af70184394ebf65a339b67af5625d570f3ab68072ad10d0a341e758e8f34fcb02aaf57640f1621923020a2fa5da2a0d28cab597f8d231db8b694102ed47249fcb07d0650d28d515d0bdc54ad3a60f30e0bd6fe665a3b13d8697d55abc07ddf96c95c118747904948b6b10dedb9d4faed2446f11ab78df34d1464ededbd68ae46bbf03b45dc5aa5d70d02dd2aa8f1c808e55ac3ded4c3c0b75642e61d9683ee21745ddd3fce18356aa4da1cc6a5fe655b5839ce5f2fb3107390777bf2f474069bc01a936ed332b44edbc45acbc8a1aa43f86f659c9bb1d94135d959ed1720f28a75f1091b3eb96c214ca65c44ad313c825f6171f4fafe4f77511abbe45c47752df66b4cc6ee5f73d62e5780fe2c8c81f2073a4766ebc0cd7e297db891b940ba980c217b12ece075d97783117b101dd1a949ca09cde032a7e35e53448767736cd15543a39c8c874ed7b73f1fb11b18a5f54d0f662ea265c413187429ae3d79509d08be53207d28ebc04b21034d18dab665d270dc2091b8c2972cc9c4bc4da3379cae91a507102746bd0903c4e505b0788e4d0573bc4c8ebd8685af9e3777321cd4506688b4294490c748bd2d7676f033a22adb45a835a216aa9cbff15bfdcf17faecafd6e752ccb29c83ce47338d9b454d895576ce757db040d92fc6ac7bd820c107e516e47283c6cebea0a7f5654fa6eca2a45b7c52a7e357d35484fcb0e1e28f732e3f7b541d9ddeff3be1ca0bdaacd416809a89af63eef5c9591ec7c54d8750e9239da6f6d505aa340fa23d3688e39d4d92d5e4c9d29a07b83b28e14dd4740556d8a6e8b5542ce5fe410ab5627a0fb12d050d4a8236fb56c5a06d078b17c5641f728d79eeb471fa75147fa6ad6d10eefc269bbd79199730fa8c61cba6b83aa8ea047c4aa3ad26a1b54cd41ab6d50d5d1e1a580cacb0244a2b97ff772f90e00a7e8aa23d366bcb8d7e0ee8b986bc4ca177dc66659e746dab8eaf7b60494feeca0fd12505af941d706a59a1ed02362a54308e87e049475043d5aac5c5138cb7abd8a550617d02dbe2158e2cb6d7b8917f7b4af82b606651d418e102c3e6c7b56880aba70d25ff3c435683d02aa3ab2505e034afb3d481d59284fb98fdfedc5aa23adb441a975801e0d4a3ddaad5b2dadbaeb10748b1c6b24e751a78ebe72777651bfb7bb72d5b605c871139e5ca744fb46993ad231b3daf6940f27227a1ddda6770ba8ea189a6440395b05d45e4c5b07481de995438ec7122f561d99133750995c41b73da054ad39c22f77506d3682b62857afa33de506e534e74b3fea4897cb42d85a3ee646ced6767cfa16504ef820f235286e7edf96885575a4b835a8ea18d7aa44acdc7f7475d435a0faf209a8a555cb661efbea183bcc55aea8238fcf739af39b00d85a3ea713dfed3b661f97ab2ada597e04941f6702edf780725b0174bd0594532d48b12edd61d66f55a4a4a594b832658d1753bd77abfb2572acdd1490ae4d494bd6f3c38c42f466748ee52aad725d01697f7bc6d985aeb32ad2b832297bc58fb991935bb68c1b940a3fe87a8f58554597a5ac01a5790af4685055d1dd285bc44a7532ee36b881caea0b6ac9d740cb1d74895875e40ca48a1c7eb3c9ae2c9ad2aa9d80c72daae87956271cb59c89aa8bf1417b1c93ea35a42fdd23564e12203574415db61845698d58b99f033af680d221c1ed1a9780ca9a0bd20d1ce54c09e4427af362afa16341d780aa86ac7851d432dd82b4ab0de353d37edca3d29e67a56d17dfe720d9acf4a023a0744700112d7ad0f0a24abb2be59ce0fc7e6cf162da5540c735a0dc1a8c8b095650196e41c65d07835300293d37b17aa5b98c475a7b5a43411ac34670caa9df9778b12afd884a7b9e95e68f769f8364a5d9f69680d2c0085269cad0f0e26d8b58b75357f2e7c71e5055dacef02da0dcfb8a9b3f1ea06d48de29737f4905c4e118957e0c95a6055dc4ea950e0b5e402581f67f5bacaaf4e1fca867969d4bdc1d524e78204dc1c097824b073a225671d4bd295b83b2d274a03da0341c820c190c7cd914cc7817b1cad5056464a630a5d41c9c3dc42ae30248a56947290fec775188bda6405bbd5188aaf4b1a8b4e75969df2d5cef90aa34e8d2be739b8b73d06d895859e980f6069d7d0764e867d73b4be5dce8122f961517b447f265bb045d0e69d5de1eafe9e50aaa4a83b64b4069f2005d23f93d4504e468f032547a8d4a7b36f1f6c9c3f5360739c710bf2f5b4097b30d412aed2a96b3f1e3f7256255a599f0ae0155a599f07650594a40eb1a50ae64415b8b55d561e88b7295c71768bfc78b394b822e8f80d2a8c3bd1bb7685ce780ee376e71f9cbf671d221922f9327482bae03b7b6e0d6569f8bf299452242134b6eb1e9ed7323274f1d5659032a6e812e0d4a79a07b5d034a83677c18e80eaa4b45412acda6974de65298162bad88a023d2dad2bf24a047a495363ddf702481be8c7576bbf8bd895536bd8022ad2e6e0ed144e9bbb8390b13d52e4b290803297ac9e63d18e879b2867e17a7c17b1fe320b83b205e1b44200c745dded9f3e3f735621503e9772dad622037c248ab5c40038a58e55301da1fd2ea82e4fe94487e4bb335c8e4678b38dbdaefc38b9d35d4bf4b40255b7b7c6d15c91d2fbf353fc530591367f6e379b2865e7883944a01da9680725503ba5c02aac429845b4039e606746d5076023a624bbeaae37c4f40751c0684351c0f7358e46a183996a71ac8a7b019ef5290fc8e429455f9a050ae0ad1650b14e5ea0ca463ee112b35089001c871a16c7efa668b550cbccec7dde7b84bbc699d3748d98141cace6b3fbb1d15738f58c52d507b315dc4401a9f512fdb87352f32ecdc72e7d821adba1c1b449a9d27ca96e6b4d862d5b0c8c0d7d2aac6e0ceb84a6bcf055f40d706f5d2c750465dedc95b8704946dedcf9163997c407b54bb8c8501b5e4f72179f39d435fc979da707bb1387f0bcedf3ae769c3377746f45ecdb8b807945b56207dcc067816f436af120f727603bf5b5ad51814e447a4951606f76f690ce7aab27d5c3f77885527cc4004c93e784f9ea91094cb0927af709eea9c95f63b2ec128df5250ab50efe8b739b815a44abfb70a756edd630cf43ce350a383859d5b2c8c775c2d4334c80c65fb3cb9457fde23563505a8bd98ab6a90418af36576cee675d94815821f31a85a9ad76574aa3a1b037a2c11abb845d95e41bd57832e0d4a41a27f4751cbfb0c44b6dc2e90dca27fb758d5171fd2f1cc82d2bf89dabca6e918b487f656eec720dca29927b75c5e18836c9f31400fa9776ed1bf5b5a69117065e11ab1ca4715748958a59283eea11f946112f4a05acd9d5bd4f388d5b905ba2a44972d8abb4972902dfe9b91e35edca2b8b7b48a5ba1b8c7f36c568abb65ea205bec995498794d7b2608b7e8f239b551dc2f92eadc025908f55b6040b8c58933dbd02198c38b9d5bbc37d7804ab6d8369780d25209225b8343a74fb4e2162d3f7ba2df51aece2daafe2eadde138f389212a4972b7a6228fe8d398c4dba051de8e497dff4df464f5e44c079585f764dbb5a041c7a02a6de15016e49c09c695b8091d9074ace5934400a32300d3302f1a917df80cd193e024eff01937f1138d32c036a804c936266ff14c0c493f69ad38a1b78f6d108f82082c3ae399b44c0914b604ec811e09b084cd96366363f1a0c8a9bceeed870428b9b0cb12798163c6fd98e3d6996d2fa821d093dbb71c4889a33cb9e035a80146860da8004ec1f357a8a41041c7c74c54e4e0211e05f04ec2de41025d372d05e24938398d5835bc056ac454671d3f2822933e8f07a70d3f1ee12054b8c30ec7627c295dc4b729bd3e21390dd287d3ed38b9587ada9dec1c5633e6c748ce9949c37bab85a86688aa0439a1c508079382c02146c603a570bf0ca3d697f9dc613606770f33b0830a7f1080483b9a196645a8d6046d09e6613576031d82a0483d153f9916663b043fdc5604b9033cd5c40d83e0f89a145663631f7c45f8a6d6d8102abd939024e3cfb63cfc4bac5ae195a395bb260305a1d805301c142ab8a4cd11a1cad2a3ae7d46296dfb35c1b83ad5186a2db488e3fe6822802c160eb9421a3361eb05dff006cb9d7b8eaf5361ec49aa6f3683feb5eae1a1193d7b6989dc1170b713e42bd478656105031b82d736003831dad32627292ad3e2e7082e9bb10af45cfb5e82906735fd0de41b3e00ce10613b4649dbf428b3930b8ad70a45d4b9c087042f6c7d4b62260cf04d8196c51d3d21c86082006a3d5bc6dade3f561c06d0e0d01f6de6bbd431082668dc208c4e52e27dff86d38a04ff4586c14fe3430d80a686960ce90112318ccfbb6b86e31d4922c2529620683f93dd4d8c60c4fd6d16a0adeb798810e311df1914c19af231055e40b31c60c09b640aa96b4346a690e0c066270d05ef726c1164023e8a2046067b0654d6348d9c0d7664f4552738edf3a390d3433b1dac1a1a0031893b6d54d3518fbbf4c820e3139790253678f000f23606e5747205ad7851243ee6c65fe58c6b2083815eedec3d47f231055bcf5edf40083c137d759f522b521c2b2e7072006034b826d261883d162b0b36804011d18e222121996a3f46a7f09e7d8d3336f0b23cee1656413ab49b085d210af31d882a8ca68930183d1e2ba35514b72ed933b870b0547abe0d64867ccb402cb9d27fd497b291b8339155715ed4118a0786b54f3722826ac41fbeb4d8bb052fa0188c1c021a326c1165023180ce6ca5143199fe496fbc0e0b627855429632903ea55b49852efa059466b27050f3a80ce58033b83db322ac0ce601e1e6a8396b8585d29230fe6e210dfe516b35cd5a5e5ec5ba34391ec520698db2a1108065b660de56c126c71f503b065344c72b1c88a348731f831efee8b095a0cb677d20a3f30f8b031e85c58da1cfd0e21b2084ab6597f61303a82c160cbae1af0eca118e8d12a782cb9229781c1dc4bcc67689531b65022661d8a5120be888d8e31435db1381b8a648f3462e6fe500482c1566643911a83adc77e00e225b0dadcf2ec04bb04bb7fe804f731a69391fe58cdbbc675914132de1aebe02059701f0ac7e0a02388c1c062b08023f28d9eed23604d81d69a2202c674fed9d97bdc756b91117478dd57ba022c068b61366db467e4cedf00732f2a02182c6631d8b5476746fb989173dced8f391a8889c18df68cd8f702cc657e043058cc6ade750dff07a45e8ec53aa833d80a8ef4aedddb4484d52d2fc0ce60de349623688ab5803545a359462b38637ad00eba2946cc2d5d3523e0bb6bbe8691ce15110806bbff2607fb0830fc0253d6054c7241abd12cd6d40afd0148230596bc59c19d60ee294a6b77f3983fa6ef5b049c7206760687c7cc8a143bb6182290920c2cc3609ee3559bb69924e2c0601e3d62a3438a8dc19cc58bc102188ce620ea4be7161941abc51d2724ebe818d36c0aec0ce6038419687128567051ce41822dd630182d060bb4d7cb455c064d82c7959c4030d84a6e7c3d18bcf50338ebbaf39e0a52f1c21e04ea0ce603eeee147488d724d8a2ad78c98fc8ba2f68caa5c5daf97aad8a033469fa63b5246f7055448b435672e4122db91468afd7069f349df06db4dac70a0e2fd1a1f06d881857721168b9f7490e18126c25371489ff6bfcb133d8d9468280569f6c7e80484966d8e040e5c81401ab55df6aaf915e20c608abb7aa0e5f731c0e9a1cb65a53c7a023a85bd8ae2ab976de1133d162a6a59c8aa35547011cb68135be1e1dc88dee4391da18c137bd386cb5d68a340cc2c096fbc0614bb996fb3046005be16be305431c216bb40a1fde91aef7e9950c372ad0c0613eebca83562105641d34f9668187c3410730d86e8137c60cb65bcb15db6d7491767400dd2da54875c6240267ee7dfde1cefaa8b80db1f1f518395c685f0d140bbb966667bb355cabe69e9b5fd26c739f3f0eaf37c11e177811d0e8c0ea95aefe6b5cda7bee37ceb26b90e4d1692db77e2981b179a59068f1cdca0e87d1118c5cacf0463038cc29be5acd26963a06ed996f16213eb79afea111b0c6028eb9bb6531c0de79797e914cb444cbcaae653470d822ae81836003b1082d0ec7ca2e32eaea5b8026167face98217d919b3d7bd797423c5612b3eb28556cded7861315a8cb3b6c3e2a0c9236b3bd9a0554acb38f5418bc51ce94fb0cb962f6c110e74285263b1fbe187dc79e328fbc0626b3ce3235a35b7b66b190d2cb68c6be020c4402d14b4d7a809b1355e8db99673adee03371ffcf669ba7d3ab7acc04db4b8c955dfcb68d5c742ee04bbc0f250c34db40a64cda631826629f9e79f605764dc5ad2722fef1dc5734541a3d56ccea1b6d7cb83276284421a8efe3da336125bd00d356a739d05dd0f40dc040e856fdcb4b0ab4e60edd672dfbbce140723f88977f5de2a4b7dd0cac6a1008c0b9aa58c555bbc3da866566d0a14748819dcb4501b5fe77d0cec7ab7bd33228316df7d6455d183f6347dcdc1eb75ca310216a86296fd412064d3ea6da811cf1f7f1c6433566d27588cb37a6bb90fdd9f435d2be75e8b54c7268c8f8e969ca5e4ab42022d48b3e402120c9adc10209a8df6b779750758a22906bb4da343cc87b36e837b9d181bc5d7175ef29861044ca0c09a40052c468366ab093047061d0a1fcc041633b7a531336872380226647f1cd2dccfdc6b1b3c62d2b2c52c2ddb391bcc74de2a2bb9c69a01c99e222d86d0467bbcc6602bb5e290f3b714dfa0434cd20aace94220186c5136c6e4650d2c3b870f2e517c83e620232056a33d23df950cb0cc2962d894093a64d4183ceeb96d16634bbcdea535400cf6c78147bb1e05ac45948075225a666de7ddbced2c5996728bdde0201d6a0c6607ab325aa1d5bf21265b833fd44242c064d1e81033186c51566ce3fca7c1d1217777e179bd8ec945809cfbe452af2277c0f67a798e460c3b16fe58bba70214bba0d53e1665181cb49733f6dc021c0abf9b5681656711604a0a5ab219a74057a4c4fff4ffb1cb969930c192cba0031832e40fc5602b345c47c798c1602bb52aa3c59857d16230e740ba059af3f6c63d10dbd0a1488dc11667c50cdf7e6caf978f60bc6e7bda1f6bef4920186ca5560cb642c3e0a0bd9a3cb6bd51de6f11a0748b3916c981fc00cb18eaa0bd31ef529f0f03d9e1e27f58cc6064c30cb498e13dc343d02c8e051a06071dc060b0855a71c802cdab4187989cd781e99525c02dabd1e2ba35196604edaf73348c98b5a52a86bd27b48c1a02c1600bb5e1f598d05c465f6a7b0434af05dbc010b75cc61ff37a8b08b8ca57cc32e7b7e3db41aa92e1ab08aa2d5a8b7a4a73a3591b438b2aa2c5604b315c478bc19662ca8816832dc53018adde63294603b1bd56f5e684688ab5bd3682764f81b57b2ac0c48996862c10bcb40a1b5f67a477af7be765acbaa2ea75543d6204dbf8075639e388bb8fc776b685bf2da85c07189b826dd659c3ab5492f863af8df515b6054d065b5fe98f410730d8669d35a6c9ed1e58dba4be6c1bf3047fc11ab1b8056a3474c8885b57bc31a6e9fb340176b6b5bb198175602a0231eff2092ce988ab02dc60db27d83bff6050ed955b5fc504679954b5b1e82204ea5e0d6679a4e06815dcf248aca0c921cb23ad187400792201cb13c9b70543b0b80a1687ec76690ab424989b2061b5081a8ad464c83a6788b9c708c3e5af86f0b867c285b89d19e1a60eeaccb0ba5170b4bab2651ce147ab8c563998811633ac6e302368d6dbeae64cb337ad1d2aa3203a94b131c36a67c8880fa062763f17311804838e31a343b9eb7e00dd9a2ce6de7919975bf8e865efa247b8a904c9825b9c1833d0aa0d3b932aa235d472f2232e162bd536162767cc5ac20283191629c5607e7d260fb4a4d7e2a4bd5e27f5bdde98613769c8281c55021c5f0f0ef1002c66b8b9b19573eb1cb247d4c0ee79e6e43aff3157919cece0fad9a0eadfbb1587a368417b3c868bf8634dad62c8a4d121a66529b03824804341934302bc76826687f221591c0a9a4d217066d42738a0bbeb838eafe310b03824602d15742867f8e2f8dc464d9afbeaee9b20c5a1385304ea1cb2c788436895d19acec14cb4aa88b7e4286872c8e68e53ff4107d0aa05d839b4ce2baf90467bccc6a1151dc033a38143bea1db8a542731a4c569bbd121f7c62157fa0ce5dc29c63ef352be1e6e2c5019970465de9bc353fe52532b63054351d04acf52d80164b41acca96bcc40c798c121bb3423183244f72f66b8e4515304ed056a1cdad0013c33ea5aa818fc50831633dcebddd22ce7ed881132e483d5434cf7cdf8e35e6b33f71de0d05e9f7905d13e9c19c8feedd335769fd1bc95a00570886d243bbd00f53968e54c69c736b438446977f948d0ac379fb833666f5a4a7b8b59073064e07854a3439ab1d5ed8d5a9f080487dc1b5e0cde5da2db5eef1c727ea7d5a86fa5ee71b1569092a138ae08ea1c62595b025bcb06010c667072ab9c29d87a59d0ac37055b15d131a6b172b7f931e4ed429500fbd866d3436df8b3d55820d032ea9b7ac033a36dccc815cbf1c7f2e7da79a7110d34e7c7fdeafeb046b2e8712a13548b010e15c10c36a62ab8bead8a68490665581983f614dbe842292e66b4fb1ea5bd959e010cc9a01c970cd98a6819f52532904213b484d5fe842aa2c5368776ced77bcfbd3a5fd772efe212bb0e01f64d9fbd5d061724ebd34ef9225545cab05719988778ca2762773e17234602b98faf07db28c52318724e292eb6b5fb2225d337d981c1364a71b1cd291a9904ed456fd3164d794c9389155826568118f05c623ebe1e53028d7978dd3518dea8bb3e74137baeee772a76f0150bffe6ce361a323947abde80e861d8970517c0e0a003186ca3298f60b08da65c65b46760244087345dddaf90dd4a2306410d5af5a636e3255ab26e23e14cb3ac9cc0601bdd7a7c3dd8367ca449ccdd8ad0679a6aefc0b58f6af9983bdbe200b9bf94f583fc5023835615f116db822687045a8275ac3f5e0bb6f1feaad6a153636ed0feba1b0cbd5117f74520740d3a75f1d226418c04733714029997d0b2df45e08c59961fa0a548d031cd601bedbb78697300d7d18a79b8d7913ad8d976382b0fea6ca375e3105ae9e18ef4d0625b68ddf17677b20506dbf87415db68ddd8869664f0e5222ee80806db68e143c1c3800d2c03b600bb72d01aa91d9e3fd3ec9a2d55bcd5a8be9d11af3126f963679bdd8056a3becbed3ee756d3629bdb102c465d8a700a8135195b75d0e49000b527689651c0a01c34c75f016c438b6d02d81674c828f6507d7cab4603016c6bb41729d8062cb609e88f41c7985415e098bbef000758a60e31b02d684a872f7c59a8a2650e8a005bdb32f2720d110c92a58cfb2e402582b8c39c18b43864ca62de47471010ff46902330b0d786bd3fd6ccfd338a62b8e12f684dd93e2ec61d236871c8ad9b6a13b4173d76fc81d54923405edcc6996a80803db2a0250834f933cd5a5cf99c59b4a46b3a071ec59e1fb0f63b5dae2c1fb7b56681da0d23413a449b629728639780155dd02a225facfc57457470c6a7e282f6249b58d2f24b2c29f29a07ad96105043b458c998cfca8d162bb95bb180a1639af6a880b5852210acb40418cae92311f1c7ce4af770b6dceb8263311a2bf9625591f62696ee89cd4aeeee49b16bd7c5b2d9f79162f8693d40cbff46a0ffebe935a1745266c8640da1b41aa85a53fc35043a94303644817d24f18939328d0e458a6d11609d9411b0e2095aad6335803f417b39c37eecc374b5be1188beccbfaa62c665c26b907c392e970175a65dc2668f9448b2a629363abc2a1b11fbeebd18217e4ebf14d3ac16ced73bd39ced5717b49866818093683504af28fdc65d5025fc02ecd66835842f0c1169b498e6e03fe9450756b8f64c79eb2ecf086c9779bd5ce67dbb3cf9f8ffe4ecdc7be33c9633ffbf3e45efc4594938afc9b990438e4c09704e9c8d01c73eb0bd97200892116724ce66441233435bda83f3dd17bfa7de7eaae4cd399b5d0176b16bfafa74bfdd55d5d5dd5ffffc75e340b83e5aae7eeea0e9de1e7ef1ce18c7c629e332edf01809b0608aba865165316d5f8129d038d962d0d02a004d344b8e990e45c26d41910034a841bbe44c850af28de214a4ed0b7eb4964860acbb2f3f85a9ad205e02cc350b45822e13ed55d2d5d69cd6b5a73df75cb1d872e351c6c224c6995ed71083211308b4c4a34c22167c96e39708f500407d001fd12c26f0416328359c0b1f34068f8a25b8a8a03c3d2526230de5c01d815240df8a6641f2fee07542cb5c04840f4e482e5db781733037f1892baa74454acf0ffb256303ea551a1047a60fcdc0d460e11089eb88520018500f532ce57497681614605c313d15e6728c9933108746000391bf14c4ddccd4a20c16e4ff287d91ab3f86f6a87c41088722b084ba4ad7f185417a85ae632ebf4e9d0a815be0625ff77c41a98c21d14c1c08a10778b820ff130b5a8a89e182c85faa130821f2bb7fb8a89ed6404b4173cef18859f3c45e0f33571df903e940717a68c2141888fc2e3d447e91de9ef1fa3224ff64b1b94db4746cc5882230d46925a6864b95ee09080ca4fb528cbc27781a32c1e0d62b3e13d1cc937ba31533e75e2ce64c38d0d21a6dd9f36aa4f553026a37b27b8f19b7da07198be16a2d34e022b3a33eb04f2cda472f01da0db5798400ed0e9a3972a9a4986e372f4ff29904cd981a04303da712a0bf454b2db9315b4c1bb3793592c12fda870b01da1db41714f235f7f2ba96215f8b642cdc71e0f91ad808609d6303ac7f1104980544fb70210010414b9e9847c4741d39d8ce8b00d0feed28804d1466a9786089786e30309f47cc1c43301943a2a59edafd85595bce1df8622696f8d3607713cdca07963a09d19908d6038dca1c43be86180c2ce56cbbb1a96630088025d48530f0c7988925e673aa83806d8410aca3e832b090a101035aea1860204bf70f4a81283d4d8130b13589d6986c1ec3b45197a72f8510de32a5f2811062b34b0fb159a483166233c4086129e7cb8356a6462f02b4870b733908414b8ed28c61e67041600621dc5d8c3a0233ed16cd0a054278b8b8dd04a24ab9150313714fb4c6645f006622849b0b9d269a050542c8c81ed621238bf478ba8b10562284199dfca0fe48918e191d5023c4b20a42d09223372f92a52f33514008212d9798313921251b21a4e32828fd80c50460cce91e97128455505a04600a21fc554a417266e7adcd1c431c1760048b7638e239182ee8773f5eca282592b13850006f66db2ddf0dce02509b1d14c040ccceb7b164b1a435a2254f1661985e77080836bc514a85b496f12a67c2867c0c18a299a70cee30ad4413d0c0c2c66e2c093058454b729c8c602696a36b4ad08c19a30de1d9582e07dc892e31c6f778618587b8dd58dfa938d425136060213e97c4d239c474572020812520d79863d179c09018c21299ba941e43b0cad404846595a9096062102df594e518a62dc704b4b67314a0d4338660f5f627a021884c6dd8ae068d83abc15e6efc4d1b7925a62319867988db82b70acb1bd4438000d8405d190471261c68656a1de587528c0ebac1b4794381a88fef7686194856819c8090ac023901b466514f2508e24c3ad0ca149238bbf8eb1965efa009c742d313b2b791d47b72ec0b5beebfc4482f567edfc8e1622592f8bd042fb75a616a3ec7ffc535446067a042dd1130a2943cb60d134391a8ab2d815d85172425b08b59be6f09ecc1cc85e33acec58bba1faf07ed2e8a1a34bcfb1906d0c28c318900ef218dc01e05a5d7342fcdc1e3c1b98ef84a9b45104f22711bed2517a8662c8ef2c3f3e3950ae893c779a6e487eb8a7eb4ed8a1842128c4a31dad687990de49e5b3017cdd2634c56f33d018d49a47e0f153c69e8362cf63539066f9836ed10c0395bd4c300677eba0ceace450508d41676ac5dea055091fe892ef54494486f22b7bd727dd5345d48105c59e5457b0f12c0cc295af21392308d2401a4d9a0233e04d866857a79e13553c679d08c29831f4c834680de112d796af8c1f487ac001aeb1457be922748c2f469731e3e0549d18e2401900cda93cf787b969b4a6d295886338e48c612925c799bac19fb8ad872b3083c73502371c6a931b14cc34c2471c241598096ca7015bc62da0f99d75085248a83db82c91eabab685649a750617af78800d32c347b07577dec8fd09267808616e12e437b901c5f87df9c078e91cf6ca1e581179a53fc73b8e72e585654f8fcd9041175b571d4a75ba1ee56fc7218bc5023c9771059a61a0b93c332a2aeb6140a55d1cfe71323861f8a458d89c40dd327e008300f04edf072601acca125798086625199e8e030f39b5df0cc3557657a2f6089af8e58f9cdea0a657eb15b272c2c59b8dbbb2e04b0f541fb9c4d80dda4a0bdd608a75148c10c1583a640ddffa818d8574533b9f6db602666d8e64117da672f02c20c277af72dda44c4cca3e330050f5a85c7fe058fc5cf209e97f48a1b2cab2e5c6fc868e422e35ec30b19e3210602c7795a0275fdd82d050868498c6e48ea597e329c5b1e937b995ba25e04335d0660a29506cd0a71a5bf98b52d3a24434c9f6be49155cd2138bb97e4810e1a85eb793908709ecfefc5e8553da4fd4447573ec3f27e1142a03a805b474a526de1c24c28909b1813a2590877e82aa6051b5e57151498d80d2f2a033d0d2d05693311668e7a1cd7e944682928a0403130142802749968afd272c09600713cbd55072ba108f77548891533361e3b95c93a0e339724641ca010cd72b9935531ad352f91f81915504381c40f145043c1f9de91592bae3d6afde8a90f893f4af75358c4087c90fc4b9562a820f91b9fab41dffbd590eb4abc570871d2705e87180a5c71401b5ae2053e783a54a6f041c8afc9b548722193c72d723cf394684712391ed0440b73ccb3e083a30deb298676cf7dc8f1e0032da5073ec8f5a59ebc1a4e2b730f7f793d48dcb82ef8843f3bc44975b738acc407799e25096ab471b3a132505706197064263e78a7032fb4c494ff166fb9e6f8414a071fa841e3b82d5f0db4d452d783c14c4910e93c4a9f273e48e7c15c243e48e9d1a282cf6a40155eae068bb03c6c45da55fafb2f575cf72de21ee4f224b172571e8553f0541f1b02820779dc48c85f3d92d72c031e446f2f0b88dc8c2968656ad8237a7bf4f17a2c90432b53b20f876b4bdd7535124fc5263c88dec0233a8ed3abe980880fe91f2c6f9b61bbe3ddc38c854709d7e5f60ee46f14b5695e6f82ed9289226826c5c424a6d121c698e1ace6287f635e88353a04300306cd3c850e4ccbd804109a44fb8822c0da1734930b1d9e92353a044027688f391b30ad43fa62cce366a033cbb34557332eef1629b124bb2044fa3b22c09c29eaca8ccee9a26e20c2313b7bd092674081905c997cff30130afc5aa292b9d50953502024bb17118e31658b66ab758415a60d4d57f3812d17886b39e7cd4b58f63de2fa694c499c63edb9616067a420faba0802983d454b4ce1c3bce02ec0257d4ceedd355e87c514163493cb351d66e2c3cb146c7ae1b452f2d4ee274c5b4108081fe461e3831c0c3ea25950e083a1dd792e06766e214e1ce74f21ee6dae7a1d60e5b097795d3cdf4b4f0479dcf2a3d75c02c20719b8e48855433f7a9f9218b814f1b8ab856c02020d81d81f3b6676e08556a640e37ad7ca44988299a0211d47e5e7091a52329b445083b6184113ed485e0c18bd206ecf056fd1c24ad0f498052ccfa4986531128a1a6f44619c2744b308ccb28a99a031335343a86b48604ceecd3e5e83659514edab2401acda41b3209d1d81e9f3470a002f36fa1a732c284f4513434862a3f7b783241df5cc7d769863e97900faea72c0100bf1e7141235c4d3e1256fb38a65d0f4c4082cfb696072444e152db9c588c4e3b5240e7011b28d386b3d9516ede81000080036e2a3ebbc6865aa79f53c2901b958f28845c953c74ec4f42c801b0c23126a1c91cd994f45b34af2e8e069da5abafc2461e647b20c709783370c78d2964bf996a9ebf237ade335da5ec45227e720ee0259efe31c81855c981ab9f8c596c4012e62bbc1455218937b331226eb8768294897ac5de168ef5e5c0e3a591e346ba91d1198f6d05400c870b037b84b46ac2a3f4f7011f0c798397211ec1904a2595080cb13b8fe1aae06f5ecd5901d1b1e343c4aeb3a5ef1bcae58c6470f2fc1b27c8e6152531c36fc9214977efd98e022ebd35bd09223e634c54c70691a8b29d4dd80fb7d30e7d643614a84e00a1fb7194f7b5610a8fb8600f9897674d00a4666828b563032bd6fc22bb99a94d00e4a4101ee15b4e7791de05e1770af077d88c5f9862762e955aef6c984ec185ef19255b27478119b650a25280a91b6808bc200b850838b4d8c910bad79eaa3616a2865cf51f2796837c1e58a560694685649c73b61e6bc376e0088d63c052eee3c051d6da5c04c70512da29e79f61ca606162ffc3af96a60fff56a35e47cbfd2f61ea4c4d2d0e1a69f5e6d1c7bf01bc8635ad834d1bc450d196a06e08a66e2981650372a53e0a2b178e023f78cc96daabfc2953f9879831a4c815b2ff821a0d5996d8052903c6260d6266a4f1466e288d91f1c45b3f2babc03a671e4215d6e769a0e9640aea7daef8374d0aeb9d5470665b785774623a18d3998ec70c110ed5f2b01700c3a568500db9e410b131c611a47020c50a86dcebc1ecc899fa0995cd74ec1f48e1b014443d152907084696183001fbb681f3f0490e580c7b21c011484a0bd741eeb1db01e5bfcbd9e0d32ffcd72b78f57dc018d67b43321a2072f7e1696b6a33045fbd82001e1887f7f293570c4f45f99c21175b57fad041893501b0d79a15838f22c5f295d97a1c14c1cc773b8a2c61155279a93e311a60606aa4ecd5338a2eaf4f1484038e2caefcaf356c3c05ba0d9c8f9804205e94be9b55ef216e94b19af87e294800b528716df231e2184761187807044fba931351ed915a84ce1585522021a8bdcd4d397145e4166db41b4542870e44ed552717dd7302d7613607e14adc985232a9171e4e82e1b8aa2d9cc058631a04b1cb9c073e08e312fbbd78b011d1de28ac72be6223db7856eb486b89ff14ce26e2da80714c79d18f1a2993870444baac98523761f773edb05639ef95dc3ecff1932ee51a5cda84e25b9ae9d84690f32024c9aa29e7f16213689964606b8a8509589b60bd3da2e380a5c54273793a7e806ee1cb37e787d312077433cd22e74bbe435b7a7767c2e783d592cd78f8d059a87735389c7913e22e61b39c4d020655fb75425c04581aac9f5b153c9ca140366297daeae654bc25dcbcd3c0c1ea87b112d8aa4d09a1c811f66828b16c5b4209a2d0f70391760c42fc6912bda63729bccc07d6336de5c5f0e1a409743ce6771632ac4d5bed44dd610d78fad0a0629d4a5a2458dcc9c01d0a29878a00617c589412a9af50b7041a832c580595ac2d3a6fc90c7a778875ae0a262d598021715cb031f2d8afca0c6112d0a7045b34a012e07920b5327a5c0d3520d0f3af381711f4d4fbcd4b6f5f532b7adaf97ba51195262e9b4286b550e5354269014cdfc708551cc4472549d8296981aa6e05e9962c02c790692ec8df8fb429f8ad2f35670981c17122d9597d20fd3761a02421215caa071ec60cc33914465a223447be571ecd18ed6c2b3e995f6b2211e6b5781e4d5908b057b22e406f500c01b8a91269a45c42cc08689a190d2a4820bbc6cea8ec9731640756166a13d354f3160bad56ca94463f2ae449882178daac614bc284f8617e5894f165af009790a25aa2617bc9c61282d5ae09500a2293622ec0f5c2966bbe5f5b576bd21ceef3ae0bd1e52a6614b85ca405d195ca4c007eaf962549f444b8e31c9a23eb986044012f5c9488616a1e4092f9f66ffcf8300ff29be3ad1deb9e85211332fff86297851a30c2f6ad3983c25207666d487ac64254f4cc0fc98f082640052e05d0d6c608bf4c4ab4113e06ac876af025e48c6d2ce270aaa2d1b04347ef09ff2e789e2440d454b72bcf560d662f05e1233e145810273d1921c06ff79e39417c1052f3b3b060d6d2a92cfed290353f0a255d59882979d9dca14bcecec185e0e5bf0e9414b8b62f482684fce1bdbd341a403b79a6adb1dd247e56a3a2002414aac19366f763fbd3d420078454b119a1c607a722000bc4147d014604b8cfda73e7a09006fd0121346fc974c390df114b93f4f02630be7b58982971f2ffa382700bc414b9ec00bd3933c01e00d9a31052f8866db39193c706b99bb76351bd8078418a199e4afd52c2f5567d354f0e25ce5cae061c51533d09238e0adea1601c15bd52d05a236397a610a5ee63023c97613c302ea1e679b893bfd447babd966522cdc8c0a136b163f26bce8606cd3406b4cd64f98092fa08d05790384e7cdc7b617785139061e54b407e96a1ef0ce8784771ef0427a0d71bba2dad03e0f1060fe1175b7a27df18e15d40355da970acec9414ce0ad5a1801c18b71af962e6881a3942e0713da544b97f7164c0b9904042fbb564692870b951fa786b399daade2470b7b04042faa99fb9123d6917c614d5ecf860f417a8e0b39a2405cc862c054007103f1d062fc403d7e50ca40126a24a594a98c3239a0948d4ccfbd30911c826675025eacacb57441cba7526a19f0a29cb973d9cfe2993da8a1603f0b28a035b9e04549ab4ce4689809af0e61ab4579829d07d4052fca9993730a63e05a364b2dab0bf9b188f4265ec832087127c49b161cf9b68d8200360ad11231e606f6ba0cb9b432955bd085c9e0adda19010d5e36238d2e6d031e686d8afa16edcce8721e85ef185a994217edac2617ba9cdaf6078156462150770e5ad9c8b443098fac0b5db433274719197020f0e991d5a5bc602046f7523b0610e313ef59f08924ba5c05c5a7c061140f3ed4b328a28c5da9652ab7a00b1374ab7a4640e8620337bae0c5b815ed43403e67ca337d1e608ea5e746364ca18b7a5693a3f2c34c7451cba2a074988139965ed0bde42cbc4a5f24baecf1e0963158355c2de543b35aa60fcd8a5d2ec54a747990502c2bd72ca5ec8888ba63d0d4226d01974d2ec09592d6d1c14e1d59e6213b986a095978a944298be405471cd622791e2183291cd9d7328e3496912f9aa5cf8523ca598d39965e7044290347d19e1c67af81d71213c72b39db403c2aaeb4230bf17714cf0f8a645e3afd8bde627b2801e1886a56128778808ae6713faa66414b9e02572a5a61ea23044b838b6a064334636abb1b66e288f6051050e388f63532ad22c1d45c83166670d9ba6271811a47f4906106f154712db71c8811ba0e1c21bd867ab79d58d6a231ade136205ae2058ee8626e1eea166d16cd1c63a1aa3a18017decd2c54a4ce1c8f02b05c9c189a6e4444ef340076a2050b74666e2889a053ad08a8e70e46aa9927c211c51bbdcdf281c036e21b604af56f2e981b8892bf9f4405ced95dc9b2025968ee66307ced18d92c54729da8160970a1c452b9349b3ee5611d078acca16014d98c42e79068e285b1e00348f4f186a20d0a7c011eaf1c85d53f40b7a55454738a25795980beca2bcd8de71c41b848bd579e07cfc40f89b3b35a6d3a1ef4bf0379e1cd374a2e36fa6129e7a1f5bc2df9cde0b3a8e1d05166cda73cb558d793932fb77ad981c142166d7aa14604313665f7c14600980594bc72108a6c7a302dcdba3f68d4028c0e10a981d4705d8fb87d905180590f261761c0980a368e2381b7801003276177f7367cb743674eb2e7fe363349dd9c78cbfd594d9d0c7232c4ef78876553002a38749e228b52962268eec524d839938b2d9cc66a9688c5ce5a94e6450b96f509b784e543463068eec5a9506068e684ae3274c9e5ce215b4e689124accaea711e00e9ba0ae127b3b5c3d3974f77a1e2fe4c2a0e97ce81606fec6b16d3a1fba83087f0bc77cde5d2cc0997bca8435e33e5b54d644112d89d1081dbf6ac5108a454b125328162d494ca1c8f75a92c768e4ac8b5144111a5b9728a208b15d08f510e5aea991d9dd11292850442132def8f2d15da2bdbb501ee43093282e02c545417111282e0a8af2e8c3d3a6cff3fa9bfc17168460e11a1bd43d17ca909889a276a8829943042528b22cdf34321ebd255adb1131e7ddc58602622c16bd474ca188de631409f0cd7025ad01d30e95aae403f6245ff0a030efadf77d5bbc36783685b7c89df802df2958d97f178122a457fb02ff6b6275a9437f53287b53e32c0d0fad47342727141c0623d43848db8998f949c3642b4f348b8e11cae258ea231145cc92678c50542177a2b41d15544628da0ec3095a9363b683d9edb20402463cfd1c931ec0b16848182f07eea283b88ef2ef83d51deaf81b2595b7d7bd36b1ed1499cdbac24e042eff13ed5eb111e05b43a52985c4ba03b3d40f63a952e41895b213ccbeac2a061bc5fcd865bf087075303b10b59e98e4c5ac0569164490f6a70a2c0c71a8bb01b5282a5fba0145916e809616b1e74241a51b38a7c3b724da4706838afb4287ae18f2f6a0ba61697f2a58fa62964317ca60a91b38b49379cd98cb71bde96629052438705e7f14b7824955d084fc15a10901ae68c95333381574f3d08422669900a4ec4453f23344d9011d684d8ee91e66373a13e06eb0a04607a90e1fa2828e5cf970e7e8ae7cfcad35e1ca0e28629128cff28b1579f51d5c585ab078a8a2b757ba927e315857985060752b0d7f63ffe211f79210932b3c6f18283056ad0c62294311330731b56520404bc9b8a9924dc1992d2970867a644b198a9889334a11f9413db2f1e7e3db875a12608366906f873bfe1ac73358ddf18cbf35075e7b7b1a166e043cbc5e124a38e3345007953d2a3e46a81b27054725146ca4e00433b1a16ab418ea76b0c914310b36527094bc8cc15074a864c1060587310df570c32d0f6c447be5d95d89fb415db69cf2b83234d7d915673161e50ca61bb660e594cf65ba140035f868287c23507f23d25094ba0001834aa39478bd22c00fa25969b62828dc4e8b0a70129798b5206ce862e6f8c69f2eeae97b5be43f8176cefee2580c2f6cd30fe955078b097e361dfafe0f7ff3b5f09e7907954c688968c94d67ae61ba7e049895a01e113c214e2ba0dda81401409896114180af45b47f2d04002268b60573ac98068296317283f69848ebb151df470e2f6103c4cc6e71b0b8866a36f3a139fe662ee5dd71b7991d9a48e97b1a881040a033b87eda6289621308d481fe5fa99f6408fdd0470401149fa0d99200025da17fad04583a45dd6178b421bf429d1c293c9e1f3410f3818bbb216ee21ccb1eac6e37e56fa44ad25bee5280eec38badaf4904f836448d045bf82021da9b82484f05a1fdd35220f2cc4902a69040ee2f4d092490f78d041b440862502381484f2150c744a2c6a960482416031a36c4e02cb0cdc1eab639fee6787f5037903d0e7a15fa5911944b5135a6a060dfdc500003820bd4fd8f5c4e5b443b6888e2c4122d4c267598169d68865a5dcedb68bb94722e8644fc62401f86b8132eb0a4c1ea9634fed662494a2f08042853d4689337ad86ba86b4832f10ea062224c38096e4ecca92a7af0e5600819198dde84a80931a410d2f1269ecfc38471e58162b1b7889898bfda16ee2e26fce900735106c0344da590e138cfa3410ea0622b53200447bbf9094c18c475565aad5c8a6a58ed16a6452f73526fbc8d32652ea17ad4626759e888a718ad439f2e8b1587da38b0d1be4cad9320d5224d4acce71717ffe5c29c5b70175672178d26aa85b8dcf0135841626071ec8db4f992a30166ef7523135989135dd6a0ceb9167693507469840a06e35f7a286d1cf8979c14bac6cf595cc4710638383135378f169220297928966676123e773839642a4bec0cc56e38c043ed012335a8de4574ae79c21c9ed35a380a60524bfd2409c93f9d14edeac137c06ec4eb998eb6141ab794bb80fbdeb683524593c73a294b904731b136843dd83087ab41a5a93eb1b44f22c4cbc5dc973de4dbe0aa8af91edfc2921d345257d505031252a20d37932c4684def403dfc301fb3240cd9ea55b41ad29bb89271065258dcfb036f569362f117b3ef162aa01a22d5d5e49a5a91290b73ceab7a7cdb3579b41a53b35b8d8999cf50346bc93b5a4a6e5b2c31d46a4ccdbdd5ec92e9b5cfc18b0aaff106abfb07c042609a4fd39842424689682f97000f2a8ad6226835ccee22a700cb284c2fa3bc2dcc5b7aa27dae20c08dc841c70612a0d5a295c9b92598360c1340ae10759e5c643ae8e8491f913c914bab67f66984856a3f9f9556e327a3b9b64cd730d56a72351404f073c389a614125020c4d598820261ae30e76372bbc8515040816db70b1004b8eb2268c767c66b466aa15f35520c4181378c93734c9a6974f017cb63b6c1ca8acf030a482f02998c22a006966c709d86ba2d38ba607587ba86c8892009ad4c1c79615ade66cee3953ca88dec04342a10d84a5b020a04b6ca64bb0066df3356004b304cdbd3d9a1a39a1c8cee8de46e1d9d65e8ae98c4c214335f0cb688cd75d929c476741effa5f7f164c9bc3824c84f332f7a2a8c7147a1fdeb27b0001b2e3bed9316010186ac67c090f1f00586bac5f89e44437c3d1dc939b43ad2ac12a72fc44cc0b0c1d28750f7213e2851a502187efb9a9e73ec70dd8e5809d845008641b6977b21db150ff8ba79586595d0564558d4230f11c0d2124ac95e4c08a06941bd0612d067819068b8f00565cc423d3fe14c12cdf0756a240fb8901cdd8332abaa923ef4a3986335730380983896881a4364bd411ef07dfde5f9d860255c617115e9705d720a0856b6e4120f3458960fc89e4f5bd44873ad29e30ba9d5e38b004ea2c897ee000263cc1c0b089b8c630ca125392e641464173205b8899c89300143d80430a851e45a53ba1a5af3c47609330b4266d4429880e1ea2056028651542c5bffe64b4ea48878ea5d0ec816108f068e4447ca590286a04b95a1ae325e22634c2bac620218f2aafb98c0983c5bcceef4d8905c83f0126178434bf24011c369299d337b14e893260ab0ef44ccfc6270e18f2af9d27c56548c1af8b3bbaf7174e0e4c090136ad850211e765781e2d560dd728e33bf125a5481c50cc41da87dbcea18b4a2a5a00b53333632b52720a45ee082d6e45aa7b0537a20eb9623e5e9f7b1c833e042faf5f78cdd93d1cdeda5a52decb32b85edb3bcb6ab653681c1ca275602c31966b112986b199721ceff5a464f918e82de1d83d5ddb5f93ba62f646177068f05d032a8c72682311f1ed42dc3e61875f10165f2c46b2ba87145308eb6f9dd47c51030d7836f421713db2c4cdb24797217b96370c918ffc4495c5631ad437a8b57810b2459faec56659ed2cda42ac39d8bf33cad85963279288588655a47748ebaf9b20c62b0a115d410e85ea360daee0453b870bf51a9a3766b617ab796a775719f3704bc343b15a73b5ec3e2b1abc5344581c5941345104fac8b292709447a7bc99cdb13a7befc0296a4033ccb2d4e1140930f3a424a0008a05e3ea91a6a36d4a399000ba36887950010400d010166a8a0bda0d9c0d559b38400431f4eef05829946814826e366db055782260b859b0c0dc14c3baf903ef9e1f58d4429da2703022c60a286006b6bd464e64f9c85146321d4eb0501de71835a9422c0d30a41b38eda2685e90f76311704f3c1d561471fb1aa40300f08203d2fddec49ea7e1a8abf05c13cb7eb7879976bfef2d22258dc7510b48f642ed6431111edb30b01e6ffa059ae4cb4d4d29a0d010d5344e51a5310703f5165b2dac3b478b4580882454280282809203f84454000e955d13d9da4ee2796f81b0304599558ccf43cadebb1828113910cdaa7430248dda21e425856754a80cba0b25c5973a9a5b5710263f27942c0b59b91dc5758292612223f7a7f8c176f117512027c86c5490842ca85b82a17da1b83b82b752e16968d110bb95e4366b63be1418e5a2eeacf838bfd19f450038823355309d463032b48c42c5f024c7085963c0318c4dc526f9e33a5f4b937e489c13a2feae65c0e007399c0700a73e0fd4fabff8b9067212ee0328081f46ebbc4e59f58b657e0e88d454eb42495b105668e70dca6a3583fd2a918b870f1a3c54cea2764b0981843027c88d05221f9aec0cc8f4eefe2aa9abe1a5831505af9d1b2e56229649683a142348b95da233d0457889bb7e4a4142cef949107d3f0d2f74fc092a91caf476fbc1050d3b0a996fca41ac1ac857099939856f0a81f164b682e20d868e961a8a7406453544ca86764eee50419a8c7218656861cd443ee6a40e2bd4a643048ea8ab944e64ae606913e40ae0299ab824c6cc2435c37ecae940875571288127c7e975472e9e1478b070434d322a3ba69eccd333ca0254f6dc153799bb50920c688ba97914da34a69978689ee10349ba84d169816af79d01677fcc12563c91427e1ba0eb8203daf6b0e2d91da5a1f6fe246563683e0a52ea9814d7857187fe48858904138a5d3a1a52abaaf0966cd33241184558ffed1da0ab5e04b00c13768569cbb6ec5ac55d7ee0c31adef2e5603bdb24a6470ac9577b88d878b552083d5b617b0d26634a4b0d4c1abb4df70c10ebbdda26e0526e228337d1261a2d7887a1cb255cf870335b0ecc847723f064c32d926f93191c1c23bc64cb49159194850c3459b59c4a0bdf32fa620c3e3b4639b5936d008a689cc05b6dc2148c642242075c909dbc6c53491e1d22c767d454b01f2338469131f014c0250cb1804ae681e2e035df6a262639ea9d8c0c46a2bdae76a02a01d342bae3103d3c81040dc1075f29990990d1d3fa4023e506e6ce9b961efd5b2e8791f16f8cd7c1005969099f9be4058f2b5c2addebbc0041026444ba93c0020a60739315461ecc0ee009c4030f742dd51e88023d3860b9842861ffa8825c08b31a2860be3305d40a3dd057321334f64706c8d35d0c8ccb50d00e993237fb30eb09fdff1c3d75709bdec5ee0d82b563aa1e1348fa029ea96e1d81b8967595ff6f4115ea186101f5e1a818793bb17b352c4f4fd3d1420531e3ffa032580e352d0acb9b64d60faabbd580c08478b0406bb68b8d2189870d98518f970d985b8710bb97a404a4279397332db062d02aa1b7eba46150b2f1bba78ddbad3080017b4c49441977adaa44c404398d8a5f4000149b8545cbb2830bdae5e5c08848b0441270de17b7b8a388cbd8bf4a2bfd0d5f322c6f8820bde60d9eeceed1f289ea21ef878d5d2bf173c3fd03b08bb2d03099f598f048c68340bea71499f45cc3480c0bca4eb78e7d5eda5250c61a899976aefe5e0fc3056b280a7c4cfe3a474089bffbd7e97f274219dab7cc9d32cb0aca8c2a27a976918e26f9ac0a59899d74c9336c2ad3b1c232d031c6a48311e45ead2e1540dfc4433cf000039d6a380a6110bea3c9702609900f052a9b69f2c2dc583ac109b21788995ea616ced6562658d94334b2c30d9cc1075ed24652a435f6c440c992bf8d1e60a05c698f98550437085ba47ae0690b9ca765c710d211342968bb9921100352a5824c10fa9cfa3000713c6b8686f208128241d8a606a31e5349e7b8f40e459aa8d9912cca02ee85ad5bece6a5f73c11f9fb0cda4dcef8ab95cd4031767492ac31ebacb45140be63c171198743ba25929573e13627a2c7057fa5485a7a22c266d61d7bd1684460ed346695edf541f7a64af06766620aef64a07bbe0cdbc6f0813d587619c920642145d2fdabb0091887e112d4c1c6d61660d3984455ba03d26ef640efaff98943742f944a6b9e1c265d72c2da27d741140290e9a89750b8998bd860430b089967239521accde058ac140e1c85dc747011a07b37f2ff409ad16ed9f1b015a2d5a0a92e517a6bf5592d2a1ee175e080587bcb40f96f694b813de9286024ca16c71bbd5338c05ca2f8d063071d513750d712ca48150b7653e90789e959973631b76087b4a706f3f02a1a89bc64e334a27d468475ec42c95615778fcc1db426246f2dc01450241c11f5ce58576422085257b2d376f586e5780af853b08fa47416e1423ea1126efba28c41339d27d48b27d285dc89acc1b916e2f7bae5418eaf6721a7faaa42946901bc504cd1ce510053381bd1ce8bdcb6cefa54cb83cc0e856b07949a743dde9ac4d5419ea01cc9743d3a0662e55c4328b5872a311e2871722e230918afa13e3d436e5425d2e005004d4455ca988ab2c224c06107f47f863d156a831d14166ad8c339bc9c907908276f4ae55c4f5e0de08351be2be902e1d0f92d8c40413150d6ad180158c8f40d445ac54c42a8b58c96718e20aafa27bca3e0b09f0e917f547852f13ad8596026275a28ebd1dcbe9308d3730c6962ea772d05d4e871a87898547d3faf8e4f60cd610d18e31018429d1c29444490e963f9733953a735b977af740a4b76139d33c00e9438204f89d8a56264b284c2fb8cc3dc00e3500cbb94a9d0f7d682f39af2857dfacee9cfb4bf9c51f27ef70e16355ee7781c723d3a2ee41e618462cd45b0ecb850a5d6453b195eb5bcc8a2db42ef29c95ab162a03e9ec144d80c12fda471d13373b17a2fd33595ea8d08b6ce9850e702c8b2bc652cfd0422c7cf00614a3e122e553fe66fabcc8b3afdc7ac6c701f5742a3f2f36fbb15378945caa1a294cf31011805fe659099e61624a2c0230f7ff81a1a8db898d960d5ed13e5809d03708c26efc9215851b1a0d2692a83c846c1f2512aea1fcd733c30154b112476caf6259215a2eb597219209f1e383d53f54fe068de560bd954ab0fa412d6d533e5b5741333f6de6c0f472c74b43ac54b90fcbeda9c88f417b5a49ad8a981b41dc574b43d952eef11013e55de0b5845d67062662618d0526c4767309d038e867f9f50cdd100ebd4796336ff98a490df9d11f31017a17ea2e47508dd2fda804c96531d28f23dcabd5ea59e3dfbffee9e5b3f84bffb61fd71f1ef7dbe3d9e3d3f1ee45fb63db6d5eb5d5d04ebbd37efbaa4d7e5cdf6f1e3eb4fdee7e7b9c0cedf661b37dd5feb5cda6edc76ffee1599b4fa1ede7bbddb11d6f0fbbc753fb75b7dfb7b7dbb6fdb8bd7d3a6d37edd7dde9aeadefdbf574fa71399db6dd87f5fbedb3b69892f459bb9842238bb1366d7358ff7a6c0795bdffd41e1f8ebbd3eee17ebb69ebfb4dbb7dd83f3c1db69ba65a9d3d6b97cae2595b4edb77dffcdcbe6fafdb8bf6e3f77fcb0f2fdbefda6cfaac5d4ddbdffdf063fbb6bd6eb3f6f30fedfb67ed7ada5afbeedbefbf69c4bd9e4e07255af63f2ae7595b11fbf73f7cf7c38f8a850040b2429fcda6d3f6fd37ffe3e7f6edb3d96cda7efafabf7df3ec5f9ffde965e0fdc58b774ff7b7b4e3c5cbf6c7e0b5767ede7e7e78ff7ebf6da7bb6dbbdbee1fbfdcec8e8ffbf5a71ee38b1793bf823f797976bbdfddfe5b66b32df934c7fb97e3e3fa763b79797652be2fc6e2f9b73d7b3c6c7fd9de9ffe76fb6efdb43fbd78f9552fe54f2fbff2b8383f6f3f3dbdfdb03ba94ef4798ff5c58bc9bb87c387c9cbb3a322fcb9bafcb23e3422b6d7ed8b17a7bbddb10eba2fced6ff73fdf18521e0dfe9d323c3ed0f3ffcf4f364e8a5f1efe9b07fd526e79f3337ebd3fa950a383b6e0fbbf57ef7bfb62f5e7e96eef8747bbb3d1e5f35834ea28245cb3e887fbb77ed85723efbe6707838b4d7af5fb7c9a47d8671fca3474eebc3fbed69f2f26c7d3a1d5e4c8e87dbc930a188573bc6f7f9fbddbbafdeae8fdbe5c53069bf8b7c7fdc1e9ff6a7c4bcffcbfcfe654bd193976777a70ffb172abe82d1da9fda767fdcfeb6ead1f9ff6e9526edffa1bce7378f6f9ef7da060abf6bcf6fce1fdf3cff6d3d6ab54a8061d49bf597875b1d6d5f3f3e6eef376dfcfc8fedf4d01e9e0eedef7ffe87ef7a6eef1e0eed05c36ad75eb7e9576dd76e1cff6cbfbd7f7fbafbaaed7ef7bbcfba0b607ba674958a79f1fc66bf7b73b36eb7fbf5f1f87ab27f586ffe658c35697787edbbd793bf9ab4dde6f5042cc65f8effb4fbe7b3dd063c26c2a8f2355bf2d3cdf9facdcdf97ef7e679a2fea7dad26fee8f4f87f8d67b066d7dd8367dd9ebb7fbed596ff0172f26679f55ede5d9dbddfde6c573457d3e785c7fd66221b4c9cf2e86679bec369f0f04e29d3e9eda6b57e39f769bf6659bfdf3d9edc3669b73416336386e632a386d3f9eda7f6e87a7fbb63bb5dd870fdbcd6e7ddaee3fb9d63112c962f2f2ec97f5fec5e9e36703fe8b17cf99179e7b0229387906e22f06d1cd79ac296f22743c7dda6fe36fd5647dd8aeffd87edd6d4e77af66d3e95f7fd507e2eec3fbf6c7f6f6e1b0d91e5eb5d9e3c7767cd8ef36eded7e7dfb6f8e7502eed6336891c3e37ab3d9ddbf7fd52e1f3f66cc4dfb63fb657b38ed6ed7fb2fd7fbddfbfb57edf4f0f8550cfc9b73d7ece6fc6ebbde50c79bb70f9b4ffcd1da4d943466fd7a329b4ee2077e3abcb939b1466e4e77af27d7d3bff64faddd8054fbb03ddd3d6c5ec7d4a851b9dbfc1d7370bb5f7fd8be7668ada5e5f5e4bce44001db8f026a8cad9e512ef1d7e1e1d7e3ebc9623a61553d5283c99b67e3ea3e2eeeed0ffbedfab86ddbfbd3f6d03ef161fecdd73f7dfbfb76b73d6c87f67088c1cb47cb787d160b7b7bb8dfb687775a418e2163b47787870f621c76efef4e5f7ed8de3f9d3d8bf5ff59ace1ad7dfddd7ffffa1f7f6a4c072aeaf1f0f0feb0fe304a11ed398beaf3763cad4fdb0fdbfbd3b3a5d6fef6ebddeef62e648fe3ddc3afa4e5bf43481b67cfae144de328fedd9c77647a5ff0ef6677fff874d26af47a124b5ca0a505b8fdb2de3f6d5f4ffefe9beffe3079f367e3f67463ec1fffebf7edf73ffced37ff69d2ceb3ac9b737ab7876fce4f1a35fc2be361fef978b89bbff929a0bc39bf9bf7b4addd3cedd5a57d462923e0e6fc69df23aa909bf3d3c18c71f4dd3eec8f8febfbd793794db9d9fda25c69fa2853340df4d79351427975ff70bffdaaa469ede66ef6e6efb7fbc79bf3bb592f867f378f6fbebd3f3da8f7dfedb6fb4d5bbf7df8654b1fb5dbf5fd38b8d6e3c81a3b7da8ddfa994879b7bd57d2bb5146b96134bf31d237e70ab7b74fa7d3c3fd190b58edfac7373fdf6d7b213168eed6bf6cdb5a220303f9b7e2ea6f6b72d89e9e0ec8a3aac9eed4deedee77c7bbedf1df2beba17d58ff9b9a7a70a14f8f9bf529d601c90b6a0d403c1d83fbee61bf7ff87577ffde02ccf1d56fdbb171d7c6bf9bcde9cdefbffdf1f7df7dd33e0eedd3d00e37e79b71f6ecff6e369b372300015a24e888f555a56d2482afdbedee70bbd7977c586f764fc7f6635bc76270fbf070d8eceed7a7edb17d1c3e9db5f6770f87be9ca8ae378f87ed9bd974949a9194a7c3946925ca6c975336d52fa74c197cd737e724b839dff85b887f6a9724eff5d0de0eedf6cf36eba771a14245388ca3677704d48d2408646c5af619ae67ad311ed687f74f4c29b1204f7edc6e26439bfc97c3767b3f19a4784cfe66ffb49db4c3f6f8b8bd3ded7e61e16beda707468cba78ddde13fdcb11b44f0f4fedd787a7fd2686f7ff172681406b7f091be9301f6743fb341bdac7f9d03ecdff2c42b4343a9e547fb6db51addadbede9d7edf65ea89d7e7d688f0fbbfbd3b1d1faddbbdd76f317ba9cccdbc514ab58e382bcb9fafdffd6c97ff8ee879f3574ff03d527eeff51fde396ee6bc7dd3d1ad5e3eee376df87ab6bdd72e09efd87472ca5315e5b8c5605f1c0e37f2c61b4ec2ff61111fe03ad8a68317bf95b3cae7fd91e194de372d6be6cfff8f0d46edebef9f0743cdd9cbf7da3f572bddf47a471fa3cf64533fabb66edf553f3d56fbfb89bf33ab3dc9c6f76bfe4fca975615c075842bb3c742bf9e037ab0192981691513769c7c3edebc9a4adf7a7d79350871012b4d2d3e8cfd7c81685218947fa513da2c29f55a9d650abdc38dbe76277732e218c1f6ecedf3e6c3ebd7976737e77fab07ff3ec7f070000ffff4583bfd760940100"
- tmp.Length = 103520
- RESOURCES = append(RESOURCES, tmp)
-
-}
-
-//
-// Return the contents of a resource.
-//
-func getResource(path string) ([]byte, error) {
- for _, entry := range RESOURCES {
- //
- // We found the file contents.
- //
- if entry.Filename == path {
- var raw bytes.Buffer
- var err error
-
- // Decode the data.
- in, err := hex.DecodeString(entry.Contents)
- if err != nil {
- return nil, err
- }
-
- // Gunzip the data to the client
- gr, err := gzip.NewReader(bytes.NewBuffer(in))
- if err != nil {
- return nil, err
- }
- defer gr.Close()
- data, err := ioutil.ReadAll(gr)
- if err != nil {
- return nil, err
- }
- _, err = raw.Write(data)
- if err != nil {
- return nil, err
- }
-
- // Return it.
- return raw.Bytes(), nil
- }
- }
- return nil, errors.New("Failed to find resource")
-}
-
-//
-// Return the available resources.
-//
-func getResources() []EmbeddedResource {
- return RESOURCES
-}
diff --git a/main.go b/main.go
index 2c71510..a308857 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,6 @@ package main
import (
"flag"
"fmt"
- "io/ioutil"
"os"
"github.com/skx/gobasic/eval"
@@ -47,7 +46,7 @@ func main() {
//
// Load the file.
//
- data, err := ioutil.ReadFile(flag.Args()[0])
+ data, err := os.ReadFile(flag.Args()[0])
if err != nil {
fmt.Printf("Error reading %s - %s\n", flag.Args()[0], err.Error())
os.Exit(3)
diff --git a/object/object.go b/object/object.go
index 7a16def..ab2a3dc 100644
--- a/object/object.go
+++ b/object/object.go
@@ -6,7 +6,6 @@
//
// Note that numbers are stored as `float64`, to allow holding both
// integers and floating-point numbers.
-//
package object
import (
@@ -50,20 +49,15 @@ type ArrayObject struct {
Y int
}
-// Type returns the type of this object.
-func (a *ArrayObject) Type() Type {
- return ARRAY
-}
-
// Array creates a new array of the given dimensions
func Array(x int, y int) *ArrayObject {
// Our semantics ensure that we allow "0-N".
if x != 0 {
- x += 1
+ x++
}
if y != 0 {
- y += 1
+ y++
}
// setup the sizes
@@ -134,6 +128,11 @@ func (a *ArrayObject) String() string {
return (out)
}
+// Type returns the type of this object.
+func (a *ArrayObject) Type() Type {
+ return ARRAY
+}
+
// StringObject holds a string.
type StringObject struct {
@@ -141,19 +140,14 @@ type StringObject struct {
Value string
}
-// Type returns the type of this object.
-func (s *StringObject) Type() Type {
- return STRING
-}
-
// String returns a string representation of this object.
func (s *StringObject) String() string {
return (fmt.Sprintf("Object{Type:string, Value:%s}", s.Value))
}
-// String is a helper for creating a new string-object with the given value.
-func String(val string) *StringObject {
- return &StringObject{Value: val}
+// Type returns the type of this object.
+func (s *StringObject) Type() Type {
+ return STRING
}
// NumberObject holds a number.
@@ -163,19 +157,14 @@ type NumberObject struct {
Value float64
}
-// Type returns the type of this object.
-func (s *NumberObject) Type() Type {
- return NUMBER
-}
-
// String returns a string representation of this object.
-func (s *NumberObject) String() string {
- return (fmt.Sprintf("Object{Type:number, Value:%f}", s.Value))
+func (no *NumberObject) String() string {
+ return (fmt.Sprintf("Object{Type:number, Value:%f}", no.Value))
}
-// Number is a helper for creating a new number-object with the given value.
-func Number(val float64) *NumberObject {
- return &NumberObject{Value: val}
+// Type returns the type of this object.
+func (no *NumberObject) Type() Type {
+ return NUMBER
}
// ErrorObject holds a string, which describes an error
@@ -185,18 +174,32 @@ type ErrorObject struct {
Value string
}
+// String returns a string representation of this object.
+func (eo *ErrorObject) String() string {
+ return (fmt.Sprintf("Object{Type:error, Value:%s}", eo.Value))
+}
+
+// Type returns the type of this object.
+func (eo *ErrorObject) Type() Type {
+ return ERROR
+}
+
+//
+// Some simple constructors
+//
+
// Error is a helper for creating a new error-object with the given message.
func Error(format string, args ...interface{}) *ErrorObject {
msg := fmt.Sprintf(format, args...)
return &ErrorObject{Value: msg}
}
-// Type returns the type of this object.
-func (s *ErrorObject) Type() Type {
- return ERROR
+// Number is a helper for creating a new number-object with the given value.
+func Number(val float64) *NumberObject {
+ return &NumberObject{Value: val}
}
-// String returns a string representation of this object.
-func (s *ErrorObject) String() string {
- return (fmt.Sprintf("Object{Type:error, Value:%s}", s.Value))
+// String is a helper for creating a new string-object with the given value.
+func String(val string) *StringObject {
+ return &StringObject{Value: val}
}
diff --git a/object/object_test.go b/object/object_test.go
index d64da5b..50df9e0 100644
--- a/object/object_test.go
+++ b/object/object_test.go
@@ -6,94 +6,6 @@ import (
"testing"
)
-// Test we can create error/int/string and that they have the correct types
-func TestTypes(t *testing.T) {
-
- v := StringObject{Value: "Steve"}
- if v.Type() != STRING {
- t.Errorf("Wrong type for String")
- }
- if !strings.Contains(v.String(), ":string") {
- t.Errorf("Unexpected value for stringified object")
- }
-
- n := NumberObject{Value: math.Pi}
- if n.Type() != NUMBER {
- t.Errorf("Wrong type for Number")
- }
- if !strings.Contains(n.String(), ":number") {
- t.Errorf("Unexpected value for stringified object")
- }
-
- e := ErrorObject{Value: "You fail!"}
- if e.Type() != ERROR {
- t.Errorf("Wrong type for Error")
- }
- if !strings.Contains(e.String(), ":error") {
- t.Errorf("Unexpected value for stringified object")
- }
-}
-
-func TestError(t *testing.T) {
-
- a := Error("Test")
- b := Error("Test %d", 3)
- c := Error("Test %s", "me")
-
- // Test types
- if a.Type() != ERROR {
- t.Errorf("Object has the wrong type!")
- }
- if b.Type() != ERROR {
- t.Errorf("Object has the wrong type!")
- }
- if c.Type() != ERROR {
- t.Errorf("Object has the wrong type!")
- }
-
- // Test values
- if a.Value != "Test" {
- t.Errorf("Wrong value for error-message")
- }
- if b.Value != "Test 3" {
- t.Errorf("Wrong value for error-message")
- }
- if c.Value != "Test me" {
- t.Errorf("Wrong value for error-message")
- }
-}
-
-func TestNumber(t *testing.T) {
-
- a := Number(33)
-
- // Test types
- if a.Type() != NUMBER {
- t.Errorf("Object has the wrong type!")
- }
-
- // Test values
- if a.Value != 33 {
- t.Errorf("Wrong value for number-object")
- }
-
-}
-
-func TestString(t *testing.T) {
-
- a := String("Test")
-
- // Test types
- if a.Type() != STRING {
- t.Errorf("Object has the wrong type!")
- }
-
- // Test values
- if a.Value != "Test" {
- t.Errorf("Wrong value for string-object")
- }
-}
-
func Test1DArray(t *testing.T) {
// Create an array of one dimension
@@ -221,3 +133,91 @@ func Test2DArray(t *testing.T) {
}
}
+
+func TestError(t *testing.T) {
+
+ a := Error("Test")
+ b := Error("Test %d", 3)
+ c := Error("Test %s", "me")
+
+ // Test types
+ if a.Type() != ERROR {
+ t.Errorf("Object has the wrong type!")
+ }
+ if b.Type() != ERROR {
+ t.Errorf("Object has the wrong type!")
+ }
+ if c.Type() != ERROR {
+ t.Errorf("Object has the wrong type!")
+ }
+
+ // Test values
+ if a.Value != "Test" {
+ t.Errorf("Wrong value for error-message")
+ }
+ if b.Value != "Test 3" {
+ t.Errorf("Wrong value for error-message")
+ }
+ if c.Value != "Test me" {
+ t.Errorf("Wrong value for error-message")
+ }
+}
+
+func TestNumber(t *testing.T) {
+
+ a := Number(33)
+
+ // Test types
+ if a.Type() != NUMBER {
+ t.Errorf("Object has the wrong type!")
+ }
+
+ // Test values
+ if a.Value != 33 {
+ t.Errorf("Wrong value for number-object")
+ }
+
+}
+
+func TestString(t *testing.T) {
+
+ a := String("Test")
+
+ // Test types
+ if a.Type() != STRING {
+ t.Errorf("Object has the wrong type!")
+ }
+
+ // Test values
+ if a.Value != "Test" {
+ t.Errorf("Wrong value for string-object")
+ }
+}
+
+// Test we can create error/int/string and that they have the correct types
+func TestTypes(t *testing.T) {
+
+ v := StringObject{Value: "Steve"}
+ if v.Type() != STRING {
+ t.Errorf("Wrong type for String")
+ }
+ if !strings.Contains(v.String(), ":string") {
+ t.Errorf("Unexpected value for stringified object")
+ }
+
+ n := NumberObject{Value: math.Pi}
+ if n.Type() != NUMBER {
+ t.Errorf("Wrong type for Number")
+ }
+ if !strings.Contains(n.String(), ":number") {
+ t.Errorf("Unexpected value for stringified object")
+ }
+
+ e := ErrorObject{Value: "You fail!"}
+ if e.Type() != ERROR {
+ t.Errorf("Wrong type for Error")
+ }
+ if !strings.Contains(e.String(), ":error") {
+ t.Errorf("Unexpected value for stringified object")
+ }
+}
diff --git a/tokenizer/tokenizer.go b/tokenizer/tokenizer.go
index 339ed57..c7e1ed0 100644
--- a/tokenizer/tokenizer.go
+++ b/tokenizer/tokenizer.go
@@ -5,7 +5,6 @@
//
// Our interpeter is intentionally naive, and executes tokens directly, without
// any intermediary representation.
-//
package tokenizer
import (
@@ -79,7 +78,17 @@ func (l *Tokenizer) NextToken() token.Token {
tok = newToken(token.PLUS, l.ch)
case rune('-'):
// -3 is "-3". "3 - 4" is -1.
- if isDigit(l.peekChar()) {
+ //
+ // However we have to add a couple of special cases such that:
+ //
+ // "... X-3" is "X - 3"
+ // and "3-3" is "3 - 3" not "3 -3"
+ //
+ // We do that by ensuring we look at the previous token and force
+ // a "minus" rather than a negative number if the prev. token was
+ // an identifier, or a number.
+ //
+ if isDigit(l.peekChar()) && l.prevToken.Type != token.IDENT && l.prevToken.Type != token.INT {
// swallow the -
l.readChar()
diff --git a/tokenizer/tokenizer_test.go b/tokenizer/tokenizer_test.go
index 08c0a37..6ee7ad8 100644
--- a/tokenizer/tokenizer_test.go
+++ b/tokenizer/tokenizer_test.go
@@ -305,3 +305,71 @@ func TestNullString(t *testing.T) {
}
}
}
+
+// TestIssue120 tests that we parse subtraction vs. negative numbers
+// as expected.
+func TestIssue120(t *testing.T) {
+ input := `10 LET A=3
+20 A=A-3
+30 LET B=20-3
+40 LET C=-3
+50 LET D= 3 - -3
+`
+
+ tests := []struct {
+ expectedType token.Type
+ expectedLiteral string
+ }{
+ {token.LINENO, "10"},
+ {token.LET, "LET"},
+ {token.IDENT, "A"},
+ {token.ASSIGN, "="},
+ {token.INT, "3"},
+ {token.NEWLINE, "\\n"},
+
+ {token.LINENO, "20"},
+ {token.IDENT, "A"},
+ {token.ASSIGN, "="},
+ {token.IDENT, "A"},
+ {token.MINUS, "-"},
+ {token.INT, "3"},
+ {token.NEWLINE, "\\n"},
+
+ {token.LINENO, "30"},
+ {token.LET, "LET"},
+ {token.IDENT, "B"},
+ {token.ASSIGN, "="},
+ {token.INT, "20"},
+ {token.MINUS, "-"},
+ {token.INT, "3"},
+ {token.NEWLINE, "\\n"},
+
+ {token.LINENO, "40"},
+ {token.LET, "LET"},
+ {token.IDENT, "C"},
+ {token.ASSIGN, "="},
+ {token.INT, "-3"},
+ {token.NEWLINE, "\\n"},
+
+ {token.LINENO, "50"},
+ {token.LET, "LET"},
+ {token.IDENT, "D"},
+ {token.ASSIGN, "="},
+ {token.INT, "3"},
+ {token.MINUS, "-"},
+ {token.INT, "-3"},
+ {token.NEWLINE, "\\n"},
+
+ {token.EOF, ""},
+ }
+ l := New(input)
+ for i, tt := range tests {
+ tok := l.NextToken()
+ if tok.Type != tt.expectedType {
+ t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%v", i, tt.expectedType, tok)
+ }
+ if tok.Literal != tt.expectedLiteral {
+ t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal)
+ }
+ }
+}