From 1d8e47b48de6b49348ca15a30368dcf83f6f5493 Mon Sep 17 00:00:00 2001 From: odino Date: Sat, 27 Jul 2019 10:43:36 +0400 Subject: [PATCH 1/4] Tracking PR for 1.6.x, here we go! New minor branch for ABS :) this is the PR that tracks everything going into `1.6`. --- CONTRIBUTING.md | 2 +- docs/installer.sh | 2 +- docs/misc/technical-details.md | 2 +- docs/types/builtin-function.md | 2 +- main.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58fdbefb..2b9b32b4 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.5.x`. Bugs, instead, +create a new version branch, such as `1.6.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/docs/installer.sh b/docs/installer.sh index a02363b5..64f793c3 100644 --- a/docs/installer.sh +++ b/docs/installer.sh @@ -27,7 +27,7 @@ if [ "${MACHINE_TYPE}" = 'x86_64' ]; then ARCH="amd64" fi -VERSION=1.5.1 +VERSION=1.6.0 echo "Trying to detect the details of your architecture." echo "" diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md index a3689c40..f9b18a88 100644 --- a/docs/misc/technical-details.md +++ b/docs/misc/technical-details.md @@ -205,7 +205,7 @@ ERROR: type mismatch: NULL + NUMBER ## Roadmap -We're currently working on [1.6](https://github.com/abs-lang/abs/milestone/13). +We're currently working on [1.7](https://github.com/abs-lang/abs/milestone/14). ## Next diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md index 56e11489..45c7b0dd 100644 --- a/docs/types/builtin-function.md +++ b/docs/types/builtin-function.md @@ -243,7 +243,7 @@ $ cat ~/.absrc source("~/abs/lib/library.abs") $ abs -Hello user, welcome to the ABS (1.5.1) programming language! +Hello user, welcome to the ABS (1.6.0) programming language! Type 'quit' when you are done, 'help' if you get lost! ⧐ adder(1, 2) 3 diff --git a/main.go b/main.go index 2c62e11d..1b2ca254 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "github.com/abs-lang/abs/repl" ) -var Version = "1.5.1" +var Version = "1.6.0" // The ABS interpreter func main() { From c7239407cafb1b0b93ca747b205afe64a3f7cbf7 Mon Sep 17 00:00:00 2001 From: odino Date: Tue, 30 Jul 2019 18:46:10 +0400 Subject: [PATCH 2/4] Deprecate / "unfavour" the legacy command syntax, closes #220 The `$()` syntax for commands is cool and works ok, but presents issues with parsing and escaping. With this PR this syntax has been removed from most of the docs, replaced by the simpler backtick commands. Since `$()` is still useful for embedding commands, we kept a section of the docs dedicated to explaining when you should use this vs backticks. --- README.md | 6 +- docs/README.md | 10 +- docs/introduction/how-to-run-abs-code.md | 2 +- .../why-another-scripting-language.md | 4 +- docs/misc/error.md | 8 +- docs/misc/technical-details.md | 4 +- docs/syntax/system-commands.md | 122 +++++++++--------- examples/basic.abs | 2 +- examples/city_selector.abs | 2 +- examples/domain_checker.abs | 2 +- examples/ip-sum.abs | 2 +- examples/ip-sum.sh | 4 +- examples/nba-game.abs | 2 +- examples/shell_interpolation.abs | 2 +- examples/shell_stdin.abs | 2 +- object/object.go | 6 +- parser/parser_test.go | 2 +- scripts/release.abs | 6 +- tests/test-abs.sh | 2 +- 19 files changed, 97 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index fc17ef2d..adca0011 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,12 @@ # The ABS programming language -ABS is a scripting language that works best when you're scripting on +ABS is a programming language that works best when you're scripting on your terminal. It tries to combine the elegance of languages such as Python, or Ruby, to the convenience of Bash. ``` bash -tz = $(cat /etc/timezone); +tz = `cat /etc/timezone` continent, city = tz.split("/") echo("Best city in the world?") @@ -73,7 +73,7 @@ And here's how you could write the same code in ABS: ``` bash # Simple program that fetches your IP and sums it up -res = $(curl -s 'https://api.ipify.org?format=json'); +res = `curl -s 'https://api.ipify.org?format=json'` if !res.ok { echo("An error occurred: %s", res) diff --git a/docs/README.md b/docs/README.md index 162b8326..515c2f7c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,12 +21,12 @@ undefined

