From b114387728e29fed78ba1a364c96c99718fcac4a Mon Sep 17 00:00:00 2001 From: cozis Date: Wed, 7 Dec 2022 23:48:16 +0100 Subject: [PATCH] example http parser --- examples/compare.noja | 41 ++++ examples/http.noja | 383 ++++++++++++++++++++++++++++++++++ examples/{json => }/json.noja | 132 ++++++++---- examples/json/test.noja | 122 ----------- examples/scan.noja | 46 ++++ src/lib/runtime/runtime.c | 2 +- 6 files changed, 558 insertions(+), 168 deletions(-) create mode 100644 examples/compare.noja create mode 100644 examples/http.noja rename examples/{json => }/json.noja (66%) delete mode 100644 examples/json/test.noja create mode 100644 examples/scan.noja diff --git a/examples/compare.noja b/examples/compare.noja new file mode 100644 index 0000000..f08daae --- /dev/null +++ b/examples/compare.noja @@ -0,0 +1,41 @@ + +fun compareLists(a: List, b: List) { + len = count(a); + if len != count(b): + return false; + i = 0; + while i < len: { + if a[i] != b[i]: + return false; + i = i+1; + } + return true; +} + +fun compareMaps(a: Map, b: Map) { + keys = keysof(a); + len = count(keys); + if len != count(b): + return false; + i = 0; + while i < len: { + k = keys[i]; + if a[k] != b[k]: + return false; + i = i+1; + } + return true; +} + +fun compareAny(a, b) { + Ta = type(a); + Tb = type(b); + + if Ta != Tb: + return false; + if Ta == List: + return compareLists(a, b); + if Ta == Map: + return compareMaps(a, b); + return a == b; +} diff --git a/examples/http.noja b/examples/http.noja new file mode 100644 index 0000000..dc64c94 --- /dev/null +++ b/examples/http.noja @@ -0,0 +1,383 @@ +scan = import("scan.noja"); + +newScanner = scan.newScanner; +Scanner = scan.Scanner; +hint = scan.hint; +consume = scan.consume; +current = scan.current; +isDigit = scan.isDigit; +consumeSpaces = scan.consumeSpaces; + +fun isAlpha(char: String) + return (string.ord(char) >= string.ord('a') and string.ord(char) <= string.ord('z')) + or (string.ord(char) >= string.ord('A') and string.ord(char) <= string.ord('Z')); + +fun isUpper(char: String) + return string.ord(char) >= string.ord('A') + and string.ord(char) <= string.ord('Z'); + +fun isUnreserved(char: String) + return isAlpha(char) + or isDigit(char) + or char == "-" + or char == "." + or char == "_" + or char == "~"; + +fun isSubdelim(char: String) + return char == "!" or char == "$" + or char == "&" or char == "'" + or char == "(" or char == ")" + or char == "*" or char == "+" + or char == "," or char == ";" + or char == "="; + +fun isPChar(char: String) + return isUnreserved(char) + or isSubdelim(char) + or char == ":" + or char == "@"; + +fun parseMethod(scan: Scanner) { + + char = current(scan); + if char == none or not isUpper(char): + return none, "Missing method"; + + method = ""; + do { + method = string.cat(method, char); + consume(scan); + char = current(scan); + } while char != none and isUpper(char); + + return method, none; +} + +fun parsePort(scan: Scanner) { + + port = none; + char = current(scan); + if char == ":" and isDigit(hint(scan, 1)): { + + consume(scan); # Skip the ":" + char = current(scan); + + port = 0; + do { + digit = string.ord(char) + - string.ord("0"); + port = port * 10 + digit; + consume(scan); + char = current(scan); + } while char != none and isDigit(char); + } + return port; +} + +fun isHostname(char: String) + return isUnreserved(char) + or isSubdelim(char); + +fun parseHost(scan: Scanner) { + + char = current(scan); + if char == none: + return none, "Missing host"; + + if not isHostname(char): + return none, "Invalid character in place of host"; + + name = ""; + do { + name = string.cat(name, char); + consume(scan); + char = current(scan); + } while char != none and isHostname(char); + + port = parsePort(scan); + return {name: name, port: port}; +} + +fun parsePath(scan: Scanner) { + + path = ""; + char = current(scan); + if char != none and char == "/": { + path = string.cat(path, char); + consume(scan); + char = current(scan); + } else { + if char == none or not isPChar(char): + return none, "Invalid character in place of path"; + } + + while char != none and isPChar(char): { + do { + path = string.cat(path, char); + consume(scan); + char = current(scan); + } while char != none and isPChar(char); + if char == none or char != "/": + break; + path = string.cat(path, char); + consume(scan); + char = current(scan); + } + + return path; +} + +fun isQuery(char: ?String) { + if char == none: + return false; + return isPChar(char) + or char == "/" + or char == "?"; +} + +fun parseQuery(scan: Scanner) { + + char = current(scan); + if char != none and char == "?": { + consume(scan); # Consume the "?" + query = ""; + while isQuery(char = current(scan)): { + query = string.cat(query, char); + consume(scan); + } + } else + query = none; + + return query; +} + +fun isFragment(char: ?String) { + if char == none: + return false; + return isPChar(char) or char == "/"; +} + +fun parseFragment(scan: Scanner) { + + char = current(scan); + if char != none and char == "#": { + consume(scan); # Consume the "#" + fragment = ""; + while isFragment(char = current(scan)): { + fragment = string.cat(fragment, char); + consume(scan); + } + } else + fragment = none; + + return fragment; +} + +Host = { name: String, port: ?int }; + +URL = { + schema: ?String, + host: ?Host, + path: ?String, + query: ?String, + fragment: ?String +}; + +fun isSchemaFirst(char: ?String) { + return char != none and isAlpha(char); +} + +fun isSchema(char: ?String) + return char != none + and (isAlpha(char) + or isDigit(char) + or char == "+" + or char == "-" + or char == "."); + +fun parseSchema(scan: Scanner) { + + char = current(scan); + schema = none; + if isSchemaFirst(char): { + start = scan.i; + schema = ""; + do { + schema = string.cat(schema, char); + consume(scan); + char = current(scan); + } while isSchema(char); + + if char == ":": + consume(scan); + else { + schema = none; + scan.i = start; + } + } + + return schema; +} + +fun followsAuthority(scan: Scanner) + return current(scan) == "/" and hint(scan, 1) == "/"; + +fun parseURL(scan: Scanner) { + + schema = parseSchema(scan); + + if followsAuthority(scan): { + consume(scan, 2); + + host, error = parseHost(scan); + if error != none: + return none, error; + + path = none; + if current(scan) == "/": + path = parsePath(scan); + + } else { + + host = none; + + char = current(scan); + if char == none or char == "?" or char == "#": + return none, "Missing path"; + + path, error = parsePath(scan); + if error != none: + return none, error; + } + + query = parseQuery(scan); + fragment = parseFragment(scan); + print(scan); + + return { + host : host, + path : path, + query : query, + schema : schema, + fragment: fragment + }; +} + +fun parseVersion(scan: Scanner) { + + if hint(scan, 0) != "H" or hint(scan, 1) != "T" + or hint(scan, 2) != "T" or hint(scan, 3) != "P" + or hint(scan, 4) != "/" or not isDigit(hint(scan, 5)): + return none, "Invalid version token"; + + consume(scan, 5); + major = string.ord(current(scan)) + - string.ord("0"); + + if hint(scan, 1) != "." and isDigit(hint(scan, 2)): { + consume(scan, 2); + minor = string.ord(current(scan)) + - string.ord("0"); + } else + minor = 0; + consume(scan); + + return {major: major, minor: minor}; +} + +Version = { + major: int, + minor: int +}; + +Request = { + method : String, + url : URL, + version: Version +}; + +fun newRequest(method: String, url: URL, + version: Version, headers: Map = {}) + return { + method: method, + url: url, + version: version, + headers: headers + }; + +fun parse(src: String) { + + scan = newScanner(src); + + method, error = parseMethod(scan); + if error != none: + return none, error; + + char = current(scan); + if char == none or char != " ": + return none, "Missing space after method"; + consume(scan); + + url, error = parseURL(scan); + if error != none: + return none, error; + + char = current(scan); + if char != " ": + return none, "Missing space after URL"; + consume(scan); + + version, error = parseVersion(scan); + if error != none: + return none, error; + + if hint(scan, 0) != "\r" or hint(scan, 1) != "\n": + # Request with no headers or body + return newRequest(method, url, version); + consume(scan, 2); # Consume the "\r\n" + + headers = {}; + while (char = hint(scan, 0)) != none and (char != "\r" or hint(scan, 1) != "\n"): { + + # Header name until the next ":" + name = ""; + do { + name = string.cat(name, char); + consume(scan); + char = current(scan); + } while char != ":" and char != none; + + if char == none: + return none, "Invalid header (missing \":\" after name)"; + consume(scan); + consumeSpaces(scan); + char = current(scan); + + # Header body until \r + body = ""; + do { + body = string.cat(body, char); + consume(scan); + char = current(scan); + } while char != "\r" and char != none; + + headers[name] = body; + } + + if char != none: + consume(scan, 2); # Consume the final "\r\n" + + return newRequest(method, url, version, headers); +} + +sample = string.cat( + "GET /search?client=firefox-b-d&q=http+request+dataset HTTP/2\r\n", + "Host: www.google.com\r\n", + "user-agent: curl/7.84.0\r\n", + "accept: */*\r\n", + "\r\n"); + +request, error = parse(sample); +print("request=", request, "\n"); +print("error=", error, "\n"); diff --git a/examples/json/json.noja b/examples/json.noja similarity index 66% rename from examples/json/json.noja rename to examples/json.noja index ad9158d..b5cd5e5 100644 --- a/examples/json/json.noja +++ b/examples/json.noja @@ -1,49 +1,12 @@ +scan = import("scan.noja"); -Scanner = {src: String, i: int}; - -fun newScanner(src: String) { - scan = {src: src, i: 0}; - assert(istypeof(Scanner, scan)); - return scan; -} - -fun isDigit(char: ?String) { - if char == none: - return false; - return string.ord(char) >= string.ord('0') - and string.ord(char) <= string.ord('9'); -} - -fun isSpace(c: String) - return c == ' ' - or c == '\t' - or c == '\n'; - -fun min(x, y) { - if x < y: - return x; - return y; -} - -fun hint(scan: Scanner, n: int = 1) { - k = scan.i + n; - if k < count(scan.src): - return scan.src[k]; - return none; -} - -fun current(scan: Scanner) - return hint(scan, 0); - -fun consume(scan: Scanner, n: int = 1) { - assert(n > 0); - limit = count(scan.src); - scan.i = min(scan.i + n, limit); -} - -fun consumeSpaces(scan: Scanner) - while isSpace(current(scan)): - consume(scan); +newScanner = scan.newScanner; +Scanner = scan.Scanner; +hint = scan.hint; +consume = scan.consume; +current = scan.current; +consumeSpaces = scan.consumeSpaces; +isDigit = scan.isDigit; fun parseString(scan: Scanner) { @@ -301,4 +264,83 @@ fun parse(src: String) { scan = newScanner(src); val, err = parseValue(scan); return val, err; +} + +fun test() { + + compareAny = import("compare.noja").compareAny; + + Error = String; + JSONValueType = (bool | float | String | Map | List | None); + TestCase = [String, ?JSONValueType, ?Error]; + + fun runAllTestCases(test_cases: List) { + total = count(test_cases); + passed = 0; + evaluated = 0; + while evaluated < total: { + + test_case = test_cases[evaluated]; + input = test_case[0]; + expres = test_case[1]; # Expected result + experr = test_case[2]; # Expected error + + res, err = parse(input); + + #print("res=", res, ", err=", err, "\n"); + #print("expres=", expres, ", experr=", experr, "\n"); + + result = compareAny(res, expres) + and compareAny(err, experr); + + if result: { + print("Test ", evaluated, ": PASSED\n"); + passed = passed+1; + } else { + + print("Test ", evaluated, ": FAILED\n"); + print("\tInput:\n"); + print("\t\t", input, "\n"); + + if experr == none: { + if err == none: + print("\tGot result ", res, " but was expected ", expres, "\n"); + else + print("\tFailed unexpectedly [", err, "]\n"); + } else if err == none: + print("\tSucceded unexpectedly. Was expected error [", experr, "]\n"); + else + print("\tWas expected error [", experr, "], but got [", err, "]\n"); + } + evaluated = evaluated+1; + } + return passed, (total-passed), total; + } + + fun newPassingTestCase(src: String, res: JSONValueType) + return [src, res, none]; + + fun newFailingTestCase(src: String, err: String) + return [src, none, err]; + + test_cases = [ + newPassingTestCase("null", none), + newPassingTestCase("true", true), + newPassingTestCase("false", false), + newPassingTestCase("\"Hello, world!\"", "Hello, world!"), + newPassingTestCase("1", 1.0), + newPassingTestCase("2.3", 2.3), + newPassingTestCase("[]", []), + newPassingTestCase("[1]", [1.0]), + newPassingTestCase("[1, 2]", [1.0, 2.0]), + newPassingTestCase("[1, 2, 3]", [1.0, 2.0, 3.0]), + newPassingTestCase("{}", {}), + newPassingTestCase("{\"k1\": true}", {k1: true}), + newPassingTestCase("{\"k1\": true, \"k2\": false}", {k1: true, k2: false}) + ]; + + passed, failed, total = runAllTestCases(test_cases); + print("passed: ", passed, "\n"); + print("failed: ", failed, "\n"); + print(" total: ", total, "\n"); } \ No newline at end of file diff --git a/examples/json/test.noja b/examples/json/test.noja deleted file mode 100644 index d1dee91..0000000 --- a/examples/json/test.noja +++ /dev/null @@ -1,122 +0,0 @@ -json = import("json.noja"); - -Error = String; -JSONValueType = (bool | float | String | Map | List | None); -TestCase = [String, ?JSONValueType, ?Error]; - -fun compareLists(a: List, b: List) { - len = count(a); - if len != count(b): - return false; - i = 0; - while i < len: { - if a[i] != b[i]: - return false; - i = i+1; - } - return true; -} - -fun compareMaps(a: Map, b: Map) { - keys = keysof(a); - len = count(keys); - if len != count(b): - return false; - i = 0; - while i < len: { - k = keys[i]; - if a[k] != b[k]: - return false; - i = i+1; - } - return true; -} - -fun compareAny(a, b) { - Ta = type(a); - Tb = type(b); - - if Ta != Tb: - return false; - if Ta == List: - return compareLists(a, b); - if Ta == Map: - return compareMaps(a, b); - return a == b; -} - -fun runTestCase(test_case: TestCase) { - res, err = json.parse(test_case[0]); - return compareAny(res, test_case[1]) - and compareAny(err, test_case[2]); -} - -fun runAllTestCases(test_cases: List) { - total = count(test_cases); - passed = 0; - evaluated = 0; - while evaluated < total: { - - test_case = test_cases[evaluated]; - input = test_case[0]; - expres = test_case[1]; # Expected result - experr = test_case[2]; # Expected error - - res, err = json.parse(input); - - #print("res=", res, ", err=", err, "\n"); - #print("expres=", expres, ", experr=", experr, "\n"); - - result = compareAny(res, expres) - and compareAny(err, experr); - - if result: { - print("Test ", evaluated, ": PASSED\n"); - passed = passed+1; - } else { - - print("Test ", evaluated, ": FAILED\n"); - print("\tInput:\n"); - print("\t\t", input, "\n"); - - if experr == none: { - if err == none: - print("\tGot result ", res, " but was expected ", expres, "\n"); - else - print("\tFailed unexpectedly [", err, "]\n"); - } else if err == none: - print("\tSucceded unexpectedly. Was expected error [", experr, "]\n"); - else - print("\tWas expected error [", experr, "], but got [", err, "]\n"); - } - evaluated = evaluated+1; - } - return passed, (total-passed), total; -} - -fun newPassingTestCase(src: String, res: JSONValueType) - return [src, res, none]; - -fun newFailingTestCase(src: String, err: String) - return [src, none, err]; - -test_cases = [ - newPassingTestCase("null", none), - newPassingTestCase("true", true), - newPassingTestCase("false", false), - newPassingTestCase("\"Hello, world!\"", "Hello, world!"), - newPassingTestCase("1", 1.0), - newPassingTestCase("2.3", 2.3), - newPassingTestCase("[]", []), - newPassingTestCase("[1]", [1.0]), - newPassingTestCase("[1, 2]", [1.0, 2.0]), - newPassingTestCase("[1, 2, 3]", [1.0, 2.0, 3.0]), - newPassingTestCase("{}", {}), - newPassingTestCase("{\"k1\": true}", {k1: true}), - newPassingTestCase("{\"k1\": true, \"k2\": false}", {k1: true, k2: false}) -]; - -passed, failed, total = runAllTestCases(test_cases); -print("passed: ", passed, "\n"); -print("failed: ", failed, "\n"); -print(" total: ", total, "\n"); diff --git a/examples/scan.noja b/examples/scan.noja new file mode 100644 index 0000000..90b5b24 --- /dev/null +++ b/examples/scan.noja @@ -0,0 +1,46 @@ + +Scanner = {src: String, i: int}; + +fun newScanner(src: String) { + scan = {src: src, i: 0}; + assert(istypeof(Scanner, scan)); + return scan; +} + +fun isDigit(char: ?String) { + if char == none: + return false; + return string.ord(char) >= string.ord('0') + and string.ord(char) <= string.ord('9'); +} + +fun isSpace(c: String) + return c == ' ' + or c == '\t' + or c == '\n'; + +fun min(x, y) { + if x < y: + return x; + return y; +} + +fun hint(scan: Scanner, n: int = 1) { + k = scan.i + n; + if k < count(scan.src): + return scan.src[k]; + return none; +} + +fun current(scan: Scanner) + return hint(scan, 0); + +fun consume(scan: Scanner, n: int = 1) { + assert(n > 0); + limit = count(scan.src); + scan.i = min(scan.i + n, limit); +} + +fun consumeSpaces(scan: Scanner) + while isSpace(current(scan)): + consume(scan); \ No newline at end of file diff --git a/src/lib/runtime/runtime.c b/src/lib/runtime/runtime.c index badd853..247fe21 100644 --- a/src/lib/runtime/runtime.c +++ b/src/lib/runtime/runtime.c @@ -931,7 +931,7 @@ static _Bool step(Runtime *runtime, Error *error) Object *callable = Stack_Top(runtime->stack, 0); ASSERT(callable != NULL); - Object *argv[8]; + Object *argv[32]; int max_argc = sizeof(argv) / sizeof(argv[0]); if(argc > max_argc)