diff --git a/docs/_config.yml b/docs/_config.yml index 6c38844d..fc6806f7 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.23.0" +version: "0.24.0" github_username: dictu-lang search_enabled: true diff --git a/docs/docs/classes.md b/docs/docs/classes.md index 58cb3c06..38ee4527 100644 --- a/docs/docs/classes.md +++ b/docs/docs/classes.md @@ -320,7 +320,7 @@ print(Test()._class); // ## 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 +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 to the class variable, and it is also shared across all instances. ```cs @@ -332,20 +332,44 @@ class SomeClass { } } -print(SomeClass.classVaraible); // 10 +print(SomeClass.classVariable); // 10 -var x = SomeClass(); -var y = SomeClass(); +const x = SomeClass(); +const y = SomeClass(); print(x.classVariable); // 10 print(y.classVariable); // 10 -SomeClass.classVaraible = 100; +SomeClass.classVariable = 100; print(x.classVariable); // 100 print(y.classVariable); // 100 ``` +## Class Constants + +Exactly the same as [Class Variables](#class-variables) except that it comes with a runtime guarantee that it will not be modified. + +```cs +class SomeClass { + const classVariable = 10; // This will be shared among all "SomeClass" instances + + init() { + this.x = 10; // "x" is set on the instance + } +} + +print(SomeClass.classVariable); // 10 + +const x = SomeClass(); +const y = SomeClass(); + +print(x.classVariable); // 10 +print(y.classVariable); // 10 + +SomeClass.classVariable = 100; // Cannot assign to class constant 'SomeClass.classVariable'. +``` + ## Static methods Static methods are methods which do not reference an object, and instead belong to a class. If a method is marked as static, `this` is not passed to the object. This means static methods can be invoked without instantiating an object. diff --git a/docs/docs/control-flow.md b/docs/docs/control-flow.md index e0b465d5..7e795bcb 100644 --- a/docs/docs/control-flow.md +++ b/docs/docs/control-flow.md @@ -41,6 +41,64 @@ if (x == 6) { print("Not going to print!"); } ``` + +## Switch Statement + +A switch statement can be seen as a more maintainable if/else if chain. It evaluates an expression and then enters a +case block depending upon if the expression matches the expression of the case block. + +The value supplied to a case block can be of any type and follows the same rules of equality as the `==` operator. + +```cs +switch (10) { + case 1: { + // 10 doesn't equal 1, so this is skipped. + } + + case 10: { + // Match! This block of code is executed! + } +} +``` + +### Default + +Sometimes there may be a need for a bit of code to be executed if none of the case blocks match the expression of the switch statement. This is where +the default block comes into play. + +``` +switch ("string") { + case "nope": { + + } + + case "nope again!": { + + } + + default: { + // This will get executed! + } +} +``` + +### Multi-Value Case + +Sometimes we need to execute a block of code based on a set of values. This is easily done by just supplying a comma separated list +of values to the case block. + +```cs +switch (10) { + case 1, 3, 5, 7, 9: { + // 10 doesn't equal any of the supplied values, this is skipped. + } + + case 0, 2, 4, 6, 8, 10: { + // Match! This block of code is executed! + } +} +``` + ## Loops ### While loop diff --git a/docs/docs/standard-lib/http.md b/docs/docs/standard-lib/http.md index d5f3d488..fcb33817 100644 --- a/docs/docs/standard-lib/http.md +++ b/docs/docs/standard-lib/http.md @@ -27,7 +27,7 @@ import HTTP; ### HTTP.get(string, list: headers -> optional, number: timeout -> optional) Sends a HTTP GET request to a given URL. Timeout is given in seconds. -Returns a Result and unwraps to a dictionary upon success. +Returns a Result and unwraps to a Response upon success. ```cs HTTP.get("https://httpbin.org/get"); @@ -40,7 +40,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. -Returns a Result and unwraps to a dictionary upon success. +Returns a Result and unwraps to a Response upon success. ```cs HTTP.post("https://httpbin.org/post"); @@ -51,8 +51,8 @@ HTTP.post("https://httpbin.org/post", {"test": 10}, ["Content-Type: application/ ### Response -Both HTTP.get() and HTTP.post() return a Result that unwraps a dictionary on success, or nil on error. -The dictionary returned has 3 keys, "content", "headers" and "statusCode". "content" is the actual content returned from the +Both HTTP.get() and HTTP.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 @@ -60,39 +60,53 @@ Example response from [httpbin.org](https://httpbin.org) ```json // GET - -{"content": '{ - "args": {}, +const response = HTTP.get("https://httpbin.org/get").unwrap(); +print(response.content); +{ + "args": {}, "headers": { - "Accept": "*/*", - "Host": "httpbin.org", - "X-Amzn-Trace-Id": "Root=1-5e58197f-21f34d683a951fc741f169c6" - }, - "origin": "...", + "Accept": "*/*", + "Accept-Encoding": "gzip", + "Host": "httpbin.org", + "X-Amzn-Trace-Id": "Root=1-620ff6d1-24de015127aa59770abce026" + }, + "origin": "...", "url": "https://httpbin.org/get" } -', "headers": ['HTTP/1.1 200 OK', 'Date: Thu, 27 Feb 2020 19:33:19 GMT', 'Content-Type: application/json', 'Content-Length: 220', 'Connection: keep-alive', 'Server: gunicorn/19.9.0', 'Access-Control-Allow-Origin: *', 'Access-Control-Allow-Credentials: true'], "statusCode": 200} +print(response.headers); +["HTTP/2 200 ", "date: Fri, 18 Feb 2022 19:43:13 GMT", "content-type: application/json", "content-length: 254", "server: gunicorn/19.9.0", "access-control-allow-origin: *", "access-control-allow-credentials: true"] +print(response.statusCode); +200 // POST - -{"content": '{ - "args": {}, - "data": "", - "files": {}, - "form": { - "test": "10" - }, +const response = HTTP.post("https://httpbin.org/post").unwrap(); +print(response.content); +{ + "args": {}, + "data": "", + "files": {}, + "form": {}, "headers": { - "Accept": "*/*", - "Content-Length": "8", - "Content-Type": "application/x-www-form-urlencoded", - "Host": "httpbin.org", - "X-Amzn-Trace-Id": "Root=1-5e5819ac-7e6a3cef0546c7606a34aa73" - }, - "json": null, - "origin": "...", + "Accept": "*/*", + "Accept-Encoding": "gzip", + "Content-Length": "0", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "httpbin.org", + "X-Amzn-Trace-Id": "Root=1-620ff777-311a6db4398c1f5325a22f8a" + }, + "json": null, + "origin": "...", "url": "https://httpbin.org/post" } -', "headers": ['HTTP/1.1 200 OK', 'Date: Thu, 27 Feb 2020 19:34:04 GMT', 'Content-Type: application/json', 'Content-Length: 390', 'Connection: keep-alive', 'Server: gunicorn/19.9.0', 'Access-Control-Allow-Origin: *', 'Access-Control-Allow-Credentials: true'], "statusCode": 200} -``` \ No newline at end of file + +print(response.headers); +["HTTP/2 200 ", "date: Fri, 18 Feb 2022 19:45:59 GMT", "content-type: application/json", "content-length: 404", "server: gunicorn/19.9.0", "access-control-allow-origin: *", "access-control-allow-credentials: true"] + +print(response.statusCode); +200 +``` + +#### Response.json() +To quickly convert the raw string contained within the Response object we can use the helper `.json` method. +This works exactly the same as `JSON.parse()` and will return a Result object. \ No newline at end of file diff --git a/examples/openweathermap.du b/examples/openweathermap.du new file mode 100644 index 00000000..055406d4 --- /dev/null +++ b/examples/openweathermap.du @@ -0,0 +1,49 @@ +/** + * Example of how to use the HTTP module and JSON modules to interact with the + * OpenWeatherMap REST API. This can serve as a base to extend with more features. + */ + +import Env; +import HTTP; +import JSON; +import System; + +const BASE_URL = "http://api.openweathermap.org/data/2.5/weather?"; + +class Client { + private token; + + init(units, lang, token) { + this.units = units; + this.lang = lang; + this.token = token; + } + + current(name) { + var url = BASE_URL; + url = url + "appid=" + this.token; + url = url + "&q=" + name; + url = url + "&units=" + this.units; + url = url + "&lang=" + this.lang; + + const res = HTTP.get(url); + return JSON.parse(res.unwrap()["content"]).unwrap(); + } +} + +// main +{ + const token = Env.get("OWM_TOKEN"); + if (not token) { + print("error: OWM_TOKEN required to be set in the environment"); + System.exit(1); + } + + const location = "Phoenix"; + + const client = Client("F", "en", token); + const current = client.current(location); + + print("Current Weather Data for: {} \n\n{}\n".format(location, current)); +} + diff --git a/examples/runExamples.du b/examples/runExamples.du index eca6ee2c..27df535e 100644 --- a/examples/runExamples.du +++ b/examples/runExamples.du @@ -17,3 +17,5 @@ import "inheritance.du"; import "isPalindrome.du"; import "factorial.du"; import "pathWalker.du"; +import "structured_logger.du"; +import "template_engine.du"; diff --git a/examples/structured_logger.du b/examples/structured_logger.du new file mode 100644 index 00000000..1bb40aa2 --- /dev/null +++ b/examples/structured_logger.du @@ -0,0 +1,82 @@ +/** + * Module logger provides a structured logger. The structured + * format is JSON. + */ + +import JSON; +import System; + +/** + * class Log implements a simple structured logger + * that outputs to either stdout or a given file. + */ +class Log { + private output; + + init(output="") { + this.output = ""; + if (output.len() > 1) { + this.output = output; + } + } + + private set_level(level) { + this.entry = {"level": level}; + } + + private run(msg, fields) { + this.entry["timestamp"] = System.time(); + + if (fields) { + fields.keys().forEach( + def(entry) => this.entry[entry] = fields[entry] + ); + } + this.entry["msg"] = msg; + + const j = JSON.parse(this.entry.toString()); + + if (this.output.len() > 1) { + with(this.output, "a") { + file.writeLine(j.unwrap().toString()); + } + } else { + print(j.unwrap()); + } + } + + info(msg, fields=nil) { + this.set_level("info"); + this.run(msg, fields); + } + + warn(msg, fields=nil) { + this.set_level("warn"); + this.run(msg, fields); + } + + error(msg, fields=nil) { + this.set_level("error"); + this.run(msg, fields); + } + + debug(msg, fields=nil) { + this.set_level("debug"); + this.run(msg, fields); + } + + trace(msg, fields=nil) { + this.set_level("trace"); + this.run(msg, fields); + } +} + +// main +{ + const log = Log(); + log.info("starting application"); + + System.sleep(2); + + log.info("shutting down application", {"extra-field-1": "more data"}); +} diff --git a/examples/switch.du b/examples/switch.du new file mode 100644 index 00000000..5177526c --- /dev/null +++ b/examples/switch.du @@ -0,0 +1,98 @@ +var a=4; +switch(a){ +case 1:{ + print("one"); +} +case 2:{ + print("two"); +} +case 3:{ + print("three"); +} +case 4:{ + print("four"); +} +case 5:{ + print("five"); +} +case 6:{ + print("six"); +} +} +//output four + +switch("five"){ +case "one":{ + print(1); +} +case "two":{ + print(2); +} +case "three":{ + print(3); +} +case "four":{ + print(4); +} +case "five":{ + print(5); +} +case "six":{ + print(6); +} +} +//output 5 + +switch("Nine"){ +case "one":{ + print(1); +} +case "two":{ + print(2); +} +case "three":{ + print(3); +} +case "four":{ + print(4); +} +case "five":{ + print(5); +} +case "six":{ + print(6); +} +default: + print("case not found"); +} +//output case not found + +switch("five"){ +case "one":{ + print(1); +} +case "two":{ + print(2); +} +case "three":{ + print(3); +} +case "four":{ + print(4); +} +case "five", "six":{ + print("5 or 6"); +} +} +//output 5 or 6 + +switch (10) { + case 1, 2: { + print("1!"); + } + + case 10: { + print("10!"); + } +} +//output 10! \ No newline at end of file diff --git a/examples/template_engine.du b/examples/template_engine.du new file mode 100644 index 00000000..5505a7ba --- /dev/null +++ b/examples/template_engine.du @@ -0,0 +1,52 @@ +import System; + +// Template is a simple template rendering engine. +class Template { + var LEFT_BRACE = "{"; + var RIGHT_BRACE = "}"; + + init(private tmpl, private klass) {} + + // render parses the given template and class and + // matches the fields in the class to the template + // fields and replaces them with the class values. + render() { + const classAttrs = this.klass.getAttributes()["properties"]; + var rendered = this.tmpl; + + classAttrs.forEach(def(attr) => { + const attrVal = this.klass.getAttribute(attr); + const tmplField = "{}{}{}".format( + Template.LEFT_BRACE, + attr, + Template.RIGHT_BRACE + ); + + if (rendered.contains(tmplField)) { + if (type(attrVal) != "string") { + rendered = rendered.replace(tmplField, attrVal.toString()); + } else { + rendered = rendered.replace(tmplField, attrVal); + } + } + }); + + return rendered; + } +} + +// Person is a class used to hold data to be passed +// to the template engine. +class Person { + init(var name, var age) {} +} + +// main +{ + const tmpl = "Hello {name}! You are {age} years old."; + const p = Person("John", 12); + + const t = Template(tmpl, p); + + print(t.render()); +} diff --git a/scripts/generate.du b/scripts/generate.du index de479af3..2f12b70d 100644 --- a/scripts/generate.du +++ b/scripts/generate.du @@ -30,7 +30,8 @@ var files = [ 'src/vm/datatypes/dicts/dict', 'src/vm/datatypes/result/result', 'src/optionals/unittest/unittest', - 'src/optionals/env/env' + 'src/optionals/env/env', + 'src/optionals/http/http' ]; for (var i = 0; i < files.len(); i += 1) { diff --git a/src/include/dictu_include.h b/src/include/dictu_include.h index 06faee5c..b24a3a7b 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 "23" +#define DICTU_MINOR_VERSION "24" #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/http/http-source.h b/src/optionals/http/http-source.h new file mode 100644 index 00000000..88335cec --- /dev/null +++ b/src/optionals/http/http-source.h @@ -0,0 +1,12 @@ +#define DICTU_HTTP_SOURCE "class Response {\n" \ +" // content, headers and statusCode set via C\n" \ +" init() {}\n" \ +"\n" \ +" json() {\n" \ +" import JSON;\n" \ +"\n" \ +" return JSON.parse(this.content);\n" \ +" }\n" \ +"}\n" \ +"\n" \ + diff --git a/src/optionals/http.c b/src/optionals/http/http.c similarity index 98% rename from src/optionals/http.c rename to src/optionals/http/http.c index 01398f29..9bddb08c 100644 --- a/src/optionals/http.c +++ b/src/optionals/http/http.c @@ -4,6 +4,8 @@ #include "http.h" +#include "http-source.h" + #define HTTP_METHOD_GET "GET" #define HTTP_METHOD_POST "POST" #define HTTP_METHOD_PUT "PUT" @@ -348,7 +350,7 @@ static bool setRequestHeaders(DictuVM *vm, struct curl_slist *list, CURL *curl, return true; } -static ObjDict *endRequest(DictuVM *vm, CURL *curl, Response response) { +static ObjInstance *endRequest(DictuVM *vm, CURL *curl, Response response) { // Get status code curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.statusCode); ObjString *content; @@ -361,35 +363,41 @@ static ObjDict *endRequest(DictuVM *vm, CURL *curl, Response response) { // Push to stack to avoid GC push(vm, OBJ_VAL(content)); - ObjDict *responseVal = newDict(vm); + Value rawModule; + tableGet(&vm->modules, copyString(vm, "HTTP", 4), &rawModule); + + Value rawResponseClass; + tableGet(&AS_MODULE(rawModule)->values, copyString(vm, "Response", 8), &rawResponseClass); + + ObjInstance *responseInstance = newInstance(vm, AS_CLASS(rawResponseClass)); // Push to stack to avoid GC - push(vm, OBJ_VAL(responseVal)); + push(vm, OBJ_VAL(responseInstance)); ObjString *string = copyString(vm, "content", 7); push(vm, OBJ_VAL(string)); - dictSet(vm, responseVal, OBJ_VAL(string), OBJ_VAL(content)); + tableSet(vm, &responseInstance->publicFields, string, OBJ_VAL(content)); pop(vm); string = copyString(vm, "headers", 7); push(vm, OBJ_VAL(string)); - dictSet(vm, responseVal, OBJ_VAL(string), OBJ_VAL(response.headers)); + tableSet(vm, &responseInstance->publicFields, string, OBJ_VAL(response.headers)); pop(vm); string = copyString(vm, "statusCode", 10); push(vm, OBJ_VAL(string)); - dictSet(vm, responseVal, OBJ_VAL(string), NUMBER_VAL(response.statusCode)); + tableSet(vm, &responseInstance->publicFields, string, NUMBER_VAL(response.statusCode)); pop(vm); - // Pop - pop(vm); + // Pop instance pop(vm); + // Pop content pop(vm); /* always cleanup */ curl_easy_cleanup(curl); curl_global_cleanup(); - return responseVal; + return responseInstance; } static Value get(DictuVM *vm, int argCount, Value *args) { @@ -600,10 +608,15 @@ static Value post(DictuVM *vm, int argCount, Value *args) { } Value createHTTPModule(DictuVM *vm) { - ObjString *name = copyString(vm, "HTTP", 4); - push(vm, OBJ_VAL(name)); - ObjModule *module = newModule(vm, name); - push(vm, OBJ_VAL(module)); + ObjClosure *closure = compileModuleToClosure(vm, "HTTP", DICTU_HTTP_SOURCE); + + if (closure == NULL) { + return EMPTY_VAL; + } + + push(vm, OBJ_VAL(closure)); + + ObjModule *module = closure->function->module; defineNativeProperty(vm, &module->values, "METHOD_GET", OBJ_VAL(copyString(vm, HTTP_METHOD_GET, strlen(HTTP_METHOD_GET)))); defineNativeProperty(vm, &module->values, "METHOD_POST", OBJ_VAL(copyString(vm, HTTP_METHOD_POST, strlen(HTTP_METHOD_POST)))); @@ -838,8 +851,7 @@ Value createHTTPModule(DictuVM *vm) { defineNative(vm, &module->values, "get", get); defineNative(vm, &module->values, "post", post); - pop(vm); pop(vm); - return OBJ_VAL(module); + return OBJ_VAL(closure); } diff --git a/src/optionals/http/http.du b/src/optionals/http/http.du new file mode 100644 index 00000000..f8eb684c --- /dev/null +++ b/src/optionals/http/http.du @@ -0,0 +1,12 @@ +class Response { + // content, headers and statusCode set via C + init() {} + + json() { + // Import needs to be local to ensure HTTP is defined correctly + import JSON; + + return JSON.parse(this.content); + } +} + diff --git a/src/optionals/http.h b/src/optionals/http/http.h similarity index 84% rename from src/optionals/http.h rename to src/optionals/http/http.h index c00f3e8c..b0537284 100644 --- a/src/optionals/http.h +++ b/src/optionals/http/http.h @@ -5,8 +5,8 @@ #include #endif -#include "optionals.h" -#include "../vm/vm.h" +#include "../optionals.h" +#include "../../vm/vm.h" typedef struct response { DictuVM *vm; diff --git a/src/optionals/optionals.c b/src/optionals/optionals.c index deb3650c..b5e2f195 100644 --- a/src/optionals/optionals.c +++ b/src/optionals/optionals.c @@ -17,7 +17,7 @@ BuiltinModules modules[] = { {"UnitTest", &createUnitTestModule, true}, {"Inspect", &createInspectModule, false}, #ifndef DISABLE_HTTP - {"HTTP", &createHTTPModule, false}, + {"HTTP", &createHTTPModule, true}, #endif {NULL, NULL, false} }; diff --git a/src/optionals/optionals.h b/src/optionals/optionals.h index 24eecd07..5572ccb9 100644 --- a/src/optionals/optionals.h +++ b/src/optionals/optionals.h @@ -7,7 +7,7 @@ #include "system.h" #include "json.h" #include "log.h" -#include "http.h" +#include "http/http.h" #include "path.h" #include "c.h" #include "datetime.h" diff --git a/src/optionals/unittest/unittest-source.h b/src/optionals/unittest/unittest-source.h index ee1d6b0d..2705872d 100644 --- a/src/optionals/unittest/unittest-source.h +++ b/src/optionals/unittest/unittest-source.h @@ -15,7 +15,7 @@ "\n" \ " filterMethods() {\n" \ " return this.methods().filter(def (method) => {\n" \ -" if (method.startsWith('test') and !method.endsWith('Provider') and !method.endsWith('_skipped')) {\n" \ +" if (method.startsWith('test') and not method.endsWith('Provider') and not method.endsWith('_skipped')) {\n" \ " return method;\n" \ " }\n" \ "\n" \ @@ -80,7 +80,7 @@ " if (success) {\n" \ " this.results['passed'] += 1;\n" \ "\n" \ -" if (!this.onlyFailures) {\n" \ +" if (not this.onlyFailures) {\n" \ " print('{}Success.'.format(UnitTest.ASSERTION_PADDING));\n" \ " }\n" \ " } else {\n" \ @@ -103,7 +103,7 @@ " }\n" \ "\n" \ " assertFalsey(value) {\n" \ -" this.printResult(!value, 'Failure: {} is not Falsey.'.format(value));\n" \ +" this.printResult(not value, 'Failure: {} is not Falsey.'.format(value));\n" \ " }\n" \ "\n" \ " assertSuccess(value) {\n" \ @@ -121,7 +121,7 @@ " return;\n" \ " }\n" \ "\n" \ -" this.printResult(!value.success(), 'Failure: {} is not a Result type in an error state.'.format(value));\n" \ +" this.printResult(not value.success(), 'Failure: {} is not a Result type in an error state.'.format(value));\n" \ " }\n" \ "}\n" \ diff --git a/src/vm/compiler.c b/src/vm/compiler.c index 43017723..143c38c3 100644 --- a/src/vm/compiler.c +++ b/src/vm/compiler.c @@ -1409,8 +1409,8 @@ ParseRule rules[] = { {NULL, dot, PREC_CALL}, // TOKEN_DOT {unary, binary, PREC_TERM}, // TOKEN_MINUS {NULL, binary, PREC_TERM}, // TOKEN_PLUS - {NULL, ternary, PREC_ASSIGNMENT}, // TOKEN_QUESTION - {NULL, chain, PREC_CHAIN}, // TOKEN_QUESTION_DOT + {NULL, ternary, PREC_ASSIGNMENT}, // TOKEN_QUESTION + {NULL, chain, PREC_CHAIN}, // TOKEN_QUESTION_DOT {NULL, NULL, PREC_NONE}, // TOKEN_PLUS_EQUALS {NULL, NULL, PREC_NONE}, // TOKEN_MINUS_EQUALS {NULL, NULL, PREC_NONE}, // TOKEN_MULTIPLY_EQUALS @@ -1455,7 +1455,10 @@ ParseRule rules[] = { {NULL, NULL, PREC_NONE}, // TOKEN_IF {NULL, and_, PREC_AND}, // TOKEN_AND {NULL, NULL, PREC_NONE}, // TOKEN_ELSE - {NULL, or_, PREC_OR}, // TOKEN_OR + {NULL, or_, PREC_OR}, // TOKEN_OR + {NULL, NULL, PREC_NONE}, // TOKEN_SWITCH + {NULL, NULL, PREC_NONE}, // TOKEN_CASE + {NULL, NULL, PREC_NONE}, // TOKEN_DEFUALT {NULL, NULL, PREC_NONE}, // TOKEN_VAR {NULL, NULL, PREC_NONE}, // TOKEN_CONST {literal, NULL, PREC_NONE}, // TOKEN_TRUE @@ -1663,8 +1666,19 @@ static void parseClassBody(Compiler *compiler) { consume(compiler, TOKEN_EQUAL, "Expect '=' after class variable identifier."); expression(compiler); emitBytes(compiler, OP_SET_CLASS_VAR, name); + emitByte(compiler, false); - consume(compiler, TOKEN_SEMICOLON, "Expect ';' after variable declaration."); + consume(compiler, TOKEN_SEMICOLON, "Expect ';' after class variable declaration."); + } else if (match(compiler, TOKEN_CONST)) { + consume(compiler, TOKEN_IDENTIFIER, "Expect class constant name."); + uint8_t name = identifierConstant(compiler, &compiler->parser->previous); + + consume(compiler, TOKEN_EQUAL, "Expect '=' after class constant identifier."); + expression(compiler); + emitBytes(compiler, OP_SET_CLASS_VAR, name); + emitByte(compiler, true); + + consume(compiler, TOKEN_SEMICOLON, "Expect ';' after class constant declaration."); } else { if (match(compiler, TOKEN_PRIVATE)) { if (match(compiler, TOKEN_IDENTIFIER)) { @@ -1958,10 +1972,12 @@ static int getArgCount(uint8_t *code, const ValueArray constants, int ip) { case OP_NEW_LIST: case OP_NEW_DICT: case OP_CLOSE_FILE: + case OP_MULTI_CASE: return 1; case OP_DEFINE_OPTIONAL: case OP_JUMP: + case OP_COMPARE_JUMP: case OP_JUMP_IF_NIL: case OP_JUMP_IF_FALSE: case OP_LOOP: @@ -2125,7 +2141,6 @@ static void continueStatement(Compiler *compiler) { // Jump to top of current innermost loop. emitLoop(compiler, compiler->loop->start); } - static void ifStatement(Compiler *compiler) { consume(compiler, TOKEN_LEFT_PAREN, "Expect '(' after 'if'."); expression(compiler); @@ -2149,6 +2164,53 @@ static void ifStatement(Compiler *compiler) { patchJump(compiler, endJump); } +static void switchStatement(Compiler *compiler) { + int caseEnds[256]; + int caseCount = 0; + + consume(compiler, TOKEN_LEFT_PAREN, "Expect '(' after 'switch'."); + expression(compiler); + consume(compiler, TOKEN_RIGHT_PAREN, "Expect ')' after expression."); + consume(compiler, TOKEN_LEFT_BRACE, "Expect '{' before switch body."); + consume(compiler, TOKEN_CASE, "Expect at least one 'case' block."); + + do { + expression(compiler); + int multipleCases = 0; + if(match(compiler, TOKEN_COMMA)) { + do { + multipleCases++; + expression(compiler); + } while(match(compiler, TOKEN_COMMA)); + emitBytes(compiler, OP_MULTI_CASE, multipleCases); + } + int compareJump = emitJump(compiler, OP_COMPARE_JUMP); + consume(compiler, TOKEN_COLON, "Expect ':' after expression."); + statement(compiler); + caseEnds[caseCount++] = emitJump(compiler,OP_JUMP); + patchJump(compiler, compareJump); + if (caseCount > 255) { + errorAtCurrent(compiler->parser, "Switch statement can not have more than 256 case blocks"); + } + + } while(match(compiler, TOKEN_CASE)); + + if (match(compiler,TOKEN_DEFAULT)){ + emitByte(compiler, OP_POP); // expression. + consume(compiler, TOKEN_COLON, "Expect ':' after default."); + statement(compiler); + } + + if (match(compiler,TOKEN_CASE)){ + error(compiler->parser, "Unexpected case after default"); + } + + consume(compiler, TOKEN_RIGHT_BRACE, "Expect '}' after switch body."); + + for (int i = 0; i < caseCount; i++) { + patchJump(compiler, caseEnds[i]); + } +} static void withStatement(Compiler *compiler) { compiler->withBlock = true; @@ -2403,6 +2465,7 @@ static void synchronize(Parser *parser) { case TOKEN_CONST: case TOKEN_FOR: case TOKEN_IF: + case TOKEN_SWITCH: case TOKEN_WHILE: case TOKEN_BREAK: case TOKEN_RETURN: @@ -2456,6 +2519,8 @@ static void statement(Compiler *compiler) { forStatement(compiler); } else if (match(compiler, TOKEN_IF)) { ifStatement(compiler); + } else if (match(compiler, TOKEN_SWITCH)) { + switchStatement(compiler); } else if (match(compiler, TOKEN_RETURN)) { returnStatement(compiler); } else if (match(compiler, TOKEN_WITH)) { diff --git a/src/vm/datatypes/instance.c b/src/vm/datatypes/instance.c index 2159c8f5..a0f7df5f 100644 --- a/src/vm/datatypes/instance.c +++ b/src/vm/datatypes/instance.c @@ -44,6 +44,10 @@ static Value hasAttribute(DictuVM *vm, int argCount, Value *args) { ObjClass *klass = instance->klass; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, AS_STRING(value), &value)) { + return TRUE_VAL; + } + if (tableGet(&klass->publicProperties, AS_STRING(value), &value)) { return TRUE_VAL; } diff --git a/src/vm/debug.c b/src/vm/debug.c index e375f8fe..8906119c 100644 --- a/src/vm/debug.c +++ b/src/vm/debug.c @@ -205,6 +205,10 @@ int disassembleInstruction(Chunk *chunk, int offset) { return simpleInstruction("OP_NEGATE", offset); case OP_JUMP: return jumpInstruction("OP_JUMP", 1, chunk, offset); + case OP_MULTI_CASE: + return byteInstruction("OP_MULTI_CASE", chunk, offset); + case OP_COMPARE_JUMP: + return jumpInstruction("OP_COMPARE_JUMP", 1, chunk, offset); case OP_JUMP_IF_FALSE: return jumpInstruction("OP_JUMP_IF_FALSE", 1, chunk, offset); case OP_JUMP_IF_NIL: diff --git a/src/vm/memory.c b/src/vm/memory.c index 0f9f78ba..bbe6fb88 100644 --- a/src/vm/memory.c +++ b/src/vm/memory.c @@ -106,6 +106,7 @@ static void blackenObject(DictuVM *vm, Obj *object) { grayTable(vm, &klass->privateMethods); grayTable(vm, &klass->abstractMethods); grayTable(vm, &klass->publicProperties); + grayTable(vm, &klass->publicConstantProperties); break; } @@ -205,6 +206,7 @@ void freeObject(DictuVM *vm, Obj *object) { freeTable(vm, &klass->privateMethods); freeTable(vm, &klass->abstractMethods); freeTable(vm, &klass->publicProperties); + freeTable(vm, &klass->publicConstantProperties); FREE(vm, ObjClass, object); break; } diff --git a/src/vm/object.c b/src/vm/object.c index 27e26fd6..8cc3dbf6 100644 --- a/src/vm/object.c +++ b/src/vm/object.c @@ -68,6 +68,7 @@ ObjClass *newClass(DictuVM *vm, ObjString *name, ObjClass *superclass, ClassType initTable(&klass->privateMethods); initTable(&klass->publicMethods); initTable(&klass->publicProperties); + initTable(&klass->publicConstantProperties); klass->annotations = NULL; return klass; } diff --git a/src/vm/object.h b/src/vm/object.h index 885d8723..772dff97 100644 --- a/src/vm/object.h +++ b/src/vm/object.h @@ -221,6 +221,7 @@ typedef struct sObjClass { Table privateMethods; Table abstractMethods; Table publicProperties; + Table publicConstantProperties; ObjDict *annotations; ClassType type; } ObjClass; diff --git a/src/vm/opcodes.h b/src/vm/opcodes.h index bacde7bc..c392fb24 100644 --- a/src/vm/opcodes.h +++ b/src/vm/opcodes.h @@ -41,6 +41,8 @@ OPCODE(MOD) OPCODE(NOT) OPCODE(NEGATE) OPCODE(JUMP) +OPCODE(MULTI_CASE) +OPCODE(COMPARE_JUMP) OPCODE(JUMP_IF_FALSE) OPCODE(JUMP_IF_NIL) OPCODE(LOOP) diff --git a/src/vm/scanner.c b/src/vm/scanner.c index 1b5e6223..050e0a92 100644 --- a/src/vm/scanner.c +++ b/src/vm/scanner.c @@ -45,7 +45,6 @@ static char peekNext(Scanner *scanner) { static bool match(Scanner *scanner, char expected) { if (isAtEnd(scanner)) return false; if (*scanner->current != expected) return false; - scanner->current++; return true; } @@ -56,7 +55,6 @@ static Token makeToken(Scanner *scanner, TokenType type) { token.start = scanner->start; token.length = (int) (scanner->current - scanner->start); token.line = scanner->line; - return token; } @@ -155,6 +153,8 @@ static TokenType identifierType(Scanner *scanner) { case 'c': if (scanner->current - scanner->start > 1) { switch (scanner->start[1]) { + case 'a': + return checkKeyword(scanner, 2, 2, "se", TOKEN_CASE); case 'l': return checkKeyword(scanner, 2, 3, "ass", TOKEN_CLASS); case 'o': { @@ -173,6 +173,12 @@ static TokenType identifierType(Scanner *scanner) { } break; case 'd': + if (scanner->current - scanner->start > 3) { + switch (scanner->start[3]) { + case 'a': + return checkKeyword(scanner, 4, 3, "ult", TOKEN_DEFAULT); + } + } return checkKeyword(scanner, 1, 2, "ef", TOKEN_DEF); case 'e': if (scanner->current - scanner->start > 1) { @@ -236,10 +242,12 @@ static TokenType identifierType(Scanner *scanner) { case 's': if (scanner->current - scanner->start > 1) { switch (scanner->start[1]) { - case 'u': - return checkKeyword(scanner, 2, 3, "per", TOKEN_SUPER); case 't': return checkKeyword(scanner, 2, 4, "atic", TOKEN_STATIC); + case 'u': + return checkKeyword(scanner, 2, 3, "per", TOKEN_SUPER); + case 'w': + return checkKeyword(scanner, 2, 4, "itch", TOKEN_SWITCH); } } break; diff --git a/src/vm/scanner.h b/src/vm/scanner.h index 08c1659d..d99ff3f1 100644 --- a/src/vm/scanner.h +++ b/src/vm/scanner.h @@ -35,7 +35,7 @@ typedef enum { TOKEN_CLASS, TOKEN_ABSTRACT, TOKEN_TRAIT, TOKEN_USE, TOKEN_STATIC, TOKEN_PRIVATE, TOKEN_THIS, TOKEN_SUPER, TOKEN_DEF, TOKEN_AS, TOKEN_ENUM, - TOKEN_IF, TOKEN_AND, TOKEN_ELSE, TOKEN_OR, + TOKEN_IF, TOKEN_AND, TOKEN_ELSE, TOKEN_OR, TOKEN_SWITCH, TOKEN_CASE, TOKEN_DEFAULT, TOKEN_VAR, TOKEN_CONST, TOKEN_TRUE, TOKEN_FALSE, TOKEN_NIL, TOKEN_FOR, TOKEN_WHILE, TOKEN_BREAK, TOKEN_RETURN, TOKEN_CONTINUE, diff --git a/src/vm/vm.c b/src/vm/vm.c index 2cb391ea..14883f3a 100644 --- a/src/vm/vm.c +++ b/src/vm/vm.c @@ -1012,6 +1012,12 @@ static DictuInterpretResult run(DictuVM *vm) { ObjClass *klass = instance->klass; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, name, &value)) { + pop(vm); // Instance. + push(vm, value); + DISPATCH(); + } + if (tableGet(&klass->publicProperties, name, &value)) { pop(vm); // Instance. push(vm, value); @@ -1049,6 +1055,12 @@ static DictuInterpretResult run(DictuVM *vm) { Value value; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, name, &value)) { + pop(vm); // Class. + push(vm, value); + DISPATCH(); + } + if (tableGet(&klass->publicProperties, name, &value)) { pop(vm); // Class. push(vm, value); @@ -1112,6 +1124,12 @@ static DictuInterpretResult run(DictuVM *vm) { ObjClass *klass = instance->klass; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, name, &value)) { + pop(vm); // Instance. + push(vm, value); + DISPATCH(); + } + if (tableGet(&klass->publicProperties, name, &value)) { pop(vm); // Instance. push(vm, value); @@ -1130,6 +1148,12 @@ static DictuInterpretResult run(DictuVM *vm) { Value value; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, name, &value)) { + pop(vm); // Class. + push(vm, value); + DISPATCH(); + } + if (tableGet(&klass->publicProperties, name, &value)) { pop(vm); // Class. push(vm, value); @@ -1166,6 +1190,11 @@ static DictuInterpretResult run(DictuVM *vm) { ObjClass *klass = instance->klass; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, name, &value)) { + push(vm, value); + DISPATCH(); + } + if (tableGet(&klass->publicProperties, name, &value)) { push(vm, value); DISPATCH(); @@ -1207,6 +1236,11 @@ static DictuInterpretResult run(DictuVM *vm) { ObjClass *klass = instance->klass; while (klass != NULL) { + if (tableGet(&klass->publicConstantProperties, name, &value)) { + push(vm, value); + DISPATCH(); + } + if (tableGet(&klass->publicProperties, name, &value)) { push(vm, value); DISPATCH(); @@ -1227,8 +1261,15 @@ static DictuInterpretResult run(DictuVM *vm) { push(vm, NIL_VAL); DISPATCH(); } else if (IS_CLASS(peek(vm, 1))) { + ObjString *key = READ_STRING(); ObjClass *klass = AS_CLASS(peek(vm, 1)); - tableSet(vm, &klass->publicProperties, READ_STRING(), peek(vm, 0)); + + Value _; + if (tableGet(&klass->publicConstantProperties, key, &_)) { + RUNTIME_ERROR("Cannot assign to class constant '%s.%s'.", klass->name->chars, key->chars); + } + + tableSet(vm, &klass->publicProperties, key, peek(vm, 0)); pop(vm); pop(vm); push(vm, NIL_VAL); @@ -1254,7 +1295,14 @@ static DictuInterpretResult run(DictuVM *vm) { CASE_CODE(SET_CLASS_VAR): { // No type check required as this opcode is only ever emitted when parsing a class ObjClass *klass = AS_CLASS(peek(vm, 1)); - tableSet(vm, &klass->publicProperties, READ_STRING(), peek(vm, 0)); + ObjString *key = READ_STRING(); + bool constant = READ_BYTE(); + + if (constant) { + tableSet(vm, &klass->publicConstantProperties, key, peek(vm, 0)); + } else { + tableSet(vm, &klass->publicProperties, key, peek(vm, 0)); + } pop(vm); DISPATCH(); } @@ -1397,6 +1445,37 @@ static DictuInterpretResult run(DictuVM *vm) { DISPATCH(); } + CASE_CODE(MULTI_CASE):{ + int count = READ_BYTE(); + Value switchValue = peek(vm, count + 1); + Value caseValue = pop(vm); + for (int i = 0; i < count; ++i) { + if (valuesEqual(switchValue, caseValue)) { + i++; + while(i <= count) { + pop(vm); + i++; + } + break; + } + caseValue = pop(vm); + } + push(vm,caseValue); + DISPATCH(); + } + + CASE_CODE(COMPARE_JUMP):{ + uint16_t offset = READ_SHORT(); + Value a = pop(vm); + if (!valuesEqual(peek(vm,0), a)) { + ip += offset; + } else { + // switch expression. + pop(vm); + } + DISPATCH(); + } + CASE_CODE(JUMP_IF_FALSE): { uint16_t offset = READ_SHORT(); if (isFalsey(peek(vm, 0))) ip += offset; diff --git a/tests/classes/classConstants.du b/tests/classes/classConstants.du new file mode 100644 index 00000000..f0265825 --- /dev/null +++ b/tests/classes/classConstants.du @@ -0,0 +1,42 @@ +/** + * classConstants.du + * + * Testing setting class constants + */ +class Test { + const x = 10; + + init() { + // + } +} + +assert(Test.x == 10); + + +/** + * Ensure inherited classes work + */ +class BaseClass { + const x = 10; +} + +class Common < BaseClass {} + +assert(Common.x == 10); +assert(Common().x == 10); + +class AnotherClass < Common {} + +assert(AnotherClass.x == 10); +assert(AnotherClass().x == 10); + + +/** + * Test abstract classes + */ +abstract class AbstractClass { + const x = "Hello"; +} + +assert(AbstractClass.x == "Hello"); \ No newline at end of file diff --git a/tests/classes/import.du b/tests/classes/import.du index 6e221983..bf93d0f0 100644 --- a/tests/classes/import.du +++ b/tests/classes/import.du @@ -13,6 +13,7 @@ import "copy.du"; import "toString.du"; import "abstract.du"; import "classVariables.du"; +import "classConstants.du"; import "parameters.du"; import "isInstance.du"; import "constructor.du"; diff --git a/tests/http/get.du b/tests/http/get.du index 8cf21329..f14e1b5d 100644 --- a/tests/http/get.du +++ b/tests/http/get.du @@ -12,28 +12,28 @@ var result = HTTP.get("http://httpbin.org/get"); assert(result.success()); var response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["content"].contains("headers")); -assert(response["headers"].len() > 0); +assert(response.statusCode == 200); +assert(response.content.contains("headers")); +assert(response.headers.len() > 0); // HTTPS result = HTTP.get("https://httpbin.org/get"); assert(result.success()); response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["content"].contains("headers")); -assert(response["headers"].len() > 0); +assert(response.statusCode == 200); +assert(response.content.contains("headers")); +assert(response.headers.len() > 0); // Headers result = HTTP.get("https://httpbin.org/get", ["Header: test"]); assert(result.success()); response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["content"].contains("headers")); -assert(response["content"].contains('"Header": "test"')); -assert(response["headers"].len() > 0); +assert(response.statusCode == 200); +assert(response.content.contains("headers")); +assert(response.content.contains('"Header": "test"')); +assert(response.headers.len() > 0); response = HTTP.get("https://BAD_URL.test_for_error", [], 1); assert(response.success() == false); @@ -44,6 +44,6 @@ result = HTTP.get("https://httpbin.org/gzip"); assert(result.success()); response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["content"].contains("headers")); -assert(response["headers"].len() > 0); +assert(response.statusCode == 200); +assert(response.content.contains("headers")); +assert(response.headers.len() > 0); diff --git a/tests/http/post.du b/tests/http/post.du index e5bea13b..472abf0e 100644 --- a/tests/http/post.du +++ b/tests/http/post.du @@ -11,20 +11,20 @@ var result = HTTP.post("http://httpbin.org/post", {"test": 10}); assert(result.success()); var response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["headers"].len() > 0); -assert(response["content"].contains("origin")); -assert(response["content"].contains('"test": "10"')); +assert(response.statusCode == 200); +assert(response.headers.len() > 0); +assert(response.content.contains("origin")); +assert(response.content.contains('"test": "10"')); // HTTPS result = HTTP.post("https://httpbin.org/post", {"test": 10}); assert(result.success()); var response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["headers"].len() > 0); -assert(response["content"].contains("origin")); -assert(response["content"].contains('"test": "10"')); +assert(response.statusCode == 200); +assert(response.headers.len() > 0); +assert(response.content.contains("origin")); +assert(response.content.contains('"test": "10"')); // HTTPS @@ -32,11 +32,11 @@ result = HTTP.post("https://httpbin.org/post", {"test": 10}, ["Test: header"]); assert(result.success()); var response = result.unwrap(); -assert(response["statusCode"] == 200); -assert(response["content"].len() > 0); -assert(response["content"].contains('"Test": "header"')); -assert(response["content"].contains("origin")); -assert(response["content"].contains('"test": "10"')); +assert(response.statusCode == 200); +assert(response.content.len() > 0); +assert(response.content.contains('"Test": "header"')); +assert(response.content.contains("origin")); +assert(response.content.contains('"test": "10"')); response = HTTP.post("https://BAD_URL.test_for_error", {"test": 10}, [], 1); assert(response.success() == false); diff --git a/tests/runTests.du b/tests/runTests.du index 5fed0503..813e3186 100644 --- a/tests/runTests.du +++ b/tests/runTests.du @@ -14,6 +14,7 @@ import "classes/import.du"; import "enum/import.du"; import "builtins/import.du"; import "files/import.du"; +import "switch/import.du"; import "maths/import.du"; import "datetime/import.du"; import "env/import.du"; diff --git a/tests/switch/data-types.du b/tests/switch/data-types.du new file mode 100644 index 00000000..7c27c197 --- /dev/null +++ b/tests/switch/data-types.du @@ -0,0 +1,61 @@ +/** + * data-types.du + * + * Testing data-types within a switch + */ + +var x = 0; + +switch ("string") { + case 1: { + x += 10; + } + + case "string": { + x += 10; + } + + case [1, 2]: { + x += 10; + } +} + +assert(x == 10); + +switch ([1, 2]) { + case 1: { + x += 10; + } + + case "string": { + x += 10; + } + + case [1, 2]: { + x += 10; + } +} + +def func() {} + +assert(x == 20); + +switch (func) { + case 1: { + x += 10; + } + + case "string": { + x += 10; + } + + case [1, 2]: { + x += 10; + } + + case func: { + x += 10; + } +} + +assert(x == 30); \ No newline at end of file diff --git a/tests/switch/import.du b/tests/switch/import.du new file mode 100644 index 00000000..2b5aa1a6 --- /dev/null +++ b/tests/switch/import.du @@ -0,0 +1,9 @@ +/** + * import.du + * + * General import file for the switch control flow + */ + +import "variables.du"; +import "multi-case.du"; +import "data-types.du"; \ No newline at end of file diff --git a/tests/switch/multi-case.du b/tests/switch/multi-case.du new file mode 100644 index 00000000..cb173f99 --- /dev/null +++ b/tests/switch/multi-case.du @@ -0,0 +1,47 @@ +/** + * multi-case.du + * + * Testing multicase within a switch + */ + +var x = 10; + +switch (x) { + case 1, 2, 3, 4: { + x += 10; + } + + case 5, 6, 7, 8, 9: { + x += 10; + } + + case 10: { + x += 10; + } +} + +assert(x == 20); + +// Default + reorder + +var x = 10; + +switch (x) { + case 1, 2, 3, 4: { + x += 10; + } + + case 10: { + x += 10; + } + + case 5, 6, 7, 8, 9: { + x += 10; + } + + default: { + x += 100; + } +} + +assert(x == 20); \ No newline at end of file diff --git a/tests/switch/variables.du b/tests/switch/variables.du new file mode 100644 index 00000000..d92595fb --- /dev/null +++ b/tests/switch/variables.du @@ -0,0 +1,67 @@ +/** + * variables.du + * + * Testing variables within a switch + */ + +var x = 10; + +switch (x) { + case 1: { + x += 10; + } + + case 5: { + x += 10; + } + + case 10: { + x += 10; + } +} + +assert(x == 20); + +// No match + +var x = 11; + +switch (x) { + case 1: { + x += 10; + } + + case 5: { + x += 10; + } + + case 5 + 5: { + x += 10; + } +} + +assert(x == 11); + +// No match - default + +var x = 11; + +switch (x) { + case 1: { + x += 10; + } + + case 5: { + x += 10; + } + + case 10: { + x += 10; + } + + default: { + x += 10; + } +} + +assert(x == 21); \ No newline at end of file