diff --git a/.gitignore b/.gitignore index bdff7767..c287260e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ docs/_site *.ignore +test-ignore-* vendor builds/* !builds/.gitkeep .idea/ .vscode/ +packages.abs.json diff --git a/.travis.yml b/.travis.yml index c45fd401..067e23b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ os: - osx go: - - "1.12.x" + - "1.13.x" before_script: - go get -u diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 501ef51e..6f06281b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ repository. Since we follow [semver](https://semver.org/), when a new feature is released we don't backport it but simply -create a new version branch, such as `1.7.x`. Bugs, instead, +create a new version branch, such as `1.8.x`. Bugs, instead, might be backported from `1.1.0` to, for example, `1.0.x` and we will have a new [release](https://github.com/abs-lang/abs/releases), say `1.0.1` for the `1.0.x` version branch. diff --git a/Dockerfile b/Dockerfile index 49d521ae..cce804a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.12 +FROM golang:1.13 RUN apt-get update RUN apt-get install bash make git curl jq -y diff --git a/README.md b/README.md index adca0011..63684f62 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ if !res.ok { ip = res.json().ip total = ip.split(".").map(int).sum() if total > 100 { - echo("The sum of [%s] is a large number, %s.", ip, total) + echo("The sum of [$ip] is a large number, $total.") } ``` diff --git a/docs/README.md b/docs/README.md index 97384915..e2cb260e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -80,7 +80,7 @@ if !res.ok { ip = res.json().ip total = ip.split(".").map(int).sum() if total > 100 { - echo("The sum of [%s] is a large number, %s.", ip, total) + echo("The sum of [$ip] is a large number, $total.") } ``` diff --git a/docs/_includes/toc.md b/docs/_includes/toc.md index d42d1d29..89a31e83 100644 --- a/docs/_includes/toc.md +++ b/docs/_includes/toc.md @@ -28,6 +28,7 @@ ## Miscellaneous +* [Installing 3rd party libraries](/misc/3pl) * [Errors](/misc/error) * [Configuring the REPL](/misc/configuring-the-repl) * [Runtime](/misc/runtime) diff --git a/docs/abs.wasm b/docs/abs.wasm index adebcb4d..cfe330b7 100644 Binary files a/docs/abs.wasm and b/docs/abs.wasm differ diff --git a/docs/installer.sh b/docs/installer.sh index 7fbc83b2..042f6242 100644 --- a/docs/installer.sh +++ b/docs/installer.sh @@ -27,7 +27,7 @@ if [ "${MACHINE_TYPE}" = 'x86_64' ]; then ARCH="amd64" fi -VERSION=1.7.0 +VERSION=1.8.0 echo "Trying to detect the details of your architecture." echo "" diff --git a/docs/misc/3pl.md b/docs/misc/3pl.md new file mode 100644 index 00000000..278edb61 --- /dev/null +++ b/docs/misc/3pl.md @@ -0,0 +1,92 @@ +# Installing 3rd party libraries + +The ABS interpreter comes with a built-in installer for 3rd party libraries, +very similar to `npm install`, `pip install` or `go get`. + +The installer, budled since the `1.8.0` release, is currently **experimental** +and a few things might change. + +In order to install a package, you simply need to run `abs get`: + +``` bash +$ abs get github.com/abs-lang/abs-sample-module +🌘 - Downloading archive +Unpacking... +Creating alias... +Install Success. You can use the module with `require("abs-sample-module")` +``` + +Modules will be saved under the `vendor/$MODULE-master` directory. Each module +also gets an alias to facilitate requiring them in your code, meaning that +both of these forms are supported: + +``` +⧐ require("abs-sample-module/sample.abs") +{"another": f() {return hello world;}} + +⧐ require("vendor/github.com/abs-lang/abs-sample-module-master/sample.abs") +{"another": f() {return hello world;}} +``` + +Note that the `-master` prefix [will be removed](https://github.com/abs-lang/abs/issues/286) in future versions of ABS. + +Module aliases are saved in the `packages.abs.json` file +which is created in the same directory where you run the +`abs get ...` command: + +``` +$ abs get github.com/abs-lang/abs-sample-module +🌗 - Downloading archive +Unpacking... +Creating alias... +Install Success. You can use the module with `require("abs-sample-module")` + +$ cat packages.abs.json +{ + "abs-sample-module": "./vendor/github.com/abs-lang/abs-sample-module-master" +} +``` + +If an alias is already taken, the installer will let you know that you +will need to use the full path when requiring the module: + +``` +$ echo '{"abs-sample-module": "xyz"}' > packages.abs.json + +$ abs get github.com/abs-lang/abs-sample-module +🌘 - Downloading archive +Unpacking... +Creating alias...This module could not be aliased because module of same name exists + +Install Success. You can use the module with `require("./vendor/github.com/abs-lang/abs-sample-module-master")` +``` + +When requiring a module, ABS will try to load the `index.abs` file unless +another file is specified: + +``` +$ ~/projects/abs/builds/abs +Hello alex, welcome to the ABS (1.8.0) programming language! +Type 'quit' when you're done, 'help' if you get lost! + +⧐ require("abs-sample-module") +{"another": f() {return hello world;}} + +⧐ require("abs-sample-module/index.abs") +{"another": f() {return hello world;}} + +⧐ require("abs-sample-module/another.abs") +f() {return hello world;} +``` + +## Supported hosting platforms + +Currently, the installer supports modules hosted on: + +* GitHub + +## Next + +That's about it for this section! + +You can now head over to read a little bit about [errors](/misc/error). \ No newline at end of file diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md index 019ca28e..9dcca02d 100644 --- a/docs/misc/technical-details.md +++ b/docs/misc/technical-details.md @@ -1,6 +1,6 @@ # A few technical details... -The ABS interpreter is built with Golang version `1.11`, and is mostly based on [the interpreter book](https://interpreterbook.com/) written by [Thorsten Ball](https://twitter.com/thorstenball). +The ABS interpreter is built with Golang version `1.13`, and is mostly based on [the interpreter book](https://interpreterbook.com/) written by [Thorsten Ball](https://twitter.com/thorstenball). ABS is extremely different from Monkey, the "fictional" language the reader builds throughout the book, but the base structure (lexer, parser, evaluator) are still very much based on Thorsten's work. diff --git a/docs/types/array.md b/docs/types/array.md index 88cb9e89..418c6206 100644 --- a/docs/types/array.md +++ b/docs/types/array.md @@ -22,14 +22,21 @@ notation: array[3] ``` -Accessing an index that does not exist returns null. +Accessing an index that does not exist returns `null`. + +You can also access the Nth last element of an array by +using a negative index: + +``` bash +["a", "b", "c", "d"][-2] # "c" +``` You can also access a range of indexes with the `[start:end]` notation: ``` bash array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -array[0:2] // [0, 1, 2] +array[0:2] # [0, 1, 2] ``` where `start` is the starting position in the array, and `end` is @@ -38,14 +45,14 @@ and if `end` is omitted it is assumed to be the last index in the array: ``` bash -array[:2] // [0, 1, 2] -array[7:] // [7, 8, 9] +array[:2] # [0, 1, 2] +array[7:] # [7, 8, 9] ``` If `end` is negative, it will be converted to `length of array - end`: ``` bash -array[:-3] // [0, 1, 2, 3, 4, 5, 6] +array[:-3] # [0, 1, 2, 3, 4, 5, 6] ``` To concatenate arrays, "sum" them: @@ -113,7 +120,7 @@ a # [1, 2, 3, 4, 99, 55, 66] An array is defined as "homogeneous" when all its elements are of a single type: -``` +``` bash [1, 2, 3] # homogeneous [null, 0, "", {}] # heterogeneous ``` @@ -291,6 +298,47 @@ Sums the elements of the array. Only supported on arrays of numbers: [1, 1, 1].sum() # 3 ``` +### tsv([separator], [header]) + +Formats the array into TSV: + +``` bash +[["LeBron", "James"], ["James", "Harden"]].tsv() +LeBron James +James Harden +``` + +You can also specify the separator to be used if you +prefer not to use tabs: + +``` bash +[["LeBron", "James"], ["James", "Harden"]].tsv(",") +LeBron,James +James,Harden +``` + +The input array needs to be an array of arrays or hashes. If +you use hashes, their keys will be used as heading of the TSV: + +```bash +[{"name": "Lebron", "last": "James", "jersey": 23}, {"name": "James", "last": "Harden"}].tsv() +jersey last name +23 James Lebron +null Harden James +``` + +The heading will, by default, be a combination of all keys present in the hashes, +sorted alphabetically. If a key is missing in an hash, `null` will be used as value. +If you wish to specify the output format, you can pass a list of keys to be used +as header: + +```bash +[{"name": "Lebron", "last": "James", "jersey": 23}, {"name": "James", "last": "Harden"}].tsv("\t", ["name", "last", "jersey", "additional_key"]) +name last jersey additional_key +Lebron James 23 null +James Harden null null +``` + ### unique() Returns an array with unique values: diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md index 7a4d4a40..7a5aafaf 100644 --- a/docs/types/builtin-function.md +++ b/docs/types/builtin-function.md @@ -229,15 +229,54 @@ Halts the process for as many `ms` you specified: sleep(1000) # sleeps for 1 second ``` -### source(path_to_file) aka require(path_to_file) +### require(path_to_file.abs) -Evaluates the script at `path_to_file` in the context of the ABS global -environment. The results of any expressions in the file become -available to other commands in the REPL command line or to other +Evaluates the script at `path_to_file.abs`, and makes +its return value available to the caller. + +For example, suppose we have a `module.abs` file: + +``` bash +adder = f(a, b) { a + b } +multiplier = f(a, b) { a * b } + +return {"adder": adder, "multiplier": multiplier} +``` + +and a `main.abs` such as: + +``` bash +mod = require("module.abs") + +echo(mod.adder(1, 2)) # 3 +``` + +This is mostly useful to create external library +functions, like NPM modules or PIP packages, that +do not have access to the global environment. Any +variable set outside of the module will not be +available inside it, and vice-versa. The only +variable available to the caller (the script requiring +the module) is the module's return value. + +Note that `require` uses paths that are relative to +the current script. Say that you have 2 files (`a.abs` and `b.abs`) +in the `/tmp` folder, `a.abs` can `require("./b.abs")` +without having to specify the full path (eg. `require("/tmp/b.abs")`). + +### source(path_to_file.abs) + +Evaluates the script at `path_to_file.abs` in the context of the +ABS global environment. The results of any expressions in the file +become available to other commands in the REPL command line or to other scripts in the current script execution chain. -This is most useful for creating `library functions` in a script -that can be used by many other scripts. Often the library functions +This is very similar to `require`, but allows the module to access +and edit the global environment. Any variable set inside the module +will also be available outside of it. + +This is most useful for creating library functions in a startup script, +or variables that can be used by many other scripts. Often these library functions are loaded via the ABS Init File `~/.absrc` (see [ABS Init File](/introduction/how-to-run-abs-code)). For example: @@ -251,7 +290,7 @@ $ cat ~/.absrc source("~/abs/lib/library.abs") $ abs -Hello user, welcome to the ABS (1.7.0) programming language! +Hello user, welcome to the ABS (1.8.0) programming language! Type 'quit' when you are done, 'help' if you get lost! ⧐ adder(1, 2) 3 @@ -291,6 +330,7 @@ For example an ABS Init File may contain: ABS_SOURCE_DEPTH = 15 source("~/path/to/abs/lib") ``` + This will limit the source inclusion depth to 15 levels for this `source()` statement and will also apply to future `source()` statements until changed. @@ -299,4 +339,4 @@ statements until changed. That's about it for this section! -You can now head over to read a little bit about [errors](/misc/error). \ No newline at end of file +You can now head over to read a little bit about [how to install 3rd party libraries](/misc/3pl). \ No newline at end of file diff --git a/docs/types/string.md b/docs/types/string.md index 91de024a..4266628a 100644 --- a/docs/types/string.md +++ b/docs/types/string.md @@ -32,7 +32,14 @@ with the index notation: "hello world"[1] # e ``` -Accessing an index that does not exist returns null. +Accessing an index that does not exist returns an empty string. + +You can access the Nth last character of the string using a +negative index: + +``` bash +"string"[-2] # "n" +``` You can also access a range of the string with the `[start:end]` notation: @@ -42,7 +49,8 @@ You can also access a range of the string with the `[start:end]` notation: where `start` is the starting position in the array, and `end` is the ending one. If `start` is not specified, it is assumed to be 0, -and if `end` is omitted it is assumed to be the character in the string: +and if `end` is omitted it is assumed to be the last character in the +string: ``` bash "string"[0:3] // "str" @@ -75,6 +83,25 @@ To test for the existence of substrings within strings use the `in` operator: "xyz" in "string" # false ``` +## Interpolation + +You can also replace parts of the string with variables +declared within your program using the `$` symbol: + +``` bash +file = "/etc/hosts" +x = "File name is: $file" +echo(x) # "File name is: /etc/hosts" +``` + +If you need `$` literals in your command, you +simply need to escape them with a `\`: + +``` bash +"$non_existing_var" # "" since the ABS variable 'non_existing_var' doesn't exist +"\$non_existing_var" # "$non_existing_var" +``` + ## Special characters embedded in strings Double and single quoted strings behave differently if the string contains diff --git a/docs/wasm_exec.js b/docs/wasm_exec.js index 165d5677..a54bb9a9 100644 --- a/docs/wasm_exec.js +++ b/docs/wasm_exec.js @@ -3,6 +3,15 @@ // license that can be found in the LICENSE file. (() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + if (typeof global !== "undefined") { // global already exists } else if (typeof window !== "undefined") { @@ -13,30 +22,15 @@ throw new Error("cannot export Go (neither global, window nor self is defined)"); } - // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). - const isNodeJS = global.process && global.process.title === "node"; - if (isNodeJS) { + if (!global.require && typeof require !== "undefined") { global.require = require; - global.fs = require("fs"); - - const nodeCrypto = require("crypto"); - global.crypto = { - getRandomValues(b) { - nodeCrypto.randomFillSync(b); - }, - }; + } - global.performance = { - now() { - const [sec, nsec] = process.hrtime(); - return sec * 1000 + nsec / 1000000; - }, - }; + if (!global.fs && global.require) { + global.fs = require("fs"); + } - const util = require("util"); - global.TextEncoder = util.TextEncoder; - global.TextDecoder = util.TextDecoder; - } else { + if (!global.fs) { let outputBuf = ""; global.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused @@ -72,6 +66,34 @@ }; } + if (!global.crypto) { + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder) { + global.TextEncoder = require("util").TextEncoder; + } + + if (!global.TextDecoder) { + global.TextDecoder = require("util").TextDecoder; + } + + // End of polyfills for common API. + const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); @@ -243,7 +265,15 @@ const id = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++; this._scheduledTimeouts.set(id, setTimeout( - () => { this._resume(); }, + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early )); mem().setInt32(sp + 16, id, true); @@ -357,6 +387,34 @@ mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); }, + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array)) { + mem().setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + mem().setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array)) { + mem().setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + mem().setUint8(sp + 48, 1); + }, + "debug": (value) => { console.log(value); }, @@ -373,7 +431,6 @@ true, false, global, - this._inst.exports.mem, this, ]; this._refs = new Map(); @@ -385,9 +442,13 @@ let offset = 4096; const strPtr = (str) => { - let ptr = offset; - new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); - offset += str.length + (8 - (str.length % 8)); + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } return ptr; }; @@ -439,9 +500,15 @@ } } - if (isNodeJS) { + if ( + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { if (process.argv.length < 3) { - process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); + console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); process.exit(1); } @@ -459,7 +526,8 @@ }); return go.run(result.instance); }).catch((err) => { - throw err; + console.error(err); + process.exit(1); }); } })(); diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index fe04667b..2eac6cfd 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -26,9 +26,6 @@ var ( Fns map[string]*object.Builtin ) -// This program's global environment can be used by builtin's to modify the env -var globalEnv *object.Environment - // This program's lexer used for error location in Eval(program) var lex *lexer.Lexer @@ -55,8 +52,6 @@ func newContinueError(tok token.Token, format string, a ...interface{}) *object. // REPL and testing modules call this function to init the global lexer pointer for error location // NB. Eval(node, env) is recursive func BeginEval(program ast.Node, env *object.Environment, lexer *lexer.Lexer) object.Object { - // global environment - globalEnv = env // global lexer lex = lexer // run the evaluator @@ -98,7 +93,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return NULL case *ast.StringLiteral: - return &object.String{Token: node.Token, Value: node.Value} + return &object.String{Token: node.Token, Value: util.InterpolateStringVars(node.Value, env)} case *ast.Boolean: return nativeBoolToBooleanObject(node.Value) @@ -178,7 +173,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return args[0] } - return applyFunction(node.Token, function, args) + return applyFunction(node.Token, function, env, args) case *ast.MethodExpression: o := Eval(node.Object, env) @@ -191,7 +186,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return args[0] } - return applyMethod(node.Token, o, node.Method.String(), args) + return applyMethod(node.Token, o, node.Method.String(), env, args) case *ast.PropertyExpression: return evalPropertyExpression(node, env) @@ -1004,7 +999,7 @@ func evalPropertyExpression(pe *ast.PropertyExpression, env *object.Environment) return newError(pe.Token, "invalid property '%s' on type %s", pe.Property.String(), o.Type()) } -func applyFunction(tok token.Token, fn object.Object, args []object.Object) object.Object { +func applyFunction(tok token.Token, fn object.Object, env *object.Environment, args []object.Object) object.Object { switch fn := fn.(type) { case *object.Function: @@ -1017,14 +1012,14 @@ func applyFunction(tok token.Token, fn object.Object, args []object.Object) obje return unwrapReturnValue(evaluated) case *object.Builtin: - return fn.Fn(tok, args...) + return fn.Fn(tok, env, args...) default: return newError(tok, "not a function: %s", fn.Type()) } } -func applyMethod(tok token.Token, o object.Object, method string, args []object.Object) object.Object { +func applyMethod(tok token.Token, o object.Object, method string, env *object.Environment, args []object.Object) object.Object { f, ok := Fns[method] if !ok { @@ -1036,7 +1031,7 @@ func applyMethod(tok token.Token, o object.Object, method string, args []object. } args = append([]object.Object{o}, args...) - return f.Fn(tok, args...) + return f.Fn(tok, env, args...) } func extendFunctionEnv( @@ -1101,7 +1096,7 @@ func evalStringIndexExpression(tok token.Token, array, index object.Object, end max := len(stringObject.Value) - 1 if isRange { - max += 1 + max++ // A range's minimum value is 0 if idx < 0 { idx = 0 @@ -1132,8 +1127,23 @@ func evalStringIndexExpression(tok token.Token, array, index object.Object, end return &object.String{Token: tok, Value: string(stringObject.Value[idx:max])} } - if idx < 0 || idx > max { - return NULL + // Out of bounds? Return an empty string + if idx > max { + return &object.String{Token: tok, Value: ""} + } + + if idx < 0 { + length := max + 1 + + // Negative out of bounds? Return an empty string + if math.Abs(float64(idx)) > float64(length) { + return &object.String{Token: tok, Value: ""} + } + + // Our index was negative, so the actual index is length of the string + the index + // eg 3 + (-2) = 1 + // "123"[-2] = "2" + idx = length + idx } return &object.String{Token: tok, Value: string(stringObject.Value[idx])} @@ -1145,7 +1155,7 @@ func evalArrayIndexExpression(tok token.Token, array, index object.Object, end o max := len(arrayObject.Elements) - 1 if isRange { - max += 1 + max++ // A range's minimum value is 0 if idx < 0 { idx = 0 @@ -1176,10 +1186,25 @@ func evalArrayIndexExpression(tok token.Token, array, index object.Object, end o return &object.Array{Token: tok, Elements: arrayObject.Elements[idx:max]} } - if idx < 0 || idx > max { + // Out of bounds? Return a null element + if idx > max { return NULL } + if idx < 0 { + length := max + 1 + + // Negative out of bounds? Return a null element + if math.Abs(float64(idx)) > float64(length) { + return NULL + } + + // Our index was negative, so the actual index is length of the string + the index + // eg 3 + (-2) = 1 + // [1,2,3][-2] = 2 + idx = length + idx + } + return arrayObject.Elements[idx] } diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index cdac3269..a4a20240 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -365,6 +365,22 @@ func TestStringWriters(t *testing.T) { } } +func TestStringInterpolation(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {`a = "123"; "abc$a"`, "abc123"}, + {`a = "123"; "abc\$a"`, "abc$a"}, + {`a = "123"; "$$a$$a$$a"`, "$123$123$123"}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + testStringObject(t, evaluated, tt.expected) + } +} + func TestForInExpressions(t *testing.T) { tests := []struct { input string @@ -786,7 +802,7 @@ func TestBuiltinFunctions(t *testing.T) { {`find([1,2,3,3], f(x) {x == 3})`, 3}, {`find([1,2], f(x) {x == "some"})`, nil}, {`arg("o")`, "argument 0 to arg(...) is not supported (got: o, allowed: NUMBER)"}, - {`arg(3)`, ""}, + {`arg(99)`, ""}, {`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1).suffix("/evaluator")`, true}, // Little trick to get travis to run this test, as the base path is not /go/src/ {`cwd = cd(); cwd == pwd()`, true}, {`cwd = cd("path/to/nowhere"); cwd == pwd()`, false}, @@ -929,6 +945,18 @@ c")`, []string{"a", "b", "c"}}, {`sleep(0.01)`, nil}, {`$()`, ""}, {`a = 1; eval("a")`, 1}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = source("test-ignore-source-vs-require.abs"); a`, 2}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = require("test-ignore-source-vs-require.abs"); a`, 1}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = source("test-ignore-source-vs-require.abs"); x`, 10}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = require("test-ignore-source-vs-require.abs"); x`, 10}, + {`[[1,2,3], [2,3,4]].tsv()`, "1\t2\t3\n2\t3\t4"}, + {`[1].tsv()`, "tsv() must be called on an array of arrays or objects, such as [[1, 2, 3], [4, 5, 6]], '[1]' given"}, + {`[{"c": 3, "b": "hello"}, {"b": 20, "c": 0}].tsv()`, "b\tc\nhello\t3\n20\t0"}, + {`[[1,2,3], [2,3,4]].tsv(",")`, "1,2,3\n2,3,4"}, + {`[[1,2,3], [2]].tsv(",")`, "1,2,3\n2"}, + {`[[1,2,3], [2,3,4]].tsv("abc")`, "1a2a3\n2a3a4"}, + {`[[1,2,3], [2,3,4]].tsv("")`, "the separator argument to the tsv() function needs to be a valid character, '' given"}, + {`[{"c": 3, "b": "hello"}, {"b": 20, "c": 0}].tsv("\t", ["c", "b", "a"])`, "c\tb\ta\n3\thello\tnull\n0\t20\tnull"}, } for _, tt := range tests { evaluated := testEval(tt.input) @@ -1373,9 +1401,25 @@ func TestArrayIndexExpressions(t *testing.T) { nil, }, { - "[1, 2, 3][-1]", + "[1, 2, 3][-2]", + 2, + }, + { + "[1, 2, 3][-10]", + nil, + }, + { + "[1, 2, 3][-3]", + 1, + }, + { + "[1, 2, 3][-4]", nil, }, + { + "[1, 2, 3][-0]", + 1, + }, { "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][1:-300]; a[0]", nil, @@ -1508,7 +1552,7 @@ func TestStringIndexExpressions(t *testing.T) { }{ { `"123"[10]`, - nil, + "", }, { `"123"[1]`, @@ -1530,6 +1574,18 @@ func TestStringIndexExpressions(t *testing.T) { `"123"[:-1]`, "12", }, + { + `"123"[-2]`, + "2", + }, + { + `"123"[-1]`, + "3", + }, + { + `"123"[-10]`, + "", + }, { `"123"[2:-10]`, "", @@ -1554,10 +1610,6 @@ func TestStringIndexExpressions(t *testing.T) { `"123"[-10:{}]`, `index ranges can only be numerical: got "{}" (type HASH)`, }, - { - `"123"[-2]`, - "", - }, { `"123"[3]`, "", @@ -1571,18 +1623,18 @@ func TestStringIndexExpressions(t *testing.T) { for _, tt := range tests { evaluated := testEval(tt.input) switch result := evaluated.(type) { - case *object.Null: - testNullObject(t, evaluated) case *object.String: testStringObject(t, evaluated, tt.expected.(string)) case *object.Error: logErrorWithPosition(t, result.Message, tt.expected) + default: + t.Errorf("object is not the right result. got=%s ('%+v' expected)", result.Inspect(), tt.expected) } } } func testEval(input string) object.Object { - env := object.NewEnvironment(os.Stdout) + env := object.NewEnvironment(os.Stdout, "") lex := lexer.New(input) p := parser.New(lex) program := p.ParseProgram() diff --git a/evaluator/functions.go b/evaluator/functions.go index f42e9052..ce9d0a49 100644 --- a/evaluator/functions.go +++ b/evaluator/functions.go @@ -3,6 +3,8 @@ package evaluator import ( "bufio" "crypto/rand" + "encoding/csv" + "encoding/json" "fmt" "io/ioutil" "math" @@ -10,6 +12,7 @@ import ( "os" "os/exec" "os/user" + "path/filepath" "runtime" "sort" "strconv" @@ -319,26 +322,31 @@ func getFns() map[string]*object.Builtin { Types: []string{object.NUMBER_OBJ}, Fn: sleepFn, }, - // source("fileName") - // aka require() + // source("file.abs") -- soure a file, with access to the global environment "source": &object.Builtin{ Types: []string{object.STRING_OBJ}, Fn: sourceFn, }, - // require("fileName") -- alias for source() + // require("file.abs") -- require a file without giving it access to the global environment "require": &object.Builtin{ Types: []string{object.STRING_OBJ}, - Fn: sourceFn, + Fn: requireFn, }, // exec(command) -- execute command with interactive stdIO "exec": &object.Builtin{ Types: []string{object.STRING_OBJ}, Fn: execFn, }, + // eval(code) -- evaluates code in the context of the current ABS environment "eval": &object.Builtin{ Types: []string{object.STRING_OBJ}, Fn: evalFn, }, + // tsv([[1,2,3,4], [5,6,7,8]]) -- converts an array into a TSV string + "tsv": &object.Builtin{ + Types: []string{object.ARRAY_OBJ}, + Fn: tsvFn, + }, } } @@ -362,7 +370,7 @@ func validateArgs(tok token.Token, name string, args []object.Object, size int, } // len(var:"hello") -func lenFn(tok token.Token, args ...object.Object) object.Object { +func lenFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "len", args, 1, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}}) if err != nil { return err @@ -379,7 +387,7 @@ func lenFn(tok token.Token, args ...object.Object) object.Object { } // rand(max:20) -func randFn(tok token.Token, args ...object.Object) object.Object { +func randFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "rand", args, 1, [][]string{{object.NUMBER_OBJ}}) if err != nil { return err @@ -397,7 +405,7 @@ func randFn(tok token.Token, args ...object.Object) object.Object { // exit(code:0) // exit(code:0, message:"Adios!") -func exitFn(tok token.Token, args ...object.Object) object.Object { +func exitFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { var err object.Object var message string @@ -413,7 +421,7 @@ func exitFn(tok token.Token, args ...object.Object) object.Object { } if message != "" { - fmt.Fprintf(globalEnv.Writer, message) + fmt.Fprintf(env.Writer, message) } arg := args[0].(*object.Number) @@ -422,7 +430,7 @@ func exitFn(tok token.Token, args ...object.Object) object.Object { } // flag("my-flag") -func flagFn(tok token.Token, args ...object.Object) object.Object { +func flagFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { // TODO: // This seems a bit more complicated than it should, // and I could probably use some unit testing for this. @@ -486,7 +494,7 @@ func flagFn(tok token.Token, args ...object.Object) object.Object { } // pwd() -func pwdFn(tok token.Token, args ...object.Object) object.Object { +func pwdFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { dir, err := os.Getwd() if err != nil { return newError(tok, err.Error()) @@ -495,7 +503,7 @@ func pwdFn(tok token.Token, args ...object.Object) object.Object { } // cd() or cd(path) returns expanded path and path.ok -func cdFn(tok token.Token, args ...object.Object) object.Object { +func cdFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { user, ok := user.Current() if ok != nil { return newError(tok, ok.Error()) @@ -521,10 +529,10 @@ func cdFn(tok token.Token, args ...object.Object) object.Object { } // echo(arg:"hello") -func echoFn(tok token.Token, args ...object.Object) object.Object { +func echoFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { if len(args) == 0 { // allow echo() without crashing - fmt.Fprintln(globalEnv.Writer, "") + fmt.Fprintln(env.Writer, "") return NULL } var arguments []interface{} = make([]interface{}, len(args)-1) @@ -534,15 +542,15 @@ func echoFn(tok token.Token, args ...object.Object) object.Object { } } - fmt.Fprintf(globalEnv.Writer, args[0].Inspect(), arguments...) - fmt.Fprintln(globalEnv.Writer, "") + fmt.Fprintf(env.Writer, args[0].Inspect(), arguments...) + fmt.Fprintln(env.Writer, "") return NULL } // int(string:"123") // int(number:123) -func intFn(tok token.Token, args ...object.Object) object.Object { +func intFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "int", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}}) if err != nil { return err @@ -555,7 +563,7 @@ func intFn(tok token.Token, args ...object.Object) object.Object { // round(string:"123.1") // round(number:123.1) -func roundFn(tok token.Token, args ...object.Object) object.Object { +func roundFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { // Validate first argument err := validateArgs(tok, "round", args[:1], 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}}) if err != nil { @@ -581,7 +589,7 @@ func roundFn(tok token.Token, args ...object.Object) object.Object { // floor(string:"123.1") // floor(number:123.1) -func floorFn(tok token.Token, args ...object.Object) object.Object { +func floorFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "floor", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}}) if err != nil { return err @@ -592,7 +600,7 @@ func floorFn(tok token.Token, args ...object.Object) object.Object { // ceil(string:"123.1") // ceil(number:123.1) -func ceilFn(tok token.Token, args ...object.Object) object.Object { +func ceilFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "ceil", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}}) if err != nil { return err @@ -627,7 +635,7 @@ func applyMathFunction(tok token.Token, arg object.Object, fn func(float64) floa } // number(string:"1.23456") -func numberFn(tok token.Token, args ...object.Object) object.Object { +func numberFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}}) if err != nil { return err @@ -651,7 +659,7 @@ func numberFn(tok token.Token, args ...object.Object) object.Object { } // is_number(string:"1.23456") -func isNumberFn(tok token.Token, args ...object.Object) object.Object { +func isNumberFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}}) if err != nil { return err @@ -669,7 +677,7 @@ func isNumberFn(tok token.Token, args ...object.Object) object.Object { } // stdin() -- implemented with 2 functions -func stdinFn(tok token.Token, args ...object.Object) object.Object { +func stdinFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { v := scanner.Scan() if !v { @@ -692,7 +700,7 @@ func stdinNextFn() (object.Object, object.Object) { } // env(variable:"PWD") -func envFn(tok token.Token, args ...object.Object) object.Object { +func envFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "env", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -703,7 +711,7 @@ func envFn(tok token.Token, args ...object.Object) object.Object { } // arg(position:1) -func argFn(tok token.Token, args ...object.Object) object.Object { +func argFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "arg", args, 1, [][]string{{object.NUMBER_OBJ}}) if err != nil { return err @@ -720,7 +728,7 @@ func argFn(tok token.Token, args ...object.Object) object.Object { } // type(variable:"hello") -func typeFn(tok token.Token, args ...object.Object) object.Object { +func typeFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "type", args, 1, [][]string{}) if err != nil { return err @@ -730,7 +738,7 @@ func typeFn(tok token.Token, args ...object.Object) object.Object { } // split(string:"hello world!", sep:" ") -func splitFn(tok token.Token, args ...object.Object) object.Object { +func splitFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "split", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -751,7 +759,7 @@ func splitFn(tok token.Token, args ...object.Object) object.Object { } // lines(string:"a\nb") -func linesFn(tok token.Token, args ...object.Object) object.Object { +func linesFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "lines", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -773,7 +781,7 @@ func linesFn(tok token.Token, args ...object.Object) object.Object { // "{}".json() // Converts a valid JSON document to an ABS hash. -func jsonFn(tok token.Token, args ...object.Object) object.Object { +func jsonFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { // One interesting thing here is that we're creating // a new environment from scratch, whereas it might // be interesting to use the existing one. That would @@ -792,7 +800,7 @@ func jsonFn(tok token.Token, args ...object.Object) object.Object { s := args[0].(*object.String) str := strings.TrimSpace(s.Value) - env := object.NewEnvironment(globalEnv.Writer) + env = object.NewEnvironment(env.Writer, env.Dir) l := lexer.New(str) p := parser.New(l) var node ast.Node @@ -841,7 +849,7 @@ func jsonFn(tok token.Token, args ...object.Object) object.Object { } // "a %s".fmt(b) -func fmtFn(tok token.Token, args ...object.Object) object.Object { +func fmtFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { list := []interface{}{} for _, s := range args[1:] { @@ -852,7 +860,7 @@ func fmtFn(tok token.Token, args ...object.Object) object.Object { } // sum(array:[1, 2, 3]) -func sumFn(tok token.Token, args ...object.Object) object.Object { +func sumFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "sum", args, 1, [][]string{{object.ARRAY_OBJ}}) if err != nil { return err @@ -882,7 +890,7 @@ func sumFn(tok token.Token, args ...object.Object) object.Object { } // sort(array:[1, 2, 3]) -func sortFn(tok token.Token, args ...object.Object) object.Object { +func sortFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "sort", args, 1, [][]string{{object.ARRAY_OBJ}}) if err != nil { return err @@ -932,7 +940,7 @@ func sortFn(tok token.Token, args ...object.Object) object.Object { } // map(array:[1, 2, 3], function:f(x) { x + 1 }) -func mapFn(tok token.Token, args ...object.Object) object.Object { +func mapFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "map", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}}) if err != nil { return err @@ -944,7 +952,7 @@ func mapFn(tok token.Token, args ...object.Object) object.Object { copy(newElements, arr.Elements) for k, v := range arr.Elements { - evaluated := applyFunction(tok, args[1], []object.Object{v}) + evaluated := applyFunction(tok, args[1], env, []object.Object{v}) if isError(evaluated) { return evaluated @@ -956,7 +964,7 @@ func mapFn(tok token.Token, args ...object.Object) object.Object { } // some(array:[1, 2, 3], function:f(x) { x == 2 }) -func someFn(tok token.Token, args ...object.Object) object.Object { +func someFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "some", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}}) if err != nil { return err @@ -967,7 +975,7 @@ func someFn(tok token.Token, args ...object.Object) object.Object { arr := args[0].(*object.Array) for _, v := range arr.Elements { - r := applyFunction(tok, args[1], []object.Object{v}) + r := applyFunction(tok, args[1], env, []object.Object{v}) if isTruthy(r) { result = true @@ -979,7 +987,7 @@ func someFn(tok token.Token, args ...object.Object) object.Object { } // every(array:[1, 2, 3], function:f(x) { x == 2 }) -func everyFn(tok token.Token, args ...object.Object) object.Object { +func everyFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "every", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}}) if err != nil { return err @@ -990,7 +998,7 @@ func everyFn(tok token.Token, args ...object.Object) object.Object { arr := args[0].(*object.Array) for _, v := range arr.Elements { - r := applyFunction(tok, args[1], []object.Object{v}) + r := applyFunction(tok, args[1], env, []object.Object{v}) if !isTruthy(r) { result = false @@ -1001,7 +1009,7 @@ func everyFn(tok token.Token, args ...object.Object) object.Object { } // find(array:[1, 2, 3], function:f(x) { x == 2 }) -func findFn(tok token.Token, args ...object.Object) object.Object { +func findFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "find", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}}) if err != nil { return err @@ -1010,7 +1018,7 @@ func findFn(tok token.Token, args ...object.Object) object.Object { arr := args[0].(*object.Array) for _, v := range arr.Elements { - r := applyFunction(tok, args[1], []object.Object{v}) + r := applyFunction(tok, args[1], env, []object.Object{v}) if isTruthy(r) { return v @@ -1021,7 +1029,7 @@ func findFn(tok token.Token, args ...object.Object) object.Object { } // filter(array:[1, 2, 3], function:f(x) { x == 2 }) -func filterFn(tok token.Token, args ...object.Object) object.Object { +func filterFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "filter", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}}) if err != nil { return err @@ -1031,7 +1039,7 @@ func filterFn(tok token.Token, args ...object.Object) object.Object { arr := args[0].(*object.Array) for _, v := range arr.Elements { - evaluated := applyFunction(tok, args[1], []object.Object{v}) + evaluated := applyFunction(tok, args[1], env, []object.Object{v}) if isError(evaluated) { return evaluated @@ -1046,7 +1054,7 @@ func filterFn(tok token.Token, args ...object.Object) object.Object { } // unique(array:[1, 2, 3]) -func uniqueFn(tok token.Token, args ...object.Object) object.Object { +func uniqueFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "filter", args, 1, [][]string{{object.ARRAY_OBJ}}) if err != nil { return err @@ -1069,7 +1077,7 @@ func uniqueFn(tok token.Token, args ...object.Object) object.Object { } // contains("str", "tr") -func containsFn(tok token.Token, args ...object.Object) object.Object { +func containsFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "contains", args, 2, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.STRING_OBJ, object.NUMBER_OBJ}}) if err != nil { return err @@ -1116,7 +1124,7 @@ func containsFn(tok token.Token, args ...object.Object) object.Object { } // str(1) -func strFn(tok token.Token, args ...object.Object) object.Object { +func strFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "str", args, 1, [][]string{}) if err != nil { return err @@ -1126,7 +1134,7 @@ func strFn(tok token.Token, args ...object.Object) object.Object { } // any("abc", "b") -func anyFn(tok token.Token, args ...object.Object) object.Object { +func anyFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "any", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1136,7 +1144,7 @@ func anyFn(tok token.Token, args ...object.Object) object.Object { } // prefix("abc", "a") -func prefixFn(tok token.Token, args ...object.Object) object.Object { +func prefixFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "prefix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1146,7 +1154,7 @@ func prefixFn(tok token.Token, args ...object.Object) object.Object { } // suffix("abc", "a") -func suffixFn(tok token.Token, args ...object.Object) object.Object { +func suffixFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "suffix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1156,7 +1164,7 @@ func suffixFn(tok token.Token, args ...object.Object) object.Object { } // repeat("abc", 3) -func repeatFn(tok token.Token, args ...object.Object) object.Object { +func repeatFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "repeat", args, 2, [][]string{{object.STRING_OBJ}, {object.NUMBER_OBJ}}) if err != nil { return err @@ -1168,7 +1176,7 @@ func repeatFn(tok token.Token, args ...object.Object) object.Object { // replace("abd", "d", "c") --> short form // replace("abd", "d", "c", -1) // replace("abc", ["a", "b"], "c", -1) -func replaceFn(tok token.Token, args ...object.Object) object.Object { +func replaceFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { var err object.Object // Support short form @@ -1203,7 +1211,7 @@ func replaceFn(tok token.Token, args ...object.Object) object.Object { } // title("some thing") -func titleFn(tok token.Token, args ...object.Object) object.Object { +func titleFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "title", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1213,7 +1221,7 @@ func titleFn(tok token.Token, args ...object.Object) object.Object { } // lower("ABC") -func lowerFn(tok token.Token, args ...object.Object) object.Object { +func lowerFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "lower", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1223,7 +1231,7 @@ func lowerFn(tok token.Token, args ...object.Object) object.Object { } // upper("abc") -func upperFn(tok token.Token, args ...object.Object) object.Object { +func upperFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "upper", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1233,7 +1241,7 @@ func upperFn(tok token.Token, args ...object.Object) object.Object { } // wait(`sleep 10 &`) -func waitFn(tok token.Token, args ...object.Object) object.Object { +func waitFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "wait", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1250,7 +1258,7 @@ func waitFn(tok token.Token, args ...object.Object) object.Object { } // kill(`sleep 10 &`) -func killFn(tok token.Token, args ...object.Object) object.Object { +func killFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "kill", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1271,7 +1279,7 @@ func killFn(tok token.Token, args ...object.Object) object.Object { } // trim("abc") -func trimFn(tok token.Token, args ...object.Object) object.Object { +func trimFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "trim", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1281,7 +1289,7 @@ func trimFn(tok token.Token, args ...object.Object) object.Object { } // trim_by("abc", "c") -func trimByFn(tok token.Token, args ...object.Object) object.Object { +func trimByFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "trim_by", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1291,7 +1299,7 @@ func trimByFn(tok token.Token, args ...object.Object) object.Object { } // index("abc", "c") -func indexFn(tok token.Token, args ...object.Object) object.Object { +func indexFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1307,7 +1315,7 @@ func indexFn(tok token.Token, args ...object.Object) object.Object { } // last_index("abcc", "c") -func lastIndexFn(tok token.Token, args ...object.Object) object.Object { +func lastIndexFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "last_index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1323,7 +1331,7 @@ func lastIndexFn(tok token.Token, args ...object.Object) object.Object { } // slice("abcc", 0, -1) -func sliceFn(tok token.Token, args ...object.Object) object.Object { +func sliceFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "slice", args, 3, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.NUMBER_OBJ}, {object.NUMBER_OBJ}}) if err != nil { return err @@ -1376,7 +1384,7 @@ func sliceStartAndEnd(l int, start int, end int) (int, int) { } // shift([1,2,3]) removes and returns first value or null if array is empty -func shiftFn(tok token.Token, args ...object.Object) object.Object { +func shiftFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "shift", args, 1, [][]string{{object.ARRAY_OBJ}}) if err != nil { return err @@ -1393,7 +1401,7 @@ func shiftFn(tok token.Token, args ...object.Object) object.Object { } // reverse([1,2,3]) -func reverseFn(tok token.Token, args ...object.Object) object.Object { +func reverseFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "reverse", args, 1, [][]string{{object.ARRAY_OBJ}}) if err != nil { return err @@ -1409,7 +1417,7 @@ func reverseFn(tok token.Token, args ...object.Object) object.Object { } // push([1,2,3], 4) -func pushFn(tok token.Token, args ...object.Object) object.Object { +func pushFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "push", args, 2, [][]string{{object.ARRAY_OBJ}, {object.NULL_OBJ, object.ARRAY_OBJ, object.NUMBER_OBJ, object.STRING_OBJ, object.HASH_OBJ}}) if err != nil { @@ -1424,7 +1432,7 @@ func pushFn(tok token.Token, args ...object.Object) object.Object { // pop([1,2,3]) removes and returns last value or null if array is empty // pop({"a":1, "b":2, "c":3}, "a") removes and returns {"key": value} or null if key not found -func popFn(tok token.Token, args ...object.Object) object.Object { +func popFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { // pop has 2 signatures: pop(array), and pop(hash, key) var err object.Object if len(args) > 0 { @@ -1465,7 +1473,7 @@ func popFn(tok token.Token, args ...object.Object) object.Object { // keys([1,2,3]) returns array of indices // keys({"a": 1, "b": 2, "c": 3}) returns array of keys -func keysFn(tok token.Token, args ...object.Object) object.Object { +func keysFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "keys", args, 1, [][]string{{object.ARRAY_OBJ, object.HASH_OBJ}}) if err != nil { return err @@ -1491,7 +1499,7 @@ func keysFn(tok token.Token, args ...object.Object) object.Object { } // values({"a": 1, "b": 2, "c": 3}) returns array of values -func valuesFn(tok token.Token, args ...object.Object) object.Object { +func valuesFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "values", args, 1, [][]string{{object.HASH_OBJ}}) if err != nil { return err @@ -1507,7 +1515,7 @@ func valuesFn(tok token.Token, args ...object.Object) object.Object { } // items({"a": 1, "b": 2, "c": 3}) returns array of [key, value] tuples: [[a, 1], [b, 2] [c, 3]] -func itemsFn(tok token.Token, args ...object.Object) object.Object { +func itemsFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "items", args, 1, [][]string{{object.HASH_OBJ}}) if err != nil { return err @@ -1524,7 +1532,7 @@ func itemsFn(tok token.Token, args ...object.Object) object.Object { return &object.Array{Elements: items} } -func joinFn(tok token.Token, args ...object.Object) object.Object { +func joinFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "join", args, 2, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}}) if err != nil { return err @@ -1541,7 +1549,7 @@ func joinFn(tok token.Token, args ...object.Object) object.Object { return &object.String{Token: tok, Value: strings.Join(newElements, args[1].(*object.String).Value)} } -func sleepFn(tok token.Token, args ...object.Object) object.Object { +func sleepFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "sleep", args, 1, [][]string{{object.NUMBER_OBJ}}) if err != nil { return err @@ -1553,14 +1561,46 @@ func sleepFn(tok token.Token, args ...object.Object) object.Object { return NULL } -// source("fileName") -// aka require() +// source("file.abs") const ABS_SOURCE_DEPTH = "10" var sourceDepth, _ = strconv.Atoi(ABS_SOURCE_DEPTH) var sourceLevel = 0 -func sourceFn(tok token.Token, args ...object.Object) object.Object { +func sourceFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { + file, _ := util.ExpandPath(args[0].Inspect()) + return doSource(tok, env, file, args...) +} + +// require("file.abs") +var history = make(map[string]string) + +var packageAliases map[string]string +var packageAliasesLoaded bool + +func requireFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { + if !packageAliasesLoaded { + a, err := ioutil.ReadFile("./packages.abs.json") + + // We couldn't open the packages, file, possibly doesn't exists + // and the code shouldn't fail + if err == nil { + // Try to decode the packages file: + // if an error occurs we will simply + // ignore it + json.Unmarshal(a, &packageAliases) + } + + packageAliasesLoaded = true + } + + a := util.UnaliasPath(args[0].Inspect(), packageAliases) + file := filepath.Join(env.Dir, a) + e := object.NewEnvironment(env.Writer, filepath.Dir(file)) + return doSource(tok, e, file, args...) +} + +func doSource(tok token.Token, env *object.Environment, fileName string, args ...object.Object) object.Object { err := validateArgs(tok, "source", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { // reset the source level @@ -1569,7 +1609,7 @@ func sourceFn(tok token.Token, args ...object.Object) object.Object { } // get configured source depth if any - sourceDepthStr := util.GetEnvVar(globalEnv, "ABS_SOURCE_DEPTH", ABS_SOURCE_DEPTH) + sourceDepthStr := util.GetEnvVar(env, "ABS_SOURCE_DEPTH", ABS_SOURCE_DEPTH) sourceDepth, _ = strconv.Atoi(sourceDepthStr) // limit source file inclusion depth @@ -1585,7 +1625,6 @@ func sourceFn(tok token.Token, args ...object.Object) object.Object { sourceLevel++ // load the source file - fileName, _ := util.ExpandPath(args[0].Inspect()) code, error := ioutil.ReadFile(fileName) if error != nil { // reset the source level @@ -1607,11 +1646,11 @@ func sourceFn(tok token.Token, args ...object.Object) object.Object { } return newError(tok, "error found in source file: %s\n%s", fileName, errMsg) } - // invoke BeginEval() passing in the sourced program, globalEnv, and our lexer + // invoke BeginEval() passing in the sourced program, env, and our lexer // we save the current global lexer and restore it after we return from BeginEval() // NB. saving the lexer allows error line numbers to be relative to any nested source files savedLexer := lex - evaluated := BeginEval(program, globalEnv, l) + evaluated := BeginEval(program, env, l) lex = savedLexer if evaluated != nil && evaluated.Type() == object.ERROR_OBJ { // use errObj.Message instead of errObj.Inspect() to avoid nested "ERROR: " prefixes @@ -1626,7 +1665,7 @@ func sourceFn(tok token.Token, args ...object.Object) object.Object { return evaluated } -func evalFn(tok token.Token, args ...object.Object) object.Object { +func evalFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "eval", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1644,11 +1683,11 @@ func evalFn(tok token.Token, args ...object.Object) object.Object { } return newError(tok, "error found in eval block: %s\n%s", args[0].Inspect(), errMsg) } - // invoke BeginEval() passing in the sourced program, globalEnv, and our lexer + // invoke BeginEval() passing in the sourced program, env, and our lexer // we save the current global lexer and restore it after we return from BeginEval() // NB. saving the lexer allows error line numbers to be relative to any nested source files savedLexer := lex - evaluated := BeginEval(program, globalEnv, l) + evaluated := BeginEval(program, env, l) lex = savedLexer if evaluated != nil && evaluated.Type() == object.ERROR_OBJ { @@ -1662,7 +1701,139 @@ func evalFn(tok token.Token, args ...object.Object) object.Object { return evaluated } -func execFn(tok token.Token, args ...object.Object) object.Object { +// [[1,2], [3,4]].tsv() +// [{"a": 1, "b": 2}, {"b": 3, "c": 4}].tsv() +func tsvFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { + // all arguments were passed + if len(args) == 3 { + err := validateArgs(tok, "tsv", args, 3, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}, {object.ARRAY_OBJ}}) + if err != nil { + return err + } + } + + // If no header was passed, let's set it to empty list by default + if len(args) == 2 { + err := validateArgs(tok, "tsv", args, 2, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}}) + if err != nil { + return err + } + args = append(args, &object.Array{Elements: []object.Object{}}) + } + + // If no separator and header was passed, let's set them to tab and empty list by default + if len(args) == 1 { + err := validateArgs(tok, "tsv", args, 1, [][]string{{object.ARRAY_OBJ}}) + if err != nil { + return err + } + args = append(args, &object.String{Value: "\t"}) + args = append(args, &object.Array{Elements: []object.Object{}}) + } + + array := args[0].(*object.Array) + separator := args[1].(*object.String).Value + + if len(separator) < 1 { + return newError(tok, "the separator argument to the tsv() function needs to be a valid character, '%s' given", separator) + } + // the final outut + out := &strings.Builder{} + tsv := csv.NewWriter(out) + tsv.Comma = rune(separator[0]) + + // whether our array is made of ALL arrays or ALL hashes + var isArray bool + var isHash bool + homogeneous := array.Homogeneous() + + if len(array.Elements) > 0 { + _, isArray = array.Elements[0].(*object.Array) + _, isHash = array.Elements[0].(*object.Hash) + } + + // if the array is not homogeneous, we cannot process it + if !homogeneous || (!isArray && !isHash) { + return newError(tok, "tsv() must be called on an array of arrays or objects, such as [[1, 2, 3], [4, 5, 6]], '%s' given as argument", array.Inspect()) + } + + headerObj := args[2].(*object.Array) + header := []string{} + + if len(headerObj.Elements) > 0 { + for _, v := range headerObj.Elements { + header = append(header, v.Inspect()) + } + } else if isHash { + // if our array is made of hashes, we will include a header in + // our TSV output, made of all possible keys found in every object + for _, rows := range array.Elements { + for _, pair := range rows.(*object.Hash).Pairs { + header = append(header, pair.Key.Inspect()) + } + } + + // When no header is provided, we will simply + // use the list of keys from all object, alphabetically + // sorted + header = util.UniqueStrings(header) + sort.Strings(header) + } + + if len(header) > 0 { + err := tsv.Write(header) + + if err != nil { + return newError(tok, err.Error()) + } + } + + for _, row := range array.Elements { + // Row values + values := []string{} + + // In the case of an array, creating the row is fairly + // straightforward: we loop through the elements and extract + // their value + if isArray { + for _, element := range row.(*object.Array).Elements { + values = append(values, element.Inspect()) + } + + } + + // In case of an hash, we want to extract values based on + // the header. If a key is not present in an hash, we will + // simply set it to null + if isHash { + for _, key := range header { + pair, ok := row.(*object.Hash).GetPair(key) + var value object.Object + + if ok { + value = pair.Value + } else { + value = NULL + } + + values = append(values, value.Inspect()) + } + } + + // Add the row to the final output, by concatenating + // it with the given separator + err := tsv.Write(values) + + if err != nil { + return newError(tok, err.Error()) + } + } + + tsv.Flush() + return &object.String{Value: strings.TrimSpace(out.String())} +} + +func execFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { err := validateArgs(tok, "exec", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { return err @@ -1671,7 +1842,7 @@ func execFn(tok token.Token, args ...object.Object) object.Object { cmd = strings.Trim(cmd, " ") // interpolate any $vars in the cmd string - cmd = util.InterpolateStringVars(cmd, globalEnv) + cmd = util.InterpolateStringVars(cmd, env) var commands []string var executor string diff --git a/examples/ip-finder.abs b/examples/ip-finder.abs new file mode 100644 index 00000000..dce43666 --- /dev/null +++ b/examples/ip-finder.abs @@ -0,0 +1,3 @@ +return f() { + return `curl icanhazip.com` +} \ No newline at end of file diff --git a/examples/ip-sum.abs b/examples/ip-sum.abs index 5248021f..2c1d01ea 100644 --- a/examples/ip-sum.abs +++ b/examples/ip-sum.abs @@ -8,5 +8,5 @@ if !res.ok { ip = res.json().ip total = ip.split(".").map(int).sum() if total > 100 { - echo("The sum of [%s] is a large number, %s.", ip, total) + echo("The sum of [$ip] is a large number, $total.") } \ No newline at end of file diff --git a/examples/require.abs b/examples/require.abs new file mode 100644 index 00000000..eb445af6 --- /dev/null +++ b/examples/require.abs @@ -0,0 +1,7 @@ +ip_finder = require("ip-finder.abs") +script = require("./second_level/script.abs") +ip_finder_2 = require("./ip-finder.abs") + +echo("My IP is %s", ip_finder()) +echo("The script said '%s'", script()) +echo("My IP is %s", ip_finder_2()) \ No newline at end of file diff --git a/examples/second_level/hello_module.abs b/examples/second_level/hello_module.abs new file mode 100644 index 00000000..9685cc5e --- /dev/null +++ b/examples/second_level/hello_module.abs @@ -0,0 +1 @@ +return "hello!" \ No newline at end of file diff --git a/examples/second_level/script.abs b/examples/second_level/script.abs new file mode 100644 index 00000000..15105ab6 --- /dev/null +++ b/examples/second_level/script.abs @@ -0,0 +1,5 @@ +hello = require("./hello_module.abs") + +return f() { + return hello +} \ No newline at end of file diff --git a/go.mod b/go.mod index b2af2efe..0cfac42e 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,10 @@ module github.com/abs-lang/abs go 1.12 require ( - github.com/c-bata/go-prompt v0.2.4-0 + github.com/c-bata/go-prompt v0.2.4-0.20190826134812-0f95e1d1de2e github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-isatty v0.0.9 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 // indirect github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 ) - -replace github.com/c-bata/go-prompt => github.com/odino/go-prompt v0.2.4-0.20190816001457-ea717205ca73412c085f2b2296f11c674f359f5c diff --git a/go.sum b/go.sum index e0402010..0ad49558 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/c-bata/go-prompt v0.2.4-0.20190826134812-0f95e1d1de2e h1:wISxI1PW3d8yWV0aY+Vxwzt56LD+SlMiLWXwNgtdGTU= +github.com/c-bata/go-prompt v0.2.4-0.20190826134812-0f95e1d1de2e/go.mod h1:Fd2OKZ3h6UdKxcSflqFDkUpTbTKwrtLbvtCp3eVuTEs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -5,6 +7,7 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= diff --git a/install/install.go b/install/install.go new file mode 100644 index 00000000..2f23ca38 --- /dev/null +++ b/install/install.go @@ -0,0 +1,231 @@ +package install + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +func valid(module string) bool { + return strings.HasPrefix(module, "github.com/") +} + +func Install(module string) { + if !valid(module) { + fmt.Printf(`Error reading URL. Please use "github.com/USER/REPO" format to install`) + return + } + + err := getZip(module) + if err != nil { + return + } + + err = unzip(module) + if err != nil { + fmt.Printf(`Error unpacking: %v`, err) + return + } + + alias, err := createAlias(module) + if err != nil { + return + } + + fmt.Printf("\nInstall Success. You can use the module with `require(\"%s\")`\n", alias) + return +} + +func printLoader(done chan int64, message string) { + var stop bool = false + symbols := []string{"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "} + i := 0 + + for { + select { + case <-done: + stop = true + default: + fmt.Printf("\r" + symbols[i] + " - " + message) + time.Sleep(100 * time.Millisecond) + i++ + if i > len(symbols)-1 { + i = 0 + } + } + + if stop { + break + } + } +} + +func getZip(module string) error { + path := fmt.Sprintf("./vendor/%s", module) + // Create all the parent directories if needed + err := os.MkdirAll(filepath.Dir(path), 0755) + + if err != nil { + fmt.Printf("Error making directory %s\n", err) + return err + } + + out, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, os.FileMode(0666)) + if err != nil { + fmt.Printf("Error opening file %s\n", err) + return err + } + defer out.Close() + + client := http.Client{ + Timeout: time.Duration(10 * time.Second), + } + + url := fmt.Sprintf("https://%s/archive/master.zip", module) + + done := make(chan int64) + go printLoader(done, "Downloading archive") + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + fmt.Printf("Error creating new request %s", err) + return err + } + + resp, err := client.Do(req) + + if err != nil { + fmt.Printf("Could not get module: %s\n", err) + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + fmt.Errorf("Bad response code: %d", resp.StatusCode) + return err + } + + _, err = io.Copy(out, resp.Body) + if err != nil { + fmt.Printf("Error copying file %s", err) + return err + } + done <- 1 + return err +} + +// Unzip will decompress a zip archive +func unzip(module string) error { + fmt.Printf("\nUnpacking...") + src := fmt.Sprintf("./vendor/%s", module) + dest := filepath.Dir(src) + + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + // Store filename/path for returning and using later on + fpath := filepath.Join(dest, f.Name) + + // Check for ZipSlip. More Info: http://bit.ly/2MsjAWE + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("%s: illegal file path", fpath) + } + + if f.FileInfo().IsDir() { + // Make Folder + os.MkdirAll(fpath, 0755) + continue + } + + // Make File + if err = os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { + return err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + _, err = io.Copy(outFile, rc) + + // Close the file without defer to close before next iteration of loop + outFile.Close() + rc.Close() + + if err != nil { + return err + } + } + return nil +} + +func createAlias(module string) (string, error) { + fmt.Printf("\nCreating alias...") + f, err := os.OpenFile("./packages.abs.json", os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + fmt.Printf("Could not open alias file %s\n", err) + return "", err + } + defer f.Close() + + b, err := ioutil.ReadAll(f) + + data := make(map[string]string) + moduleName := filepath.Base(module) + // Appending "master" as Github zip file has "-master" suffix + modulePath := fmt.Sprintf("./vendor/%s-master", module) + + // If package.abs.json file is empty + if len(b) == 0 { + // Add alias key-value pair to file + data[moduleName] = modulePath + } else { + err = json.Unmarshal(b, &data) + if err != nil { + fmt.Printf("Could not unmarshal alias json %s\n", err) + return "", err + } + // module already installed and aliased + if data[moduleName] == modulePath { + return moduleName, nil + } + + if data[moduleName] != "" { + fmt.Printf("This module could not be aliased because module of same name exists\n") + return modulePath, nil + } + + data[moduleName] = modulePath + } + + newData, err := json.MarshalIndent(data, "", " ") + + if err != nil { + fmt.Printf("Could not marshal alias json when installing module %s\n", err) + return "", err + } + + _, err = f.WriteAt(newData, 0) + if err != nil { + fmt.Printf("Could not write to alias file %s\n", err) + return "", err + } + return moduleName, err + +} diff --git a/js/js.go b/js/js.go index 7e836a34..ec601478 100644 --- a/js/js.go +++ b/js/js.go @@ -23,7 +23,7 @@ func runCode(this js.Value, i []js.Value) interface{} { var buf bytes.Buffer // the first argument to our function code := i[0].String() - env := object.NewEnvironment(&buf) + env := object.NewEnvironment(&buf, "") lex := lexer.New(code) p := parser.New(lex) diff --git a/main.go b/main.go index ded25775..32f9ed08 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,11 @@ import ( "fmt" "os" + "github.com/abs-lang/abs/install" "github.com/abs-lang/abs/repl" ) -var Version = "1.7.0" +var Version = "1.8.0" // The ABS interpreter func main() { @@ -16,6 +17,12 @@ func main() { fmt.Println(Version) return } + + if len(args) == 3 && args[1] == "get" { + install.Install(args[2]) + return + } + // begin the REPL repl.BeginRepl(args, Version) } diff --git a/object/environment.go b/object/environment.go index b5dec938..01cbaa84 100644 --- a/object/environment.go +++ b/object/environment.go @@ -10,16 +10,18 @@ import ( // new environment has access to identifiers stored // in the outer one. func NewEnclosedEnvironment(outer *Environment) *Environment { - env := NewEnvironment(outer.Writer) + env := NewEnvironment(outer.Writer, outer.Dir) env.outer = outer return env } // NewEnvironment creates a new environment to run -// ABS in -func NewEnvironment(w io.Writer) *Environment { +// ABS in, specifying a writer for the output of the +// program and the base dir (which is used to require +// other scripts) +func NewEnvironment(w io.Writer, dir string) *Environment { s := make(map[string]Object) - return &Environment{store: s, outer: nil, Writer: w} + return &Environment{store: s, outer: nil, Writer: w, Dir: dir} } // Environment represent the environment associated @@ -31,6 +33,16 @@ type Environment struct { // Used to capture output. This is typically os.Stdout, // but you could capture in any io.Writer of choice Writer io.Writer + // Dir represents the directory from which we're executing code. + // It starts as the directory from which we invoke the ABS + // executable, but changes when we call require("...") as each + // require call resets the dir to its own directory, so that + // relative imports work. + // + // If we have script A and B in /tmp, A can require("B") + // wihout having to specify its full absolute path + // eg. require("/tmp/B") + Dir string } // Get returns an identifier stored within the environment diff --git a/object/object.go b/object/object.go index d2c2c2a1..0d964c85 100644 --- a/object/object.go +++ b/object/object.go @@ -13,7 +13,7 @@ import ( "github.com/abs-lang/abs/token" ) -type BuiltinFunction func(tok token.Token, args ...Object) Object +type BuiltinFunction func(tok token.Token, env *Environment, args ...Object) Object type ObjectType string @@ -304,6 +304,9 @@ func (ao *Array) Next() (Object, Object) { func (ao *Array) Reset() { ao.position = 0 } + +// Homogeneous returns whether the array is homogeneous, +// meaning all of its elements are of a single type func (ao *Array) Homogeneous() bool { if ao.Empty() { return true diff --git a/repl/repl.go b/repl/repl.go index 75d34d26..fe005bb8 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "os/user" + "path/filepath" "strings" "github.com/abs-lang/abs/evaluator" @@ -36,7 +37,8 @@ var ( ) func init() { - env = object.NewEnvironment(os.Stdout) + d, _ := os.Getwd() + env = object.NewEnvironment(os.Stdout, d) } func completer(d prompt.Document) []prompt.Suggest { @@ -200,6 +202,10 @@ func BeginRepl(args []string, version string) { } else { interactive = false env.Set("ABS_INTERACTIVE", evaluator.FALSE) + // Make sure we set the right Dir when evaluating a script, + // so that the script thinks it's running from its location + // and things like relative require() calls work. + env.Dir = filepath.Dir(args[1]) } // get abs init file diff --git a/repl/reverse_search.go b/repl/reverse_search.go index a1db80f1..4d73bbcc 100644 --- a/repl/reverse_search.go +++ b/repl/reverse_search.go @@ -9,7 +9,7 @@ import ( var reverseSearchStr string var lastSearchPosition int -func clearReverseSearch() { +func clearReverseSearch(p *prompt.Document) { reverseSearchStr = "" initReverseSearch() } diff --git a/scripts/release.abs b/scripts/release.abs index d115bbda..1c7dd021 100644 --- a/scripts/release.abs +++ b/scripts/release.abs @@ -54,7 +54,7 @@ if !rm.ok { version = `cat ./main.go | grep "var Version"` version = version.slice(15, len(version) - 1) -echo("Running builds for version %s, confirm by typing \"y\"".fmt(version)) +echo("Running builds for version $version, confirm by typing \"y\"") selection = stdin() if selection != "y" { @@ -63,7 +63,7 @@ if selection != "y" { for platform in platforms { goos, goarch = platform.split("/") - output_name = "builds/abs-%s-%s-%s".fmt(version, goos, goarch) + output_name = "builds/abs-$version-$goos-$goarch" entry_point = "main.go" if goos == "windows" { diff --git a/util/util.go b/util/util.go index 1e28d0d7..e5a06c43 100644 --- a/util/util.go +++ b/util/util.go @@ -6,6 +6,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "github.com/abs-lang/abs/object" ) @@ -86,3 +87,53 @@ func InterpolateStringVars(str string, env *object.Environment) string { }) return str } + +// UniqueStrings takes an input list of strings +// and returns a version without duplicate values +func UniqueStrings(slice []string) []string { + keys := make(map[string]bool) + list := []string{} + for _, entry := range slice { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} + +// UnaliasPath translates a path alias +// to the full path in the filesystem. +func UnaliasPath(path string, packageAlias map[string]string) string { + // An alias can come in different forms: + // - package + // - package/file.abs + // but we only really need to resolve the + // first path in the alias. + parts := strings.Split(path, string(os.PathSeparator)) + + if len(parts) < 1 { + return path + } + + if packageAlias[parts[0]] != "" { + // If we are able to resolve a path, then + // we should join in back with the rest of the + // paths + p := []string{packageAlias[parts[0]]} + p = append(p, parts[1:]...) + path = filepath.Join(p...) + } + return appendIndexFile(path) +} + +// If our path didn't end with an ABS file (.abs), +// let's assume it's a directory and we will +// auto-include the index.abs file from it +func appendIndexFile(path string) string { + if filepath.Ext(path) != ".abs" { + return filepath.Join(path, "index.abs") + } + + return path +} diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 00000000..8cf83df0 --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,78 @@ +package util + +import ( + "os" + "testing" +) + +func TestUnaliasPath(t *testing.T) { + tests := []struct { + path string + aliases map[string]string + expected string + }{ + {"test", map[string]string{}, "test" + string(os.PathSeparator) + "index.abs"}, + {"test" + string(os.PathSeparator) + "sample.abs", map[string]string{}, "test" + string(os.PathSeparator) + "sample.abs"}, + {"test" + string(os.PathSeparator) + "sample.abs", map[string]string{"test": "path"}, "path" + string(os.PathSeparator) + "sample.abs"}, + {"test", map[string]string{"test": "path"}, "path" + string(os.PathSeparator) + "index.abs"}, + {"." + string(os.PathSeparator) + "test", map[string]string{"test": "path"}, "test" + string(os.PathSeparator) + "index.abs"}, + } + + for _, tt := range tests { + res := UnaliasPath(tt.path, tt.aliases) + + if res != tt.expected { + t.Fatalf("error unaliasing path, expected %s, got %s", tt.expected, res) + } + } +} + +func TestUniqueStrings(t *testing.T) { + tests := []struct { + strings []string + len int + }{ + {[]string{"a", "b", "c"}, 3}, + {[]string{"a", "a", "a"}, 1}, + } + + for _, tt := range tests { + if len(UniqueStrings(tt.strings)) != tt.len { + t.Fatalf("expected %d, got %d", tt.len, len(UniqueStrings(tt.strings))) + } + } +} + +func TestContains(t *testing.T) { + tests := []struct { + strings []string + match string + expected bool + }{ + {[]string{"a", "b", "c"}, "a", true}, + {[]string{"a", "a", "a"}, "d", false}, + } + + for _, tt := range tests { + if tt.expected != Contains(tt.strings, tt.match) { + t.Fatalf("expected %v", tt.expected) + } + } +} + +func TestIsNumber(t *testing.T) { + tests := []struct { + number string + expected bool + }{ + {"12", true}, + {"12a", false}, + {"12.2", true}, + } + + for _, tt := range tests { + if tt.expected != IsNumber(tt.number) { + t.Fatalf("expected %v (%s)", tt.expected, tt.number) + } + } +}