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 @@
-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 {