-ABS is a scripting language that works best when you're scripting on +ABS is a programming language that works best when you're scripting on your terminal. It tries to combine the elegance of languages such as Python, or Ruby, to the convenience of Bash. ``` bash -tz = $(cat /etc/timezone); +tz = `cat /etc/timezone`; continent, city = tz.split("/") echo("Best city in the world?") @@ -48,14 +48,14 @@ in Bash: ``` bash # Simple program that fetches your IP and sums it up -RES=$(curl -s 'https://api.ipify.org?format=json' || "ERR") +RES=`curl -s 'https://api.ipify.org?format=json' || "ERR"` if [ "$RES" = "ERR" ]; then echo "An error occurred" exit 1 fi -IP=$(echo $RES | jq -r ".ip") +IP=`echo $RES | jq -r ".ip"` IFS=. read first second third fourth < "10.10.10.12" echo(ip) diff --git a/docs/misc/error.md b/docs/misc/error.md index a1e49096..4876136e 100644 --- a/docs/misc/error.md +++ b/docs/misc/error.md @@ -21,10 +21,10 @@ $ cat examples/error-parse.abs m.a = 'abc' # this is a command terminated with a semi -d/d = $(command); +d/d = `command`; # this is a command terminated with a LF -c/c = $(command) +c/c = `command` # this is a bad infix operator b %% c @@ -34,9 +34,9 @@ $ abs examples/error-parse.abs no prefix parse function for '=' found [4:5] m.a = 'abc' no prefix parse function for '=' found - [7:5] d/d = $(command); + [7:5] d/d = `command`; no prefix parse function for '=' found - [10:5] c/c = $(command) + [10:5] c/c = `command` no prefix parse function for '%' found [13:4] b %% c diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md index f9b18a88..8e90aade 100644 --- a/docs/misc/technical-details.md +++ b/docs/misc/technical-details.md @@ -37,9 +37,9 @@ tests/test-parser.abs no prefix parse function for '=' found [4:5] m.a = 'abc' no prefix parse function for '=' found - [7:5] d/d = $(command); + [7:5] d/d = `command`; no prefix parse function for '=' found - [10:5] c/c = $(command) + [10:5] c/c = `command` no prefix parse function for '%' found [13:4] b %% c no prefix parse function for '&&' found diff --git a/docs/syntax/system-commands.md b/docs/syntax/system-commands.md index 89ee8b5c..2ec36a9e 100644 --- a/docs/syntax/system-commands.md +++ b/docs/syntax/system-commands.md @@ -4,11 +4,10 @@ Executing system commands is one of the most important features of ABS, as it allows the mixing of conveniency of the shell with the syntax of a modern programming language. -Commands are executed either with `$(command)` or `` `command` ``, +Commands are executed with the `` `command` `` syntax, which resemble Bash's syntax to execute commands in a subshell: ``` bash -date = $(date) # "Sun Apr 1 04:30:59 +01 1995" date = `date` # "Sun Apr 1 04:30:59 +01 1995" ``` @@ -18,7 +17,7 @@ encounter an error, the same string would hold the error message: ``` bash -date = $(dat) # "bash: dat: command not found" +date = `dat` # "bash: dat: command not found" ``` It would be fairly painful to have to parse strings @@ -27,60 +26,11 @@ in ABS, the returned string has a special property `ok` that checks whether the command was successful: ``` bash -ls = $(ls -la) - -if ls.ok { - echo("hello world") -} - -# or - if `ls -la`.ok { echo("hello world") } ``` -It is also possible to execute a shell command without capturing its -input or output using the `exec(command)` function. This allows long running -or interactive programs to be run using the terminal's Standard IO -(stdin, stdout, stderr). For example: -```bash -exec("sudo visudo") -``` -would open the default text editor in super user mode on the /etc/sudoers file. -Unlike the normal backtick command execution syntax above, -the `exec(command)` function call does not return a result string unless it fails. -Therefore, the `exec(command)` may be the last command executed in a script -file leaving the executed command in charge of the terminal IO until it -terminates. - -For example, an ABS script might be used to marshall the command line args -for an interactive program such as the nano editor: - -``` bash -$ cat abs/tests/test-exec.abs -# marshall the args for the nano editor -# if the filename is not given in the args, prompt for it -# if the file is located outside the user's home dir, invoke sudo nano filename - -cmd = 'nano' -filename = arg(2) -homedir = env("HOME") - -while filename == '' { - echo("Please enter file name for %s: ", cmd) - filename = stdin() -} - -if filename.prefix('~/') || filename.prefix(homedir) { - sudo = '' -} else { - sudo = 'sudo' -} - -# execute the command with live stdIO -exec("$sudo $cmd $filename") -``` ## Executing commands in background Sometimes you might want to execute a command in @@ -155,13 +105,21 @@ and if you need `$` literals in your command, you simply need to escape them with a `\`: ``` bash -$(echo $PWD) # "" since the ABS variable PWD doesn't exist -$(echo \$PWD) # "/go/src/github.com/abs-lang/abs" +`echo $PWD` # "" since the ABS variable PWD doesn't exist +`echo \$PWD` # "/go/src/github.com/abs-lang/abs" ``` -## Limitations +## Alternative $() syntax + +Even though the use of backticks is the standard recommended +way to run system commands, for the ease of embedding ABS also +allows you to use the `$(command)` syntax: + +``` +$(basename $(dirname "/tmp/make/life/easy")) // "easy" +``` -Currently, commands that use the `$()` syntax need to be +Commands that use the `$()` syntax need to be on their own line, meaning that you will not be able to have additional code on the same line. This will throw an error: @@ -170,10 +128,56 @@ This will throw an error: $(sleep 10); echo("hello world") ``` -Note that this is currently a limitation that will likely -be removed in the future (see [#41](https://github.com/abs-lang/abs/issues/41)). +## Executing commands without capturing I/O + +It is also possible to execute a shell command without capturing its +input or output using the `exec(command)` function. This allows long running +or interactive programs to be run using the terminal's Standard IO +(stdin, stdout, stderr). For example: + +```bash +exec("sudo visudo") +``` + +would open the default text editor in super user mode on the /etc/sudoers file. + +Unlike the normal backtick command execution syntax above, +the `exec(command)` function call does not return a result string unless it fails. +Therefore, the `exec(command)` may be the last command executed in a script +file leaving the executed command in charge of the terminal IO until it +terminates. + +For example, an ABS script might be used to marshall the command line args +for an interactive program such as the nano editor: + +``` bash +$ cat abs/tests/test-exec.abs +# marshall the args for the nano editor +# if the filename is not given in the args, prompt for it +# if the file is located outside the user's home dir, invoke sudo nano filename + +cmd = 'nano' +filename = arg(2) +homedir = env("HOME") + +while filename == '' { + echo("Please enter file name for %s: ", cmd) + filename = stdin() +} + +if filename.prefix('~/') || filename.prefix(homedir) { + sudo = '' +} else { + sudo = 'sudo' +} + +# execute the command with live stdIO +exec("$sudo $cmd $filename") +``` + +## Limitations -Also note that, currently, the implementation of system commands +Note that the implementation of system commands requires the `bash` executable to [be available on the system](https://github.com/abs-lang/abs/blob/5b5b0abf3115a5dd4dfe8485501f8765985ad0db/evaluator/evaluator.go#L696-L722). On Windows, commands are executed through [cmd.exe](https://github.com/abs-lang/abs/blob/ee793641be09ad8572c3e913fef8468f69b0c0a2/evaluator/evaluator.go#L1101-L1103). Future work will make it possible to select which shell to use, diff --git a/examples/basic.abs b/examples/basic.abs index 9c122998..9543edf6 100644 --- a/examples/basic.abs +++ b/examples/basic.abs @@ -1,2 +1,2 @@ -ip = $(curl -s 'https://api.ipify.org?format=json' | jq -rj ".ip" | awk '{print "[" $1 "]"}'); +ip = `curl -s 'https://api.ipify.org?format=json' | jq -rj ".ip"` echo "your ip is " + ip \ No newline at end of file diff --git a/examples/city_selector.abs b/examples/city_selector.abs index d07026a1..e2934749 100644 --- a/examples/city_selector.abs +++ b/examples/city_selector.abs @@ -1,4 +1,4 @@ -tz = $(cat /etc/timezone) +tz = `cat /etc/timezone` cont, city = tz.split("/") echo("Best city in the world?") diff --git a/examples/domain_checker.abs b/examples/domain_checker.abs index e8852984..afe301c9 100644 --- a/examples/domain_checker.abs +++ b/examples/domain_checker.abs @@ -2,7 +2,7 @@ # Check if a domain is in your hostfile echo("What domain are we looking for today?") domain = stdin() -matches = $(cat /etc/hosts | grep $domain | wc -l) +matches = `cat /etc/hosts | grep $domain | wc -l` if !matches.ok { echo("How do you even...") diff --git a/examples/ip-sum.abs b/examples/ip-sum.abs index 5b046635..5248021f 100644 --- a/examples/ip-sum.abs +++ b/examples/ip-sum.abs @@ -1,4 +1,4 @@ -res = $(curl -s 'https://api.ipify.org?format=json'); +res = `curl -s 'https://api.ipify.org?format=json'` if !res.ok { echo("An error occurred: %s", res) diff --git a/examples/ip-sum.sh b/examples/ip-sum.sh index a5eb8c19..b873cc41 100755 --- a/examples/ip-sum.sh +++ b/examples/ip-sum.sh @@ -1,12 +1,12 @@ # Simple program that fetches your IP and sums it up -RES=$(curl -s 'https://api.ipify.org?format=json' || "ERR") +RES=`curl -s 'https://api.ipify.org?format=json' || "ERR"` if [ "$RES" = "ERR" ]; then echo "An error occurred" exit 1 fi -IP=$(echo $RES | jq -r ".ip") +IP=`echo $RES | jq -r ".ip"` IFS=. read first second third fourth < Date: Tue, 30 Jul 2019 18:46:31 +0400 Subject: [PATCH 3/4] Index ranges, closes #138 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the ability to specify ranges as indexes (`[x]`). A range is a colon-separated pair of boundaries which can be either an integer (`[1:10]`) or omitted (`[1:]`). ``` ⧐ "Hello world"[0:2] He ⧐ "Hello world"[0:5] Hello ⧐ "Hello world"[:5] Hello ⧐ "Hello world"[5:] world ⧐ "Hello world"[:-1] Hello worl ⧐ ``` --- ast/ast.go | 22 +++++++-- docs/types/array.md | 24 +++++++++ docs/types/string.md | 25 ++++++++++ evaluator/evaluator.go | 98 +++++++++++++++++++++++++++++++------ evaluator/evaluator_test.go | 82 +++++++++++++++++++++++++++++-- examples/index_ranges.abs | 4 ++ lexer/lexer_test.go | 7 +++ parser/parser.go | 23 +++++++-- parser/parser_test.go | 81 ++++++++++++++++++++++++++++++ 9 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 examples/index_ranges.abs diff --git a/ast/ast.go b/ast/ast.go index 207fdc46..09023fd5 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -492,10 +492,18 @@ func (al *ArrayLiteral) String() string { return out.String() } +// IndexExpression allows accessing a single index, or a range, +// over a string or an array. +// +// array[1:10] -> left[index:end] +// array[1] -> left[index] +// string[1] -> left[index] type IndexExpression struct { - Token token.Token // The [ token - Left Expression - Index Expression + Token token.Token // The [ token + Left Expression // the argument on which the index is access eg array of array[1] + Index Expression // the left-most index eg. 1 in array[1] or array[1:10] + IsRange bool // whether the expression is a range (1:10) + End Expression // the end of the range, if the expression is a range } func (ie *IndexExpression) expressionNode() {} @@ -506,7 +514,13 @@ func (ie *IndexExpression) String() string { out.WriteString("(") out.WriteString(ie.Left.String()) out.WriteString("[") - out.WriteString(ie.Index.String()) + + if ie.IsRange { + out.WriteString(ie.Index.String() + ":" + ie.End.String()) + } else { + out.WriteString(ie.Index.String()) + } + out.WriteString("])") return out.String() diff --git a/docs/types/array.md b/docs/types/array.md index a6539106..88cb9e89 100644 --- a/docs/types/array.md +++ b/docs/types/array.md @@ -24,6 +24,30 @@ array[3] Accessing an index that does not exist returns null. +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] +``` + +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 last index in the +array: + +``` bash +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] +``` + To concatenate arrays, "sum" them: ``` bash diff --git a/docs/types/string.md b/docs/types/string.md index 886bce5c..606e90a9 100644 --- a/docs/types/string.md +++ b/docs/types/string.md @@ -34,6 +34,27 @@ with the index notation: Accessing an index that does not exist returns null. +You can also access a range of the string with the `[start:end]` notation: + +``` bash +"string"[0:3] // "str" +``` + +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: + +``` bash +"string"[0:3] // "str" +"string"[1:] // "tring" +``` + +If `end` is negative, it will be converted to `length of string - end`: + +``` bash +"string"[0:-1] // "strin" +``` + To concatenate strings, "sum" them: ``` bash @@ -383,6 +404,10 @@ Returns the last index at which `str` is found: ### slice(start, end) +> This function is deprecated and might be removed in future versions. +> +> Use the index notation instead: `"string"[0, 3]` + Returns a portion of the string, from `start` to `end`: ``` bash diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 8a582c3a..5eb6821c 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -204,15 +204,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return &object.Array{Token: node.Token, Elements: elements} case *ast.IndexExpression: - left := Eval(node.Left, env) - if isError(left) { - return left - } - index := Eval(node.Index, env) - if isError(index) { - return index - } - return evalIndexExpression(node.Token, left, index) + return evalIndexExpression(node, env) case *ast.HashLiteral: return evalHashLiteral(node, env) @@ -1072,23 +1064,68 @@ func unwrapReturnValue(obj object.Object) object.Object { return obj } -func evalIndexExpression(tok token.Token, left, index object.Object) object.Object { +func evalIndexExpression(node *ast.IndexExpression, env *object.Environment) object.Object { + tok := node.Token + left := Eval(node.Left, env) + if isError(left) { + return left + } + index := Eval(node.Index, env) + if isError(index) { + return index + } + end := Eval(node.End, env) + if isError(end) { + return end + } + switch { case left.Type() == object.ARRAY_OBJ && index.Type() == object.NUMBER_OBJ: - return evalArrayIndexExpression(left, index) + return evalArrayIndexExpression(tok, left, index, end, node.IsRange) case left.Type() == object.HASH_OBJ && index.Type() == object.STRING_OBJ: return evalHashIndexExpression(tok, left, index) case left.Type() == object.STRING_OBJ && index.Type() == object.NUMBER_OBJ: - return evalStringIndexExpression(tok, left, index) + return evalStringIndexExpression(tok, left, index, end, node.IsRange) default: return newError(tok, "index operator not supported: %s on %s", index.Inspect(), left.Type()) } } -func evalStringIndexExpression(tok token.Token, array, index object.Object) object.Object { +func evalStringIndexExpression(tok token.Token, array, index object.Object, end object.Object, isRange bool) object.Object { stringObject := array.(*object.String) idx := index.(*object.Number).Int() - max := len(stringObject.Value) - 1 + max := len(stringObject.Value) + + if isRange { + // A range's minimum value is 0 + if idx < 0 { + idx = 0 + } + endIdx, ok := end.(*object.Number) + + // check if the range end is a number + if ok { + // if it's lower than zero, then the end is len(x) - end, + // else it's the end value itself + if endIdx.Int() < 0 { + max = int(math.Max(float64(max+endIdx.Int()), 0)) + } else if endIdx.Int() < max { + max = endIdx.Int() + } + } else if end != NULL { + // if the end index is not a number nor null, then we have an error + // (null would mean no end, so it's valid) + return newError(tok, `index ranges can only be numerical: got "%s" (type %s)`, end.Inspect(), end.Type()) + } + + // if the start is higher than the end, let's return + // a skeleton + if idx > max { + return &object.String{Token: tok, Value: ""} + } + + return &object.String{Token: tok, Value: string(stringObject.Value[idx:max])} + } if idx < 0 || idx > max { return NULL @@ -1097,11 +1134,42 @@ func evalStringIndexExpression(tok token.Token, array, index object.Object) obje return &object.String{Token: tok, Value: string(stringObject.Value[idx])} } -func evalArrayIndexExpression(array, index object.Object) object.Object { +func evalArrayIndexExpression(tok token.Token, array, index object.Object, end object.Object, isRange bool) object.Object { arrayObject := array.(*object.Array) idx := index.(*object.Number).Int() max := len(arrayObject.Elements) - 1 + if isRange { + // A range's minimum value is 0 + if idx < 0 { + idx = 0 + } + endIdx, ok := end.(*object.Number) + + // check if the range end is a number + if ok { + // if it's lower than zero, then the end is len(x) - end, + // else it's the end value itself + if endIdx.Int() < 0 { + max = int(math.Max(float64(max+endIdx.Int()), 0)) + } else if endIdx.Int() < max { + max = endIdx.Int() - 1 + } + } else if end != NULL { + // if the end index is not a number nor null, then we have an error + // (null would mean no end, so it's valid) + return newError(tok, `index ranges can only be numerical: got "%s" (type %s)`, end.Inspect(), end.Type()) + } + + // if the start is higher than the end, let's return + // a skeleton + if idx > max { + return &object.Array{Token: tok, Elements: []object.Object{}} + } + + return &object.Array{Token: tok, Elements: arrayObject.Elements[idx : max+1]} + } + if idx < 0 || idx > max { return NULL } diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 4f58f87e..6acc3851 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -1345,6 +1345,38 @@ func TestArrayIndexExpressions(t *testing.T) { "[1, 2, 3][-1]", nil, }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][1:-300]; a[0]", + nil, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][1:4]; a[0] + a[1] + a[2]", + 9, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][1:4]; a.len()", + 3, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][200:3]; a[0]", + nil, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][7:-1]; a[0]", + 8, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][100:]; a[0]", + nil, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][0:100]; a[0]", + 1, + }, + { + "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][-10:]; a[0]", + 1, + }, } for _, tt := range tests { @@ -1447,15 +1479,57 @@ func TestStringIndexExpressions(t *testing.T) { `"123"[1]`, "2", }, + { + `"123"[1:]`, + "23", + }, + { + `"123"[1:1]`, + "", + }, + { + `"123"[:2]`, + "12", + }, + { + `"123"[:-1]`, + "12", + }, + { + `"123"[2:-10]`, + "", + }, + { + `"123"[2:1]`, + "", + }, + { + `"123"[200:]`, + "", + }, + { + `"123"[0:10]`, + "123", + }, + { + `"123"[-10:]`, + "123", + }, + { + `"123"[-10:{}]`, + `index ranges can only be numerical: got "{}" (type HASH)`, + }, } for _, tt := range tests { evaluated := testEval(tt.input) - s, ok := tt.expected.(string) - if ok { - testStringObject(t, evaluated, s) - } else { + 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) } } } diff --git a/examples/index_ranges.abs b/examples/index_ranges.abs new file mode 100644 index 00000000..a2d40ef1 --- /dev/null +++ b/examples/index_ranges.abs @@ -0,0 +1,4 @@ +str = "Hello world!" + +echo(str[:5]) // Hello +echo(str.split("")[:5]) // ["H", "e", "l", "l", "o"] \ No newline at end of file diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 46626138..ec6a32b4 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -96,6 +96,7 @@ for true { for true { continue } +a[1:3] ` tests := []struct { @@ -330,6 +331,12 @@ for true { {token.LBRACE, "{"}, {token.CONTINUE, "continue"}, {token.RBRACE, "}"}, + {token.IDENT, "a"}, + {token.LBRACKET, "["}, + {token.NUMBER, "1"}, + {token.COLON, ":"}, + {token.NUMBER, "3"}, + {token.RBRACKET, "]"}, {token.EOF, ""}, } diff --git a/parser/parser.go b/parser/parser.go index f7017e89..42d38520 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -804,12 +804,29 @@ func (p *Parser) ParseArrayLiteral() ast.Expression { return array } -// some["thing"] +// some["thing"] or some[1:10] func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression { exp := &ast.IndexExpression{Token: p.curToken, Left: left} - p.nextToken() - exp.Index = p.parseExpression(LOWEST) + if p.peekTokenIs(token.COLON) { + exp.Index = &ast.NumberLiteral{Value: 0, Token: token.Token{Type: token.NUMBER, Position: 0, Literal: "0"}} + exp.IsRange = true + } else { + p.nextToken() + exp.Index = p.parseExpression(LOWEST) + } + + if p.peekTokenIs(token.COLON) { + exp.IsRange = true + p.nextToken() + + if p.peekTokenIs(token.RBRACKET) { + exp.End = nil + } else { + p.nextToken() + exp.End = p.parseExpression(LOWEST) + } + } if !p.expectPeek(token.RBRACKET) { return nil diff --git a/parser/parser_test.go b/parser/parser_test.go index 1aebefdc..36d7e417 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -1352,6 +1352,87 @@ func TestParsingIndexExpressions(t *testing.T) { } } +func TestParsingIndexRangeExpressions(t *testing.T) { + input := "myArray[99 : 101]" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + indexExp, ok := stmt.Expression.(*ast.IndexExpression) + if !ok { + t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression) + } + + if !indexExp.IsRange { + t.Fatalf("exp is not range") + } + + if !testIdentifier(t, indexExp.Left, "myArray") { + return + } + + testNumberLiteral(t, indexExp.Index, 99) + testNumberLiteral(t, indexExp.End, 101) +} + +func TestParsingIndexRangeWithoutStartExpressions(t *testing.T) { + input := "myArray[: 101]" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + indexExp, ok := stmt.Expression.(*ast.IndexExpression) + if !ok { + t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression) + } + + if !indexExp.IsRange { + t.Fatalf("exp is not range") + } + + if !testIdentifier(t, indexExp.Left, "myArray") { + return + } + + testNumberLiteral(t, indexExp.Index, 0) + testNumberLiteral(t, indexExp.End, 101) +} + +func TestParsingIndexRangeWithoutEndExpressions(t *testing.T) { + input := "myArray[99 : ]" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + indexExp, ok := stmt.Expression.(*ast.IndexExpression) + if !ok { + t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression) + } + + if !testIdentifier(t, indexExp.Left, "myArray") { + return + } + + if !indexExp.IsRange { + t.Fatalf("exp is not range") + } + + testNumberLiteral(t, indexExp.Index, 99) + + if indexExp.End != nil { + t.Fatalf("range end is not nil. got=%T", indexExp.End) + } +} + func TestParsingProperty(t *testing.T) { input := "var.prop" From fa3bd94c12a6892ff904c1c5e4b8c4a3e7874d14 Mon Sep 17 00:00:00 2001 From: odino Date: Sun, 4 Aug 2019 18:40:30 +0400 Subject: [PATCH 4/4] Implicit return values (`null`), closes #217 This PR makes it possible to skip specifying a return value, a common scenario when we want to exit, for example, an IF statement: ``` if something { return; } ``` The semicolon is optional. --- docs/syntax/return.md | 8 ++++++++ evaluator/evaluator_test.go | 14 ++++++++++++-- examples/implicit_retur_value.abs | 10 ++++++++++ parser/parser.go | 13 ++++++++++++- parser/parser_test.go | 19 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 examples/implicit_retur_value.abs diff --git a/docs/syntax/return.md b/docs/syntax/return.md index e3422598..3b9ada59 100644 --- a/docs/syntax/return.md +++ b/docs/syntax/return.md @@ -20,6 +20,14 @@ func = f(x) { func(9) # 10 ``` +The default value of a `return` is `null`: + +``` +if x { + return # null +} +``` + ## Next That's about it for this section! diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 6acc3851..93a68d6a 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -455,8 +455,11 @@ func TestWhileExpressions(t *testing.T) { func TestReturnStatements(t *testing.T) { tests := []struct { input string - expected float64 + expected interface{} }{ + {"return;", nil}, + {"return", nil}, + {"fn = f() { return }; fn()", nil}, {"return 10;", 10}, {"return 10; 9;", 10}, {"return 2 * 5; 9;", 10}, @@ -497,7 +500,14 @@ fn(10);`, for _, tt := range tests { evaluated := testEval(tt.input) - testNumberObject(t, evaluated, tt.expected) + switch tt.expected.(type) { + case int: + testNumberObject(t, evaluated, float64(tt.expected.(int))) + case nil: + testNullObject(t, evaluated) + default: + panic("should not reach here") + } } } diff --git a/examples/implicit_retur_value.abs b/examples/implicit_retur_value.abs new file mode 100644 index 00000000..3f11116b --- /dev/null +++ b/examples/implicit_retur_value.abs @@ -0,0 +1,10 @@ +fn = f() { + if !flag("value") { + return; + } + + return flag("value") +} + +echo("Call this script with the flag --value to output it. If the flag is not passed, an implicit return will trigger and 'null' will be printed.") +echo(fn()) \ No newline at end of file diff --git a/parser/parser.go b/parser/parser.go index 42d38520..c166a0bb 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -210,6 +210,7 @@ func (p *Parser) ParseProgram() *ast.Program { } p.nextToken() } + return program } @@ -327,10 +328,20 @@ func (p *Parser) parseAssignStatement() ast.Statement { // return x func (p *Parser) parseReturnStatement() *ast.ReturnStatement { stmt := &ast.ReturnStatement{Token: p.curToken} + returnToken := p.curToken p.nextToken() - stmt.ReturnValue = p.parseExpression(LOWEST) + // return; + if p.curTokenIs(token.SEMICOLON) { + stmt.ReturnValue = &ast.NullLiteral{Token: p.curToken} + } else if p.peekTokenIs(token.RBRACE) || p.peekTokenIs(token.EOF) { + // return + stmt.ReturnValue = &ast.NullLiteral{Token: returnToken} + } else { + // return xyz + stmt.ReturnValue = p.parseExpression(LOWEST) + } if p.peekTokenIs(token.SEMICOLON) { p.nextToken() diff --git a/parser/parser_test.go b/parser/parser_test.go index 36d7e417..d53ca3cf 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -56,6 +56,8 @@ func TestReturnStatements(t *testing.T) { input string expectedValue interface{} }{ + {"return", nil}, + {"return;", nil}, {"return 5;", 5}, {"return true;", true}, {"return foobar;", "foobar"}, @@ -1696,6 +1698,8 @@ func testLiteralExpression( return testIdentifier(t, exp, v) case bool: return testBooleanLiteral(t, exp, v) + case nil: + return testNullLiteral(t, exp, v) } t.Errorf("type of exp not handled. got=%T", exp) return false @@ -1805,6 +1809,21 @@ func testBooleanLiteral(t *testing.T, exp ast.Expression, value bool) bool { return true } +func testNullLiteral(t *testing.T, exp ast.Expression, value interface{}) bool { + nl, ok := exp.(*ast.NullLiteral) + if !ok { + t.Errorf("exp not *ast.NullLiteral. got=%T", exp) + return false + } + + if nl.TokenLiteral() != "null" { + t.Errorf("nl.TokenLiteral not %t. got=%s", value, nl.TokenLiteral()) + return false + } + + return true +} + func checkParserErrors(t *testing.T, p *Parser) { errors := p.Errors() if len(errors) == 0 {