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) + } + } +}