diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 361bd158..87482065 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,15 @@ on: - master jobs: + check-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Make dictu and run checkTests + run: | + cmake -DCMAKE_BUILD_TYPE=Debug -DDISABLE_HTTP=1 -B ./build + cmake --build ./build + ./dictu scripts/checkTests.du ci test-ubuntu-cmake: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -20,7 +29,7 @@ jobs: run: | cmake -DCMAKE_BUILD_TYPE=Debug -DDISABLE_HTTP=1 -B ./build cmake --build ./build - ./dictu tests/runTests.du | tee /dev/stderr | grep -q 'Total bytes lost: 0' + ./dictu tests/runTests.du ci | tee /dev/stderr | grep -q 'Total bytes lost: 0' - name: Remove build directory run: | rm -rf build @@ -30,7 +39,7 @@ jobs: sudo apt-get install -y libcurl4-openssl-dev cmake -DCMAKE_BUILD_TYPE=Debug -B ./build cmake --build ./build - ./dictu tests/runTests.du | tee /dev/stderr | grep -q 'Total bytes lost: 0' + ./dictu tests/runTests.du ci | tee /dev/stderr | grep -q 'Total bytes lost: 0' test-mac-cmake: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -44,7 +53,7 @@ jobs: run: | cmake -DCMAKE_BUILD_TYPE=Debug -DDISABLE_HTTP=1 -B ./build cmake --build ./build - ./dictu tests/runTests.du | tee /dev/stderr | grep -q 'Total bytes lost: 0' + ./dictu tests/runTests.du ci | tee /dev/stderr | grep -q 'Total bytes lost: 0' - name: Remove build directory run: | rm -rf build @@ -52,7 +61,7 @@ jobs: run: | cmake -DCMAKE_BUILD_TYPE=Debug -B ./build cmake --build ./build - ./dictu tests/runTests.du | tee /dev/stderr | grep -q 'Total bytes lost: 0' + ./dictu tests/runTests.du ci | tee /dev/stderr | grep -q 'Total bytes lost: 0' test-windows-cmake: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -66,7 +75,7 @@ jobs: run: | cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_SYSTEM_VERSION="10.0.18362.0" -DCICD=1 -DDISABLE_HTTP=1 -DDISABLE_LINENOISE=1 -B build cmake --build build - Debug\dictu.exe tests/runTests.du + Debug\dictu.exe tests/runTests.du ci run-examples: name: Test Examples runs-on: ubuntu-latest @@ -78,4 +87,4 @@ jobs: sudo apt-get install -y libcurl4-openssl-dev cmake -DCMAKE_BUILD_TYPE=Debug -B ./build cmake --build ./build - ./dictu examples/runExamples.du | tee /dev/stderr | grep -q 'Total bytes lost: 0' + ./dictu examples/runExamples.du ci | tee /dev/stderr | grep -q 'Total bytes lost: 0' diff --git a/README.md b/README.md index 97956303..7592b9d7 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,32 @@ $ ./build/Dictu Refer to [Dictu Docker](https://github.com/dictu-lang/Dictu/blob/develop/Docker/README.md) +### FreeBSD Installation + +For a full installation, make sure `curl` and `linenoise` are installed. They can be installed from the commands below: + +```bash +$ pkg install -y curl linenoise-ng +``` + +The following variables need to be set/available to run `cmake` successfully. + +For Bourne compatible shells... + +```bash +export CPATH=/usr/local/include +export LIBRARY_PATH=/usr/local/lib +export LD_LIBRARY_PATH=/usr/local/lib +``` + +```bash +$ git clone -b master https://github.com/dictu-lang/Dictu.git +$ cd Dictu +$ cmake -DCMAKE_BUILD_TYPE=Release -B ./build +$ cmake --build ./build +$ ./dictu +``` + ## Extensions Dictu has a Visual Studio Code extension [here](https://marketplace.visualstudio.com/items?itemName=Dictu.dictuvsc) with the implementation located diff --git a/docs/_config.yml b/docs/_config.yml index 54575b53..a634b5ac 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -5,7 +5,7 @@ description: >- color_scheme: "dictu" # Custom theme logo: "/assets/images/dictu-logo/dictu-wordmark.svg" -version: "0.25.0" +version: "0.26.0" github_username: dictu-lang search_enabled: true diff --git a/docs/docs/built-ins.md b/docs/docs/built-ins.md index ca934094..8f84a4bc 100644 --- a/docs/docs/built-ins.md +++ b/docs/docs/built-ins.md @@ -25,7 +25,7 @@ The path name of the compilation unit. Global functions which are built into Dictu. -### print(...values...) +### print(...values) Prints a given list of values to stdout. @@ -35,7 +35,7 @@ print("test"); // "test" print(10, "test", nil, true); // 10, "test", nil, true ``` -### printError(...values...) +### printError(...values) Prints a given list of values to stderr. diff --git a/docs/docs/classes.md b/docs/docs/classes.md index 18320ff3..97d99dcd 100644 --- a/docs/docs/classes.md +++ b/docs/docs/classes.md @@ -318,6 +318,16 @@ class Test {} print(Test()._class); // ``` +### _name + +`_name` is a special attribute that is added to classes that returns a string representation of the class name. + +```cs +class Test {} + +print(Test.name); // Test +``` + ## Class variables A class variable is a variable that is defined on the class and not the instance. This means that all instances of the class will have access diff --git a/docs/docs/collections/sets.md b/docs/docs/collections/sets.md index db03ff75..c541b945 100644 --- a/docs/docs/collections/sets.md +++ b/docs/docs/collections/sets.md @@ -83,6 +83,17 @@ print(mySet.contains("Dictu!")); // true print(mySet.contains("Other!")); // false ``` +### set.containsAll(value) + +To check if a set contains all elements in a given list use `.containsAll()` + +```cs +var mySet = set("one",1,2,3);; +print(mySet.containsAll(["one",1])); // true +print(mySet.containsAll([1,2,3])); // true +print(mySet.containsAll(["one",1,2,3,"x"])); // false +``` + ### set.remove(value) To remove a value from a set use `.remove()`. diff --git a/docs/docs/functions.md b/docs/docs/functions.md index e534ebbf..c7e58009 100644 --- a/docs/docs/functions.md +++ b/docs/docs/functions.md @@ -189,4 +189,21 @@ class BadClass { def badFunction(a=5, ...x) { // ... } +``` + +### Argument Unpacking + +Sometimes you may have a list of values and you wish to pass them all to a function. Rather than having +to loop over a list within the function and pull out singular values, you can unpack this list at the call site. + +Note: This will work on built-ins, class constructors and methods along with functions. + +``` +const myList = [1, 2, 3]; + +def printMyList(a, b, c) { + print(a, b, c); +} + +printMyList(...myList); // 1 2 3 ``` \ No newline at end of file diff --git a/docs/docs/standard-lib/env.md b/docs/docs/standard-lib/env.md index 0d67dabf..51409d6c 100644 --- a/docs/docs/standard-lib/env.md +++ b/docs/docs/standard-lib/env.md @@ -48,6 +48,16 @@ Env.set("key", nil); // Remove env var Env.set("key", 10); // set() arguments must be a string or nil. ``` +### Env.clearAll() + +Clears all set environment variables. + +Note: This is not available on Windows systems. + +```cs +Env.clearAll(); +``` + ### Env.readFile(string: path -> optional) To read environment variables from a file this helper method is provided. diff --git a/docs/docs/standard-lib/http.md b/docs/docs/standard-lib/http.md index 5f9a273d..a31d0a34 100644 --- a/docs/docs/standard-lib/http.md +++ b/docs/docs/standard-lib/http.md @@ -39,7 +39,7 @@ HTTP.get("https://httpbin.org/get", ["Content-Type: application/json"], 1); ### HTTP.post(string, dictionary: postArgs -> optional, list: headers -> optional, number: timeout -> optional) -Sends a HTTP POST request to a given URL.Timeout is given in seconds. +Sends a HTTP POST request to a given URL. Timeout is given in seconds. Returns a Result and unwraps to a Response upon success. ```cs @@ -49,9 +49,51 @@ HTTP.post("https://httpbin.org/post", {"test": 10}, ["Content-Type: application/ HTTP.post("https://httpbin.org/post", {"test": 10}, ["Content-Type: application/json"], 1); ``` +### HTTP.newClient(dict) + +Creates a new HTTP client with a given set of options. +Returns a Result and neecds to be unwraped upon success. + +```cs +const opts = { + "timeout": 20, + "headers": [ + "Content-Type: application/json", + "Accept: application/json", + "User-Agent: Dictu" + ], + "insecure": false, + "keyFile": "", + "certFile": "", + "keyPasswd": "" +}; +var httpClient = HTTP.newClient(opts); +``` + +### httpClient.get(string) + +Sends a HTTP GET request to a given URL. +Returns a Result and unwraps to a Response upon success. + +```cs +httpClient.get("https://httpbin.org/get"); + +{"content": "...", "headers": ["...", "..."], "statusCode": 200} +``` + +### HTTP.post(string, dictionary: postArgs) + +Sends a HTTP POST request to a given URL. +Returns a Result and unwraps to a Response upon success. + +```cs +httpClient.post("https://httpbin.org/post"); +httpClient.post("https://httpbin.org/post", {"test": 10}); +``` + ### Response -Both HTTP.get() and HTTP.post() return a Result that unwraps a Response object on success, or nil on error. +Both HTTP.get(), HTTP.post(), httpClient.get(), and httpClient.post() return a Result that unwraps a Response object on success, or nil on error. The Response object returned has 3 public properties, "content", "headers" and "statusCode". "content" is the actual content returned from the HTTP request as a string, "headers" is a list of all the response headers and "statusCode" is a number denoting the status code from the response @@ -59,17 +101,17 @@ the response #### Quick Reference Table ##### Properties -| Property | Description | -|------------|--------------------------------------------------------| -| content | Raw string content returned from the HTTP request | -| headers | A list of headers returned from the HTTP request | -| statusCode | The status code returned from the HTTP request | +| Property | Description | +| ---------- | ------------------------------------------------- | +| content | Raw string content returned from the HTTP request | +| headers | A list of headers returned from the HTTP request | +| statusCode | The status code returned from the HTTP request | ##### Methods -| Method | Description | -|------------|--------------------------------------------------------| -| json | Convert the content property to JSON | +| Method | Description | +| ------ | ------------------------------------ | +| json | Convert the content property to JSON | Example response from [httpbin.org](https://httpbin.org) diff --git a/docs/docs/standard-lib/object.md b/docs/docs/standard-lib/object.md new file mode 100644 index 00000000..4e4c742e --- /dev/null +++ b/docs/docs/standard-lib/object.md @@ -0,0 +1,53 @@ +--- +layout: default +title: Object +nav_order: 17 +parent: Standard Library +--- + +# Object +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Object + +To make use of the Object module an import is required. + +```cs +import Object; +``` + +### Object.getClassRef(string) + +This method will attempt to get a class reference from the class name provided as a string. + +Returns a Result and unwraps to an Object upon success. + +**NOTE:** This currently only works if the class is defined in the global scope + +```cs +class Test {} + +Object.getClassRef("Test").unwrap(); // +``` + +### Object.createFrom(string) + +This method will attempt to create a new object from the class name provided as a string. + +Returns a Result and unwraps to an Object upon success. + +**NOTE:** This currently only works if the class is defined in the global scope + +```cs +class Test {} + +Object.createFrom("Test").unwrap(); // +``` diff --git a/docs/docs/standard-lib/system.md b/docs/docs/standard-lib/system.md index d80b7314..3e4c4ba2 100644 --- a/docs/docs/standard-lib/system.md +++ b/docs/docs/standard-lib/system.md @@ -246,3 +246,28 @@ Note: This is not available on Windows systems. ```cs System.chown("/path/to/file", 0, 0); ``` + +### System.uname() + +Returns the name and version of the system along with operating system and hardware information. + +Note: This is not available on Windows systems. + +```cs +System.uname(); +``` + +### System.mkdirTemp(string: directory_template -> optional) + +Makes a temporary directory. If an empty string is given, the temporary directory's name will be a random string created in the current working directory. If a string is passed in, the temporary directory will be created with that name in the current working directory. + +The directory template passed in **must** end with "XXXXXX". + +Returns a Result type and on success will unwrap to a the created directory name. + +Note: This is not available on Windows systems. + +```cs +System.mkdirTemp().unwrap(); // "VOO16s" +System.mkdirTemp("test_XXXXXX").unwrap(); // "test_0bL2qS" +``` \ No newline at end of file diff --git a/docs/docs/standard-lib/term.md b/docs/docs/standard-lib/term.md new file mode 100644 index 00000000..6eba74ad --- /dev/null +++ b/docs/docs/standard-lib/term.md @@ -0,0 +1,43 @@ +--- +layout: default +title: Term +nav_order: 18 +parent: Standard Library +--- + +# Term +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Term + +To make use of the Term module an import is required. + +```cs +import Term; +``` + +### Term.isatty(number) + +Returns a boolean indicating whether the file descriptor passed is attached to a tty. + +```cs +Term.isatty(0); +``` + +### Term.getSize() + +Returns the number of rows, columns, horizontal and vertical pixels of the attached terminal. + +```cs +Term.getSize(); +print(Term.getSize()); +// {"rows": 13, "columns": 145, "horizontal_pixels": 145, "vertical_pixels": 145} +``` diff --git a/docs/docs/strings.md b/docs/docs/strings.md index b2176af0..d46f28b3 100644 --- a/docs/docs/strings.md +++ b/docs/docs/strings.md @@ -257,4 +257,13 @@ Returns a title cased version of string with first letter of each word capitaliz "dictu language".title(); // Dictu Language "this documentation".title(); // This Documentation "once upon a time".title(); // Once Upon A Time +``` + +### string.repeat(number) + +Returns a new string with the original string repeated the given number of times. + +```cs +"ba"+"na".repeat(2); // banana +"la".repeat(2) + " land"; // lala land ``` \ No newline at end of file diff --git a/examples/httpClient.du b/examples/httpClient.du new file mode 100644 index 00000000..22eacf4f --- /dev/null +++ b/examples/httpClient.du @@ -0,0 +1,56 @@ +import HTTP; +import JSON; +import System; + +// main +{ + const opts = { + "timeout": 20, + "headers": [ + "Content-Type: application/json", + "Accept: application/json", + "User-Agent: Dictu" + ], + "insecure": false, + "keyFile": "", + "certFile": "", + "keyPasswd": "" + }; + var httpClient = HTTP.newClient(opts); + + var res = httpClient.get("https://httpbin.org/get"); + if (not res.success()) { + print(res.unwrapError()); + System.exit(1); + } + print("Status Code: {}".format(res.unwrap().statusCode)); + print(res.unwrap().json().unwrap()); + + httpClient.setInsecure(true); + // res = httpClient.get("https://httpbin.org/uuid"); + res = httpClient.get("https://localhost:4433"); + if (not res.success()) { + print(res.unwrapError()); + System.exit(1); + } + print(res.unwrap().content); + // const uuid = res.unwrap().json().unwrap()["uuid"]; + // print("UUID: {}".format(uuid)); + + res = httpClient.post("https://httpbin.org/post", {}); + if (not res.success()) { + print(res.unwrapError()); + System.exit(1); + } + print("Status Code: {}".format(res.unwrap().statusCode)); + print(res.unwrap().json().unwrap()); + + System.exit(0); +} + + + + + + + diff --git a/examples/mkdirTemp.du b/examples/mkdirTemp.du new file mode 100644 index 00000000..e7582552 --- /dev/null +++ b/examples/mkdirTemp.du @@ -0,0 +1,8 @@ +import Env; +import System; + +const tmpDir = Env.get("TMPDIR") + "XXXXXX"; + +var res = System.mkdirTemp(tmpDir); + +print(res); diff --git a/examples/reverse_proxy.du b/examples/reverse_proxy.du new file mode 100644 index 00000000..41155921 --- /dev/null +++ b/examples/reverse_proxy.du @@ -0,0 +1,103 @@ +// To test the code below, run the commmand below to setup +// some backend servers to connect to: +// +// for i in $(seq 10001 10003); do +// socat tcp-listen:${i},reuseaddr,fork exec:cat,nofork & +// done + +import Log; +import Random; +import Socket; +import System; + +const readBuffer = 2048; + +class Server { + init(var server, var port) {} +} + +const backendServers = [ + Server("127.0.0.1", 10001), + Server("127.0.0.1", 10002), + Server("127.0.0.1", 10003) +]; + +def selectServer() { + const i = Random.range(0, backendServers.len()-1); + return backendServers[i]; +} + +// main +{ + const log = Log.new(Log.stdout).unwrap(); + + log.println("Starting proxy server..."); + + var res = Socket.create(Socket.AF_INET, Socket.SOCK_STREAM); + if (not res.success()) { + log.fatalln(res.unwrapError()); + } + var socket = res.unwrap(); + + res = socket.setsockopt(Socket.SOL_SOCKET, Socket.SO_REUSEADDR); + if (not res.success()) { + log.fatalln(res.unwrapError()); + } + + res = socket.bind("127.0.0.1", 10000); + if (not res.success()) { + log.fatalln(res.unwrapError()); + } + + res = socket.listen(10000); + if (not res.success()) { + log.fatalln(res.unwrapError()); + } + + var [client, address] = socket.accept().unwrap(); + + res = Socket.create(Socket.AF_INET, Socket.SOCK_STREAM); + if (not res.success()) { + log.fatalln(res.unwrapError()); + } + var remote = res.unwrap(); + + const backendServer = selectServer(); + res = remote.connect(backendServer.server, backendServer.port); + if (not res.success()) { + log.fatalln(res.unwrapError()); + } + + while { + res = client.recv(readBuffer); + if (not res.success()) { + log.println(res.unwrapError()); + break; + } + var userInput = res.unwrap(); + + res = remote.write(userInput); + if (not res.success()) { + log.println(res.unwrapError()); + break; + } + + res = remote.recv(readBuffer); + if (not res.success()) { + log.println(res.unwrapError()); + break; + } + + res = client.write(res.unwrap()); + if (not res.success()) { + log.println(res.unwrapError()); + break; + } + } + remote.close(); + socket.close(); + + log.println("Shutting down proxy server..."); + + System.exit(0); +} diff --git a/examples/system.du b/examples/system.du new file mode 100644 index 00000000..2d5ecc13 --- /dev/null +++ b/examples/system.du @@ -0,0 +1,3 @@ +import System; + +print(System.uname()); diff --git a/examples/term.du b/examples/term.du new file mode 100644 index 00000000..ca415544 --- /dev/null +++ b/examples/term.du @@ -0,0 +1,14 @@ +import System; +import Term; + +// main +{ + const terminalSize = Term.getSize(); + print(terminalSize); + + if (Term.isatty(0)) { + print("we're in a terminal!"); + } + + System.exit(0); +} diff --git a/scripts/checkTests.du b/scripts/checkTests.du new file mode 100644 index 00000000..4ecc6688 --- /dev/null +++ b/scripts/checkTests.du @@ -0,0 +1,33 @@ +import System; +import Path; + +const ignored = [ + 'runTests.du', + 'benchmarks' +]; + +const dir = Path.listDir('tests').filter(def (dir) => { + return not ignored.contains(dir); +}); +const imports = []; + +with ('tests/runTests.du', 'r') { + var line; + + while((line = file.readLine()) != nil) { + line = line.strip(); + if (line.startsWith('import')) { + const importName = line[8:].split('/')[0]; + imports.push(importName); + } + } +} + +const missingImports = dir.filter(def (importName) => { + return not imports.contains(importName); +}); + +if (missingImports) { + print('There are missing imports in runTests.du: {}'.format(missingImports)); + System.exit(1); +} \ No newline at end of file diff --git a/scripts/generate.du b/scripts/generate.du index 2f12b70d..941efe65 100644 --- a/scripts/generate.du +++ b/scripts/generate.du @@ -31,7 +31,8 @@ var files = [ 'src/vm/datatypes/result/result', 'src/optionals/unittest/unittest', 'src/optionals/env/env', - 'src/optionals/http/http' + 'src/optionals/http/http', + 'src/optionals/object/object' ]; for (var i = 0; i < files.len(); i += 1) { diff --git a/src/include/dictu_include.h b/src/include/dictu_include.h index 3a144b92..5461a4a6 100644 --- a/src/include/dictu_include.h +++ b/src/include/dictu_include.h @@ -4,7 +4,7 @@ #include #define DICTU_MAJOR_VERSION "0" -#define DICTU_MINOR_VERSION "25" +#define DICTU_MINOR_VERSION "26" #define DICTU_PATCH_VERSION "0" #define DICTU_STRING_VERSION "Dictu Version: " DICTU_MAJOR_VERSION "." DICTU_MINOR_VERSION "." DICTU_PATCH_VERSION "\n" diff --git a/src/optionals/env/env.c b/src/optionals/env/env.c index 7d2458a6..51aea6b8 100644 --- a/src/optionals/env/env.c +++ b/src/optionals/env/env.c @@ -78,6 +78,29 @@ static Value set(DictuVM *vm, int argCount, Value *args) { return newResultSuccess(vm, NIL_VAL); } +#ifndef _WIN32 +static Value clearAll(DictuVM *vm, int argCount, Value *args) { + UNUSED(args); + + if (argCount != 0) { + runtimeError(vm, "clearAll() does not take arguments (%d given).", argCount); + return EMPTY_VAL; + } + + extern char **environ; + + for (; *environ; ++environ) { + char *name = strtok(*environ, "="); + int ret = unsetenv(name); + if (ret == -1) { + ERROR_RESULT; + } + } + + return newResultSuccess(vm, NIL_VAL); +} +#endif + Value createEnvModule(DictuVM *vm) { ObjClosure *closure = compileModuleToClosure(vm, "Env", DICTU_ENV_SOURCE); @@ -92,7 +115,9 @@ Value createEnvModule(DictuVM *vm) { */ defineNative(vm, &closure->function->module->values, "get", get); defineNative(vm, &closure->function->module->values, "set", set); - +#ifndef _WIN32 + defineNative(vm, &closure->function->module->values, "clearAll", clearAll); +#endif pop(vm); return OBJ_VAL(closure); diff --git a/src/optionals/http/http.c b/src/optionals/http/http.c index 472e78ff..60d3c26c 100644 --- a/src/optionals/http/http.c +++ b/src/optionals/http/http.c @@ -1,9 +1,9 @@ +#include #include #include #include #include "http.h" - #include "http-source.h" #define HTTP_METHOD_GET "GET" @@ -233,6 +233,8 @@ #define HTTP_MAX_HEADER_BYTES 1 << 20 // 1 MB +#define DEFAULT_REQUEST_TIMEOUT 20 + static void createResponse(DictuVM *vm, Response *response) { response->vm = vm; response->headers = newList(vm); @@ -350,7 +352,7 @@ static bool setRequestHeaders(DictuVM *vm, struct curl_slist *list, CURL *curl, return true; } -static ObjInstance *endRequest(DictuVM *vm, CURL *curl, Response response) { +static ObjInstance *endRequest(DictuVM *vm, CURL *curl, Response response, bool cleanup) { // Get status code curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.statusCode); ObjString *content; @@ -395,10 +397,12 @@ static ObjInstance *endRequest(DictuVM *vm, CURL *curl, Response response) { // Pop headers from createResponse pop(vm); - /* always cleanup */ - curl_easy_cleanup(curl); - curl_global_cleanup(); - + if (cleanup) { + /* always cleanup */ + curl_easy_cleanup(curl); + curl_global_cleanup(); + } + return responseInstance; } @@ -408,7 +412,7 @@ static Value get(DictuVM *vm, int argCount, Value *args) { return EMPTY_VAL; } - long timeout = 20; + long timeout = DEFAULT_REQUEST_TIMEOUT; ObjList *headers = NULL; if (argCount == 3) { @@ -481,7 +485,7 @@ static Value get(DictuVM *vm, int argCount, Value *args) { return newResultError(vm, errorString); } - return newResultSuccess(vm, OBJ_VAL(endRequest(vm, curl, response))); + return newResultSuccess(vm, OBJ_VAL(endRequest(vm, curl, response, true))); } /* always cleanup */ @@ -499,7 +503,7 @@ static Value post(DictuVM *vm, int argCount, Value *args) { return EMPTY_VAL; } - long timeout = 20; + long timeout = DEFAULT_REQUEST_TIMEOUT; ObjDict *postValuesDict = NULL; ObjString *postValueString = NULL; ObjList *headers = NULL; @@ -597,7 +601,7 @@ static Value post(DictuVM *vm, int argCount, Value *args) { return newResultError(vm, errorString); } - return newResultSuccess(vm, OBJ_VAL(endRequest(vm, curl, response))); + return newResultSuccess(vm, OBJ_VAL(endRequest(vm, curl, response, true))); } /* always cleanup */ @@ -609,6 +613,428 @@ static Value post(DictuVM *vm, int argCount, Value *args) { return newResultError(vm, errorString); } +typedef struct { + CURL *curl; +} HttpClient; + +#define AS_HTTP_CLIENT(v) ((HttpClient*)AS_ABSTRACT(v)->data) + +struct curl_slist *headerChunk = NULL; + +void freeHttpClient(DictuVM *vm, ObjAbstract *abstract) { + HttpClient *httpClient = (HttpClient*)abstract->data; + + curl_easy_cleanup(httpClient->curl); + curl_global_cleanup(); + + FREE(vm, HttpClient, abstract->data); +} + +char *httpClientToString(ObjAbstract *abstract) { + UNUSED(abstract); + + char *httpClientString = malloc(sizeof(char) * 13); + snprintf(httpClientString, 13, ""); + return httpClientString; +} + +static Value httpClientSetTimeout(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "setTimeout() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_NUMBER(args[1])) { + runtimeError(vm, "timeout value must be a number"); + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + curl_easy_setopt(httpClient->curl, CURLOPT_TIMEOUT, AS_NUMBER(args[1])); + + return NIL_VAL; +} + +static Value httpClientSetInsecure(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "setInsecure() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_BOOL(args[1])) { + runtimeError(vm, "insecure value must be a bool"); + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + if (AS_BOOL(args[1])) { + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYSTATUS, 0); + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYHOST, 0); + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYPEER, 0); + } else { + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYSTATUS, 1); + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYHOST, 1); + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYPEER, 1); + } + + return NIL_VAL; +} + +static Value httpClientSetHeaders(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "setHeaders() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_LIST(args[1])) { + runtimeError(vm, "headers value must be a ist"); + return EMPTY_VAL; + } + + if (IS_EMPTY(args[1])) { + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + ObjList *headers = AS_LIST(args[1]); + + headerChunk = NULL; + + for (int h = 0; h < headers->values.count; h++) { + headerChunk = curl_slist_append(headerChunk, AS_STRING(headers->values.values[h])->chars); + } + + curl_easy_setopt(httpClient->curl, CURLOPT_HTTPHEADER, headerChunk); + + return NIL_VAL; +} + +static Value httpClientSetKeyFile(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "setKeyFile() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_STRING(args[1])) { + runtimeError(vm, "keyFile value must be a string"); + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + curl_easy_setopt(httpClient->curl, CURLOPT_SSLKEY, AS_STRING(args[1])->chars); + + return NIL_VAL; +} + +static Value httpClientSetCertFile(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "setCertFile() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_STRING(args[1])) { + runtimeError(vm, "certFile value must be a string"); + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + curl_easy_setopt(httpClient->curl, CURLOPT_SSLKEY, AS_STRING(args[1])->chars); + + return NIL_VAL; +} + +static Value httpClientSetKeyPass(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "setKeyPasswd() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_STRING(args[1])) { + runtimeError(vm, "keyPasswd value must be a string"); + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + curl_easy_setopt(httpClient->curl, CURLOPT_SSLKEY, AS_STRING(args[1])->chars); + + return NIL_VAL; +} + +static Value httpClientGet(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "get() takes 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + if (!IS_STRING(args[1])) { + runtimeError(vm, "URL passed to get() must be a string."); + return EMPTY_VAL; + } + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + CURLcode curlResponse; + + if (httpClient) { + Response response; + createResponse(vm, &response); + char *url = AS_CSTRING(args[1]); + + curl_easy_setopt(httpClient->curl, CURLOPT_URL, url); + curl_easy_setopt(httpClient->curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(httpClient->curl, CURLOPT_WRITEFUNCTION, writeResponse); + curl_easy_setopt(httpClient->curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(httpClient->curl, CURLOPT_HEADERDATA, &response); + + curlResponse = curl_easy_perform(httpClient->curl); + + if (curlResponse != CURLE_OK) { + pop(vm); + + char *errorString = (char *)curl_easy_strerror(curlResponse); + return newResultError(vm, errorString); + } + + return newResultSuccess(vm, OBJ_VAL(endRequest(vm, httpClient->curl, response, false))); + } + + pop(vm); + + char *errorString = (char *)curl_easy_strerror(CURLE_FAILED_INIT); + return newResultError(vm, errorString); +} + +static Value httpClientPost(DictuVM *vm, int argCount, Value *args) { + if (argCount < 1 || argCount > 2) { + runtimeError(vm, "post() takes at least 1 argument (%d given).", argCount); + return EMPTY_VAL; + } + + ObjDict *postValuesDict = NULL; + ObjString *postValueString = NULL; + + if (argCount == 2) { + if (IS_DICT(args[2])) { + postValuesDict = AS_DICT(args[2]); + } else if (IS_STRING(args[2])) { + postValueString = AS_STRING(args[2]); + } else { + runtimeError(vm, "Post values passed to post() must be a dictionary or a string."); + return EMPTY_VAL; + } + } + + if (!IS_STRING(args[1])) { + runtimeError(vm, "URL passed to post() must be a string."); + return EMPTY_VAL; + } + + CURLcode curlResponse; + + HttpClient *httpClient = AS_HTTP_CLIENT(args[0]); + + if (httpClient) { + Response response; + createResponse(vm, &response); + char *url = AS_CSTRING(args[1]); + char *postValue = ""; + + if (postValuesDict != NULL) { + postValue = dictToPostArgs(postValuesDict); + } else if (postValueString != NULL) { + postValue = postValueString->chars; + } + + curl_easy_setopt(httpClient->curl, CURLOPT_URL, url); + curl_easy_setopt(httpClient->curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(httpClient->curl, CURLOPT_POSTFIELDS, postValue); + curl_easy_setopt(httpClient->curl, CURLOPT_WRITEFUNCTION, writeResponse); + curl_easy_setopt(httpClient->curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(httpClient->curl, CURLOPT_HEADERDATA, &response); + + curlResponse = curl_easy_perform(httpClient->curl); + + if (postValuesDict != NULL) { + free(postValue); + } + + if (curlResponse != CURLE_OK) { + pop(vm); + + char *errorString = (char *)curl_easy_strerror(curlResponse); + return newResultError(vm, errorString); + } + + return newResultSuccess(vm, OBJ_VAL(endRequest(vm, httpClient->curl, response, false))); + } + + pop(vm); + + char *errorString = (char *) curl_easy_strerror(CURLE_FAILED_INIT); + return newResultError(vm, errorString); +} + +ObjAbstract *newHttpClient(DictuVM *vm, ObjDict *opts) { + ObjAbstract *abstract = newAbstract(vm, freeHttpClient, httpClientToString); + push(vm, OBJ_VAL(abstract)); + + HttpClient *httpClient = ALLOCATE(vm, HttpClient, 1); + httpClient->curl = curl_easy_init(); + + curl_easy_setopt(httpClient->curl, CURLOPT_HEADERFUNCTION, writeHeaders); + + if (opts->count != 0) { + for (int i = 0; i <= opts->capacityMask; i++) { + DictItem *entry = &opts->entries[i]; + if (IS_EMPTY(entry->key)) { + continue; + } + + char *key; + + if (IS_STRING(entry->key)) { + ObjString *s = AS_STRING(entry->key); + key = s->chars; + } else { + runtimeError(vm, "HTTP client options key must be a string"); + return abstract; + } + + if (strstr(key, "timeout")) { + if (IS_EMPTY(entry->value)) { + continue; + } + if (!IS_NUMBER(entry->value)) { + runtimeError(vm, "HTTP client option \"timeout\" value must be a number"); + return abstract; + } + + curl_easy_setopt(httpClient->curl, CURLOPT_TIMEOUT, AS_NUMBER(entry->value)); + } else if (strstr(key, "headers")) { + if (IS_EMPTY(entry->value)) { + continue; + } + + if (!IS_LIST(entry->value)) { + runtimeError(vm, "HTTP client option \"headers\" value must be a list"); + return abstract; + } + + ObjList *headers = AS_LIST(entry->value); + + for (int h = 0; h < headers->values.count; h++) { + headerChunk = curl_slist_append(headerChunk, AS_STRING(headers->values.values[h])->chars); + } + + curl_easy_setopt(httpClient->curl, CURLOPT_HTTPHEADER, headerChunk); + } else if (strstr(key, "insecure")) { + if (IS_EMPTY(entry->value)) { + continue; + } + + if (!IS_BOOL(entry->value)) { + runtimeError(vm, "HTTP client option \"insecure\" value must be a bool"); + return abstract; + } + + if (AS_BOOL(entry->value)) { + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYSTATUS, 0); + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYHOST, 0); + curl_easy_setopt(httpClient->curl, CURLOPT_SSL_VERIFYPEER, 0); + } + } else if (strstr(key, "keyFile")) { + if (IS_EMPTY(entry->value)) { + continue; + } + + if (!IS_STRING(entry->value)) { + runtimeError(vm, "HTTP client option \"keyFile\" value must be a string"); + return abstract; + } + + char *keyFile = AS_STRING(entry->value)->chars; + if (keyFile[0] == '\0') { + continue; + } + + curl_easy_setopt(httpClient->curl, CURLOPT_SSLKEY, keyFile); + } else if (strstr(key, "certFile")) { + if (IS_EMPTY(entry->value)) { + continue; + } + + if (!IS_STRING(entry->value)) { + runtimeError(vm, "HTTP client option \"certFile\" value must be a string"); + return abstract; + } + + char *certFile = AS_STRING(entry->value)->chars; + if (certFile[0] == '\0') { + continue; + } + + curl_easy_setopt(httpClient->curl, CURLOPT_SSLCERT, certFile); + } else if (strstr(key, "keyPasswd")) { + if (IS_EMPTY(entry->value)) { + continue; + } + + if (!IS_STRING(entry->value)) { + runtimeError(vm, "HTTP client option key \"keyPasswd\" value must be a string"); + return abstract; + } + + char *keyPasswd = AS_STRING(entry->value)->chars; + if (keyPasswd[0] == '\0') { + continue; + } + + curl_easy_setopt(httpClient->curl, CURLOPT_KEYPASSWD, keyPasswd); + } + } + } + + abstract->data = httpClient; + + /** + * Setup HTTP object methods + */ + defineNative(vm, &abstract->values, "get", httpClientGet); + defineNative(vm, &abstract->values, "post", httpClientPost); + defineNative(vm, &abstract->values, "setTimeout", httpClientSetTimeout); + defineNative(vm, &abstract->values, "setHeaders", httpClientSetHeaders); + defineNative(vm, &abstract->values, "setInsecure", httpClientSetInsecure); + defineNative(vm, &abstract->values, "setKeyFile", httpClientSetKeyFile); + defineNative(vm, &abstract->values, "setCertFile", httpClientSetCertFile); + defineNative(vm, &abstract->values, "setKeyPass", httpClientSetKeyPass); + pop(vm); + + return abstract; +} + +static Value newClient(DictuVM *vm, int argCount, Value *args) { + if (argCount > 1) { + runtimeError(vm, "newClient() takes 1 argument (%d given)", argCount); + return EMPTY_VAL; + } + + if (!IS_DICT(args[0])) { + runtimeError(vm, "Options dict passed to newClient() must be a dict."); + return EMPTY_VAL; + } + + ObjDict *opts = AS_DICT(args[0]); + + ObjAbstract *hc = newHttpClient(vm, opts); + return OBJ_VAL(hc); +} + Value createHTTPModule(DictuVM *vm) { ObjClosure *closure = compileModuleToClosure(vm, "HTTP", DICTU_HTTP_SOURCE); @@ -853,6 +1279,8 @@ Value createHTTPModule(DictuVM *vm) { defineNative(vm, &module->values, "get", get); defineNative(vm, &module->values, "post", post); + defineNative(vm, &module->values, "newClient", newClient); + pop(vm); return OBJ_VAL(closure); diff --git a/src/optionals/object/object-source.h b/src/optionals/object/object-source.h new file mode 100644 index 00000000..48c9fa33 --- /dev/null +++ b/src/optionals/object/object-source.h @@ -0,0 +1,9 @@ +#define DICTU_OBJECT_SOURCE "import Object;\n" \ +"\n" \ +"def createFrom(className, ...arguments) {\n" \ +" return Object.__getClassRef(className).matchWrap(\n" \ +" def (klass) => klass(...arguments),\n" \ +" def (error) => error\n" \ +" );\n" \ +"}\n" \ + diff --git a/src/optionals/object/object.c b/src/optionals/object/object.c new file mode 100644 index 00000000..0b2bb61f --- /dev/null +++ b/src/optionals/object/object.c @@ -0,0 +1,69 @@ +#include "object.h" + +#include "object-source.h" + +static Value objectGetClassRefImpl(DictuVM *vm, int argCount, Value *args, bool internal) { + if (argCount != 1) { + runtimeError(vm, "getClassRef() takes 1 argument (%d given)", argCount); + return EMPTY_VAL; + } + + if (!IS_STRING(args[0])) { + runtimeError(vm, "getClassRef() argument must be a string"); + return EMPTY_VAL; + } + + ObjString *classString = AS_STRING(args[0]); + + Value klass; + CallFrame *frame; + if (internal) { + // -2 as we want to go to the callee site, not the Object.du code + frame = &vm->frames[vm->frameCount - 2]; + } else { + frame = &vm->frames[vm->frameCount - 1]; + } + + if (tableGet(&frame->closure->function->module->values, classString, &klass) && IS_CLASS(klass)) { + return newResultSuccess(vm, klass); + } + + char *error = ALLOCATE(vm, char, classString->length + 26); + memcpy(error, classString->chars, classString->length); + memcpy(error + classString->length, " class could not be found", 25); + error[classString->length + 25] = '\0'; + + Value result = newResultError(vm, error); + + FREE_ARRAY(vm, char, error, classString->length + 26); + + return result; +} + +static Value objectGetClassRef(DictuVM *vm, int argCount, Value *args) { + return objectGetClassRefImpl(vm, argCount, args, false); +} + +static Value objectGetClassRefInternal(DictuVM *vm, int argCount, Value *args) { + return objectGetClassRefImpl(vm, argCount, args, true); +} + +Value createObjectModule(DictuVM *vm) { + ObjClosure *closure = compileModuleToClosure(vm, "Object", DICTU_OBJECT_SOURCE); + + if (closure == NULL) { + return EMPTY_VAL; + } + + push(vm, OBJ_VAL(closure)); + + /** + * Define Object methods + */ + defineNative(vm, &closure->function->module->values, "__getClassRef", objectGetClassRefInternal); + defineNative(vm, &closure->function->module->values, "getClassRef", objectGetClassRef); + + pop(vm); + + return OBJ_VAL(closure); +} \ No newline at end of file diff --git a/src/optionals/object/object.du b/src/optionals/object/object.du new file mode 100644 index 00000000..e8471041 --- /dev/null +++ b/src/optionals/object/object.du @@ -0,0 +1,8 @@ +import Object; + +def createFrom(className, ...arguments) { + return Object.__getClassRef(className).matchWrap( + def (klass) => klass(...arguments), + def (error) => error + ); +} \ No newline at end of file diff --git a/src/optionals/object/object.h b/src/optionals/object/object.h new file mode 100644 index 00000000..c8384cf8 --- /dev/null +++ b/src/optionals/object/object.h @@ -0,0 +1,11 @@ +#ifndef dictu_object_module_h +#define dictu_object_module_h + +#include + +#include "../optionals.h" +#include "../../vm/vm.h" + +Value createObjectModule(DictuVM *vm); + +#endif //dictu_object_module_h diff --git a/src/optionals/optionals.c b/src/optionals/optionals.c index b5e2f195..41413bb4 100644 --- a/src/optionals/optionals.c +++ b/src/optionals/optionals.c @@ -14,8 +14,10 @@ BuiltinModules modules[] = { {"Sqlite", &createSqliteModule, false}, {"Process", &createProcessModule, false}, {"System", &createSystemModule, false}, + {"Term", &createTermModule, false}, {"UnitTest", &createUnitTestModule, true}, {"Inspect", &createInspectModule, false}, + {"Object", &createObjectModule, true}, #ifndef DISABLE_HTTP {"HTTP", &createHTTPModule, true}, #endif diff --git a/src/optionals/optionals.h b/src/optionals/optionals.h index 5572ccb9..090d90ab 100644 --- a/src/optionals/optionals.h +++ b/src/optionals/optionals.h @@ -18,6 +18,8 @@ #include "sqlite.h" #include "process.h" #include "inspect.h" +#include "term.h" +#include "object/object.h" #include "unittest/unittest.h" typedef Value (*BuiltinModule)(DictuVM *vm); diff --git a/src/optionals/system.c b/src/optionals/system.c index db6622be..1cc9df9f 100644 --- a/src/optionals/system.c +++ b/src/optionals/system.c @@ -100,6 +100,107 @@ static Value chownNative(DictuVM *vm, int argCount, Value *args) { return newResultSuccess(vm, EMPTY_VAL); } + +static Value unameNative(DictuVM *vm, int argCount, Value *args) { + UNUSED(args); + + if (argCount != 0) { + runtimeError(vm, "uname() doesn't take any arguments (%d given)).", argCount); + return EMPTY_VAL; + } + + struct utsname u; + if (uname(&u) == -1) { + runtimeError(vm, "uname() failed to retrieve information"); + return EMPTY_VAL; + } + + ObjDict *unameDict = newDict(vm); + push(vm, OBJ_VAL(unameDict)); + + ObjString *sysname = copyString(vm, "sysname", 7); + push(vm, OBJ_VAL(sysname)); + ObjString *sysnameVal = copyString(vm, u.sysname, strlen(u.sysname)); + push(vm, OBJ_VAL(sysnameVal)); + dictSet(vm, unameDict, OBJ_VAL(sysname), OBJ_VAL(sysnameVal)); + pop(vm); + pop(vm); + + ObjString *nodename = copyString(vm, "nodename", 8); + push(vm, OBJ_VAL(nodename)); + ObjString *nodenameVal = copyString(vm, u.nodename, strlen(u.nodename)); + push(vm, OBJ_VAL(nodenameVal)); + dictSet(vm, unameDict, OBJ_VAL(nodename), OBJ_VAL(nodenameVal)); + pop(vm); + pop(vm); + + ObjString *machine = copyString(vm, "machine", 7); + push(vm, OBJ_VAL(machine)); + ObjString *machineVal = copyString(vm, u.machine, strlen(u.machine)); + push(vm, OBJ_VAL(machineVal)); + dictSet(vm, unameDict, OBJ_VAL(machine), OBJ_VAL(machineVal)); + pop(vm); + pop(vm); + + ObjString *release = copyString(vm, "release", 7); + push(vm, OBJ_VAL(release)); + ObjString *releaseVal = copyString(vm, u.release, strlen(u.release)); + push(vm, OBJ_VAL(releaseVal)); + dictSet(vm, unameDict, OBJ_VAL(release), OBJ_VAL(releaseVal)); + pop(vm); + pop(vm); + + ObjString *version = copyString(vm, "version", 7); + push(vm, OBJ_VAL(version)); + ObjString *versionVal = copyString(vm, u.version, strlen(u.version)); + push(vm, OBJ_VAL(versionVal)); + dictSet(vm, unameDict, OBJ_VAL(version), OBJ_VAL(versionVal)); + pop(vm); + pop(vm); + + pop(vm); + + return OBJ_VAL(unameDict); +} + +static Value mkdirTempNative(DictuVM *vm, int argCount, Value *args) { + if (argCount > 1) { + runtimeError(vm, "mkdirTemp() takes 0 or 1 argument(s) (%d given)", argCount); + return EMPTY_VAL; + } + + char *template = "XXXXXX"; + + if (argCount == 1) { + if (!IS_STRING(args[0])) { + runtimeError(vm, "mkdirTemp() first argument must be a string"); + return EMPTY_VAL; + } + + template = AS_CSTRING(args[0]); + } + + char *tmpl = {0}; + int size; + + if (template[0] != '\0') { + size = strlen(template) + 1; + tmpl = ALLOCATE(vm, char, size); + strcpy(tmpl, template); + } else { + size = 7; + tmpl = ALLOCATE(vm, char, size); + strcpy(tmpl, "XXXXXX"); + } + + char *tmpDir = mkdtemp(tmpl); + if (!tmpDir) { + FREE_ARRAY(vm, char, tmpl, size); + ERROR_RESULT; + } + + return newResultSuccess(vm, OBJ_VAL(takeString(vm, tmpDir, size - 1))); +} #endif static Value rmdirNative(DictuVM *vm, int argCount, Value *args) { @@ -414,6 +515,8 @@ Value createSystemModule(DictuVM *vm) { defineNative(vm, &module->values, "getppid", getppidNative); defineNative(vm, &module->values, "getpid", getpidNative); defineNative(vm, &module->values, "chown", chownNative); + defineNative(vm, &module->values, "uname", unameNative); + defineNative(vm, &module->values, "mkdirTemp", mkdirTempNative); #endif defineNative(vm, &module->values, "rmdir", rmdirNative); defineNative(vm, &module->values, "mkdir", mkdirNative); diff --git a/src/optionals/term.c b/src/optionals/term.c new file mode 100644 index 00000000..aebc7c3b --- /dev/null +++ b/src/optionals/term.c @@ -0,0 +1,102 @@ +#include "term.h" + +#ifdef _WIN32 +#define isatty(fd) _isatty(fd) +#endif + +static Value termIsattyNative(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "isatty() takes 1 argument (%d given)", argCount); + return EMPTY_VAL; + } + + if (!IS_NUMBER(args[0])) { + runtimeError(vm, "arg passed to isatty must be a number."); + return EMPTY_VAL; + } + + if (isatty(AS_NUMBER(args[0]))) { + return BOOL_VAL(true); + } + + return BOOL_VAL(false); +} + +static Value getSizeNative(DictuVM *vm, int argCount, Value *args) { + UNUSED(args); + + if (argCount != 0) { + runtimeError(vm, "getSize() doesn't take any arguments (%d given)).", argCount); + return EMPTY_VAL; + } + + ObjDict *terminalSizeDict = newDict(vm); + push(vm, OBJ_VAL(terminalSizeDict)); + + ObjString *lines = copyString(vm, "rows", 4); + push(vm, OBJ_VAL(lines)); + + ObjString *columns = copyString(vm, "columns", 7); + push(vm, OBJ_VAL(columns)); + +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + + int height = (int)(csbi.srWindow.Bottom-csbi.srWindow.Top+1); + int width = (int)(csbi.srWindow.Right-csbi.srWindow.Left+1); + + dictSet(vm, terminalSizeDict, OBJ_VAL(lines), NUMBER_VAL(height)); + pop(vm); + + dictSet(vm, terminalSizeDict, OBJ_VAL(columns), NUMBER_VAL(width)); + pop(vm); +#else + ObjString *horizontalPixesl = copyString(vm, "horizontal_pixels", 17); + push(vm, OBJ_VAL(horizontalPixesl)); + + ObjString *verticalPixels = copyString(vm, "vertical_pixels", 15); + push(vm, OBJ_VAL(verticalPixels)); + + struct winsize w; + ioctl(0, TIOCGWINSZ, &w); + + dictSet(vm, terminalSizeDict, OBJ_VAL(lines), NUMBER_VAL(w.ws_row)); + pop(vm); + + dictSet(vm, terminalSizeDict, OBJ_VAL(columns), NUMBER_VAL(w.ws_col)); + pop(vm); + + dictSet(vm, terminalSizeDict, OBJ_VAL(horizontalPixesl), NUMBER_VAL(w.ws_col)); + pop(vm); + + dictSet(vm, terminalSizeDict, OBJ_VAL(verticalPixels), NUMBER_VAL(w.ws_col)); + pop(vm); +#endif + + pop(vm); + + return OBJ_VAL(terminalSizeDict); +} + +Value createTermModule(DictuVM *vm) { + ObjString *name = copyString(vm, "Term", 4); + push(vm, OBJ_VAL(name)); + ObjModule *module = newModule(vm, name); + push(vm, OBJ_VAL(module)); + + /** + * Define Term methods + */ + defineNative(vm, &module->values, "isatty", termIsattyNative); + defineNative(vm, &module->values, "getSize", getSizeNative); + + /** + * Define Term properties + */ + + pop(vm); + pop(vm); + + return OBJ_VAL(module); +} diff --git a/src/optionals/term.h b/src/optionals/term.h new file mode 100644 index 00000000..b0c9b0f1 --- /dev/null +++ b/src/optionals/term.h @@ -0,0 +1,17 @@ +#ifndef dictu_term_h +#define dictu_term_h + +#ifdef _WIN32 +#include "windowsapi.h" +#else +#include +#include +#endif + +#include "optionals.h" +#include "../vm/vm.h" +#include "../vm/memory.h" + +Value createTermModule(DictuVM *vm); + +#endif //dictu_term_h diff --git a/src/vm/compiler.c b/src/vm/compiler.c index a6e1fbc8..e1ac1283 100644 --- a/src/vm/compiler.c +++ b/src/vm/compiler.c @@ -61,6 +61,12 @@ static void advance(Parser *parser) { } } +static void recede(Parser *parser){ + for (int i = 0; i < parser->current.length; ++i) { + backTrack(&parser->scanner); + } +} + static void consume(Compiler *compiler, TokenType type, const char *message) { if (compiler->parser->current.type == type) { advance(compiler->parser); @@ -161,7 +167,6 @@ static void initCompiler(Parser *parser, Compiler *compiler, Compiler *parent, F if (parent != NULL) { compiler->class = parent->class; - compiler->loop = parent->loop; } compiler->type = type; @@ -433,10 +438,19 @@ static void defineVariable(Compiler *compiler, uint8_t global, bool constant) { } } -static int argumentList(Compiler *compiler) { +static int argumentList(Compiler *compiler, bool *unpack) { int argCount = 0; + if (!check(compiler, TOKEN_RIGHT_PAREN)) { do { + if (*unpack) { + errorAtCurrent(compiler->parser, "Value unpacking must be the last argument."); + } + + if (match(compiler, TOKEN_DOT_DOT_DOT)) { + *unpack = true; + } + expression(compiler); argCount++; @@ -641,9 +655,12 @@ static void ternary(Compiler *compiler, Token previousToken, bool canAssign) { static void call(Compiler *compiler, Token previousToken, bool canAssign) { UNUSED(previousToken); UNUSED(canAssign); + bool unpack = false; + + int argCount = argumentList(compiler, &unpack); - int argCount = argumentList(compiler); emitBytes(compiler, OP_CALL, argCount); + emitByte(compiler, unpack); } static bool privatePropertyExists(Token name, Compiler *compiler) { @@ -662,13 +679,16 @@ static void dot(Compiler *compiler, Token previousToken, bool canAssign) { Token identifier = compiler->parser->previous; if (match(compiler, TOKEN_LEFT_PAREN)) { - int argCount = argumentList(compiler); + bool unpack = false; + + int argCount = argumentList(compiler, &unpack); if (compiler->class != NULL && (previousToken.type == TOKEN_THIS || identifiersEqual(&previousToken, &compiler->class->name))) { emitBytes(compiler, OP_INVOKE_INTERNAL, argCount); } else { emitBytes(compiler, OP_INVOKE, argCount); } - emitByte(compiler, name); + + emitBytes(compiler, name, unpack); return; } @@ -1300,11 +1320,12 @@ static void super_(Compiler *compiler, bool canAssign) { namedVariable(compiler, syntheticToken("this"), false); if (match(compiler, TOKEN_LEFT_PAREN)) { - int argCount = argumentList(compiler); + bool unpack = false; + int argCount = argumentList(compiler, &unpack); pushSuperclass(compiler); emitBytes(compiler, OP_SUPER, argCount); - emitByte(compiler, name); + emitBytes(compiler, name, unpack); } else { pushSuperclass(compiler); emitBytes(compiler, OP_GET_SUPER, name); @@ -1906,7 +1927,7 @@ static void varDeclaration(Compiler *compiler, bool constant) { consume(compiler, TOKEN_SEMICOLON, "Expect ';' after variable declaration."); } -static void expressionStatement(Compiler *compiler) { +static void expressionStatement(Compiler *compiler) { Token previous = compiler->parser->previous; advance(compiler->parser); TokenType t = compiler->parser->current.type; @@ -1919,7 +1940,7 @@ static void expressionStatement(Compiler *compiler) { expression(compiler); consume(compiler, TOKEN_SEMICOLON, "Expect ';' after expression."); - if (compiler->parser->vm->repl && t != TOKEN_EQUAL) { + if (compiler->parser->vm->repl && t != TOKEN_EQUAL && compiler->type == TYPE_TOP_LEVEL) { emitByte(compiler, OP_POP_REPL); } else { emitByte(compiler, OP_POP); @@ -1982,7 +2003,6 @@ static int getArgCount(uint8_t *code, const ValueArray constants, int ip) { case OP_SET_INIT_PROPERTIES: case OP_SET_PRIVATE_INIT_PROPERTIES: case OP_GET_SUPER: - case OP_CALL: case OP_METHOD: case OP_IMPORT: case OP_NEW_LIST: @@ -1997,14 +2017,17 @@ static int getArgCount(uint8_t *code, const ValueArray constants, int ip) { case OP_JUMP_IF_NIL: case OP_JUMP_IF_FALSE: case OP_LOOP: - case OP_INVOKE: - case OP_INVOKE_INTERNAL: - case OP_SUPER: case OP_CLASS: case OP_SUBCLASS: case OP_IMPORT_BUILTIN: + case OP_CALL: return 2; + case OP_INVOKE: + case OP_INVOKE_INTERNAL: + case OP_SUPER: + return 3; + case OP_IMPORT_BUILTIN_VARIABLE: { int argCount = code[ip + 2]; @@ -2464,6 +2487,71 @@ static void whileStatement(Compiler *compiler) { endLoop(compiler); } +static void unpackListStatement(Compiler *compiler){ + int varCount = 0; + Token variables[255]; + Token previous=compiler->parser->previous; + do { + if(!check(compiler,TOKEN_IDENTIFIER)){ + if(varCount>0){ + recede(compiler->parser); + } + int varsAndCommas=varCount+(varCount-1); + for(int var=0;varparser); + } + recede(compiler->parser); + compiler->parser->current=previous; + expressionStatement(compiler); + return; + } + consume(compiler, TOKEN_IDENTIFIER, "Expect variable name."); + variables[varCount] = compiler->parser->previous; + varCount++; + } while (match(compiler, TOKEN_COMMA)); + + consume(compiler, TOKEN_RIGHT_BRACKET, "Expect ']' after list destructure."); + + if(!check(compiler, TOKEN_EQUAL)){ + recede(compiler->parser); + int varsAndCommas=varCount+(varCount-1); + for(int var=0;varparser); + } + recede(compiler->parser); + compiler->parser->current=previous; + expressionStatement(compiler); + return; + } + + consume(compiler, TOKEN_EQUAL, "Expect '=' after list destructure."); + + expression(compiler); + + emitBytes(compiler, OP_UNPACK_LIST, varCount); + + + for(int i=varCount-1;i>-1;i--){ + Token token=variables[i]; + + uint8_t setOp; + int arg = resolveLocal(compiler, &token, false); + if (arg != -1) { + setOp = OP_SET_LOCAL; + } else if ((arg = resolveUpvalue(compiler, &token)) != -1) { + setOp = OP_SET_UPVALUE; + } else { + arg = identifierConstant(compiler, &token); + setOp = OP_SET_MODULE; + } + checkConst(compiler, setOp, arg); + emitBytes(compiler, setOp, (uint8_t) arg); + emitByte(compiler, OP_POP); + } + + consume(compiler, TOKEN_SEMICOLON, "Expect ';' after variable declaration."); +} + static void synchronize(Parser *parser) { parser->panicMode = false; @@ -2547,6 +2635,8 @@ static void statement(Compiler *compiler) { breakStatement(compiler); } else if (match(compiler, TOKEN_WHILE)) { whileStatement(compiler); + } else if (match(compiler, TOKEN_LEFT_BRACKET)) { + unpackListStatement(compiler); } else if (match(compiler, TOKEN_LEFT_BRACE)) { Parser *parser = compiler->parser; Token previous = parser->previous; diff --git a/src/vm/datatypes/sets.c b/src/vm/datatypes/sets.c index 59e49c98..f898a21f 100644 --- a/src/vm/datatypes/sets.c +++ b/src/vm/datatypes/sets.c @@ -70,12 +70,36 @@ static Value containsSetItem(DictuVM *vm, int argCount, Value *args) { return setGet(set, args[1]) ? TRUE_VAL : FALSE_VAL; } +static Value containsAllSet(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "containsAll() takes 1 argument (%d given)", argCount); + return EMPTY_VAL; + } + + if(!IS_LIST(args[1])){ + runtimeError(vm, "containsAll() argument must be a list"); + return EMPTY_VAL; + } + + ObjSet *set = AS_SET(args[0]); + ObjList *list = AS_LIST(args[1]); + + int listSize = list->values.count; + for(int index=0;indexvalues.values[index])==false){ + return FALSE_VAL; + } + } + return TRUE_VAL; +} + void declareSetMethods(DictuVM *vm) { defineNative(vm, &vm->setMethods, "toString", toStringSet); defineNative(vm, &vm->setMethods, "len", lenSet); defineNative(vm, &vm->setMethods, "add", addSetItem); defineNative(vm, &vm->setMethods, "remove", removeSetItem); defineNative(vm, &vm->setMethods, "contains", containsSetItem); + defineNative(vm, &vm->setMethods, "containsAll", containsAllSet); defineNative(vm, &vm->setMethods, "toBool", boolNative); // Defined in util } diff --git a/src/vm/datatypes/strings.c b/src/vm/datatypes/strings.c index e234239a..296084e0 100644 --- a/src/vm/datatypes/strings.c +++ b/src/vm/datatypes/strings.c @@ -530,6 +530,31 @@ static Value titleString(DictuVM *vm, int argCount, Value *args) { return OBJ_VAL(takeString(vm, temp, string->length)); } +static Value repeatString(DictuVM *vm, int argCount, Value *args) { + if (argCount != 1) { + runtimeError(vm, "repeat() takes one argument (%d given)", argCount); + return EMPTY_VAL; + } + + if (!IS_NUMBER(args[1])) { + runtimeError(vm, "repeat() count argument must be a number"); + return EMPTY_VAL; + } + + ObjString *string = AS_STRING(args[0]); + int count = AS_NUMBER(args[1]); + + int tempLen = (string->length * count) + 1; + char *temp = ALLOCATE(vm, char, tempLen); + + strcpy(temp, string->chars); + while (--count > 0) { + strcat(temp, string->chars); + } + + return OBJ_VAL(takeString(vm, temp, tempLen - 1)); +} + void declareStringMethods(DictuVM *vm) { defineNative(vm, &vm->stringMethods, "len", lenString); defineNative(vm, &vm->stringMethods, "toNumber", toNumberString); @@ -548,4 +573,6 @@ void declareStringMethods(DictuVM *vm) { defineNative(vm, &vm->stringMethods, "count", countString); defineNative(vm, &vm->stringMethods, "toBool", boolNative); // Defined in util defineNative(vm, &vm->stringMethods, "title", titleString); + defineNative(vm, &vm->stringMethods, "repeat", repeatString); + } diff --git a/src/vm/debug.c b/src/vm/debug.c index 8906119c..4b72b197 100644 --- a/src/vm/debug.c +++ b/src/vm/debug.c @@ -22,14 +22,22 @@ static int constantInstruction(const char *name, Chunk *chunk, return offset + 2; } +static int callInstruction(const char *name, Chunk *chunk, int offset) { + uint8_t argCount = chunk->code[offset + 1]; + uint8_t unpack = chunk->code[offset + 2]; + printf("%-16s (%d args) Unpack - %d '", name, argCount, unpack); + return offset + 3; +} + static int invokeInstruction(const char* name, Chunk* chunk, int offset) { uint8_t argCount = chunk->code[offset + 1]; uint8_t constant = chunk->code[offset + 2]; - printf("%-16s (%d args) %4d '", name, argCount, constant); + uint8_t unpack = chunk->code[offset + 3]; + printf("%-16s (%d args) %4d unpack - %d '", name, argCount, constant, unpack); printValue(chunk->constants.values[constant]); printf("'\n"); - return offset + 3; + return offset + 4; } static int importFromInstruction(const char *name, Chunk *chunk, @@ -242,7 +250,7 @@ int disassembleInstruction(Chunk *chunk, int offset) { case OP_NEW_DICT: return byteInstruction("OP_NEW_DICT", chunk, offset); case OP_CALL: - return byteInstruction("OP_CALL", chunk, offset); + return callInstruction("OP_CALL", chunk, offset); case OP_INVOKE_INTERNAL: return invokeInstruction("OP_INVOKE_INTERNAL", chunk, offset); case OP_INVOKE: diff --git a/src/vm/object.c b/src/vm/object.c index 75da20b3..e300a587 100644 --- a/src/vm/object.c +++ b/src/vm/object.c @@ -70,6 +70,14 @@ ObjClass *newClass(DictuVM *vm, ObjString *name, ObjClass *superclass, ClassType initTable(&klass->publicProperties); initTable(&klass->publicConstantProperties); klass->annotations = NULL; + + push(vm, OBJ_VAL(klass)); + ObjString *nameString = copyString(vm, "_name", 5); + push(vm, OBJ_VAL(nameString)); + tableSet(vm, &klass->publicConstantProperties, nameString, OBJ_VAL(name)); + pop(vm); + pop(vm); + return klass; } @@ -266,7 +274,10 @@ char *listToString(Value value) { char *element; int elementSize; - if (IS_STRING(listValue)) { + if (listValue == value) { + element = "[...]"; + elementSize = 5; + } else if (IS_STRING(listValue)) { ObjString *s = AS_STRING(listValue); element = s->chars; elementSize = s->length; @@ -292,7 +303,10 @@ char *listToString(Value value) { listString = newB; } - if (IS_STRING(listValue)) { + if (listValue == value) { + memcpy(listString + listStringLength, element, elementSize); + listStringLength += elementSize; + } else if (IS_STRING(listValue)) { memcpy(listString + listStringLength, "\"", 1); memcpy(listString + listStringLength + 1, element, elementSize); memcpy(listString + listStringLength + 1 + elementSize, "\"", 1); @@ -374,8 +388,10 @@ char *dictToString(Value value) { char *element; int elementSize; - - if (IS_STRING(item->value)) { + if (item->value == value){ + element = "{...}"; + elementSize = 5; + } else if (IS_STRING(item->value)) { ObjString *s = AS_STRING(item->value); element = s->chars; elementSize = s->length; @@ -401,7 +417,10 @@ char *dictToString(Value value) { dictString = newB; } - if (IS_STRING(item->value)) { + if (item->value == value) { + memcpy(dictString + dictStringLength, element, elementSize); + dictStringLength += elementSize; + } else if (IS_STRING(item->value)) { memcpy(dictString + dictStringLength, "\"", 1); memcpy(dictString + dictStringLength + 1, element, elementSize); memcpy(dictString + dictStringLength + 1 + elementSize, "\"", 1); diff --git a/src/vm/vm.c b/src/vm/vm.c index c192b86a..4f9417ac 100644 --- a/src/vm/vm.c +++ b/src/vm/vm.c @@ -33,6 +33,23 @@ static void resetStack(DictuVM *vm) { vm->compiler = NULL; } +#define HANDLE_UNPACK \ + if (unpack) { \ + if (!IS_LIST(peek(vm, 0))) { \ + runtimeError(vm, "Attempted to unpack a value that is not a list"); \ + return false; \ + } \ + \ + ObjList *list = AS_LIST(pop(vm)); \ + \ + for (int i = 0; i < list->values.count; ++i) { \ + push(vm, list->values.values[i]); \ + } \ + \ + argCount += (list->values.count - 1); \ + unpack = false; \ + } + void runtimeError(DictuVM *vm, const char *format, ...) { for (int i = vm->frameCount - 1; i >= 0; i--) { CallFrame *frame = &vm->frames[i]; @@ -264,8 +281,10 @@ static bool call(DictuVM *vm, ObjClosure *closure, int argCount) { return true; } -static bool callValue(DictuVM *vm, Value callee, int argCount) { +static bool callValue(DictuVM *vm, Value callee, int argCount, bool unpack) { if (IS_OBJ(callee)) { + HANDLE_UNPACK + switch (OBJ_TYPE(callee)) { case OBJ_BOUND_METHOD: { ObjBoundMethod *bound = AS_BOUND_METHOD(callee); @@ -301,6 +320,9 @@ static bool callValue(DictuVM *vm, Value callee, int argCount) { case OBJ_CLOSURE: { vm->stackTop[-argCount - 1] = callee; + + + return call(vm, AS_CLOSURE(callee), argCount); } @@ -343,7 +365,9 @@ static bool callNativeMethod(DictuVM *vm, Value method, int argCount) { } static bool invokeFromClass(DictuVM *vm, ObjClass *klass, ObjString *name, - int argCount) { + int argCount, bool unpack) { + HANDLE_UNPACK + // Look for the method. Value method; if (!tableGet(&klass->publicMethods, name, &method)) { @@ -359,9 +383,11 @@ static bool invokeFromClass(DictuVM *vm, ObjClass *klass, ObjString *name, return call(vm, AS_CLOSURE(method), argCount); } -static bool invokeInternal(DictuVM *vm, ObjString *name, int argCount) { +static bool invokeInternal(DictuVM *vm, ObjString *name, int argCount, bool unpack) { Value receiver = peek(vm, argCount); + HANDLE_UNPACK + if (IS_INSTANCE(receiver)) { ObjInstance *instance = AS_INSTANCE(receiver); @@ -383,7 +409,7 @@ static bool invokeInternal(DictuVM *vm, ObjString *name, int argCount) { // Look for a field which may shadow a method. if (tableGet(&instance->publicFields, name, &value)) { vm->stackTop[-argCount - 1] = value; - return callValue(vm, value, argCount); + return callValue(vm, value, argCount, unpack); } } else if (IS_CLASS(receiver)) { ObjClass *instance = AS_CLASS(receiver); @@ -399,7 +425,7 @@ static bool invokeInternal(DictuVM *vm, ObjString *name, int argCount) { return false; } - return callValue(vm, method, argCount); + return callValue(vm, method, argCount, unpack); } if (tableGet(&instance->publicMethods, name, &method)) { @@ -413,7 +439,7 @@ static bool invokeInternal(DictuVM *vm, ObjString *name, int argCount) { return false; } - return callValue(vm, method, argCount); + return callValue(vm, method, argCount, unpack); } if (tableGet(&vm->classMethods, name, &method)) { @@ -425,9 +451,11 @@ static bool invokeInternal(DictuVM *vm, ObjString *name, int argCount) { return false; } -static bool invoke(DictuVM *vm, ObjString *name, int argCount) { +static bool invoke(DictuVM *vm, ObjString *name, int argCount, bool unpack) { Value receiver = peek(vm, argCount); + HANDLE_UNPACK + if (!IS_OBJ(receiver)) { if (IS_NUMBER(receiver)) { Value value; @@ -464,7 +492,7 @@ static bool invoke(DictuVM *vm, ObjString *name, int argCount) { runtimeError(vm, "Undefined property '%s'.", name->chars); return false; } - return callValue(vm, value, argCount); + return callValue(vm, value, argCount, unpack); } case OBJ_CLASS: { @@ -481,7 +509,7 @@ static bool invoke(DictuVM *vm, ObjString *name, int argCount) { return false; } - return callValue(vm, method, argCount); + return callValue(vm, method, argCount, unpack); } if (tableGet(&vm->classMethods, name, &method)) { @@ -509,7 +537,7 @@ static bool invoke(DictuVM *vm, ObjString *name, int argCount) { // Look for a field which may shadow a method. if (tableGet(&instance->publicFields, name, &value)) { vm->stackTop[-argCount - 1] = value; - return callValue(vm, value, argCount); + return callValue(vm, value, argCount, unpack); } if (tableGet(&instance->klass->privateMethods, name, &value)) { @@ -628,7 +656,7 @@ static bool invoke(DictuVM *vm, ObjString *name, int argCount) { Value value; if (tableGet(&enumObj->values, name, &value)) { - return callValue(vm, value, argCount); + return callValue(vm, value, argCount, false); } runtimeError(vm, "'%s' enum has no property '%s'.", enumObj->name->chars, name->chars); @@ -1537,31 +1565,32 @@ static DictuInterpretResult run(DictuVM *vm) { ObjString *fileName = READ_STRING(); Value moduleVal; + char path[PATH_MAX]; + if (!resolvePath(frame->closure->function->module->path->chars, fileName->chars, path)) { + RUNTIME_ERROR("Could not open file \"%s\".", fileName->chars); + } + + ObjString *pathObj = copyString(vm, path, strlen(path)); + push(vm, OBJ_VAL(pathObj)); + // If we have imported this file already, skip. - if (tableGet(&vm->modules, fileName, &moduleVal)) { + if (tableGet(&vm->modules, pathObj, &moduleVal)) { + pop(vm); vm->lastModule = AS_MODULE(moduleVal); push(vm, NIL_VAL); DISPATCH(); } - char path[PATH_MAX]; - if (!resolvePath(frame->closure->function->module->path->chars, fileName->chars, path)) { - RUNTIME_ERROR("Could not open file \"%s\".", fileName->chars); - } - char *source = readFile(vm, path); if (source == NULL) { RUNTIME_ERROR("Could not open file \"%s\".", fileName->chars); } - ObjString *pathObj = copyString(vm, path, strlen(path)); - push(vm, OBJ_VAL(pathObj)); ObjModule *module = newModule(vm, pathObj); module->path = dirname(vm, path, strlen(path)); vm->lastModule = module; pop(vm); - push(vm, OBJ_VAL(module)); ObjFunction *function = compile(vm, module, source); pop(vm); @@ -2000,8 +2029,10 @@ static DictuInterpretResult run(DictuVM *vm) { CASE_CODE(CALL): { int argCount = READ_BYTE(); + bool unpack = READ_BYTE(); + frame->ip = ip; - if (!callValue(vm, peek(vm, argCount), argCount)) { + if (!callValue(vm, peek(vm, argCount), argCount, unpack)) { return INTERPRET_RUNTIME_ERROR; } frame = &vm->frames[vm->frameCount - 1]; @@ -2012,8 +2043,10 @@ static DictuInterpretResult run(DictuVM *vm) { CASE_CODE(INVOKE): { int argCount = READ_BYTE(); ObjString *method = READ_STRING(); + bool unpack = READ_BYTE(); + frame->ip = ip; - if (!invoke(vm, method, argCount)) { + if (!invoke(vm, method, argCount, unpack)) { return INTERPRET_RUNTIME_ERROR; } frame = &vm->frames[vm->frameCount - 1]; @@ -2024,8 +2057,10 @@ static DictuInterpretResult run(DictuVM *vm) { CASE_CODE(INVOKE_INTERNAL): { int argCount = READ_BYTE(); ObjString *method = READ_STRING(); + bool unpack = READ_BYTE(); + frame->ip = ip; - if (!invokeInternal(vm, method, argCount)) { + if (!invokeInternal(vm, method, argCount, unpack)) { return INTERPRET_RUNTIME_ERROR; } frame = &vm->frames[vm->frameCount - 1]; @@ -2036,9 +2071,11 @@ static DictuInterpretResult run(DictuVM *vm) { CASE_CODE(SUPER): { int argCount = READ_BYTE(); ObjString *method = READ_STRING(); + bool unpack = READ_BYTE(); + frame->ip = ip; ObjClass *superclass = AS_CLASS(pop(vm)); - if (!invokeFromClass(vm, superclass, method, argCount)) { + if (!invokeFromClass(vm, superclass, method, argCount, unpack)) { return INTERPRET_RUNTIME_ERROR; } frame = &vm->frames[vm->frameCount - 1]; @@ -2247,7 +2284,7 @@ DictuInterpretResult dictuInterpret(DictuVM *vm, char *moduleName, char *source) ObjClosure *closure = newClosure(vm, function); pop(vm); push(vm, OBJ_VAL(closure)); - callValue(vm, OBJ_VAL(closure), 0); + callValue(vm, OBJ_VAL(closure), 0, false); DictuInterpretResult result = run(vm); return result; diff --git a/tests/classes/properties.du b/tests/classes/properties.du index 76fe6f63..b90c6e33 100644 --- a/tests/classes/properties.du +++ b/tests/classes/properties.du @@ -57,6 +57,11 @@ class TestClassProperties < UnitTest { this.assertEquals(Test()._class, Test); this.assertEquals(Inherit()._class, Inherit); } + + testClassName() { + this.assertEquals(Test._name, "Test"); + this.assertEquals(Inherit._name, "Inherit"); + } } TestClassProperties().run(); \ No newline at end of file diff --git a/tests/env/env.du b/tests/env/env.du index fd8cb368..ea9ed88e 100644 --- a/tests/env/env.du +++ b/tests/env/env.du @@ -2,13 +2,14 @@ * end.du * * Testing the Env functions: - * - get(), set(), readFile() + * - get(), set(), clearAll(), readFile() * */ from UnitTest import UnitTest; import Env; import Path; +import System; class TestEnvModule < UnitTest { testEnvGet() { @@ -23,6 +24,14 @@ class TestEnvModule < UnitTest { this.assertEquals(Env.get("test"), nil); } + testEnvClearAll() { + if (System.platform != "windows") { + this.assertTruthy(Env.set("test", "test").success()); + this.assertTruthy(Env.clearAll().success()); + this.assertEquals(Env.get("test"), nil); + } + } + testEnvRead() { const result = Env.readFile(Path.dirname(__file__) + '/.env'); this.assertEquals(Env.get("TEST"), "10"); diff --git a/tests/http/client.du b/tests/http/client.du new file mode 100644 index 00000000..9b2e0c73 --- /dev/null +++ b/tests/http/client.du @@ -0,0 +1,58 @@ +/** + * Test HTTP client + */ +from UnitTest import UnitTest; + +import HTTP; + +class TestHttpClient < UnitTest { + + private httpClient; + + setUp() { + this.httpClient = HTTP.newClient({}); + } + + testHttpNewClient() { + const client = HTTP.newClient({"timeout": 20}); + this.assertNotNil(client); + } + + testSetTimeout() { + const ret = this.httpClient.setTimeout(20); + this.assertNil(ret); + } + + testSetHeaders() { + const headers = [ + "Content-Type: application/json", + "Accept: application/json", + "User-Agent: Dictu" + ]; + const ret = this.httpClient.setHeaders(headers); + + this.assertNil(ret); + } + + testSetInsecure() { + const ret = this.httpClient.setInsecure(true); + this.assertNil(ret); + } + + testSetKeyFile() { + const ret = this.httpClient.setKeyFile("/path/to/key/file"); + this.assertNil(ret); + } + + testSetCertFile() { + const ret = this.httpClient.setCertFile("/path/to/cert/file"); + this.assertNil(ret); + } + + testSetKeyPass() { + const ret = this.httpClient.setKeyPass("keyPass"); + this.assertNil(ret); + } +} + +TestHttpClient().run(); diff --git a/tests/http/clientGet.du b/tests/http/clientGet.du new file mode 100644 index 00000000..535cfe93 --- /dev/null +++ b/tests/http/clientGet.du @@ -0,0 +1,72 @@ +/** + * clientGet.du + * + * Testing the HTTP.HttpClient.get() function + * + */ +from UnitTest import UnitTest; + +import HTTP; + +class TestHttpClientGet < UnitTest { + + private httpClient; + + setUp() { + this.httpClient = HTTP.newClient({}); + } + + testHttpGet() { + var response = this.httpClient.get("http://httpbin.org/get"); + this.assertTruthy(response.success()); + + response = response.unwrap(); + + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.content.contains("headers")); + this.assertTruthy(response.headers.len() > 0); + } + + testHttpsGet() { + var response = this.httpClient.get("https://httpbin.org/get"); + this.assertTruthy(response.success()); + + response = response.unwrap(); + + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.content.contains("headers")); + this.assertTruthy(response.headers.len() > 0); + } + + testHttpsHeaders() { + this.httpClient.setHeaders(["Header: test"]); + var response = this.httpClient.get("https://httpbin.org/get"); + this.assertTruthy(response.success()); + + response = response.unwrap(); + + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.content.contains("headers")); + this.assertTruthy(response.content.contains('"Header": "test"')); + this.assertTruthy(response.headers.len() > 0); + } + + testHttpsBadURL() { + this.httpClient.setTimeout(1); + const response = this.httpClient.get("https://BAD_URL.test_for_error"); + this.assertFalsey(response.success()); + } + + testHttpGzip() { + var response = this.httpClient.get("https://httpbin.org/gzip"); + this.assertTruthy(response.success()); + + response = response.unwrap(); + + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.content.contains("headers")); + this.assertTruthy(response.headers.len() > 0); + } +} + +TestHttpClientGet().run(); diff --git a/tests/http/clientPost.du b/tests/http/clientPost.du new file mode 100644 index 00000000..0df1acbd --- /dev/null +++ b/tests/http/clientPost.du @@ -0,0 +1,61 @@ +/** + * clientPost.du + * + * Testing the HTTP.HttpClient.post() function + * + */ +from UnitTest import UnitTest; + +import HTTP; +import System; + +class TestHttpClientPost < UnitTest { + + private httpClient; + + setUp() { + this.httpClient = HTTP.newClient({}); + } + + testHttpPost() { + var response = this.httpClient.post("http://httpbin.org/post", {"test": 10}); + this.assertTruthy(response.success()); + response = response.unwrap(); + + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.headers.len() > 0); + this.assertTruthy(response.content.contains("origin")); + this.assertTruthy(response.content.contains('"test": "10"')); + } + + testHttpsPost() { + var response = this.httpClient.post("https://httpbin.org/post", {"test": 10}); + this.assertTruthy(response.success()); + response = response.unwrap(); + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.headers.len() > 0); + this.assertTruthy(response.content.contains("origin")); + this.assertTruthy(response.content.contains('"test": "10"')); + } + + testHttpsPostWithHeaders() { + this.httpClient.setHeaders(["Test: header"]); + var response = this.httpClient.post("https://httpbin.org/post", {"test": 10}); + this.assertTruthy(response.success()); + response = response.unwrap(); + + this.assertEquals(response.statusCode, 200); + this.assertTruthy(response.headers.len() > 0); + this.assertTruthy(response.content.contains("origin")); + this.assertTruthy(response.content.contains('"Test": "header"')); + this.assertTruthy(response.content.contains('"test": "10"')); + } + + testHttpsPostBadUrl() { + this.httpClient.setTimeout(1); + const response = this.httpClient.post("https://BAD_URL.test_for_error", {"test": 10}); + this.assertFalsey(response.success()); + } +} + +TestHttpClientPost().run(); diff --git a/tests/http/import.du b/tests/http/import.du index ecda35de..0378e530 100644 --- a/tests/http/import.du +++ b/tests/http/import.du @@ -7,3 +7,6 @@ import "get.du"; import "post.du"; import "constants.du"; +import "client.du"; +import "clientGet.du"; +import "clientPost.du"; diff --git a/tests/object/createFrom.du b/tests/object/createFrom.du new file mode 100644 index 00000000..d50ae649 --- /dev/null +++ b/tests/object/createFrom.du @@ -0,0 +1,21 @@ +/** + * createFrom.du + * + * Testing the Object.createFrom method + */ +from UnitTest import UnitTest; + +import Object; + +class Test {} + +class TestObjectCreateFrom < UnitTest { + testObjectCreateFrom() { + this.assertSuccess(Object.createFrom('Test')); + this.assertError(Object.createFrom('Unknown')); + + this.assertType(Object.createFrom('Test').unwrap(), 'Test'); + } +} + +TestObjectCreateFrom().run(); \ No newline at end of file diff --git a/tests/object/import.du b/tests/object/import.du new file mode 100644 index 00000000..2341a1fe --- /dev/null +++ b/tests/object/import.du @@ -0,0 +1,7 @@ +/** + * import.du + * + * General import file for all the Object module tests + */ + +import "createFrom.du"; \ No newline at end of file diff --git a/tests/operators/unpack.du b/tests/operators/unpack.du new file mode 100644 index 00000000..9cd5552a --- /dev/null +++ b/tests/operators/unpack.du @@ -0,0 +1,81 @@ +/** + * unpack.du + * + * Testing unpack operator + */ +from UnitTest import UnitTest; + +import Math; + +class UnpackOperatorTest < UnitTest { + testUnpackOperatorOnBuiltIn() { + this.assertEquals(Math.sum(...[1, 2, 3]), 6); + } + + testUnpackOperatorOnFunction() { + const f = def (a, b, c) => a + b + c; + + this.assertEquals(f(...[1, 2, 3]), 6); + } + + testUnpackOperatorOnFunctionWithPositional() { + const f = def (a, b, c) => a + b + c; + + this.assertEquals(f(1, 2, ...[3]), 6); + this.assertEquals(f(1, ...[2, 3]), 6); + } + + testUnpackOperatorOnFunctionWithOptional() { + const f = def (a, b=10, c=20) => a + b + c; + + this.assertEquals(f(...[1]), 31); + this.assertEquals(f(...[1, 2]), 23); + this.assertEquals(f(...[1, 2, 3]), 6); + } + + testUnpackOperatorOnVariadicFunction() { + const f = def (...a) => a; + + this.assertEquals(f(...[1, 2, 3]), [1, 2, 3]); + } + + testUnpackOperatorOnClass() { + class Test { + init(var a, var b) {} + } + + const obj = Test(...[1, 2]); + + this.assertEquals(obj.a, 1); + this.assertEquals(obj.b, 2); + } + + testUnpackOperatorOnClassWithPositional() { + class Test { + init(var a, var b) {} + } + + const obj = Test(1, ...[2]); + + this.assertEquals(obj.a, 1); + this.assertEquals(obj.b, 2); + } + + testUnpackOperatorOnClassWithOptional() { + class Test { + init(var a=10, var b=20) {} + } + + const obj = Test(...[1, 2]); + + this.assertEquals(obj.a, 1); + this.assertEquals(obj.b, 2); + + const obj1 = Test(...[1]); + + this.assertEquals(obj1.a, 1); + this.assertEquals(obj1.b, 20); + } +} + +UnpackOperatorTest().run(); \ No newline at end of file diff --git a/tests/runTests.du b/tests/runTests.du index 97300278..694995f3 100644 --- a/tests/runTests.du +++ b/tests/runTests.du @@ -40,6 +40,8 @@ import "modules/import.du"; import "imports/import.du"; import "random/import.du"; import "hashlib/import.du"; +import "object/import.du"; +import "term/import.du"; // If we got here no runtime errors were thrown, therefore all tests passed. print("All tests passed successfully!"); diff --git a/tests/sets/containsAll.du b/tests/sets/containsAll.du new file mode 100644 index 00000000..31a2a50a --- /dev/null +++ b/tests/sets/containsAll.du @@ -0,0 +1,20 @@ +/** + * containsAll.du + * + * Testing the containsAll method + * + * .containsAll() checks if a set contains all elements in a given list + */ +from UnitTest import UnitTest; + +class TestSetContainsAll < UnitTest { + testcontainsAll() { + var set_a = set("a",1,2,3); + this.assertTruthy(set_a.containsAll(["a"])); + this.assertTruthy(set_a.containsAll([1,2,3])); + this.assertTruthy(set_a.containsAll(["a",2,3,1])); + this.assertFalsey(set_a.containsAll(["a",2,3,1,"b"])); + } +} + +TestSetContainsAll().run(); \ No newline at end of file diff --git a/tests/sets/import.du b/tests/sets/import.du index 91cb63b5..523e86d5 100644 --- a/tests/sets/import.du +++ b/tests/sets/import.du @@ -13,3 +13,4 @@ import "remove.du"; import "len.du"; import "toString.du"; import "toBool.du"; +import "containsAll.du"; diff --git a/tests/strings/import.du b/tests/strings/import.du index e35c4cea..d622ef82 100644 --- a/tests/strings/import.du +++ b/tests/strings/import.du @@ -23,3 +23,4 @@ import "rawStrings.du"; import "toNumber.du"; import "toBool.du"; import "escapeCodes.du"; +import "repeat.du"; diff --git a/tests/strings/repeat.du b/tests/strings/repeat.du new file mode 100644 index 00000000..60b6f846 --- /dev/null +++ b/tests/strings/repeat.du @@ -0,0 +1,17 @@ +/** + * repeat.du + * + * Testing the str.repeat() method + * + * .repeat() returns a new string with the original string repeated the given number of times + */ +from UnitTest import UnitTest; + +class TestStringRepeat < UnitTest { + testStringRepeat() { + this.assertEquals("ba" + "na".repeat(2), "banana"); + this.assertEquals("ha".repeat(6), "hahahahahaha"); + } +} + +TestStringRepeat().run(); diff --git a/tests/system/import.du b/tests/system/import.du index b36c1a40..e3e04d74 100644 --- a/tests/system/import.du +++ b/tests/system/import.du @@ -17,3 +17,5 @@ import "mkdir.du"; import "constants.du"; import "chmod.du"; import "chown.du"; +import "uname.du"; +import "mkdirTemp.du"; diff --git a/tests/system/mkdirTemp.du b/tests/system/mkdirTemp.du new file mode 100644 index 00000000..4b929e44 --- /dev/null +++ b/tests/system/mkdirTemp.du @@ -0,0 +1,40 @@ +/** + * mkdirTemp.du + * + * Testing the System.mkdirTemp function + * + * mkdirTemp makes a temporary directory. + */ +from UnitTest import UnitTest; + +import Path; +import System; + +class TestSystemMkdirTest < UnitTest { + testSystemMkdir(tempDirName) { + if (System.platform != "windows") { + if (Path.exists(tempDirName)) { + this.assertSuccess(System.rmdir(tempDirName)); + } + + System.mkdirTemp(tempDirName).match( + def (result) => { + this.assertEquals(result[0:-6], tempDirName[0:-6]); + this.assertSuccess(System.rmdir(result)); + }, + def (error) => { + this.assertEquals(error, "Invalid argument"); + } + ); + } + } + + testSystemMkdirProvider() { + return [ + 'test_mkdir_temp', + 'test_mkdir_tempXXXXXX' + ]; + } +} + +TestSystemMkdirTest().run(); diff --git a/tests/system/uname.du b/tests/system/uname.du new file mode 100644 index 00000000..c3acf310 --- /dev/null +++ b/tests/system/uname.du @@ -0,0 +1,23 @@ +/** + * uname.du + * + * Testing the System.uname() function + * + * uname() returns the name and version of the system along with operating + * system and hardware information + */ +from UnitTest import UnitTest; + +import System; + +class TestSystemUname < UnitTest { + testSystemUname() { + if (System.platform != "windows") { + const uname = System.uname(); + this.assertNotNil(uname); + this.assertType(uname, "dict"); + } + } +} + +TestSystemUname().run(); diff --git a/tests/term/getSize.du b/tests/term/getSize.du new file mode 100644 index 00000000..41ad8ead --- /dev/null +++ b/tests/term/getSize.du @@ -0,0 +1,27 @@ +/** + * getSize.du + * + * Testing the Term.getSize() function + * + * getSize() returns the number of rows, columns, horizontal and vertical pixels. + */ +import System; + +from UnitTest import UnitTest; + +import Term; + +class TestTermGetSize < UnitTest { + + testTermGetSize() { + const termSize = Term.getSize(); + const len = System.platform == 'windows' ? 2 : 4; + + this.assertNotNil(termSize); + this.assertTruthy(termSize.len() == len); + this.assertTruthy(termSize.exists("rows")); + this.assertType(termSize.get("columns"), "number"); + } +} + +TestTermGetSize().run(); diff --git a/tests/term/import.du b/tests/term/import.du new file mode 100644 index 00000000..4102f075 --- /dev/null +++ b/tests/term/import.du @@ -0,0 +1,8 @@ +/** + * import.du + * + * General import file for all the term tests + */ + +import "getSize.du"; +import "isatty.du"; diff --git a/tests/term/isatty.du b/tests/term/isatty.du new file mode 100644 index 00000000..4673eea6 --- /dev/null +++ b/tests/term/isatty.du @@ -0,0 +1,26 @@ +/** + * isatty.du + * + * Testing the Term.isatty() function + * + * isatty() returns a boolean indicating whether the file descriptor passed is + * attached to a tty. + */ +import System; + +from UnitTest import UnitTest; + +import Term; + +class TestIsatty < UnitTest { + + testIsatty() { + if (System.argv.contains('ci')) { + return; + } + + this.assertTruthy(Term.isatty(0)); + } +} + +TestIsatty().run(); diff --git a/tests/variables/list-unpacking.du b/tests/variables/list-unpacking.du index b0bf630f..479654d1 100644 --- a/tests/variables/list-unpacking.du +++ b/tests/variables/list-unpacking.du @@ -12,6 +12,20 @@ class TestListUnpacking < UnitTest { this.assertEquals(a, 1); this.assertEquals(b, 2); this.assertEquals(c, 3); + + var o,i,g; + + [o,i,g]=[111,222,333]; + + this.assertEquals(o, 111); + this.assertEquals(i, 222); + this.assertEquals(g, 333); + + var [d, e, f] = [1 + 1, 2 + 2, 3 + 3]; + + this.assertEquals(d, 2); + this.assertEquals(e, 4); + this.assertEquals(f, 6); } testListUnpackingExpression() {