diff --git a/data/expression2/tests/compiler/compiler/restrictions/array_in_array.txt b/data/expression2/tests/compiler/compiler/restrictions/array_in_array.txt new file mode 100644 index 0000000000..5362538258 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/restrictions/array_in_array.txt @@ -0,0 +1,5 @@ +## SHOULD_FAIL:COMPILE + +# Cannot hold an array in an array + +local A = array(array()) \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/restrictions/fn_override.txt b/data/expression2/tests/compiler/compiler/restrictions/fn_override.txt new file mode 100644 index 0000000000..ab0c610974 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/restrictions/fn_override.txt @@ -0,0 +1,11 @@ +## SHOULD_FAIL:COMPILE + +# Cannot override existing function with function that has different return type + +function test() { + +} + +function number test() { + return 5 +} \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/restrictions/fn_override_pass.txt b/data/expression2/tests/compiler/compiler/restrictions/fn_override_pass.txt new file mode 100644 index 0000000000..cadd28b174 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/restrictions/fn_override_pass.txt @@ -0,0 +1,10 @@ +## SHOULD_PASS:COMPILE + +# Can override existing function with function that has different return type + +function test() { +} + +function test() { + print("zoo wee mama") +} \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/restrictions/void_expr_builtin.txt b/data/expression2/tests/compiler/compiler/restrictions/void_expr_builtin.txt new file mode 100644 index 0000000000..e2e6ab6ad0 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/restrictions/void_expr_builtin.txt @@ -0,0 +1,5 @@ +## SHOULD_FAIL:COMPILE + +# Cannot use void return as an expression + +local E = print() \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/restrictions/void_expr_fn.txt b/data/expression2/tests/compiler/compiler/restrictions/void_expr_fn.txt new file mode 100644 index 0000000000..3ac5804456 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/restrictions/void_expr_fn.txt @@ -0,0 +1,7 @@ +## SHOULD_FAIL:COMPILE + +# Cannot use void return as an expression + +function void test() {} + +local E = test() \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/varargs/array/call_with_array.txt b/data/expression2/tests/compiler/compiler/varargs/array/call_with_array.txt new file mode 100644 index 0000000000..7ce30f6978 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/varargs/array/call_with_array.txt @@ -0,0 +1,7 @@ +## SHOULD_FAIL:COMPILE + +# Cannot call array variadic function with array argument. + +function arrayVariadic(N:number, ...X:array) {} + +arrayVariadic(1, array()) \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/varargs/array/method_call_with_array.txt b/data/expression2/tests/compiler/compiler/varargs/array/method_call_with_array.txt new file mode 100644 index 0000000000..dc770fdf64 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/varargs/array/method_call_with_array.txt @@ -0,0 +1,7 @@ +## SHOULD_FAIL:COMPILE + +# Cannot call array variadic method with array argument. + +function number:arrayVariadic(N:number, ...X:array) {} + +1:arrayVariadic(1, array(1, 2, 3)) \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/varargs/override/bad.txt b/data/expression2/tests/compiler/compiler/varargs/override/bad.txt new file mode 100644 index 0000000000..5bd2754ae4 --- /dev/null +++ b/data/expression2/tests/compiler/compiler/varargs/override/bad.txt @@ -0,0 +1,4 @@ +## SHOULD_FAIL:COMPILE + +# Can't override existing function with variadic +function print(...A:array) {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/compiler/varargs/override/good.txt b/data/expression2/tests/compiler/compiler/varargs/override/good.txt new file mode 100644 index 0000000000..a3102b08ae --- /dev/null +++ b/data/expression2/tests/compiler/compiler/varargs/override/good.txt @@ -0,0 +1,4 @@ +## SHOULD_PASS:COMPILE + +# You can however override if there is a parameter beforehand. +function print(N:number, ...A:array) {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/array.txt b/data/expression2/tests/compiler/parser/array.txt new file mode 100644 index 0000000000..a6e9a26cb5 --- /dev/null +++ b/data/expression2/tests/compiler/parser/array.txt @@ -0,0 +1,7 @@ +## SHOULD_PASS:COMPILE + +array() + +array(1, 2, 3) + +array(2 = 4, 5 = 7) \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/assign.txt b/data/expression2/tests/compiler/parser/assign.txt new file mode 100644 index 0000000000..63cdf9a3f3 --- /dev/null +++ b/data/expression2/tests/compiler/parser/assign.txt @@ -0,0 +1,14 @@ +## SHOULD_PASS:COMPILE + +Var = 5 + +local OtherVar = 2 + +A=B=C=5 + +A++ +A-- +A += 1 +A -= 1 +A *= 1 +A /= 1 \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/call.txt b/data/expression2/tests/compiler/parser/call.txt new file mode 100644 index 0000000000..651555c523 --- /dev/null +++ b/data/expression2/tests/compiler/parser/call.txt @@ -0,0 +1,6 @@ +## SHOULD_PASS:COMPILE + +min(1, 2) +print("foo", 2) + +array():count() \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/constants.txt b/data/expression2/tests/compiler/parser/constants.txt new file mode 100644 index 0000000000..9efb085330 --- /dev/null +++ b/data/expression2/tests/compiler/parser/constants.txt @@ -0,0 +1,4 @@ +## SHOULD_PASS:COMPILE + +_PI +_E \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/events.txt b/data/expression2/tests/compiler/parser/events.txt new file mode 100644 index 0000000000..bad25b4e3e --- /dev/null +++ b/data/expression2/tests/compiler/parser/events.txt @@ -0,0 +1,4 @@ +## SHOULD_PASS:COMPILE + +event tick() {} +event chat(_:entity, _:string, _) {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/for.txt b/data/expression2/tests/compiler/parser/for.txt new file mode 100644 index 0000000000..2352328b47 --- /dev/null +++ b/data/expression2/tests/compiler/parser/for.txt @@ -0,0 +1,9 @@ +## SHOULD_PASS:COMPILE + +for (B = 1, 2) { + break +} + +for (B = 1, 2, 3) { continue } + +for (_ = 1, 5) {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/foreach.txt b/data/expression2/tests/compiler/parser/foreach.txt new file mode 100644 index 0000000000..393d434566 --- /dev/null +++ b/data/expression2/tests/compiler/parser/foreach.txt @@ -0,0 +1,11 @@ +## SHOULD_PASS:COMPILE + +foreach(K,V:number=array(1, 2, 3)) { break } +foreach (K:number,V:number=array(1, 2, 3)) { continue } + +foreach (K: number, V: number = array(1, 2, 3)) { + continue +} + +foreach (K:number, _:entity = table()) {} +foreach (_, _:entity = table()) {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/functions.txt b/data/expression2/tests/compiler/parser/functions.txt new file mode 100644 index 0000000000..da9dc43526 --- /dev/null +++ b/data/expression2/tests/compiler/parser/functions.txt @@ -0,0 +1,18 @@ +## SHOULD_PASS:COMPILE + +function unimplemented(_) {} +unimplemented(5) + +function f() {} +function r() { return } + +function number test() { return 55 } +function entity:test() { return void } +function number test(X, Y: vector) { return X } +function number test([X Y]) { return X + Y } +function number test([X Y]: number) { return X + Y } + +function number entity:test(...Variadic:array) { + Expression = ( test() + 2 ) + return Expression +} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/if.txt b/data/expression2/tests/compiler/parser/if.txt new file mode 100644 index 0000000000..d49f6346e4 --- /dev/null +++ b/data/expression2/tests/compiler/parser/if.txt @@ -0,0 +1,7 @@ +## SHOULD_PASS:COMPILE + +if (1) {} + +if (1) {} elseif (2) {} + +if (1) {} elseif (2) {} else {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/index.txt b/data/expression2/tests/compiler/parser/index.txt new file mode 100644 index 0000000000..ac8ae710e5 --- /dev/null +++ b/data/expression2/tests/compiler/parser/index.txt @@ -0,0 +1,6 @@ +## SHOULD_PASS:COMPILE + +array()[1, number] + +A = array() +A[1, number] = 5 \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/io.txt b/data/expression2/tests/compiler/parser/io.txt new file mode 100644 index 0000000000..9a236f41b9 --- /dev/null +++ b/data/expression2/tests/compiler/parser/io.txt @@ -0,0 +1,9 @@ +## SHOULD_PASS:COMPILE + +@inputs In +@outputs Out + +~In +$In +->In +->Out \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/numbers.txt b/data/expression2/tests/compiler/parser/numbers.txt new file mode 100644 index 0000000000..2b520836b0 --- /dev/null +++ b/data/expression2/tests/compiler/parser/numbers.txt @@ -0,0 +1,30 @@ +## SHOULD_PASS:COMPILE + +# Hexadecimal +0x2042 +0xDEADBEEF +0xdeadbeef + +# Binary +0b00111001 +0b0011111 + +# Decimal +2432.2352 +2193.23e2 +-2139.123e63 + +# Integer +1 +-512 + +# Quaternion literals +2j +4.4k +1k +1j + +# Complex literals +3i +2.7i +1i diff --git a/data/expression2/tests/compiler/parser/ops.txt b/data/expression2/tests/compiler/parser/ops.txt new file mode 100644 index 0000000000..fdef6adae7 --- /dev/null +++ b/data/expression2/tests/compiler/parser/ops.txt @@ -0,0 +1,5 @@ +## SHOULD_PASS:COMPILE + +!(+(-(0 >> 1 << 2 * 3 + 4 - 5 / 6 % 7 && 8 || 9 != 10 == 11 <= 12 >= 13 ^^ 14))) + +0 | 1 & 2 != 3 == 4 \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/strings.txt b/data/expression2/tests/compiler/parser/strings.txt new file mode 100644 index 0000000000..a99246ef59 --- /dev/null +++ b/data/expression2/tests/compiler/parser/strings.txt @@ -0,0 +1,12 @@ +## SHOULD_PASS:COMPILE + +"foo bar" + +"foo\nbar" + +"\n\t\r\a\v\b\\" +"\"" + +" + Multiline +" \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/switch.txt b/data/expression2/tests/compiler/parser/switch.txt new file mode 100644 index 0000000000..c8559b0be8 --- /dev/null +++ b/data/expression2/tests/compiler/parser/switch.txt @@ -0,0 +1,13 @@ +## SHOULD_PASS:COMPILE + +switch (2) { + case 1, + case Var, + default, +} + +switch (3) { + case 1, break, + case 2, + default, break +} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/table.txt b/data/expression2/tests/compiler/parser/table.txt new file mode 100644 index 0000000000..c2f55a2c77 --- /dev/null +++ b/data/expression2/tests/compiler/parser/table.txt @@ -0,0 +1,9 @@ +## SHOULD_PASS:COMPILE + +table(1, 2, 3) + +N = 5 +table(N = 1, 2 = 2) + +K = "" +table(K = 5, "" = 5) \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/ternary.txt b/data/expression2/tests/compiler/parser/ternary.txt new file mode 100644 index 0000000000..8fe66372e7 --- /dev/null +++ b/data/expression2/tests/compiler/parser/ternary.txt @@ -0,0 +1,7 @@ +## SHOULD_PASS:COMPILE + +1 ? 2 : 3 +"1" ? "2" : "3" + +5 ?: 3 +"a" ?: "b" \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/try.txt b/data/expression2/tests/compiler/parser/try.txt new file mode 100644 index 0000000000..e4f4d72e49 --- /dev/null +++ b/data/expression2/tests/compiler/parser/try.txt @@ -0,0 +1,7 @@ +## SHOULD_PASS:COMPILE + +try {} catch (E) {} +try {} catch (Err:string) {} + +try {} catch(_) {} +try {} catch(_:string) {} \ No newline at end of file diff --git a/data/expression2/tests/compiler/parser/while.txt b/data/expression2/tests/compiler/parser/while.txt new file mode 100644 index 0000000000..2a88951066 --- /dev/null +++ b/data/expression2/tests/compiler/parser/while.txt @@ -0,0 +1,10 @@ +## SHOULD_PASS:COMPILE + +while (1) { A = 0 } + +while (1) { break } +while (1) { continue } + +do { A = 0 } while (1) +do { break } while (2) +do { continue } while (3) \ No newline at end of file diff --git a/data/expression2/tests/compiler/preprocessor/commands/error.txt b/data/expression2/tests/compiler/preprocessor/commands/error.txt new file mode 100644 index 0000000000..53365cb647 --- /dev/null +++ b/data/expression2/tests/compiler/preprocessor/commands/error.txt @@ -0,0 +1,3 @@ +## SHOULD_FAIL:COMPILE + +#error Hello, world! \ No newline at end of file diff --git a/data/expression2/tests/compiler/preprocessor/commands/ifdef.txt b/data/expression2/tests/compiler/preprocessor/commands/ifdef.txt new file mode 100644 index 0000000000..3af4549e93 --- /dev/null +++ b/data/expression2/tests/compiler/preprocessor/commands/ifdef.txt @@ -0,0 +1,6 @@ +## SHOULD_PASS:COMPILE + +#ifdef print(...) +#else + #error Uhh +#endif \ No newline at end of file diff --git a/data/expression2/tests/compiler/preprocessor/commands/ifndef.txt b/data/expression2/tests/compiler/preprocessor/commands/ifndef.txt new file mode 100644 index 0000000000..aef44a5b98 --- /dev/null +++ b/data/expression2/tests/compiler/preprocessor/commands/ifndef.txt @@ -0,0 +1,10 @@ +## SHOULD_PASS:COMPILE + +#ifndef print(...) + #error Uhh +#endif + +#ifndef print(...) + #error Uhh 2 +#else +#endif \ No newline at end of file diff --git a/data/expression2/tests/compiler/preprocessor/commands/warning.txt b/data/expression2/tests/compiler/preprocessor/commands/warning.txt new file mode 100644 index 0000000000..3ab4af3ad7 --- /dev/null +++ b/data/expression2/tests/compiler/preprocessor/commands/warning.txt @@ -0,0 +1,3 @@ +## SHOULD_PASS:COMPILE + +#warning XYZ \ No newline at end of file diff --git a/data/expression2/tests/compiler/preprocessor/comments.txt b/data/expression2/tests/compiler/preprocessor/comments.txt new file mode 100644 index 0000000000..a165453b88 --- /dev/null +++ b/data/expression2/tests/compiler/preprocessor/comments.txt @@ -0,0 +1,13 @@ +## SHOULD_PASS:COMPILE + +#[ + Hello, world! +]# + +#[]# +#[ content ]# + + # +# + +# \ No newline at end of file diff --git a/data/expression2/tests/compiler/preprocessor/directives.txt b/data/expression2/tests/compiler/preprocessor/directives.txt new file mode 100644 index 0000000000..1a3a5b0c30 --- /dev/null +++ b/data/expression2/tests/compiler/preprocessor/directives.txt @@ -0,0 +1,10 @@ +## SHOULD_PASS:COMPILE + +@name Test +@persist [X Y]:number Z:number W +@inputs [J K]:number L:number M +@outputs [A B]:number C:number D +@trigger X Y +@autoupdate +@model f +@strict \ No newline at end of file diff --git a/data/expression2/tests/parsing.txt b/data/expression2/tests/parsing.txt deleted file mode 100644 index 48a930ff27..0000000000 --- a/data/expression2/tests/parsing.txt +++ /dev/null @@ -1,113 +0,0 @@ -## SHOULD_PASS:COMPILE - -@name Parsing Test -@persist [XYZ ZYX]:number FOO BAR:entity -@inputs In -@outputs Out -@strict -@trigger none - -#[ Comment ]# -# Single line - -Str = "abcdefghijklm -nopqrs\n\t\r\a\v\b\\" - -Num1 = 0x2042 -Num2 = 0b00111001 -Num3 = 2432.2352 -Num4 = 2193e2 -Num5 = 2j + 4.4k + k + j -Num6 = 3i + 2.7i + i - -function f() {} -function r() { return } - -function number test() { return 55 } -function entity:test() { return void } -function number test(X, Y: vector) { return X } -function number test([X Y]) { return X + Y } -function number test([X Y]: number) { return X + Y } - -function number entity:test(...Variadic:array) { - Expression = ( test() + 2 ) - return Expression -} - -local Var = 2 -switch (2) { - case 1, - break - - case Var, - break - - default, - break -} - -local A = 1 -if (A) {} elseif(2) {} else {} -while (A) { A = 0 } - -for (B = 1, 2) { A = 0 } -for (B = 1, 2, 3) { A = 0 } - -foreach (K, V: number = array(1, 2, 3)) { A = 0 } - -while (A) { break } -while (A) { continue } - -do { A = 0 } while (A) -do { break } while (A) -do { continue } while (A) - -try {} catch(Err) {} - -event tick() {} -event chat(_:entity, _:string, _) {} - -for (_ = 1, 5) {} -foreach (K:number, _:entity = table()) {} -foreach (_, _:entity = table()) {} - -function unimplemented(_) {} -unimplemented(5) - -try {} catch(_) {} - - -A++ -A-- -A += 1 -A -= 1 -A *= 1 -A /= 1 - -Ternary = 1 ?: 2 -Ternary2 = 1 ? 2 : 1 - -Ops = !(+(-(0 >> 1 << 2 * 3 + 4 - 5 / 6 % 7 && 8 || 9 != 10 == 11 <= 12 >= 13 ^^ 14))) -Logical = 0 | 1 & 2 != 3 == 4 - -array():count() -array()[1, number] -table(1 = 1, 2 = 2) - -~In -$In -->In - -0 -1 -"Hello" - -X = _PI - -#ifdef print(...) -#else -#endif - -#ifndef print(...) -#else -#endif \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/assignment.txt b/data/expression2/tests/runtime/base/assignment.txt new file mode 100644 index 0000000000..4c16eba784 --- /dev/null +++ b/data/expression2/tests/runtime/base/assignment.txt @@ -0,0 +1,25 @@ +## SHOULD_PASS:EXECUTE +@persist Ran:number + +Ran = 0 + +function number increment() { + if (Ran) { + error("Ran twice") + } else { + Ran = 1 + return 1 + } +} + +X = increment() + +Ran = 0 + +X = Y = increment() + +Z = 1 + +Z /= 2 + +assert(Z == 0.5, Z:toString()) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/if.txt b/data/expression2/tests/runtime/base/if.txt new file mode 100644 index 0000000000..59bc7b97f3 --- /dev/null +++ b/data/expression2/tests/runtime/base/if.txt @@ -0,0 +1,46 @@ +## SHOULD_PASS:EXECUTE + +local Num = 0 +if (1) { + Num = 1 +} + +assert(Num == 1) + +if (0) { + error("fail") +} elseif (1) { + Num = 2 +} + +assert(Num == 2) + +if (0) { + error("fail") +} else { + Num = 3 +} + +assert(Num == 3) + +if (1) { + # pass +} elseif (0) { + error("fail") +} else { + error("fail") +} + +if (vec(0, 0, 0)) { # Ensure if statements are using operator_is + error("fail") +} + +if (ang(0, 0, 0)) { # ensure it wasn't a fluke + error("fail") +} + +if ("") { # falsy string + error("fail") +} + +assert(!"") # I don't think this was a thing before, using not on string, but since ! just checks the inverse of operator_is now, it works. \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/loops/for.txt b/data/expression2/tests/runtime/base/loops/for.txt new file mode 100644 index 0000000000..d51efc33d2 --- /dev/null +++ b/data/expression2/tests/runtime/base/loops/for.txt @@ -0,0 +1,32 @@ +## SHOULD_PASS:EXECUTE + +local Num = 0 +for(I = 1, 1000) { + Num++ + if(I == 500) { + break + } +} + +assert(Num == 500) + +for(_ = 1, 500) { + Num++ +} + +print(Num) + +assert(Num == 1000, Num:toString()) + +# Ensure continue does not bleed into next iteration +local Inc = 1 +for (I = 1, 10, 1) { + assert(I == Inc, "Continue bled into another iteration") + Inc++ + + if (1) { + continue + } + + error("unreachable") +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/loops/foreach.txt b/data/expression2/tests/runtime/base/loops/foreach.txt new file mode 100644 index 0000000000..90e03ebf04 --- /dev/null +++ b/data/expression2/tests/runtime/base/loops/foreach.txt @@ -0,0 +1,50 @@ +## SHOULD_PASS:EXECUTE + +local I = 0 +foreach(K, V:number = array(1, 2, 3, 4, 5)) { + assert(array(1, 2, 3, 4, 5)[K, number] == V) + I++ +} + +assert(I == 5) + +foreach(_, V:number = array(1, 2, 3, 4, 5)) { + I++ +} + +assert(I == 10) + +# Ensure break works + +foreach(K: number, V:number = array(1, 2, 3, 4)) { + assert(K == 1, "Should never iterate past break") + + if (1) { + break + } + + error("unreachable") +} + +# Ensure continue works + +foreach(K: number, V:number = array(1, 2, 3, 4)) { + if (1) { + continue + } + + error("unreachable") +} + +# Ensure continue does not bleed into next iteration +local Inc = 1 +foreach (K:number, V:number = array(1, 2, 3, 4)) { + assert(K == Inc, "Continue bled into another iteration") + Inc++ + + if (1) { + continue + } + + error("unreachable") +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/loops/while.txt b/data/expression2/tests/runtime/base/loops/while.txt new file mode 100644 index 0000000000..0abd007a26 --- /dev/null +++ b/data/expression2/tests/runtime/base/loops/while.txt @@ -0,0 +1,35 @@ +## SHOULD_PASS:EXECUTE + +local Num = 0 +while(Num < 1000) { + Num++ + if(Num == 500) { + break + } +} + +assert(Num == 500) + +do { + Num++ +} while(Num < 1000) + +assert(Num == 1000) + +Calls = 1 +Inc = 1 +function number calls() { + assert(Calls == Inc, "Continue bled into another iteration") + Calls++ + return Calls +} + +while (calls() < 5) { + Inc++ + + if (1) { + continue + } + + error("unreachable") +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/operators.txt b/data/expression2/tests/runtime/base/operators.txt new file mode 100644 index 0000000000..f7cbb27e00 --- /dev/null +++ b/data/expression2/tests/runtime/base/operators.txt @@ -0,0 +1,21 @@ +## SHOULD_PASS:EXECUTE + +# Ensure correct precedence of operators +# Very much incomplete, a complete set for this would be significantly larger. +# So anyone messing with that area of the parser should be wary + +assert(1 + 2 * 0, "Should parse as 1 + (2 * 0) but parsed as (1 + 2) * 0") +assert(1 - 2 * 0, "Should parse as 1 - (2 * 0) but parsed as (1 - 2) * 0") + +assert(2 * 4 - 1 == 7, "Should parse as (2 * 4) - 1 but parsed as 2 * (4 - 1)") +assert(1 / 2 + 0.5 == 1, "Should parse as (1 / 2) + 0.5 but parsed as 1 / (2 + 0.5)") + +assert(2 ^ 4 * 3 == 48, "Should parse as (2 ^ 4) * 3") + +assert(1 | 1 & 0, "Should parse as 1 | (1 & 0) but parsed as (1 | 1) & 0") + +assert(1 || 1 && 0, "Should parse as 1 || (1 && 0) but parsed as (1 || 1) && 0") +assert(1 ^^ 2 && 0 == ((1 ^^ 2) && 0), "Should parse as (1 ^^ 2) && 0 but parsed as 1 ^^ (2 && 0)") + +assert(2 >> 3 + 2 == (2 >> (3 + 2))) +assert(2 << 3 + 2 == (2 << (3 + 2))) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/scoping.txt b/data/expression2/tests/runtime/base/scoping.txt new file mode 100644 index 0000000000..aa664e541c --- /dev/null +++ b/data/expression2/tests/runtime/base/scoping.txt @@ -0,0 +1,76 @@ +## SHOULD_PASS:EXECUTE + +local Number = 55 + +if (1) { + local Number = 60 + assert(Number == 60) +} + +assert(Number == 55) + +while (1) { + local Number = 70 + assert(Number == 70) + break +} + +assert(Number == 55) + +for (I = 1, 2) { + switch (I) { + case 1, + local Number = 200 + assert(Number == 200) + + default, + local Number = 120 + assert(Number == 120) + } +} + +assert(Number == 55) + +foreach(_, V:number = array(1, 2, 3)) { + local Number = V + assert(Number == V) +} + +assert(Number == 55) + +do { + local Number = 1230 + assert(Number == 1230) +} while(0) + +assert(Number == 55) + +function test() { + local Number = 210 + assert(Number == 210) +} + +assert(Number == 55) +test() +assert(Number == 55) + +try { + local Number = 777 + assert(Number == 777) +} catch(_) { + error("assertion failed") +} + +assert(Number == 55) + +try { + local Number = 777 + error("e") +} catch(_) { + assert(Number == 55) + + local Number = 123 + assert(Number == 123) +} + +assert(Number == 55) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/stringcall/array.txt b/data/expression2/tests/runtime/base/stringcall/array.txt new file mode 100644 index 0000000000..a821542187 --- /dev/null +++ b/data/expression2/tests/runtime/base/stringcall/array.txt @@ -0,0 +1,17 @@ +## SHOULD_PASS:EXECUTE + +# Ensure that stringcalls don't bypass the array type blocklist + +try { + "array"(1, array())[array] + error("Fail") +} catch (E) { + assert(E != "Fail") + + try { + "array"(array())[array] + error("Fail") + } catch (E) { + assert(E != "Fail") + } +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/stringcall/builtins.txt b/data/expression2/tests/runtime/base/stringcall/builtins.txt new file mode 100644 index 0000000000..56d5543e13 --- /dev/null +++ b/data/expression2/tests/runtime/base/stringcall/builtins.txt @@ -0,0 +1,20 @@ +## SHOULD_PASS:EXECUTE + +assert( "format"("%s", "test")[string] == "test" ) +assert( "format"("%q", "test")[string] == "\"test\"" ) +assert( "format"("%d %d %s", 52, 20, "test")[string] == "52 20 test" ) + +"print"(1, 2, "test") + +# Type enforcing + +try { + local X = "format"("%s", "test")[number] # Should error at runtime, format does not return number. + error("Failed") +} catch(Err) { + assert(Err != "Failed") +} + +assert( "select"(1, "test", "foo", "bar")[string] == "test" ) +assert( "min"(1, 2)[number] == 1 ) +assert( "max"(1, 2)[number] == 2 ) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/stringcall/userfunctions.txt b/data/expression2/tests/runtime/base/stringcall/userfunctions.txt new file mode 100644 index 0000000000..85b1fc3a2d --- /dev/null +++ b/data/expression2/tests/runtime/base/stringcall/userfunctions.txt @@ -0,0 +1,49 @@ +## SHOULD_PASS:EXECUTE + +@persist Called:number + +# Test user functions + +function test() { + Called = 1 +} + +"test"() + +assert(Called) + +function argument(A:string) { + assert(A == "Hello") +} + +"argument"("Hello") + +function variadic(...A:array) { + assert(A[1, number] == 120) +} + +"variadic"( 120 ) + +function variadictbl(...T:table) { + assert(T:typeids()[1, string] == "n") + assert(T:typeids()[2, string] == "t") +} + +"variadictbl"( 1, table() ) + +function number returning() { + return 5 +} + +assert( "returning"()[number] == 5 ) + + +Called = 0 + +function number:numMethod() { + Called = 1 +} + +"numMethod"(1) + +assert(Called) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/switch.txt b/data/expression2/tests/runtime/base/switch.txt new file mode 100644 index 0000000000..3c9a199ffd --- /dev/null +++ b/data/expression2/tests/runtime/base/switch.txt @@ -0,0 +1,52 @@ +## SHOULD_PASS:EXECUTE + +local Num = 2 +switch (1) { + case 1, + Num = 5 + break + + default, + error("fail") +} + +assert(Num == 5) + +switch (5) { # Ensure fallthrough works + case 5, + case 2, + Num = 120 + default, + Num++ +} + +assert(Num == 121) + +switch (1) { + case 1, + Num++ + case 2, +} + +assert(Num == 122, "fallthrough ran multiple times") + +switch (vec()) { + case vec(), + break + + default, + error("fail") +} + +switch (1) { + case 1, + if (1) { + break + } + + error("unreachable") + break + + default, + break +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/try.txt b/data/expression2/tests/runtime/base/try.txt new file mode 100644 index 0000000000..b9d514e8e1 --- /dev/null +++ b/data/expression2/tests/runtime/base/try.txt @@ -0,0 +1,23 @@ +## SHOULD_PASS:EXECUTE + +A = 1 +try { + A = 2 + error("Foo") + A = 3 +} catch (E) { + assert(E == "Foo") +} + +assert(A == 2) + +A = 1 +try { + A = 2 + error("Bar") + A = 3 +} catch (E:string) { + assert(E == "Bar") +} + +assert(A == 2) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/userfunctions/functions.txt b/data/expression2/tests/runtime/base/userfunctions/functions.txt new file mode 100644 index 0000000000..27e172ac26 --- /dev/null +++ b/data/expression2/tests/runtime/base/userfunctions/functions.txt @@ -0,0 +1,114 @@ +## SHOULD_PASS:EXECUTE + +# Ensure functions get called in the first place + +Called = 0 +function myfunction() { + Called = 1 +} + +myfunction() + +assert(Called) + + +local X = 500 +local Y = 1000 +local Z = 5000 + +# Ensure function scoping doesn't affect outer scope + +function test(X, Y, Z) { + assert(X == 1) + assert(Y == 2) + assert(Z == 3) +} + +test(1, 2, 3) + +assert(X == 500) +assert(Y == 1000) +assert(Z == 5000) + +# Ensure functions return properly + +function number returning() { + return 5 +} + +assert(returning() == 5) + +function number returning2(X:array) { + return X[1, number] + 5 +} + +assert(returning2(array(5)) == 10) +assert(returning2(array()) == 5) + +function array returningref(X:array) { + return X +} + +local A = array() +assert(returningref(A):id() == A:id()) + +function returnvoid() { + if (1) { return } + error("unreachable") +} + +returnvoid() + +function void returnvoid2() { + return +} + +returnvoid2() + +function returnvoid3() { + return void +} + +returnvoid3() + +# Test recursion + +function number recurse(N:number) { + if (N == 1) { + return 5 + } else { + return recurse(N - 1) + 1 + } +} + +assert(recurse(10) == 14, recurse(10):toString()) + +Sentinel = -1 +function recursevoid() { + Sentinel++ + if (Sentinel == 0) { + recursevoid() + } +} + +recursevoid() + +assert(Sentinel == 1) + +function number nilInput(X, Y:ranger, Z:vector) { + assert(Z == vec(1, 2, 3)) + return 5 +} + +assert( nilInput(1, noranger(), vec(1, 2, 3)) == 5 ) + +if (0) { + function undefined() {} +} + +try { + undefined() + error("unreachable") +} catch(Err) { + assert(Err == "No such function defined at runtime: undefined()") +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/userfunctions/methods.txt b/data/expression2/tests/runtime/base/userfunctions/methods.txt new file mode 100644 index 0000000000..85d3c018e5 --- /dev/null +++ b/data/expression2/tests/runtime/base/userfunctions/methods.txt @@ -0,0 +1,117 @@ +## SHOULD_PASS:EXECUTE + +# Ensure methods get called in the first place + +Called = 0 +function number:mymethod() { + Called = 1 +} + +1:mymethod() + +assert(Called) + +local This = 10 +local X = 500 +local Y = 1000 +local Z = 5000 + +# Ensure function scoping doesn't affect outer scope + +function number number:method(X, Y, Z) { + assert(This == 500) + assert(X == 1) + assert(Y == 2) + assert(Z == 4) + + return 5 +} + +assert( 500:method(1, 2, 4) == 5 ) + +assert(This == 10) +assert(X == 500) +assert(Y == 1000) +assert(Z == 5000) + +# Ensure functions return properly + +function number number:returning() { + return 5 +} + +assert(1:returning() == 5) + +function number number:returning2(X:array) { + return X[1, number] + 5 +} + +assert(1:returning2(array(5)) == 10) +assert(1:returning2(array()) == 5) + +function array number:returningref(X:array) { + return X +} + +local A = array() +assert(1:returningref(A):id() == A:id()) + +function number:returnvoid() { + if (1) { return } +} + +1:returnvoid() + +function void number:returnvoid2() { + return +} + +1:returnvoid2() + +function number:returnvoid3() { + return void +} + +1:returnvoid3() + +# Test recursion + +function number number:recurse(N:number) { + if (N == 1) { + return 5 + } else { + return This:recurse(N - 1) + 1 + } +} + +assert(1:recurse(10) == 14, 1:recurse(10):toString()) + +Sentinel = -1 +function number:recursevoid() { + Sentinel++ + if (Sentinel == 0) { + This:recursevoid() + } +} + +1:recursevoid() + +assert(Sentinel == 1) + +function number number:nilInput(X, Y:ranger, Z:vector) { + assert(Z == vec(1, 2, 3)) + return 5 +} + +assert( 1:nilInput(1, noranger(), vec(1, 2, 3)) == 5 ) + +if (0) { + function number:undefined() {} +} + +try { + 1:undefined() + error("unreachable") +} catch(Err) { + assert(Err == "No such method defined at runtime: undefined(n:)") +} \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/userfunctions/override.txt b/data/expression2/tests/runtime/base/userfunctions/override.txt new file mode 100644 index 0000000000..c41018d367 --- /dev/null +++ b/data/expression2/tests/runtime/base/userfunctions/override.txt @@ -0,0 +1,73 @@ +## SHOULD_PASS:EXECUTE + +# Ensure that functions aren't improperly overridden at compile time + +CalledA = 0 +function a() { + CalledA = 1 +} + +if (0) { + function a() { + error("Unreachable function called - Should never be emitted") + } +} + +a() +assert(CalledA == 1) + +# Ensure that methods aren't improperly overridden at compile time + +CalledB = 0 +function number:b() { + CalledB = 1 +} + +if (0) { + function number:b() { + error("Unreachable method called - Should never be emitted") + } +} + +1:b() +assert(CalledB == 1) + +# Ensure that functions can be overridden at compile time + +Value = 50 + +function c() { + Value = 100 +} + +c() +assert(Value == 100) + +if (1) { + function c() { + Value = 200 + } +} + +c() +assert(Value == 200) + +# Ensure that methods can be overridden at compile time + +Value = 50 + +function number:d() { + Value = 100 +} + +1:d() +assert(Value == 100) + +if (1) { + function number:d() { + Value = 200 + } +} + +1:d() +assert(Value == 200) \ No newline at end of file diff --git a/data/expression2/tests/runtime/base/userfunctions/variadic.txt b/data/expression2/tests/runtime/base/userfunctions/variadic.txt new file mode 100644 index 0000000000..ef43f751ad --- /dev/null +++ b/data/expression2/tests/runtime/base/userfunctions/variadic.txt @@ -0,0 +1,47 @@ +## SHOULD_PASS:EXECUTE + +# Test variadic syntax sugar: Functions + +function number foo(...A:table) { + assert(A:typeids()[1, string] == "s") + assert(A:typeids()[2, string] == "n") + + assert(A[1, string] == "foo") + assert(A[2, number] == 55) + + return 5 +} + +assert(foo("foo", 55) == 5) + +function number bar(...A:array) { + assert(A[1, string] == "foo") + assert(A[2, number] == 55) + + return 5 +} + +assert( bar("foo", 55) == 5 ) + +# Test variadic syntax sugar: Methods + +function number number:foo(...A:table) { + assert(A:typeids()[1, string] == "s") + assert(A:typeids()[2, string] == "n") + + assert(A[1, string] == "foo") + assert(A[2, number] == 55) + + return This +} + +assert(5:foo("foo", 55) == 5) + +function number number:bar(...A:array) { + assert(A[1, string] == "foo") + assert(A[2, number] == 55) + + return This +} + +assert( 5:bar("foo", 55) == 5 ) \ No newline at end of file diff --git a/data/expression2/tests/runtime/libraries/core.txt b/data/expression2/tests/runtime/libraries/core.txt new file mode 100644 index 0000000000..5096ef18a9 --- /dev/null +++ b/data/expression2/tests/runtime/libraries/core.txt @@ -0,0 +1,3 @@ +## SHOULD_PASS:EXECUTE + +assert(select(3, 1, 2, 3, 4) == 3) \ No newline at end of file diff --git a/data/expression2/tests/runtime/libraries/debug.txt b/data/expression2/tests/runtime/libraries/debug.txt new file mode 100644 index 0000000000..495aec232a --- /dev/null +++ b/data/expression2/tests/runtime/libraries/debug.txt @@ -0,0 +1,7 @@ +## SHOULD_PASS:EXECUTE + +# Ensure variadic extpp functions are working correctly + +printColor(vec(), "foo") + +print("foo bar", 2, vec(), array()) \ No newline at end of file diff --git a/data/expression2/tests/runtime/libraries/string.txt b/data/expression2/tests/runtime/libraries/string.txt new file mode 100644 index 0000000000..8bda92eaa2 --- /dev/null +++ b/data/expression2/tests/runtime/libraries/string.txt @@ -0,0 +1,28 @@ +## SHOULD_PASS:EXECUTE + +@name String Library Tests +@strict + +local Str = "Hello, world!" + +assert("122":toNumber() == 122) +assert("0.2":toNumber() == 0.2) + +assert(Str:sub(1, 2) == "He") +assert(Str:sub(1, 100) == "Hello, world!") + +assert(toChar(72) == "H") +assert(toByte("H") == 72) +assert(toByte(Str, 1) == 72) + +local Threw = 0 +try { + toChar(256) +} catch (_) { + Threw = 1 +} + +assert(Threw) + +assert(format("%s %d", "foo", 232) == "foo 232") +assert(format("%u", 232) == "232") \ No newline at end of file diff --git a/data/expression2/tests/strict.txt b/data/expression2/tests/runtime/strict.txt similarity index 60% rename from data/expression2/tests/strict.txt rename to data/expression2/tests/runtime/strict.txt index a4870d906f..21781da81d 100644 --- a/data/expression2/tests/strict.txt +++ b/data/expression2/tests/runtime/strict.txt @@ -6,23 +6,23 @@ try { # chatClk should throw an error for a NULL player parameter. chatClk( noentity() ) } catch(Err) { - assert(Err == "Invalid player!") + assert(Err == "Invalid player!", "L9") try { # Nonexistent function stringcalls should be catchable "notreal"() } catch(Err) { - assert(Err == "No such function: notreal(void)") + assert(Err == "No such function: notreal()") try { error("exit") - # Chip should NOT exit here. error() will now throw separate table errors that are catchable and don't care about the name. + # Chip should NOT exit here. error() will throw separate table errors that are catchable and don't care about the name. } catch(Err) { assert(Err == "exit") - print("@strict tests passed (Unless there's an error after this)") - - try { exit() } catch(Err) { + try { + exit() + } catch(Err) { error("exit() threw an error, when it should have exited the chip") } } diff --git a/data/expression2/tests/runtime/types/angle.txt b/data/expression2/tests/runtime/types/angle.txt new file mode 100644 index 0000000000..404659b65e --- /dev/null +++ b/data/expression2/tests/runtime/types/angle.txt @@ -0,0 +1,22 @@ +## SHOULD_PASS:EXECUTE + +local Ang = ang(1, 2, 3) + +assert(Ang == ang(1, 2, 3)) +assert(Ang != ang(2, 2, 2)) + +assert(Ang >= ang(0, 0, 0)) +assert(Ang <= ang(3, 3, 3)) + +assert(Ang > ang(0, 0, 0)) +assert(Ang < ang(4, 4, 4)) + +assert(Ang[1] == 1) +assert(Ang[3] == 3) +assert(ang()[1] == 0) + +assert(ang() == ang(0, 0, 0)) +assert(ang(1, 2, 5) != ang(1, 2, 5.2)) +assert(ang(1, 2, 5) == ang(1, 2, 5)) + +assert(ang() + ang(1, 2, 3) == ang(1, 2, 3)) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/array.txt b/data/expression2/tests/runtime/types/array.txt new file mode 100644 index 0000000000..f53186de55 --- /dev/null +++ b/data/expression2/tests/runtime/types/array.txt @@ -0,0 +1,32 @@ +## SHOULD_PASS:EXECUTE + +local A = array() + +A:pushNumber(55) + +assert(A[1, number] == 55) +assert(A:count() == 1) + +assert(A:popNumber() == 55) +assert(A:count() == 0) + +local KV = array( + 1 = 20, + 50 = 200, + 100 = 2000 +) + +assert(KV[1, number] == 20) +assert(KV[50, number] == 200) +assert(KV[100, number] == 2000) + +local Rec = 0 +foreach(I, V:number = KV) { + Rec++ + assert(KV[I, number] == V) +} + +assert(Rec == 1) # Only 1 because this breaks the internal ipairs impl (Shouldn't use an array like this anyway.) + +A[1, vector] = vec() +assert(A[1, vector] == vec()) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/entity.txt b/data/expression2/tests/runtime/types/entity.txt new file mode 100644 index 0000000000..6d6c7766ae --- /dev/null +++ b/data/expression2/tests/runtime/types/entity.txt @@ -0,0 +1,8 @@ +## SHOULD_PASS:EXECUTE + +local Ent = noentity() + +assert(!Ent:isValid()) +assert(Ent == Ent) +assert(!(Ent & Ent)) +assert(!(Ent | Ent)) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/arithmetic.txt b/data/expression2/tests/runtime/types/number/arithmetic.txt new file mode 100644 index 0000000000..208871d084 --- /dev/null +++ b/data/expression2/tests/runtime/types/number/arithmetic.txt @@ -0,0 +1,32 @@ +## SHOULD_PASS:EXECUTE + +assert( (1 + 2 + 3) == 6 ) +assert( (2 * 3 + 1) == 7 ) +assert( (4 / 2 + 1) == 3 ) + +assert( (5 % 4) == 1 ) + +# Addition +assert(1 + 2 == 3) +assert(1e20 + 1e21 == 1.1e21) + +# Subtraction +assert(1 - 2 == -1) +assert(1e20 - 1e21 == -0.9e21) + +# Multiplication +assert(1 * 2 == 2) +assert(1e20 * 2 == 2e20) + +# Div +assert(1 / 2 == 0.5) +assert(isnan(0 / 0)) +assert(isinf(1 / 0)) + +# Modulus +assert(10 % 2 == 0) +assert(10.5 % 1 == 0.5) + +# Exponentation +assert(2 ^ 2 == 4) +assert(2 ^ 4 == 16) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/binary.txt b/data/expression2/tests/runtime/types/number/binary.txt new file mode 100644 index 0000000000..3636f4f638 --- /dev/null +++ b/data/expression2/tests/runtime/types/number/binary.txt @@ -0,0 +1,23 @@ +## SHOULD_PASS:EXECUTE + +# binary and +assert( (0b011 && 0b011) == 3 ) +assert( (0b011 && 0b011) == bAnd(0b011, 0b011) ) + +# binary or +assert( (0b11 || 0b00) == 3 ) +assert( (0b11 || 0b00) == bOr(0b11, 0b00) ) + +# binary right shift +assert( (0b11 >> 2) == 0b00 ) +assert( (0b10 >> 1) == 0b01 ) + +# binary left shift +assert( (0b11 << 2) == 0b1100 ) +assert( (0b10 << 1) == 0b100 ) +assert( (0b01 << 1) == 0b10 ) + +# binary exclusive or +assert( (0b01 ^^ 0b11) == 2 ) +assert( (0b11 ^^ 0b11) == 0 ) +assert( (0b00 ^^ 0b00) == 0 ) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/compare.txt b/data/expression2/tests/runtime/types/number/compare.txt new file mode 100644 index 0000000000..5130c037fd --- /dev/null +++ b/data/expression2/tests/runtime/types/number/compare.txt @@ -0,0 +1,34 @@ +## SHOULD_PASS:EXECUTE + +assert(1 == 1) +assert(2 == 2) + +assert(2 != 3) +assert(5000 != 200) + +assert(0 == 0) + +assert(1 <= 1) +assert(1 >= -1) + +assert(1 < 2) +assert(1 > -1) + +assert(2 > 1) +assert(2 >= 2) + +assert(2 >= 2) +assert(2 >= 1) + +assert(-1 == -1) +assert(1 == 1) +assert(-1 < 0) + +assert(50 < 100) +assert(50 <= 100) + +assert(100 > 50) +assert(100 >= 50) + +assert(-1 <= -1) +assert(-1 >= -1) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/compound.txt b/data/expression2/tests/runtime/types/number/compound.txt new file mode 100644 index 0000000000..5789a5bca6 --- /dev/null +++ b/data/expression2/tests/runtime/types/number/compound.txt @@ -0,0 +1,21 @@ +## SHOULD_PASS:EXECUTE + +local X = 0 + +X -= 1 +assert(X == -1) + +X += 5 +assert(X == 4) + +X *= 2 +assert(X == 8) + +X /= 2 +assert(X == 4) + +X++ +assert(X == 5) + +X-- +assert(X == 4) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/delta.txt b/data/expression2/tests/runtime/types/number/delta.txt new file mode 100644 index 0000000000..66e43baf89 --- /dev/null +++ b/data/expression2/tests/runtime/types/number/delta.txt @@ -0,0 +1,15 @@ +## SHOULD_PASS:EXECUTE + +@persist X:number + +assert($X == 0) + +X = 5 + +assert($X == 5) + +assert($X == 0) + +X++ + +assert($X == 1) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/index.txt b/data/expression2/tests/runtime/types/number/index.txt new file mode 100644 index 0000000000..dea152b431 --- /dev/null +++ b/data/expression2/tests/runtime/types/number/index.txt @@ -0,0 +1,5 @@ +## SHOULD_PASS:EXECUTE + +local X = nowirelink() + +assert(X:number("Foo") == X["Foo", number]) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/logical.txt b/data/expression2/tests/runtime/types/number/logical.txt new file mode 100644 index 0000000000..40e6695f4d --- /dev/null +++ b/data/expression2/tests/runtime/types/number/logical.txt @@ -0,0 +1,12 @@ +## SHOULD_PASS:EXECUTE + +assert(1 & 1) +assert(!(0 & 2)) + +assert(0 | 1) +assert(1 | 1) + + +assert(1) +assert(-1, "-1 should count as truthy") +assert(!0, "0 should be falsy") \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/number/ternary.txt b/data/expression2/tests/runtime/types/number/ternary.txt new file mode 100644 index 0000000000..35d3421b39 --- /dev/null +++ b/data/expression2/tests/runtime/types/number/ternary.txt @@ -0,0 +1,8 @@ +## SHOULD_PASS:EXECUTE + +assert(1 ? 50 : 20 == 50) +assert(0 ? 500 : 10 == 10) +assert(-1 ? 100 : 1 == 100) + +assert(50 ?: 20 == 50) +assert(0 ?: 100 == 100) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/string.txt b/data/expression2/tests/runtime/types/string.txt new file mode 100644 index 0000000000..17f3cf0bb0 --- /dev/null +++ b/data/expression2/tests/runtime/types/string.txt @@ -0,0 +1,44 @@ +## SHOULD_PASS:EXECUTE + +if ("") { + error("fail") +} + +assert(!!"String") +assert(!"") + +assert("Foo" != "Bar") +assert("Foo" == "Foo") + +assert("Foo" + 5 == "Foo5") +assert(5 + "Foo" == "5Foo") + +assert("A" < "B") +assert("a" < "b") + +assert("A" <= "A") +assert("A" >= "A") +assert("A" == "A") + +assert("Foo" + "Bar" == "FooBar") + +# Ensure foreach string works + +local Chars = array("F", "o", "o") +local Bytes = array(70, 111, 111) + +I = 0 +foreach(K, Char:string = "Foo") { + assert(Chars[K, string] == Char) + I++ +} + +assert(I == 3) + +I = 0 +foreach(K, Byte:number = "Foo") { + assert(Bytes[K, number] == Byte) + I++ +} + +assert(I == 3) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/table.txt b/data/expression2/tests/runtime/types/table.txt new file mode 100644 index 0000000000..f301e00ead --- /dev/null +++ b/data/expression2/tests/runtime/types/table.txt @@ -0,0 +1,27 @@ +## SHOULD_PASS:EXECUTE + +local T = table() + +T:pushNumber(55) +assert(T[1, number] == 55) +assert(T:count() == 1) +assert(T:typeids()[1, string] == "n") + +assert(T:popNumber() == 55) +assert(T:count() == 0) +assert(T:typeids()[1, string] == "") + +local KV = table( + "Foo" = 1, + "Bar" = 2, + "Baz" = 3, + "Qux" = 4 +) + +local Looped = 0 +foreach(K, V:number = KV) { # K should default to "string" on tables. (Dumb behavior I know.) + Looped++ + assert(KV[K, number] == V) +} + +assert(Looped == 4, Looped:toString()) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/vector.txt b/data/expression2/tests/runtime/types/vector.txt new file mode 100644 index 0000000000..9884d59467 --- /dev/null +++ b/data/expression2/tests/runtime/types/vector.txt @@ -0,0 +1,20 @@ +## SHOULD_PASS:EXECUTE + +local Vec = vec(1, 2, 3) + +assert(Vec == vec(1, 2, 3)) +assert(Vec != vec(2, 2, 2)) + +assert(Vec[1] == 1) +assert(Vec[3] == 3) +assert(vec()[1] == 0) + +assert(vec() == vec(0, 0, 0)) +assert(vec(1, 2, 5) != vec(1, 2, 5.2)) +assert(vec(1, 2, 5) == vec(1, 2, 5)) + +assert(Vec:x() == Vec[1]) +assert(Vec:y() == Vec[2]) +assert(Vec:z() == Vec[3]) + +assert(Vec:toString() == "vec(1,2,3)", Vec:toString()) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/vector2.txt b/data/expression2/tests/runtime/types/vector2.txt new file mode 100644 index 0000000000..a650fe0d67 --- /dev/null +++ b/data/expression2/tests/runtime/types/vector2.txt @@ -0,0 +1,21 @@ +## SHOULD_PASS:EXECUTE + +local Vec = vec2(1, 2) + +assert(Vec == vec2(1, 2)) +assert(Vec != vec2(2, 2)) + +assert(Vec[1] == 1) +assert(Vec[2] == 2) +assert(vec2()[1] == 0) + +assert(vec2() == vec2(0, 0)) +assert(vec2(1, 2) != vec2(1, 2.3)) +assert(vec2(1, 2) == vec2(1, 2)) + +assert(Vec:x() == Vec[1]) +assert(Vec:y() == Vec[2]) + +assert(vec2(1, 2) + vec2(1, 2) == vec2(2, 4)) + +assert(Vec:toString() == "[1,2]", Vec:toString()) \ No newline at end of file diff --git a/data/expression2/tests/runtime/types/vector4.txt b/data/expression2/tests/runtime/types/vector4.txt new file mode 100644 index 0000000000..e0597d144a --- /dev/null +++ b/data/expression2/tests/runtime/types/vector4.txt @@ -0,0 +1,45 @@ +## SHOULD_PASS:EXECUTE + +local Vec = vec4(1, 2, 3, 4) + +assert(Vec == vec4(1, 2, 3, 4)) +assert(Vec != vec4(2, 2, 1, 0)) + +assert(Vec[1] == 1) +assert(Vec[2] == 2) +assert(vec4()[1] == 0) + +assert(vec4() == vec4(0, 0, 0, 0)) + +assert(vec4(1, 2, 2, 1) != vec4(1, 2.3, 2, 1)) +assert(vec4(1, 2, 3, 5) == vec4(1, 2, 3, 5)) + +assert(vec4(1, 2, 3, 4) + 1 == vec4(2, 3, 4, 5)) +assert(vec4(1, 2, 3, 4) - 1 == vec4(0, 1, 2, 3)) +assert(vec4(1, 2, 3, 4) * 2 == vec4(2, 4, 6, 8)) +assert(vec4(1, 2, 3, 4) / 2 == vec4(0.5, 1, 1.5, 2)) + +assert(vec4(1, 2, 3, 4) + vec4(1, 1, 1, 1) == vec4(2, 3, 4, 5)) +assert(vec4(1, 2, 3, 4) - vec4(1, 1, 1, 1) == vec4(0, 1, 2, 3)) +assert(vec4(1, 2, 3, 4) * vec4(2, 2, 2, 2) == vec4(2, 4, 6, 8)) +assert(vec4(1, 2, 3, 4) / vec4(2, 2, 2, 2) == vec4(0.5, 1, 1.5, 2)) + +assert(Vec:x() == Vec[1]) +assert(Vec:y() == Vec[2]) +assert(Vec:z() == Vec[3]) +assert(Vec:w() == Vec[4]) + +assert(vec4(1, 2, 3, 4) + vec4(1, 2, 3, 4) == vec4(2, 4, 6, 8)) + +# Ensure syntax sugar works with old operators + +local W = vec4(1) + +W += 1 +W -= 1 +W /= 1 +W *= 1 +W++ +W-- + +assert(Vec:toString() == "[1,2,3,4]", Vec:toString()) \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/base/ast.lua b/lua/entities/gmod_wire_expression2/base/ast.lua deleted file mode 100644 index 3972747ffc..0000000000 --- a/lua/entities/gmod_wire_expression2/base/ast.lua +++ /dev/null @@ -1,104 +0,0 @@ --- The E2 source code is parsed into a tree structure, known as an 'abstract --- syntax tree' or AST. This file provides utilities for operating on nodes of --- this tree generically. - -AddCSLuaFile() - -E2Lib.AST = {} - -local function genericChildVisitor(node, action) - for i = 3, #node do - local child = node[i] - if istable(child) and child.__instruction then - node[i] = action(child) or child - end - end -end -local childVisitors = {} -function childVisitors.call(node, action) - local arguments = node[4] - for i, argument in ipairs(arguments) do - arguments[i] = action(argument) or argument - end -end -function childVisitors.methodcall(node, action) - local this, arguments = node[4], node[5] - node[4] = action(this) or this - for i, argument in ipairs(arguments) do - arguments[i] = action(argument) or argument - end -end -function childVisitors.stringcall(node, action) - local name, arguments = node[3], node[4] - node[3] = action(name) or name - for i, argument in ipairs(arguments) do - arguments[i] = action(argument) or argument - end -end -function childVisitors.kvtable(node, action) - local entries = node[3] - local additions = {} - for key, value in pairs(entries) do - local newKey, newValue = action(key) or key, action(value) or value - if key ~= newKey then - entries[key] = nil - additions[newKey] = newValue - else - entries[key] = newValue - end - end - for key, value in pairs(additions) do entries[key] = value end -end -childVisitors.kvarray = childVisitors.kvtable -function childVisitors.switch(node, action) - local expression = node[3] - node[3] = action(expression) or expression - for _, case in pairs(node[4]) do - local condition, result = case[1], case[2] - case[1] = condition and action(condition) or condition - case[2] = action(result) or result - end -end - ---- Call `action` on every child of `node`. --- If it returns a value, then that value will replace the node. --- For example: --- E2Lib.AST.visitChildren(node, function(child) --- print(string.format("Node has a child of type %s", child[1])) --- -- replace (sub x x) → 0 --- if child[1] == "sub" and child[3] == child[4] then --- return { "literal", child[2], 0, "n" } --- end --- end) -function E2Lib.AST.visitChildren(node, action) - local visitor = childVisitors[node[1]] or genericChildVisitor - return visitor(node, action) -end - ---- Return a string representation of the tree. -function E2Lib.AST.dump(tree, indentation) - indentation = indentation or "" - local str = indentation .. tree[1] - - local summary = {} - for i = 3, #tree do - local v = tree[i] - if isstring(v) then - table.insert(summary, string.format("%q", v)) - elseif isnumber(v) then - table.insert(summary, tostring(v)) - end - end - if next(summary) then - str = str .. " [" .. table.concat(summary, ", ") .. "]" - end - - str = str .. " (" .. tree[2][1] .. ":" .. tree[2][2] .. ")" - local childIndentation = indentation .. "| " - - E2Lib.AST.visitChildren(tree, function(child) - str = str .. "\n" .. E2Lib.AST.dump(child, childIndentation) - end) - - return str -end diff --git a/lua/entities/gmod_wire_expression2/base/compiler.lua b/lua/entities/gmod_wire_expression2/base/compiler.lua index 485fd7b2d9..05aad5507e 100644 --- a/lua/entities/gmod_wire_expression2/base/compiler.lua +++ b/lua/entities/gmod_wire_expression2/base/compiler.lua @@ -1,1377 +1,1843 @@ --[[ - Expression 2 Compiler for Garry's Mod - Andreas "Syranide" Svensson, me@syranide.com + Expression 2 Compiler + by Vurv ]] AddCSLuaFile() ----@class Compiler ----@field warnings table # Array of warnings (main file) with keys to included file warnings. ----@field include string? # Current include file or nil if main file ----@field Scopes Scope[] ----@field Scope Scope? ----@field ScopeID integer ----@field GlobalScope Scope ----@field persist table # Variable: Type ----@field inputs table # Variable: Type ----@field outputs table # Variable: Type ----@field registered_events table -local Compiler = {} -Compiler.__index = Compiler -E2Lib.Compiler = Compiler +local Warning, Error = E2Lib.Debug.Warning, E2Lib.Debug.Error +local Node, NodeVariant = E2Lib.Parser.Node, E2Lib.Parser.Variant +local Operator = E2Lib.Operator -local BLOCKED_ARRAY_TYPES = E2Lib.blocked_array_types - ----@return boolean ok ----@return function script ----@return Compiler self -function Compiler.Execute(root, inputs, outputs, persist, delta, includes) - -- instantiate Compiler - local instance = setmetatable({}, Compiler) +local TickQuota = GetConVar("wire_expression2_quotatick"):GetInt() - -- and pcall the new instance's Process method. - local ok, script = xpcall(Compiler.Process, E2Lib.errorHandler, instance, root, inputs, outputs, persist, delta, includes) +cvars.RemoveChangeCallback("wire_expression2_quotatick", "compiler_quota_check") +cvars.AddChangeCallback("wire_expression2_quotatick", function(_, old, new) + TickQuota = tonumber(new) +end, "compiler_quota_check") - return ok, script, instance +---@class ScopeData +---@field dead boolean? +---@field loop boolean? +---@field switch_case boolean? +---@field function { [1]: string, [2]: EnvFunction}? +---@field ops integer + +---@alias VarData { type: string, trace_if_unused: Trace?, initialized: boolean, depth: integer } + +---@class Scope +---@field parent Scope? +---@field data ScopeData +---@field vars table +local Scope = {} +Scope.__index = Scope + +---@param parent Scope? +function Scope.new(parent) + return setmetatable({ data = { ops = 0 }, vars = {}, parent = parent }, Scope) end -function Compiler:Error(message, instr) - error(message .. " at line " .. instr[2][1] .. ", char " .. instr[2][2], 0) +function Scope:Depth() + return self.parent and (1 + self.parent:Depth()) or 0 end -function Compiler:Warning(message, instr) - if self.include then - local tbl = self.warnings[self.include] - tbl[#tbl + 1] = { message = message, line = instr[2][1], char = instr[2][2] } - else - self.warnings[#self.warnings + 1] = { message = message, line = instr[2][1], char = instr[2][2] } +---@param name string +---@param data VarData +function Scope:DeclVar(name, data) + if name ~= "_" then + data.depth = self:Depth() + self.vars[name] = data end end -local string_upper = string.upper - -function Compiler:CallInstruction(name, trace, ...) - return self["Instr" .. string_upper(name)](self, trace, ...) +function Scope:IsGlobalScope() + return self.parent == nil end ----@return function script -function Compiler:Process(root, inputs, outputs, persist, delta, includes) -- Took params out becuase it isnt used. - self.context = {} - self.registered_events = {} - self.warnings = {} - - self:InitScope() -- Creates global scope! - - self.inputs = inputs[3] - self.outputs = outputs[3] - self.persist = persist[3] - self.includes = includes or {} - self.prfcounter = 0 - self.prfcounters = {} - self.funcs = {} - self.dvars = {} - self.funcs_ret = {} - self.EnclosingFunctions = { --[[ { ReturnType: string } ]] } - - for name, v in pairs(inputs[3]) do - self:SetGlobalVariableType(name, wire_expression_types[v][1], { nil, inputs[5][name] }, true) - end - - for name, v in pairs(outputs[3]) do - self:SetGlobalVariableType(name, wire_expression_types[v][1], { nil, outputs[5][name] }, false) - end - - for name, v in pairs(persist[3]) do - self:SetGlobalVariableType(name, wire_expression_types[v][1], { nil, persist[5][name] }, false) - end - - for name, v in pairs(delta) do - self.dvars[name] = v - end - - self:PushScope() - - local script = self:CallInstruction(root[1], root) - - for name, var in pairs(self.GlobalScope) do - if var.var_tok then - self:Warning("Unused global variable [" .. name .. "]", var.var_tok) - end - end - - self:PopScope() - - return script +---@param name string +---@return VarData? +function Scope:LookupVar(name) + return self.vars[name] or (self.parent and self.parent:LookupVar(name)) end -function tps_pretty(tps) - if not tps or #tps == 0 then return "void" end - if type(tps) == "string" then tps = { tps } end - local ttt = {} - for i = 1, #tps do - local _, typenames = E2Lib.splitType(tps[i]) - for j = 1, #typenames do table.insert(ttt, typenames[j]) end - end - return table.concat(ttt, ", ") +---@param field string +function Scope:ResolveData(field) + return self.data[field] or (self.parent and self.parent:ResolveData(field)) end -local function op_find(name) - return E2Lib.optable_inv[name] or "unknown?!" -end +---@class Compiler +--- Base Data +---@field warnings table # Array of warnings (main file) with keys to included file warnings. +---@field global_scope Scope +---@field scope Scope +--- Below is analyzed data +---@field include string? # Current include file or nil if main file +---@field registered_events table +---@field user_functions table> # applyForce -> v +---@field user_methods table>> # e: -> applyForce -> vava +--- External Data +---@field includes table +---@field delta_vars table # Variable: True +---@field persist IODirective +---@field inputs IODirective +---@field outputs IODirective +local Compiler = {} +Compiler.__index = Compiler ----@class ScopeData ----@field type string ----@field var_tok table? # Instance token pointing to the declaration, only exists if variable has yet to be used. ----@field initialized boolean # Whether the variable is defined as initialized (e.g. whether a @persist variable has been assigned to by the user) - ----@alias Scope table - -function Compiler:InitScope() - self.Scopes = {} - self.ScopeID = 0 - self.Scopes[0] = self.GlobalScope or {} --for creating new enviroments - self.Scope = self.Scopes[0] - self.GlobalScope = self.Scope -end +E2Lib.Compiler = Compiler -function Compiler:PushScope(Scope) - self.ScopeID = self.ScopeID + 1 - self.Scope = Scope or {} - self.Scopes[self.ScopeID] = self.Scope +function Compiler.new() + local global_scope = Scope.new() + return setmetatable({ + global_scope = global_scope, scope = global_scope, warnings = {}, registered_events = {}, + user_functions = {}, user_methods = {}, delta_vars = {} + }, Compiler) end -function Compiler:PopScope() - self.ScopeID = self.ScopeID - 1 - self.Scope = self.Scopes[self.ScopeID] - self.Scopes[self.ScopeID] = self.Scope - return table.remove(self.Scopes, self.ScopeID + 1) +---@param directives PPDirectives +---@param dvars table? +---@param includes table? +function Compiler.from(directives, dvars, includes) + local global_scope = Scope.new() + return setmetatable({ + persist = directives.persist, inputs = directives.inputs, outputs = directives.outputs, + global_scope = global_scope, scope = global_scope, warnings = {}, registered_events = {}, user_functions = {}, user_methods = {}, + delta_vars = dvars or {}, includes = includes or {} + }, Compiler) end -function Compiler:SaveScopes() - return { self.Scopes, self.ScopeID, self.Scope } -end +local BLOCKED_ARRAY_TYPES = E2Lib.blocked_array_types -function Compiler:LoadScopes(Scopes) - self.Scopes = Scopes[1] - self.ScopeID = Scopes[2] - self.Scope = Scopes[3] +---@param ast Node +---@param directives PPDirectives +---@param dvars table +---@param includes table +---@return boolean ok, function|Error script, Compiler self +function Compiler.Execute(ast, directives, dvars, includes) + local instance = Compiler.from(directives, dvars, includes) + local ok, script = xpcall(Compiler.Process, E2Lib.errorHandler, instance, ast) + return ok, script, instance end --- Should not be used with discard (_) variables -function Compiler:SetLocalVariableType(name, type, instance, binding) - local var = self.Scope[name] - if var and var.type ~= type then - self:Error("Variable (" .. E2Lib.limitString(name, 10) .. ") of type [" .. tps_pretty({ var.type }) .. "] cannot be assigned value of type [" .. tps_pretty({ type }) .. "]", instance) - end - - self.Scope[name] = { type = type, var_tok = instance, initialized = true, binding = binding } - return self.ScopeID +---@param message string +---@param trace Trace +function Compiler:Error(message, trace) + error( Error.new(message, trace), 0) end --- Should not be used with discard (_) variables ----@param initialized boolean -function Compiler:SetGlobalVariableType(name, type, instance, initialized) - for i = self.ScopeID, 0, -1 do - local var = self.Scopes[i][name] - if var then - if var.type ~= type then - self:Error("Variable (" .. E2Lib.limitString(name, 10) .. ") of type [" .. tps_pretty({ var.type }) .. "] cannot be assigned value of type [" .. tps_pretty({ type }) .. "]", instance) - elseif i == 0 and not var.initialized then - var.var_tok = instance - var.initialized = true - return i - end - return i - end - if var and var.type ~= type then - self:Error("Variable (" .. E2Lib.limitString(name, 10) .. ") of type [" .. tps_pretty({ var.type }) .. "] cannot be assigned value of type [" .. tps_pretty({ type }) .. "]", instance) - elseif var then - return i - end - end - - self.GlobalScope[name] = { type = type, var_tok = instance, initialized = initialized } - return 0 +---@generic T +---@param v? T +---@param message string +---@param trace Trace +---@return T +function Compiler:Assert(v, message, trace) + if not v then error(Error.new(message, trace), 0) end + return v end -function Compiler:GetVariableType(instance, name) - for i = self.ScopeID, 0, -1 do - local var = self.Scopes[i][name] - if var then - var.var_tok = nil -- Mark variable as used - return var.type, i, var.initialized - end - end - - self:Error("Variable (" .. E2Lib.limitString(name, 10) .. ") does not exist", instance) - return nil +---@generic T +---@param v? T +---@param message string +---@param trace Trace +---@return T +function Compiler:AssertW(v, message, trace) + if not v then self:Warning(message, trace) end + return v end --- --------------------------------------------------------------------------- - ---- May return nil in the case of a statement without any runtime side effects. ----@return table?, string, string, any -function Compiler:EvaluateStatement(args, index) - local trace = args[index + 2] - local name = string_upper(trace[1]) - - local ex, tp, extra = self:CallInstruction(name, trace) - if ex then - ex.TraceName = name - ex.Trace = trace[2] +---@param message string +---@param trace Trace +function Compiler:Warning(message, trace) + if self.include then + local tbl = self.warnings[self.include] + tbl[#tbl + 1] = Warning.new(message, trace) + else + self.warnings[#self.warnings + 1] = Warning.new(message, trace) end - - return ex, tp, name, extra end -function Compiler:Evaluate(args, index) - local ex, tp, name = self:EvaluateStatement(args, index) - - if tp == "" then - self:Error("Function has no return value (void), cannot be part of expression or assigned", args[index + 2]) - end - - return ex, tp, name +---@generic T +---@generic T2 +---@generic T3 +---@param fn fun(scope: Scope): T?, T2?, T3? +---@return T?, T2?, T3? +function Compiler:Scope(fn) + self.scope = Scope.new(self.scope) + local ret, ret2, ret3 = fn(self.scope) + self.scope = self.scope.parent + return ret, ret2, ret3 end -function Compiler:HasOperator(instr, name, tps) - local pars = table.concat(tps) - local a = wire_expression2_funcs["op:" .. name .. "(" .. pars .. ")"] - return a and true or false +---@generic T +---@generic T2 +---@generic T3 +---@param fn fun(scope: Scope): T?, T2?, T3? +---@return T?, T2?, T3? +function Compiler:IsolatedScope(fn) + local old = self.scope + self.scope = Scope.new(self.global_scope) + local ret, ret2, ret3 = fn(self.scope) + self.scope = old + return ret, ret2, ret3 end -function Compiler:GetOperator(instr, name, tps) - local pars = table.concat(tps) - local a = wire_expression2_funcs["op:" .. name .. "(" .. pars .. ")"] - if not a then - self:Error("No such operator: " .. op_find(name) .. "(" .. tps_pretty(tps) .. ")", instr) - return - end +--- Ensure that a token of variant LowerIdent is a valid type +---@param ty Token +---@return string? type_id # Type id or nil if void +function Compiler:CheckType(ty) + if ty.value == "number" then return "n" end + if ty.value == "void" then return end + return self:Assert(wire_expression_types[ty.value:upper()], "Invalid type (" .. ty.value .. ")", ty.trace)[1] +end - self.prfcounter = self.prfcounter + (a[4] or 3) +---@alias RuntimeOperator fun(self: RuntimeContext, ...): any - return { a[3], a[2], a[1] } -end +---@type fun(self: Compiler, trace: Trace, data: { [1]: Node, [2]: Operator, [3]: self }): RuntimeOperator, string? +local function handleInfixOperation(self, trace, data) + local lhs, lhs_ty = self:CompileExpr(data[1]) + local rhs, rhs_ty = self:CompileExpr(data[3]) + local op, op_ret, legacy = self:GetOperator(E2Lib.OperatorNames[data[2]]:lower(), { lhs_ty, rhs_ty }, trace) -function Compiler:UDFunction(Sig) - if self.funcs_ret and self.funcs_ret[Sig] then - return { - Sig, self.funcs_ret[Sig], - function(self, args) - if self.funcs and self.funcs[Sig] then - return self.funcs[Sig](self, args) - elseif self.funcs_ret and self.funcs_ret[Sig] then - -- This only occurs if a function's definition isn't executed before the function is called - -- Would probably only accidentally come about when pasting an E2 that has function definitions in - -- if(first()) instead of if(first() || duped()) - error("UDFunction: " .. Sig .. " undefined at runtime!", -1) - -- return wire_expression_types2[self.funcs_ret[Sig]][2] -- This would return the default value for the type, probably better to error though - end - end, - 20 - } + if legacy then + local largs = { [1] = {}, [2] = { lhs }, [3] = { rhs }, [4] = { lhs_ty, rhs_ty } } + return function(state) + return op(state, largs) + end, op_ret + else + return function(state) + return op(state, lhs(state), rhs(state)) + end, op_ret end end - -function Compiler:GetFunction(instr, Name, Args) - local Params = table.concat(Args) - local Func = wire_expression2_funcs[Name .. "(" .. Params .. ")"] - - if not Func then - Func = self:UDFunction(Name .. "(" .. Params .. ")") - - if not Func then - for I = #Params, 0, -1 do - local sig = Name .. "(" .. Params:sub(1, I) - local arrsig, tblsig = sig .. "..r)", sig .. "..t)" - if self.funcs_ret[arrsig] then - Func = self:UDFunction(arrsig) - break - elseif self.funcs_ret[tblsig] then - Func = self:UDFunction(tblsig) - break +---@type table +local CompileVisitors = { + ---@param data Node[] + [NodeVariant.Block] = function(self, trace, data) + local stmts, traces = {}, {} + for _, node in ipairs(data) do + if not self.scope.data.dead then + local trace, stmt = node.trace, self:CompileStmt(node) + if stmt then -- Need to append because Compile* can return nil (despite me not annotating it as such) for compile time constructs + local i = #stmts + 1 + stmts[i], traces[i] = stmt, trace + + if node:isExpr() and node.variant ~= NodeVariant.ExprStringCall and node.variant ~= NodeVariant.ExprCall and node.variant ~= NodeVariant.ExprMethodCall then + self:Warning("This expression has no effect", node.trace) + end end + else + self:Warning("Unreachable code detected", node.trace) + break end end - end - if not Func then - for I = #Params, 0, -1 do - Func = wire_expression2_funcs[Name .. "(" .. Params:sub(1, I) .. "...)"] - if Func then break end + for name, var in pairs(self.scope.vars) do + if name ~= "_" and var.trace_if_unused then + self:Warning("Unused variable: " .. name, var.trace_if_unused) + end end - end - - if not Func then - self:Error("No such function: " .. Name .. "(" .. tps_pretty(Args) .. ")", instr) - return - end - - self.prfcounter = self.prfcounter + (Func[4] or 20) - - return { Func[3], Func[2], Func[1], Func.attributes } -end - -function Compiler:GetMethod(instr, Name, Meta, Args) - local Params = Meta .. ":" .. table.concat(Args) - local Func = wire_expression2_funcs[Name .. "(" .. Params .. ")"] + local cost, nstmts = self.scope.data.ops, #stmts + self.scope.data.ops = 0 - if not Func then - Func = self:UDFunction(Name .. "(" .. Params .. ")") + if self.scope:ResolveData("loop") or self.scope:ResolveData("switch_case") then -- Inside loop or switch case, check if continued or broken + return function(state) ---@param state RuntimeContext + state.prf = state.prf + cost + if state.prf > TickQuota then error("perf", 0) end - if not Func then - for I = #Params, 0, -1 do - local sig = Name .. "(" .. Params:sub(1, I) - local arrsig, tblsig = sig .. "..r)", sig .. "..t)" - if self.funcs_ret[arrsig] then - Func = self:UDFunction(arrsig) - break - elseif self.funcs_ret[tblsig] then - Func = self:UDFunction(tblsig) - break + for i = 1, nstmts do + state.trace = traces[i] + stmts[i](state) + if state.__break__ or state.__return__ or state.__continue__ then break end + end + end + elseif self.scope:ResolveData("function") then -- If inside a function, check if returned. + return function(state) ---@param state RuntimeContext + state.prf = state.prf + cost + if state.prf > TickQuota then error("perf", 0) end + + for i = 1, nstmts do + state.trace = traces[i] + stmts[i](state) + if state.__return__ then break end + end + end + else -- Most optimized case, not inside a function or loop. + return function(state) ---@param state RuntimeContext + state.prf = state.prf + cost + if state.prf > TickQuota then error("perf", 0) end + + for i = 1, nstmts do + state.trace = traces[i] + stmts[i](state) end end end - end - - if not Func then - for I = #Params, #Meta + 1, -1 do - Func = wire_expression2_funcs[Name .. "(" .. Params:sub(1, I) .. "...)"] - if Func then break end + end, + + ---@param data { [1]: Node?, [2]: Node }[] + [NodeVariant.If] = function (self, trace, data) + local chain = {} ---@type { [1]: RuntimeOperator?, [2]: RuntimeOperator }[] + for i, ifeif in ipairs(data) do + self:Scope(function() + if ifeif[1] then -- if or elseif + local expr, expr_ty = self:CompileExpr(ifeif[1]) + + if expr_ty == "n" then -- Optimization: Don't need to run operator_is on number (since we already check if ~= 0 here.) + chain[i] = { + expr, + self:CompileStmt(ifeif[2]) + } + else + local op = self:GetOperator("is", { expr_ty }, trace) + + chain[i] = { + function(state) + return op(state, expr(state)) + end, + self:CompileStmt(ifeif[2]) + } + end + else -- else block + chain[i] = { nil, self:CompileStmt(ifeif[2]) } + end + end) end - end - - if not Func then - self:Error("No such function: " .. tps_pretty({ Meta }) .. ":" .. Name .. "(" .. tps_pretty(Args) .. ")", instr) - return - end - - self.prfcounter = self.prfcounter + (Func[4] or 20) - - return { Func[3], Func[2], Func[1], Func.attributes } -end - -function Compiler:PushPrfCounter() - self.prfcounters[#self.prfcounters + 1] = self.prfcounter - self.prfcounter = 0 -end - -function Compiler:PopPrfCounter() - local prfcounter = self.prfcounter - self.prfcounter = self.prfcounters[#self.prfcounters] - self.prfcounters[#self.prfcounters] = nil - return prfcounter -end - --- ------------------------------------------------------------------------ - ---- Warnings for expressions in statement positions --- "add", "sub", "mul", "div", "mod", "exp", "eq", "neq", "geq", "leq", "gth", "lth", "band", "band", "bor", "bxor", "bshl", "bshr" -local ExprWarnings = { - ["GET"] = "Cannot discard value of an index (Remove this pointless indexing)", - ["LITERAL"] = "This literal won't have any effect on the code", - - ["NOT"] = "Cannot discard result of logical NOT operation (!)", - ["AND"] = "Cannot discard result of logical AND operation (&)", - ["OR"] = "Cannot discard result of logical OR operation (|)", - - ["BAND"] = "Cannot discard result of binary AND operation (&&)", - ["BOR"] = "Cannot discard result of binary OR operation (||)", - - ["BXOR"] = "Cannot discard result of binary XOR operation (^^)", + return function(state) ---@param state RuntimeContext + for _, data in ipairs(chain) do + local cond, block = data[1], data[2] + if cond then + if cond(state) ~= 0 then + state:PushScope() + block(state) + state:PopScope() + break + end + else + -- Else block + state:PushScope() + block(state) + state:PopScope() + break + end + end + end + end, + + ---@param data { [1]: Node, [2]: Node, [3]: boolean } + [NodeVariant.While] = function(self, trace, data) + local expr, block = self:Scope(function(scope) + scope.data.loop = true + return self:CompileExpr(data[1]), self:CompileStmt(data[2]) + end) + + if data[3] then + -- do while + return function(state) ---@param state RuntimeContext + state:PushScope() + repeat + state.prf = state.prf + 1 / 20 + + block(state) + + if state.__break__ then + state.__break__ = false + break + elseif state.__return__ then + break + elseif state.__continue__ then + state.__continue__ = false + end + until expr(state) == 0 + state:PopScope() + end + else + return function(state) ---@param state RuntimeContext + state:PushScope() + while expr(state) ~= 0 do + state.prf = state.prf + 1 / 20 - ["BSHL"] = "Cannot discard result of binary left shift operation (<<)", - ["BSHR"] = "Cannot discard result of binary right shift operation (>>)", + block(state) - ["ADD"] = "Cannot discard result of addition operation (+)", - ["SUB"] = "Cannot discard result of subtraction operation (-)", - ["MUL"] = "Cannot discard result of multiplication operation (*)", - ["DIV"] = "Cannot discard result of division operation (/)", - ["MOD"] = "Cannot discard result of modulus operation (%)", - ["EXP"] = "Cannot discard result of exponential operation (^)", + if state.__break__ then + state.__break__ = false + break + elseif state.__return__ then + break + elseif state.__continue__ then + state.__continue__ = false + end + end + state:PopScope() + end + end + end, - ["EQ"] = "Cannot discard result of equal to comparison (==)", - ["NEQ"] = "Cannot discard result of not equal to comparison (!=)", - ["GEQ"] = "Cannot discard result of greater than or equal to comparison (>=)", - ["LEQ"] = "Cannot discard result of less than than or equal to comparison (<=)", - ["GTH"] = "Cannot discard result of greater than comparison (>)", - ["LTH"] = "Cannot discard result of less than comparison (<)", + ---@param data { [1]: Token, [2]: Node, [3]: Node, [4]: Node?, [5]: Node } var start stop step block + [NodeVariant.For] = function (self, trace, data) + local var, start, stop, step = data[1], self:CompileExpr(data[2]), self:CompileExpr(data[3]), data[4] and self:CompileExpr(data[4]) or data[4] - ["TRG"] = "Cannot discard result of triggered (~) operation", - ["DLT"] = "Cannot discard result of delta ($) operator", - ["IWC"] = "Cannot discard result of connected (->) operator", + local block = self:Scope(function(scope) + scope.data.loop = true + scope:DeclVar(var.value, { initialized = true, type = "n", trace_if_unused = var.trace }) - ["VAR"] = "Cannot discard variable" -} + return self:CompileStmt(data[5]) + end) -function Compiler:InstrSEQ(args) - -- args = { "seq", trace, subexpressions... } - self:PushPrfCounter() + local var = var.value + if var == "_" then -- Discarded for loop value + return function(state) ---@param state RuntimeContext + state:PushScope() -- Push scope only first time, compiler should enforce not using variables ahead of time. + local step = step and step(state) or 1 + for _ = start(state), stop(state), step do + state.prf = state.prf + 1 / 20 - local stmts = { self:GetOperator(args, "seq", {})[1], 0 } + block(state) - for i = 1, #args - 2 do - if self.Scope._dead then - -- Don't compile dead code. - self:Warning("Unreachable code detected", args[i + 2]) - break - else - local stmt, _, instr, extra = self:EvaluateStatement(args, i) - if (instr == "CALL" or instr == "METHODCALL") and (extra and extra.nodiscard) then - self:Warning("The return value of this function cannot be discarded", args[i + 2]) - elseif ExprWarnings[instr] then - self:Warning(ExprWarnings[instr], args[i + 2]) + if state.__break__ then + state.__break__ = false + break + elseif state.__return__ then + break + elseif state.__continue__ then + state.__continue__ = false + end + end + state:PopScope() end + else + return function(state) ---@param state RuntimeContext + state:PushScope() -- Push scope only first time, compiler should enforce not using variables ahead of time. + local step, scope = step and step(state) or 1, state.Scope + for i = start(state), stop(state), step do + state.prf = state.prf + 1 / 20 + scope[var] = i - if stmt then - -- Statement has a runtime side effect. - stmts[#stmts + 1] = stmt - end - end - end + block(state) - for varname, var in pairs(self.Scope) do - if var ~= true and var.var_tok then - if var.binding then - self:Warning("Unused variable [" .. varname .. "] (You can use _ to discard it)", var.var_tok) - else - self:Warning("Unused variable [" .. varname .. "]", var.var_tok) + if state.__break__ then + state.__break__ = false + break + elseif state.__return__ then + break + elseif state.__continue__ then + state.__continue__ = false + end + end + state:PopScope() end end - end - - stmts[2] = self:PopPrfCounter() + end, - return stmts -end - -function Compiler:InstrBRK(args) - -- args = { "brk", trace } - self.Scope._dead = true - return { self:GetOperator(args, "brk", {})[1] } -end + ---@param data { [1]: Token, [2]: Token?, [3]: Token, [4]: Token, [5]: Node, [6]: Node } key key_type value value_type iterator block + [NodeVariant.Foreach] = function (self, trace, data) + local key, key_type, value, value_type = data[1], data[2] and self:CheckType(data[2]), data[3], self:CheckType(data[4]) -function Compiler:InstrCNT(args) - -- args = { "cnt", trace } - self.Scope._dead = true - return { self:GetOperator(args, "cnt", {})[1] } -end + local item, item_ty = self:CompileExpr(data[5]) -function Compiler:InstrFOR(args) - -- args = { "for", trace, variable name, start expression, stop expression, step expression or nil, loop body } - local var = args[3] - - local estart, tp1 = self:Evaluate(args, 2) - local estop, tp2 = self:Evaluate(args, 3) - - local estep, tp3 - if args[6] then - estep, tp3 = self:Evaluate(args, 4) - if tp1 ~= "n" or tp2 ~= "n" or tp3 ~= "n" then self:Error("for(" .. tps_pretty({ tp1 }) .. ", " .. tps_pretty({ tp2 }) .. ", " .. tps_pretty({ tp3 }) .. ") is invalid, only supports indexing by number", args) end - else - if tp1 ~= "n" or tp2 ~= "n" then self:Error("for(" .. tps_pretty({ tp1 }) .. ", " .. tps_pretty({ tp2 }) .. ") is invalid, only supports indexing by number", args) end - end + if not key_type then -- If no key type specified, fall back to string for tables and number for everything else. + if item_ty == "t" then + self:Warning("This key will default to type (string). Annotate it with :string or :number", key.trace) + key_type = "s" + else + self:Warning("This key will default to type (number). Annotate it with :number / :type", key.trace) + key_type = "n" + end + end - self:PushScope() - if var ~= "_" then - self:SetLocalVariableType(var, "n", args, true) - end + local block, cost = self:Scope(function(scope) + scope.data.loop = true - local stmt = self:EvaluateStatement(args, 5) - self:PopScope() + scope:DeclVar(key.value, { initialized = true, trace_if_unused = key.trace, type = key_type }) + scope:DeclVar(value.value, { initialized = true, trace_if_unused = value.trace, type = value_type }) - return { self:GetOperator(args, "for", {})[1], var, estart, estop, estep, stmt } -end + return self:CompileStmt(data[6]), 1 / 15 + end) -function Compiler:InstrWHL(args) - -- args = { "whl", trace, condition expression, loop body, skip condition check first time? } + local into_iter = self:GetOperator("iter", { key_type, value_type, "=", item_ty }, trace) + local key, value = key.value, value.value - local skipCondFirstTime = args[5] + if key == "_" then -- Not using key + return function(state) ---@param state RuntimeContext + local iter = into_iter(state, item(state)) - self:PushScope() + state:PushScope() -- Only push scope once as an optimization, compiler should disallow using variable ahead of time anyway. + local scope = state.Scope + for _, v in iter() do + state.prf = state.prf + cost + scope[value] = v - self:PushPrfCounter() - local cond = self:Evaluate(args, 1) - local prf_cond = self:PopPrfCounter() + block(state) - local stmt = self:EvaluateStatement(args, 2) - self:PopScope() + if state.__break__ then + state.__break__ = false + break + elseif state.__return__ then + break + elseif state.__continue__ then + state.__continue__ = false + end + end + state:PopScope() + end + else -- todo: optimize for discard value case + return function(state) ---@param state RuntimeContext + local iter = into_iter(state, item(state)) - return { self:GetOperator(args, "whl", {})[1], cond, stmt, prf_cond, skipCondFirstTime } -end + state:PushScope() -- Only push scope once as an optimization, compiler should disallow using variable ahead of time anyway. + local scope = state.Scope + for k, v in iter() do + state.prf = state.prf + cost + scope[key], scope[value] = k, v + block(state) -function Compiler:InstrIF(args) - -- args = { "if", trace, condition expression, true case body, false case body } - self:PushPrfCounter() - local ex1, tp1 = self:Evaluate(args, 1) - local prf_cond = self:PopPrfCounter() + if state.__break__ then + state.__break__ = false + break + elseif state.__return__ then + break + elseif state.__continue__ then + state.__continue__ = false + end + end + state:PopScope() + end + end + end, + + ---@param data { [1]: Node, [2]: {[1]: Node, [2]: Node}[], [3]: Node? } + [NodeVariant.Switch] = function (self, trace, data) + local expr, expr_ty = self:CompileExpr(data[1]) + + local cases = {} ---@type { [1]: RuntimeOperator, [2]: RuntimeOperator }[] + for i, case in ipairs(data[2]) do + local cond, cond_ty = self:CompileExpr(case[1]) + local block + self:Scope(function(scope) + scope.data.switch_case = true + block = self:CompileStmt(case[2]) + end) + + local eq = self:GetOperator("eq", { expr_ty, cond_ty }, case[1].trace) + cases[i] = { + function(state, expr) + return eq(state, cond(state), expr) + end, + block + } + end - self:PushScope() - local st1 = self:EvaluateStatement(args, 2) - self:PopScope() + local default = data[3] and self:Scope(function() return self:CompileStmt(data[3]) end) + local ncases = #cases + + return function(state) ---@param state RuntimeContext + local expr = expr(state) + + state:PushScope() + for i = 1, ncases do + local case = cases[i] + if case[1](state, expr) ~= 0 then + case[2](state) + + if state.__break__ then + state.__break__ = false + state:PopScope() + return + else -- Fallthrough, run every case until break found. + for j = i + 1, ncases do + cases[j][2](state) + if state.__break__ then + state.__break__ = false + state:PopScope() + return + end + end + end + end + end - self:PushScope() - local st2 = self:EvaluateStatement(args, 3) - self:PopScope() + if default then + default(state) + end - local rtis = self:GetOperator(args, "is", { tp1 }) - local rtif = self:GetOperator(args, "if", { rtis[2] }) - return { rtif[1], prf_cond, { rtis[1], ex1 }, st1, st2 } -end + state:PopScope() + end + end, -function Compiler:InstrDEF(args) - -- args = { "def", trace, primary expression, fallback expression } - local ex1, tp1 = self:Evaluate(args, 1) + ---@param data { [1]: Node, [2]: Token, [3]: Token?, [4]: Node } + [NodeVariant.Try] = function (self, trace, data) + local try_block, catch_block, err_var, err_ty = nil, nil, data[2], data[3] + self:Scope(function(scope) + try_block = self:CompileStmt(data[1]) + end) - self:PushPrfCounter() - local ex2, tp2 = self:Evaluate(args, 2) - local prf_ex2 = self:PopPrfCounter() + if err_ty then + self:Assert(err_ty.value == "string", "Error type can only be string, for now", err_ty.trace) + else + self:Warning("You should explicitly annotate the error type as :string", err_var.trace) + end - local rtis = self:GetOperator(args, "is", { tp1 }) - local rtif = self:GetOperator(args, "def", { rtis[2] }) - local rtdat = self:GetOperator(args, "dat", {}) + self:Scope(function (scope) + scope:DeclVar(err_var.value, { initialized = true, trace_if_unused = err_var.trace, type = "s" }) + catch_block = self:CompileStmt(data[4]) + end) + + self.scope.data.ops = self.scope.data.ops + 5 + + return function(state) ---@param state RuntimeContext + state:PushScope() + local ok, err = pcall(try_block, state) + state:PopScope() + if not ok then + local catchable, msg = E2Lib.unpackException(err) + if catchable then + state:PushScope() + state.Scope[err_var.value] = (type(msg) == "string") and msg or "" + catch_block(state) + state:PopScope() + else + error(err, 0) + end + end + end + end, - if tp1 ~= tp2 then - self:Error("Different types (" .. tps_pretty({ tp1 }) .. ", " .. tps_pretty({ tp2 }) .. ") returned in default conditional", args) - end + ---@param data { [1]: Token, [2]: Token?, [3]: Token, [4]: Parameter[], [5]: Node } + [NodeVariant.Function] = function (self, trace, data) + local name = data[3] - return { rtif[1], { rtis[1], { rtdat[1], nil } }, ex1, ex2, prf_ex2 }, tp1 -end + local return_type + if data[1] then + return_type = self:CheckType(data[1]) + end -function Compiler:InstrCND(args) - -- args = { "cnd", trace, conditional expression, true expression, false expression } - local ex1, tp1 = self:Evaluate(args, 1) + local meta_type + if data[2] then + meta_type = self:Assert(self:CheckType(data[2]), "Cannot use void as meta type", trace) + end - self:PushPrfCounter() - local ex2, tp2 = self:Evaluate(args, 2) - local prf_ex2 = self:PopPrfCounter() + local param_types, param_names, variadic_ind, variadic_ty = {}, {}, nil, nil + if data[4] then -- Has parameters + local existing = {} + for i, param in ipairs(data[4]) do + if param.type then + local t = self:CheckType(param.type) + if param.variadic then + self:Assert(t == "r" or t == "t", "Variadic parameter must be of type array or table", param.type.trace) + variadic_ind, variadic_ty = i, t + end + param_types[i] = t + elseif param.variadic then + self:Error("Variadic parameter requires explicit type", param.name.trace) + else + param_types[i] = "n" + self:Warning("Use of implicit parameter type is deprecated (add :number)", param.name.trace) + end - self:PushPrfCounter() - local ex3, tp3 = self:Evaluate(args, 3) - local prf_ex3 = self:PopPrfCounter() + if param.name.value ~= "_" and existing[param.name.value] then + self:Error("Variable '" .. param.name.value .. "' is already used as a parameter", param.name.trace) + else + param_names[i] = param.name.value + existing[param.name.value] = true + end + end + end - local rtis = self:GetOperator(args, "is", { tp1 }) - local rtif = self:GetOperator(args, "cnd", { rtis[2] }) + local fn_data, lookup_variadic, userfunction = self:GetFunction(name.value, param_types, meta_type) + if fn_data then + if not userfunction then + if not lookup_variadic or variadic_ind == 1 then -- Allow overrides like print(nnn) and print(n..r) to override print(...), but not print(...r) + self:Error("Cannot overwrite existing function: " .. (meta_type and (meta_type .. ":") or "") .. name.value .. "(" .. table.concat(fn_data.args, ", ") .. ")", name.trace) + end + else + if return_type then + self:Assert(fn_data.returns and fn_data.returns[1] == return_type, "Cannot override with differing return type", trace) + else + self:Assert(fn_data.returns == nil, "Cannot override function returning void with differing return type", trace) + end - if tp2 ~= tp3 then - self:Error("Different types (" .. tps_pretty({ tp2 }) .. ", " .. tps_pretty({ tp3 }) .. ") returned in conditional", args) - end + -- Tag function if it is ever re-declared. Used as an optimization + fn_data.const = fn_data.op == nil + end + end - return { rtif[1], { rtis[1], ex1 }, ex2, ex3, prf_ex2, prf_ex3 }, tp2 -end + local fn = { args = param_types, returns = return_type and { return_type }, meta = meta_type, cost = 20, attrs = {} } + local sig = table.concat(param_types, "", 1, #param_types - 1) .. ((variadic_ty and ".." or "") .. (param_types[#param_types] or "")) + if meta_type then + self.user_methods[meta_type] = self.user_methods[meta_type] or {} -function Compiler:InstrCALL(args) - -- args = { "call", trace, function name, { argument expressions... } } - local exprs = { false } + self.user_methods[meta_type][name.value] = self.user_methods[meta_type][name.value] or {} - local tps, fname = {}, args[3] - if fname == "array" then - -- Hack for array creation. - -- Check if illegal arguments are passed - for i = 1, #args[4] do - local ex, tp = self:Evaluate(args[4], i - 2) - if BLOCKED_ARRAY_TYPES[tp] then - self:Error("Cannot have type " .. tps_pretty(tp) .. " in array creation argument #" .. i, args[4][i]) + if variadic_ty then + local opposite = variadic_ty == "r" and "t" or "r" + if self.user_methods[meta_type][name.value][sig:gsub(".." .. variadic_ty, ".." .. opposite)] then + self:Error("Cannot override variadic " .. opposite .. " function with variadic " .. variadic_ty .. " function to avoid ambiguity.", trace) + end end - exprs[i + 1] = ex - tps[i] = tp - end - elseif fname == "changed" then - for i = 1, #args[4] do - local ex, tp, instr = self:Evaluate(args[4], i - 2) - if instr == "LITERAL" then - self:Warning("Using changed on a literal will only evaluate once", args[4][i]) - elseif instr == "VAR" then - local varname = args[4][i][3] - if self.inputs[varname] then - self:Warning("Using changed on an input is bad, use the ~ or -> operators instead", args[4][i]) + self.user_methods[meta_type][name.value][sig] = fn + + -- Insert "This" variable + table.insert(param_names, 1, "This") + table.insert(param_types, 1, meta_type) + else + self.user_functions[name.value] = self.user_functions[name.value] or {} + if variadic_ty then + local opposite = variadic_ty == "r" and "t" or "r" + if self.user_functions[name.value][sig:gsub(".." .. variadic_ty, ".." .. opposite)] then + self:Error("Cannot override variadic " .. opposite .. " function with variadic " .. variadic_ty .. " function to avoid ambiguity.", trace) end end - exprs[i + 1], tps[i] = ex, tp + self.user_functions[name.value][sig] = fn end - else - for i = 1, #args[4] do - exprs[i + 1], tps[i] = self:Evaluate(args[4], i - 2) - end - end - local rt = self:GetFunction(args, args[3], tps) - exprs[1] = rt[1] - exprs[#exprs + 1] = tps - if rt[4] then - if rt[4].deprecated ~= nil and rt[4].deprecated ~= true then - -- Deprecation message (string) - self:Warning("Use of deprecated function: " .. args[3] .. "(" .. tps_pretty(tps) .. "): '" .. rt[4].deprecated .. "'", args) - elseif rt[4].deprecated then - self:Warning("Use of deprecated function: " .. args[3] .. "(" .. tps_pretty(tps) .. ")", args) - end + local block + if variadic_ty then + local last, non_variadic = #param_types, #param_types - 1 + if variadic_ty == "r" then + function fn.op(state, args) ---@param state RuntimeContext + local s_scopes, s_scopeid, s_scope = state.Scopes, state.ScopeID, state.Scope - if rt[4].noreturn then - self.Scope._dead = true - end - end + local scope = { vclk = {} } -- Hack in the fact that functions don't have upvalues right now. + state.Scopes = { [0] = state.GlobalScope, [1] = scope } + state.Scope = scope + state.ScopeID = 1 - return exprs, rt[2], rt[4] -end + for i = 1, non_variadic do + scope[param_names[i]] = args[i] + end -function Compiler:InstrSTRINGCALL(args) - -- args = { "stringcall", trace, function name expression, { argument expressions... }, return type } - local exprs = { false } + local a, n = {}, 1 + for i = last, #args do + a[n] = args[i] + n = n + 1 + end - local fexp, ftp = self:Evaluate(args, 1) + scope[param_names[last]] = a + block(state) - if ftp ~= "s" then - self:Error("User function is not string-type", args) - end + state.Scopes, state.ScopeID, state.Scope = s_scopes, s_scopeid, s_scope - local tps = {} - for i = 1, #args[4] do - local ex, tp = self:Evaluate(args[4], i - 2) - tps[#tps + 1] = tp - exprs[#exprs + 1] = ex - end + if state.__return__ then + state.__return__ = false + return state.__returnval__ + elseif return_type then + state:forceThrow("Expected function return at runtime of type (" .. return_type .. ")") + end + end + else -- table + function fn.op(state, args, arg_types) ---@param state RuntimeContext + local s_scopes, s_scopeid, s_scope = state.Scopes, state.ScopeID, state.Scope - exprs[#exprs + 1] = tps + local scope = { vclk = {} } -- Hack in the fact that functions don't have upvalues right now. + state.Scopes = { [0] = state.GlobalScope, [1] = scope } + state.Scope = scope + state.ScopeID = 1 - local rtsfun = self:GetOperator(args, "stringcall", {})[1] + for i = 1, non_variadic do + scope[param_names[i]] = args[i] + end - local typeids_str = table.concat(tps, "") + local n, ntypes = {}, {} + for i = last, #args do + n[i - last + 1], ntypes[i - last + 1] = args[i], arg_types[i - (meta_type and 1 or 0)] + end - return { rtsfun, fexp, exprs, tps, typeids_str, args[5] }, args[5] -end + scope[param_names[last]] = { s = {}, stypes = {}, n = n, ntypes = ntypes, size = last } -function Compiler:InstrMETHODCALL(args) - -- args = { "methodcall", trace, method name, object expression, { argument expressions... } } - local exprs = { false } + block(state) - local tps = {} + state.Scopes, state.ScopeID, state.Scope = s_scopes, s_scopeid, s_scope - local ex, tp = self:Evaluate(args, 2) - exprs[#exprs + 1] = ex + if state.__return__ then + state.__return__ = false + return state.__returnval__ + elseif return_type then + state:forceThrow("Expected function return at runtime of type (" .. return_type .. ")") + end + end + end + else -- Todo: Make this output a different function when it doesn't early return, and/or has no parameters as an optimization. + local nargs = #param_types + function fn.op(state, args) ---@param state RuntimeContext + local s_scopes, s_scopeid, s_scope = state.Scopes, state.ScopeID, state.Scope + + local scope = { vclk = {} } -- Hack in the fact that functions don't have upvalues right now. + state.Scopes = { [0] = state.GlobalScope, [1] = scope } + state.Scope = scope + state.ScopeID = 1 + + for i = 1, nargs do + scope[param_names[i]] = args[i] + end - for i = 1, #args[5] do - local ex, tp = self:Evaluate(args[5], i - 2) - tps[#tps + 1] = tp - exprs[#exprs + 1] = ex - end + block(state) - local rt = self:GetMethod(args, args[3], tp, tps) - exprs[1] = rt[1] - exprs[#exprs + 1] = tps + state.Scopes, state.ScopeID, state.Scope = s_scopes, s_scopeid, s_scope - if rt[4] then - if rt[4].deprecated ~= nil and rt[4].deprecated ~= true then - -- Deprecation message (string) - self:Warning("Use of deprecated method: " .. tps_pretty(tp) .. ":" .. args[3] .. "(" .. tps_pretty(tps) .. "): '" .. rt[4].deprecated .. "'", args) - elseif rt[4].deprecated then - self:Warning("Use of deprecated method: " .. tps_pretty(tp) .. ":" .. args[3] .. "(" .. tps_pretty(tps) .. ")", args) + if state.__return__ then + state.__return__ = false + return state.__returnval__ + elseif return_type then + state:forceThrow("Expected function function at runtime of type (" .. return_type .. ")") + end + end end - if rt[4].noreturn then - self.Scope._dead = true - end - end + block = self:IsolatedScope(function (scope) + for i, type in ipairs(param_types) do + scope:DeclVar(param_names[i], { type = type, trace_if_unused = data[4][i] and data[4][i].name.trace or trace, initialized = true }) + end - return exprs, rt[2], rt[4] -end + scope.data["function"] = { name.value, fn } -function Compiler:InstrASS(args) - -- args = { "ass", trace, variable name, assigned expression } - local op = args[3] - local ex, tp = self:Evaluate(args, 2) + return self:CompileStmt(data[5]) + end) - local keep_as_used = self.persist[op] and not self.GlobalScope[op].var_tok + self:Assert((fn.returns and fn.returns[1]) == return_type, "Function " .. name.value .. " expects to return type (" .. (return_type or "void") .. ") but got type (" .. ((fn.returns and fn.returns[1]) or "void") .. ")", trace) - local ScopeID = self:SetGlobalVariableType(op, tp, args, true) - if keep_as_used or (ScopeID == 0 and self.outputs[op]) then - -- Mark output variable as being used to prevent warnings. - -- Also mark @persist variable as used if already used in InstrVAR prior to assignment - -- (Without this, the InstrASS would mark it as unused once again even if it was used prior) - self.Scopes[ScopeID][op].var_tok = nil - end + local sig = name.value .. "(" .. (meta_type and (meta_type .. ":") or "") .. sig .. ")" + local fn = fn.op - local rt = self:GetOperator(args, "ass", { tp }) + return function(state) ---@param state RuntimeContext + state.funcs[sig] = fn + state.funcs_ret[sig] = return_type + end + end, + + ---@param data string + [NodeVariant.Include] = function (self, trace, data) + local include = self.includes[data] + self:Assert(include and include[1], "Problem including file '" .. data .. "'", trace) + + if not include[2] then + include[2] = true -- Prevent self-compiling infinite loop + + local last_file = self.include + self.include = data + self.warnings[data] = self.warnings[data] or {} + + local status, script = self:IsolatedScope(function(_) + return pcall(self.CompileStmt, self, include[1]) + end) + + if not status then ---@cast script Error + local reason = script.message + if reason:find("C stack overflow") then reason = "Include depth too deep" end + + if not self.IncludeError then + -- Otherwise Errors messages will be wrapped inside other error messages! + self.IncludeError = true + self:Error("include '" .. data .. "' -> " .. reason, trace) + else + error(script, 0) -- re-throw + end + else + self.include = last_file - if ScopeID == 0 and self.dvars[op] then - local stmts = { self:GetOperator(args, "seq", {})[1], 0 } - stmts[3] = { self:GetOperator(args, "ass", { tp })[1], "$" .. op, { self:GetOperator(args, "var", {})[1], op, ScopeID }, ScopeID } - stmts[4] = { rt[1], op, ex, ScopeID } - return stmts, tp - else - return { rt[1], op, ex, ScopeID }, tp - end -end + local nwarnings = #self.warnings[data] + if nwarnings ~= 0 then + self:Warning("include '" .. data .. "' has " .. nwarnings .. " warning(s).", trace) + end + end -function Compiler:InstrASSL(args) - -- args = { "assl", trace, variable name, assigned expression } - local op = args[3] - local ex, tp = self:Evaluate(args, 2) - local ScopeID = self:SetLocalVariableType(op, tp, args) - local rt = self:GetOperator(args, "ass", { tp }) + include[2] = script + end - if ScopeID == 0 then - self:Error("Invalid use of 'local' inside the global scope.", args) - end -- Just to make code look neater. + return function(state) ---@param state RuntimeContext + local s_scopes, s_scopeid, s_scope = state.Scopes, state.ScopeID, state.Scope - return { rt[1], op, ex, ScopeID }, tp -end + local scope = { vclk = {} } -- Isolated scope, except global variables are shared. + state.Scope = scope + state.ScopeID = 1 + state.Scopes = { [0] = state.GlobalScope, [1] = scope } -function Compiler:InstrGET(args) - -- args = { "get", trace, object expression, field expression, return type or nil } - local ex, tp = self:Evaluate(args, 1) - local ex1, tp1 = self:Evaluate(args, 2) - local tp2 = args[5] + include[2](state) - if tp2 == nil then - if not self:HasOperator(args, "idx", { tp, tp1 }) then - self:Error("No such operator: get " .. tps_pretty({ tp }) .. "[" .. tps_pretty({ tp1 }) .. "]", args) + state.Scopes, state.ScopeID, state.Scope = s_scopes, s_scopeid, s_scope end + end, - local rt = self:GetOperator(args, "idx", { tp, tp1 }) - return { rt[1], ex, ex1 }, rt[2] - - - else - if not self:HasOperator(args, "idx", { tp2, "=", tp, tp1 }) then - self:Error("No such operator: get " .. tps_pretty({ tp }) .. "[" .. tps_pretty({ tp1, tp2 }) .. "]", args) + ---@param data {} + [NodeVariant.Continue] = function(self, trace, data) + self.scope.data.dead = true + return function(state) ---@param state RuntimeContext + state.__continue__ = true end + end, - local rt = self:GetOperator(args, "idx", { tp2, "=", tp, tp1 }) - return { rt[1], ex, ex1 }, tp2 - end -end + ---@param data {} + [NodeVariant.Break] = function(self, trace, data) + self.scope.data.dead = true + return function(state) ---@param state RuntimeContext + state.__break__ = true + end + end, -function Compiler:InstrSET(args) - -- args = { "set", trace, object expression, field expression, value expression, value type or nil } - local ex, tp = self:Evaluate(args, 1) - local ex1, tp1 = self:Evaluate(args, 2) - local ex2, tp2 = self:Evaluate(args, 3) + ---@param data Node? + [NodeVariant.Return] = function (self, trace, data) + local fn = self.scope:ResolveData("function") + self:Assert(fn, "Cannot use `return` outside of a function", trace) - if args[6] == nil then - if not self:HasOperator(args, "idx", { tp, tp1, tp2 }) then - self:Error("No such operator: set " .. tps_pretty({ tp }) .. "[" .. tps_pretty({ tp1 }) .. "]=" .. tps_pretty({ tp2 }), args) + local retval, ret_ty + if data then + retval, ret_ty = self:CompileExpr(data) end - local rt = self:GetOperator(args, "idx", { tp, tp1, tp2 }) + local name, fn = fn[1], fn[2] - return { rt[1], ex, ex1, ex2, nil }, rt[2] - else - if tp2 ~= args[6] then - self:Error("Indexing type mismatch, specified [" .. tps_pretty({ args[6] }) .. "] but value is [" .. tps_pretty({ tp2 }) .. "]", args) + if fn.returns then + self:Assert(fn.returns[1] == ret_ty, "Function " .. name .. " expects return type (" .. (fn.returns[1] or "void") .. ") but was given (" .. (ret_ty or "void") .. ")", trace) + else + fn.returns = { ret_ty } end - if not self:HasOperator(args, "idx", { tp2, "=", tp, tp1, tp2 }) then - self:Error("No such operator: set " .. tps_pretty({ tp }) .. "[" .. tps_pretty({ tp1, tp2 }) .. "]", args) + if ret_ty then + return function(state) ---@param state RuntimeContext + state.__returnval__, state.__return__ = retval(state), true + end + else -- return void (or just return) + return function(state) ---@param state RuntimeContext + state.__returnval__, state.__return__ = nil, true + end + end + end, + + ---@param data { [1]: boolean, [2]: { [1]: Token, [2]: { [1]: Node, [2]: Token?, [3]: Trace }[] }[], [3]: Node } is_local, vars, value + [NodeVariant.Assignment] = function (self, trace, data) + local value, value_ty = self:CompileExpr(data[3]) + + if data[1] then + -- Local declaration. Fastest case. + local var_name = data[2][1][1].value + self:AssertW(not self.scope.vars[var_name], "Do not redeclare existing variable " .. var_name, trace) + self.scope:DeclVar(var_name, { initialized = true, trace_if_unused = data[2][1][1].trace, type = value_ty }) + return function(state) ---@param state RuntimeContext + state.Scope[var_name] = value(state) + end end - local rt = self:GetOperator(args, "idx", { tp2, "=", tp, tp1, tp2 }) - - return { rt[1], ex, ex1, ex2 }, tp2 - end -end - - --- generic code for all binary non-boolean operators -for _, operator in ipairs({ "add", "sub", "mul", "div", "mod", "exp", "eq", "neq", "geq", "leq", "gth", "lth", "band", "band", "bor", "bxor", "bshl", "bshr" }) do - - Compiler["Instr" .. operator:upper()] = function(self, args) - -- args = { operator, trace, left expression, right expression } - local ex1, tp1 = self:Evaluate(args, 1) - local ex2, tp2 = self:Evaluate(args, 2) - local rt = self:GetOperator(args, operator, { tp1, tp2 }) - return { rt[1], ex1, ex2 }, rt[2] - end -end - -function Compiler:InstrINC(args) - -- args = { "inc", trace, variable name } - local op = args[3] - local tp, ScopeID = self:GetVariableType(args, op) - local rt = self:GetOperator(args, "inc", { tp }) - - if ScopeID == 0 and self.dvars[op] then - local stmts = { self:GetOperator(args, "seq", {})[1], 0 } - stmts[3] = { self:GetOperator(args, "ass", { tp })[1], "$" .. op, { self:GetOperator(args, "var", {})[1], op, ScopeID }, ScopeID } - stmts[4] = { rt[1], op, ScopeID } - return stmts - else - return { rt[1], op, ScopeID } - end -end - -function Compiler:InstrDEC(args) - -- args = { "dec", trace, variable name } - local op = args[3] - local tp, ScopeID = self:GetVariableType(args, op) - local rt = self:GetOperator(args, "dec", { tp }) - - if ScopeID == 0 and self.dvars[op] then - local stmts = { self:GetOperator(args, "seq", {})[1], 0 } - stmts[3] = { self:GetOperator(args, "ass", { tp })[1], "$" .. op, { self:GetOperator(args, "var", {})[1], op, ScopeID }, ScopeID } - stmts[4] = { rt[1], op, ScopeID } - return stmts - else - return { rt[1], op, ScopeID } - end -end - -function Compiler:InstrNEG(args) - -- args = { "neg", trace, expression } - local ex1, tp1 = self:Evaluate(args, 1) - local rt = self:GetOperator(args, "neg", { tp1 }) - return { rt[1], ex1 }, rt[2] -end - - -function Compiler:InstrNOT(args) - -- args = { "not", trace, expression } - local ex1, tp1 = self:Evaluate(args, 1) - local rt1is = self:GetOperator(args, "is", { tp1 }) - local rt = self:GetOperator(args, "not", { rt1is[2] }) - return { rt[1], { rt1is[1], ex1 } }, rt[2] -end - -function Compiler:InstrAND(args) - -- args = { "and", trace, left expression, right expression } - local ex1, tp1 = self:Evaluate(args, 1) - local ex2, tp2 = self:Evaluate(args, 2) - local rt1is = self:GetOperator(args, "is", { tp1 }) - local rt2is = self:GetOperator(args, "is", { tp2 }) - local rt = self:GetOperator(args, "and", { rt1is[2], rt2is[2] }) - return { rt[1], { rt1is[1], ex1 }, { rt2is[1], ex2 } }, rt[2] -end - -function Compiler:InstrOR(args) - -- args = { "or", trace, left expression, right expression } - local ex1, tp1 = self:Evaluate(args, 1) - local ex2, tp2 = self:Evaluate(args, 2) - local rt1is = self:GetOperator(args, "is", { tp1 }) - local rt2is = self:GetOperator(args, "is", { tp2 }) - local rt = self:GetOperator(args, "or", { rt1is[2], rt2is[2] }) - return { rt[1], { rt1is[1], ex1 }, { rt2is[1], ex2 } }, rt[2] -end - - -function Compiler:InstrTRG(args) - -- args = { "trg", trace, variable name } - local op = args[3] - local _tp, ScopeID = self:GetVariableType(args, op) - if ScopeID ~= 0 or not self.inputs[op] then - self:Error("Triggered operator (~" .. E2Lib.limitString(op, 10) .. ") can only be used on inputs", args) - end + local stmts = {} + for i, v in ipairs(data[2]) do + local var, indices, trace = v[1].value, v[2], v[3] - -- Necessary since this doesn't use Compiler:Evaluate (which would call InstrVAR, and do this.) - self.Scopes[0][op].var_tok = nil + local existing = self.scope:LookupVar(var) + if existing then + local expr_ty = existing.type + existing.trace_if_unused = nil - local rt = self:GetOperator(args, "trg", {}) - return { rt[1], op }, rt[2] -end + -- It can have indices, it already exists + if #indices > 0 then + local setter, id = table.remove(indices), existing.depth + stmts[i] = function(state) + return state.Scopes[id][var] + end -function Compiler:InstrDLT(args) - -- args = { "dlt", trace, variable name } - local op = args[3] - local tp, ScopeID = self:GetVariableType(args, op) + for _, index in ipairs(indices) do + local key, key_ty = self:CompileExpr(index[1]) + + local op + if index[2] then -- [, ] + local ty = self:CheckType(index[2]) + op, expr_ty = self:GetOperator("indexget", { expr_ty, key_ty, ty }, index[3]) + else -- [] + op, expr_ty = self:GetOperator("indexget", { expr_ty, key_ty }, index[3]) + end + + local handle = stmts[i] -- need this, or stack overflow... + stmts[i] = function(state) + return op(state, handle(state), key(state)) + end + end - if ScopeID ~= 0 or not self.dvars[op] then - self:Error("Delta operator ($" .. E2Lib.limitString(op, 10) .. ") cannot be used on temporary variables", args) - end + local key, key_ty = self:CompileExpr(setter[1]) - self.dvars[op] = true - local rt = self:GetOperator(args, "sub", { tp, tp }) - local rtvar = self:GetOperator(args, "var", {}) - return { rt[1], { rtvar[1], op, ScopeID }, { rtvar[1], "$" .. op, ScopeID } }, rt[2] -end + local op + if setter[2] then -- [, ] + local ty = self:CheckType(setter[2]) + self:Assert(ty == value_ty, "Cannot assign type " .. value_ty .. " to object expecting " .. ty, trace) + op, expr_ty = self:GetOperator("indexset", { expr_ty, key_ty, ty }, setter[3]) + else -- [] + op, expr_ty = self:GetOperator("indexset", { expr_ty, key_ty, value_ty }, setter[3]) + end -function Compiler:InstrIWC(args) - -- args = { "iwc", trace, variable name } - local op = args[3] - local _tp, ScopeID = self:GetVariableType(args, op) - - if ScopeID == 0 then - if self.inputs[op] then - local rt = self:GetOperator(args, "iwc", {}) - return { rt[1], op }, rt[2] - elseif self.outputs[op] then - local rt = self:GetOperator(args, "owc", {}) - return { rt[1], op }, rt[2] + local handle = stmts[i] -- need this, or stack overflow... + stmts[i] = function(state, val) ---@param state RuntimeContext + op(state, handle(state), key(state), val) + end + else + self:Assert(existing.type == value_ty, "Cannot assign type (" .. value_ty .. ") to variable of type (" .. existing.type .. ")", trace) + existing.initialized = true + + local id = existing.depth + if id == 0 then + if E2Lib.IOTableTypes[value_ty] then + stmts[i] = function(state, val) ---@param state RuntimeContext + state.GlobalScope[var], state.GlobalScope.vclk[var] = val, true + + if state.GlobalScope.lookup[val] then + state.GlobalScope.lookup[val][var] = true + else + state.GlobalScope.lookup[val] = { [var] = true } + end + end + else + stmts[i] = function(state, val) ---@param state RuntimeContext + state.GlobalScope[var], state.GlobalScope.vclk[var] = val, true + end + end + else + stmts[i] = function(state, val) ---@param state RuntimeContext + state.Scopes[id][var] = val + end + end + end + else + -- Cannot have indices. + self:Assert(#indices == 0, "Variable (" .. var .. ") does not exist", trace) + self.global_scope:DeclVar(var, { type = value_ty, initialized = true, trace_if_unused = trace }) + + if E2Lib.IOTableTypes[value_ty] then + stmts[i] = function(state, val) ---@param state RuntimeContext + state.GlobalScope[var], state.GlobalScope.vclk[var] = val, true + + if state.GlobalScope.lookup[val] then + state.GlobalScope.lookup[val][var] = true + else + state.GlobalScope.lookup[val] = { [var] = true } + end + end + else + stmts[i] = function(state, val) ---@param state RuntimeContext + state.GlobalScope[var], state.GlobalScope.vclk[var] = val, true + end + end + end end - end - self:Error("Connected operator (->" .. E2Lib.limitString(op, 10) .. ") can only be used on inputs or outputs", args) -end + return function(state) ---@param state RuntimeContext + local val = value(state) + for _, stmt in ipairs(stmts) do + stmt(state, val) + end + end + end, + + ---@param data Token + [NodeVariant.Increment] = function (self, trace, data) + -- Transform V-- to V = V + 1 + local one = Node.new(NodeVariant.ExprLiteral, { "n", 1 }, trace) + local var = Node.new(NodeVariant.ExprIdent, data, data.trace) + + local result = Node.new( + NodeVariant.ExprArithmetic, + { var, Operator.Add, one }, + trace + ) + + return self:CompileStmt(Node.new( + NodeVariant.Assignment, + { false, { { data, {}, trace } }, result }, + trace + )) + end, + + ---@param data Token + [NodeVariant.Decrement] = function (self, trace, data) + -- Transform V-- to V = V - 1 + local one = Node.new(NodeVariant.ExprLiteral, { "n", 1 }, trace) + local var = Node.new(NodeVariant.ExprIdent, data, data.trace) + + local result = Node.new( + NodeVariant.ExprArithmetic, + { var, Operator.Sub, one }, + trace + ) + + return self:CompileStmt(Node.new( + NodeVariant.Assignment, + { false, { { data, {}, trace } }, result }, + trace + )) + end, + + ---@param data { [1]: Token, [2]: Operator, [3]: Node } + [NodeVariant.CompoundArithmetic] = function(self, trace, data) + -- Transform V = E -> V = V E + local result = Node.new( + NodeVariant.ExprArithmetic, + { Node.new(NodeVariant.ExprIdent, data[1], data[1].trace), data[2], data[3] }, + trace + ) + + return self:CompileStmt(Node.new( + NodeVariant.Assignment, + { false, { { data[1], {}, trace } }, result }, + trace + )) + end, + + ---@param data { [1]: Node, [2]: Node } + [NodeVariant.ExprDefault] = function(self, trace, data) + local cond, cond_ty = self:CompileExpr(data[1]) + local expr, expr_ty = self:CompileExpr(data[2]) + + self:Assert(cond_ty == expr_ty, "Cannot use default (?:) operator with differing types", trace) + + local op = self:GetOperator("is", { cond_ty }, trace) + return function(state) ---@param state RuntimeContext + local iff = cond(state) + return op(state, iff) ~= 0 and iff or expr(state) + end, cond_ty + end, + + ---@param data { [1]: Node, [2]: Node } + [NodeVariant.ExprTernary] = function(self, trace, data) + local cond, cond_ty = self:CompileExpr(data[1]) + local iff, iff_ty = self:CompileExpr(data[2]) + local els, els_ty = self:CompileExpr(data[3]) + + self:Assert(iff_ty == els_ty, "Cannot use ternary (A ? B : C) operator with differing types", trace) + + local op = self:GetOperator("is", { cond_ty }, trace) + return function(state) ---@param state RuntimeContext + return op(state, cond(state)) ~= 0 and iff(state) or els(state) + end, iff_ty + end, + + ---@param data { [1]: string, [2]: string|number|table } + [NodeVariant.ExprLiteral] = function (self, trace, data) + local val = data[2] + self.scope.data.ops = self.scope.data.ops + 0.125 + return function() + return val + end, data[1] + end, + + ---@param data Token + [NodeVariant.ExprIdent] = function (self, trace, data) + local var, name = self:Assert(self.scope:LookupVar(data.value), "Undefined variable (" .. data.value .. ")", trace), data.value + var.trace_if_unused = nil + + self:AssertW(var.initialized, "Use of variable [" .. name .. "] before initialization", trace) + self.scope.data.ops = self.scope.data.ops + 0.25 + + local id = var.depth + return function(state) ---@param state RuntimeContext + return state.Scopes[id][name] + end, var.type + end, + + ---@param data Node[]|{ [1]: Node, [2]:Node }[] + [NodeVariant.ExprArray] = function (self, trace, data) + if #data == 0 then + return function() + return {} + end, "r" + elseif data[1][2] then -- key value array + ---@cast data { [1]: Node, [2]: Node }[] # Key value pair arguments + + local numbers = {} + for _, kvpair in ipairs(data) do + local key, key_ty = self:CompileExpr(kvpair[1]) + + if key_ty == "n" then + local value, ty = self:CompileExpr(kvpair[2]) + self:Assert(not BLOCKED_ARRAY_TYPES[ty], "Cannot use type " .. ty .. " as array value", kvpair[2].trace) + numbers[key] = value + else + self:Error("Cannot use type " .. key_ty .. " as array key", kvpair[1].trace) + end + end -function Compiler:InstrLITERAL(args) - -- args = { "literal", trace, value, value type } - self.prfcounter = self.prfcounter + 0.5 - local value = args[3] - return { function() return value end }, args[4] -end + return function(state) ---@param state RuntimeContext + local array = {} -function Compiler:InstrVAR(args) - -- args = { "var", trace, variable name } - self.prfcounter = self.prfcounter + 1.0 - local name = args[3] - local tp, ScopeID, initialized = self:GetVariableType(args, name) + for key, value in pairs(numbers) do + array[key(state)] = value(state) + end - -- Mark variable as used. - self.Scopes[ScopeID][name].var_tok = nil + return array + end, "r" + else + local args = {} + for k, arg in ipairs(data) do + local value, ty = self:CompileExpr(arg) + self:Assert(not BLOCKED_ARRAY_TYPES[ty], "Cannot use type " .. ty .. " as array value", trace) + args[k] = value + end - if ScopeID == 0 and not initialized then - self:Warning("Use of variable [" .. name .. "] before initialization", args) - end + return function(state) ---@param state RuntimeContext + local array = {} + for i, val in ipairs(args) do + array[i] = val(state) + end + return array + end, "r" + end + end, + + [NodeVariant.ExprTable] = function (self, trace, data) + if #data == 0 then + return function() + return { n = {}, ntypes = {}, s = {}, stypes = {}, size = 0 } + end, "t" + elseif data[1][2] then + ---@cast data { [1]: Node, [2]: Node }[] # Key value pair arguments + + local strings, numbers, nstrings, nnumbers, size = {}, {}, 0, 0, #data + for _, kvpair in ipairs(data) do + local key, key_ty = self:CompileExpr(kvpair[1]) + local value, value_ty = self:CompileExpr(kvpair[2]) + + if key_ty == "s" then + nstrings = nstrings + 1 + strings[nstrings] = { key, value, value_ty } + elseif key_ty == "n" then + nnumbers = nnumbers + 1 + numbers[nnumbers] = { key, value, value_ty } + else + self:Error("Cannot use type " .. key_ty .. " as table key", kvpair[1].trace) + end + end - return {function(self) - return self.Scopes[ScopeID][name] - end}, tp -end + return function(state) ---@param state RuntimeContext + local s, stypes, n, ntypes = {}, {}, {}, {} -function Compiler:InstrFEA(args) - -- args = { "fea", trace, key variable name, key type, value variable name, value type, table expression, loop body } - local keyvar, keytype, valvar, valtype = args[3], args[4], args[5], args[6] - local tableexpr, tabletp = self:Evaluate(args, 5) + for i = 1, nstrings do + local data = strings[i] + local key, value, valuetype = data[1](state), data[2], data[3] + s[key], stypes[key] = value(state), valuetype + end - local op + for i = 1, nnumbers do + local data = numbers[i] + local key, value, valuetype = data[1](state), data[2], data[3] + n[key], ntypes[key] = value(state), valuetype + end - if keytype then - op = self:GetOperator(args, "fea", {keytype, valtype, tabletp}) - else - -- If no key type is specified, fallback to old behavior - - -- The type of the keys iterated over depends on what's being iterated over (ie. tabletp). - -- The 'table' returned by tableexpr can be a table, an array, a gtable, or others in future. - -- If the type has an indexing operator that takes strings, then we iterate over strings, - -- otherwise we iterator over numbers. - - if self:HasOperator(args, "fea", {"s", valtype, tabletp}) then - op = self:GetOperator(args, "fea", {"s", valtype, tabletp}) - keytype = "s" - elseif self:HasOperator(args, "fea", {"n", valtype, tabletp}) then - op = self:GetOperator(args, "fea", {"n", valtype, tabletp}) - keytype = "n" + return { s = s, stypes = stypes, n = n, ntypes = ntypes, size = size } + end, "t" else - self:Error("Type '" .. tps_pretty(tabletp) .. "' has no valid default foreach operator", args) + ---@cast data Node[] + local args, argtypes, len = {}, {}, #data + for k, arg in ipairs(data) do + args[k], argtypes[k] = self:CompileExpr(arg) + end + + return function(state) ---@param state RuntimeContext + local array = {} + for i = 1, len do + array[i] = args[i](state) + end + return { n = array, ntypes = argtypes, s = {}, stypes = {}, size = len } + end, "t" end - end + end, - self:PushScope() + [NodeVariant.ExprArithmetic] = handleInfixOperation, - if keyvar ~= "_" then - self:SetLocalVariableType(keyvar, keytype, args, true) - end + ---@param data { [1]: Node, [2]: Operator, [3]: self } + [NodeVariant.ExprLogicalOp] = function(self, trace, data) + local lhs, lhs_ty = self:CompileExpr(data[1]) + local rhs, rhs_ty = self:CompileExpr(data[3]) - if valvar ~= "_" then - self:SetLocalVariableType(valvar, valtype, args, true) - end + -- self:Assert(lhs_ty == rhs_ty, "Cannot perform logical operation on differing types", trace) - local stmt = self:EvaluateStatement(args, 6) + local op_lhs, op_lhs_ret = self:GetOperator("is", { lhs_ty }, trace) + local op_rhs, op_rhs_ret = self:GetOperator("is", { rhs_ty }, trace) - self:PopScope() + self:Assert(op_lhs_ret == "n", "Cannot perform logical operation on type " .. op_lhs_ret, trace) + self:Assert(op_rhs_ret == "n", "Cannot perform logical operation on type " .. op_rhs_ret, trace) - return {op[1], keyvar, valvar, tableexpr, stmt} -end + if data[2] == Operator.Or then + return function(state) + return ((op_lhs(state, lhs(state)) ~= 0) or (op_rhs(state, rhs(state)) ~= 0)) and 1 or 0 + end, "n" + else -- Operator.And + return function(state) + return (op_lhs(state, lhs(state)) ~= 0 and op_rhs(state, rhs(state)) ~= 0) and 1 or 0 + end, "n" + end + end, + [NodeVariant.ExprBinaryOp] = handleInfixOperation, + [NodeVariant.ExprComparison] = handleInfixOperation, -function Compiler:InstrFUNCTION(args) - -- args = { "function", trace, signature, return type, object type, { { parameter name, parameter type }... }, function body } - local Sig, Return, methodType, Args = args[3], args[4], args[5], args[6] - Return = Return or "" + [NodeVariant.ExprEquals] = function(self, trace, data) + local lhs, lhs_ty = self:CompileExpr(data[1]) + local rhs, rhs_ty = self:CompileExpr(data[3]) - local OldScopes = self:SaveScopes() - self:InitScope() -- Create a new Scope Enviroment - self:PushScope() + self:Assert(lhs_ty == rhs_ty, "Cannot perform equality operation on differing types", trace) - local VariadicType - for _, D in pairs(Args) do - local Name, Type, Variadic, Discard = D[1], wire_expression_types[D[2]][1], D[3], D[4] - VariadicType = Variadic and Type + local op, op_ret, legacy = self:GetOperator("eq", { lhs_ty, rhs_ty }, trace) + self:Assert(op_ret == "n", "Cannot use perform equality operation on type " .. lhs_ty, trace) - if not Discard then - self:SetLocalVariableType(Name, Type, args, true) + if data[2] == Operator.Eq then + if legacy then + local largs = { [1] = {}, [2] = { lhs }, [3] = { rhs }, [4] = { lhs_ty, rhs_ty } } + return function(state) + return op(state, largs) + end, "n" + else + return function(state) + return op(state, lhs(state), rhs(state)) + end, "n" + end + elseif data[2] == Operator.Neq then + if legacy then + local largs = { [1] = {}, [2] = { lhs }, [3] = { rhs }, [4] = { lhs_ty, rhs_ty } } + return function(state) + return op(state, largs) == 0 and 1 or 0 + end, "n" + else + return function(state) + return op(state, lhs(state), rhs(state)) == 0 and 1 or 0 + end, "n" + end end - end - - if VariadicType then - -- Don't allow users to define two functions with different variadic types - -- Because that'd cause ambiguity. - local opposite = VariadicType == "r" and "t" or "r" - if self.funcs_ret[Sig:gsub("%.%." .. VariadicType, ".." .. opposite)] then - self:Error("Cannot override variadic " .. tps_pretty(opposite) .. " function with variadic " .. tps_pretty(VariadicType) .. " function to avoid ambiguity.", args) + end, + + [NodeVariant.ExprBitShift] = handleInfixOperation, + + ---@param data { [1]: Operator, [2]: Node, [3]: self } + [NodeVariant.ExprUnaryOp] = function(self, trace, data) + local exp, ty = self:CompileExpr(data[2]) + + if data[1] == Operator.Not then -- Return opposite of operator_is result + local op = self:GetOperator("is", { ty }, trace) + return function(state) + return op(state, exp(state)) == 0 and 1 or 0 + end, "n" + elseif data[1] == Operator.Sub then -- Negate + local op, op_ret, legacy = self:GetOperator("neg", { ty }, trace) + if legacy then + local largs = { [1] = {}, [2] = { exp }, [3] = { ty } } + return function(state) + return op(state, largs) + end, op_ret + else + return function(state) + return op(state, exp(state)) + end, op_ret + end end - end + end, + + ---@param data { [1]: Operator, [2]: Token } + [NodeVariant.ExprUnaryWire] = function(self, trace, data) + local var_name = data[2].value + local var = self:Assert(self.scope:LookupVar(var_name), "Undefined variable (" .. var_name .. ")", trace) + var.trace_if_unused = nil + self:AssertW(var.initialized, "Use of variable [" .. var_name .. "] before initialization", trace) + + if data[1] == Operator.Dlt then -- $ + self:Assert(var.depth == 0, "Delta operator ($) can not be used on temporary variables", trace) + self.delta_vars[var_name] = true + + local sub_op, sub_ty = self:GetOperator("sub", { var.type, var.type }, trace) + + return function(state) ---@param state RuntimeContext + local current, past = state.GlobalScope[var_name], state.GlobalScope["$" .. var_name] + local diff = sub_op(state, current, past) + state.GlobalScope["$" .. var_name] = current + return diff + end, sub_ty + elseif data[1] == Operator.Trg then -- ~ + return function(state) ---@param state RuntimeContext + return state.triggerinput == var_name and 1 or 0 + end, "n" + elseif data[1] == Operator.Imp then -- -> + if self.inputs[3][var_name] then + return function(state) ---@param state RuntimeContext + return IsValid(state.entity.Inputs[var_name].Src) and 1 or 0 + end, "n" + elseif self.outputs[3][var_name] then + return function(state) ---@param state RuntimeContext + local tbl = state.entity.Outputs[var_name].Connected + local ret = #tbl + for i = 1, ret do + if not IsValid(tbl[i].Entity)then + ret = ret - 1 + end + end + return ret + end, "n" + else + self:Error("Can only use connected (->) operator on inputs or outputs", trace) + end + end + end, + + ---@param data { [1]: Node, [2]: Index[] } + [NodeVariant.ExprIndex] = function (self, trace, data) + local expr, expr_ty = self:CompileExpr(data[1]) + for i, index in ipairs(data[2]) do + local key, key_ty = self:CompileExpr(index[1]) + + local op + if index[2] then -- [, ] + local ty = self:CheckType(index[2]) + op, expr_ty = self:GetOperator("indexget", { expr_ty, key_ty, ty }, index[3]) + else -- [] + op, expr_ty = self:GetOperator("indexget", { expr_ty, key_ty }, index[3]) + end - if self.funcs_ret[Sig] and self.funcs_ret[Sig] ~= Return then - local TP = tps_pretty(self.funcs_ret[Sig]) - self:Error("Function " .. Sig .. " must be given return type " .. TP, args) - end + local handle = expr -- need this, or stack overflow... + expr = function(state) + return op(state, handle(state), key(state)) + end + end - self.funcs_ret[Sig] = Return + return expr, expr_ty + end, - table.insert(self.EnclosingFunctions, { ReturnType = Return }) + ---@param data { [1]: Token, [2]: Node[] } + [NodeVariant.ExprCall] = function (self, trace, data, used_as_stmt) + local name, args, types = data[1], {}, {} + for k, arg in ipairs(data[2]) do + args[k], types[k] = self:CompileExpr(arg) + self:Assert(types[k], "Cannot use void expression as call argument", arg.trace) + end - local Stmt = self:EvaluateStatement(args, 5) -- Offset of -2 + local arg_sig = table.concat(types) + local fn_data = self:Assert(self:GetFunction(data[1].value, types), "No such function: " .. name.value .. "(" .. table.concat(types, ", ") .. ")", name.trace) - table.remove(self.EnclosingFunctions) + self:AssertW(not (used_as_stmt and fn_data.attrs.nodiscard), "The return value of this function cannot be discarded", trace) - self:PopScope() - self:LoadScopes(OldScopes) -- Reload the old enviroment + if fn_data.attrs["deprecated"] then + local value = fn_data.attrs["deprecated"] + self:Warning("Use of deprecated function (" .. name.value .. ") " .. (type(value) == "string" and value or ""), trace) + end - self.prfcounter = self.prfcounter + (VariadicType and 80 or 40) + self.scope.data.ops = self.scope.data.ops + ((fn_data.cost or 15) + (fn_data.attrs["legacy"] and 10 or 0)) - -- This is the function that will be bound to to the function name, ie. the - -- one that's called at runtime when code calls the function - local function body(self, runtimeArgs) - -- runtimeArgs = { body, parameterExpression1, ..., parameterExpressionN, parameterTypes } - -- we need to evaluate the arguments before switching to the new scope + if fn_data.attrs["noreturn"] then + self.scope.data.dead = true + end - local parameterValues = {} - if VariadicType then - local nargs = #Args - -- There's 100% a better way to structure this mess but this works fine for now... - local offset = methodType ~= "" and 1 or 0 + local nargs = #args + local user_function = self.user_functions[name.value] and self.user_functions[name.value][arg_sig] + if user_function then + -- Calling a user function - chance of being overridden. Also not legacy. + if user_function.const then + local fn = user_function.op + return function(state) + local rargs = {} + for k = 1, nargs do + rargs[k] = args[k](state) + end + return fn(state, rargs, types) + end, fn_data.returns and (fn_data.returns[1] ~= "" and fn_data.returns[1] or nil) + else + local full_sig = name.value .. "(" .. arg_sig .. ")" + return function(state) ---@param state RuntimeContext + local rargs = {} + for k = 1, nargs do + rargs[k] = args[k](state) + end - for parameterIndex = 2, nargs do - local parameterExpression = runtimeArgs[parameterIndex] - local parameterValue = parameterExpression[1](self, parameterExpression) - parameterValues[parameterIndex - 1] = parameterValue + local fn = state.funcs[full_sig] + if fn then + return state.funcs[full_sig](state, rargs, types) + else + state:forceThrow("No such function defined at runtime: " .. full_sig) + end + end, fn_data.returns and (fn_data.returns[1] ~= "" and fn_data.returns[1] or nil) + end + elseif fn_data.attrs["legacy"] then -- Not a user function. Can get function to call at compile time. + local fn, largs = fn_data.op, { [1] = {}, [nargs + 2] = types } + for i = 1, nargs do + largs[i + 1] = { [1] = args[i] } end + return function(state) ---@param state RuntimeContext + return fn(state, largs) + end, fn_data.returns and (fn_data.returns[1] ~= "" and fn_data.returns[1] or nil) + else + local fn = fn_data.op + return function(state) ---@param state RuntimeContext + local rargs = {} + for k = 1, nargs do + rargs[k] = args[k](state) + end - local types = runtimeArgs[#runtimeArgs] - if VariadicType == "t" then - -- Table argument. - local tbl, len = E2Lib.newE2Table(), 1 - local n, ntypes = tbl.n, tbl.ntypes + return fn(state, rargs, types) + end, fn_data.returns and (fn_data.returns[1] ~= "" and fn_data.returns[1] or nil) + end + end, - for parameterIndex = nargs + 1, #runtimeArgs - 1 do - local ty = types[nargs - 1 - offset + len] + ---@param data { [1]: Node, [2]: Token, [3]: Node[] } + [NodeVariant.ExprMethodCall] = function (self, trace, data, used_as_stmt) + local name, args, types = data[2], {}, {} + for k, arg in ipairs(data[3]) do + args[k], types[k] = self:CompileExpr(arg) + end - local parameterExpression = runtimeArgs[parameterIndex] - local parameterValue = parameterExpression[1](self, parameterExpression) + local arg_sig = table.concat(types) + local meta, meta_type = self:CompileExpr(data[1]) - n[len], ntypes[len] = parameterValue, ty - len = len + 1 - end + local fn_data = self:Assert(self:GetFunction(name.value, types, meta_type), "No such method: " .. (meta_type or "void") .. ":" .. name.value .. "(" .. table.concat(types, ", ") .. ")", name.trace) - tbl.size = len - 1 - parameterValues[nargs] = tbl - else - -- Array - -- Construct array here w/ dynamic values - local arr, len = {}, 1 + self:AssertW(not (used_as_stmt and fn_data.attrs.nodiscard), "The return value of this function cannot be discarded", trace) - for parameterIndex = nargs + 1, #runtimeArgs - 1 do - local ty = types[nargs - 1 - offset + len] + if fn_data.attrs["deprecated"] then + local value = fn_data.attrs["deprecated"] + self:Warning("Use of deprecated function (" .. name.value .. ") " .. (type(value) == "string" and value or ""), trace) + end - if BLOCKED_ARRAY_TYPES[ty] then - self:throw("Cannot use type " .. tps_pretty(ty) .. " as an argument for variadic array function", nil) - break + local nargs = #args + local user_method = self.user_methods[meta_type] and self.user_methods[meta_type][name.value] and self.user_methods[meta_type][name.value][arg_sig] + if user_method then + -- Calling a user function - chance of being overridden. Also not legacy. + if user_method.const then + local fn = user_method.op + return function(state) + local rargs = { meta(state) } + for k = 1, nargs do + rargs[k + 1] = args[k](state) end - - local parameterExpression = runtimeArgs[parameterIndex] - local parameterValue = parameterExpression[1](self, parameterExpression) - - arr[len] = parameterValue - len = len + 1 + return fn(state, rargs, types) end + else + local full_sig = name.value .. "(" .. meta_type .. ":" .. arg_sig .. ")" + return function(state) ---@param state RuntimeContext + local rargs = { meta(state) } + for k = 1, nargs do + rargs[k + 1] = args[k](state) + end - parameterValues[nargs] = arr + local fn = state.funcs[full_sig] + if fn then + return state.funcs[full_sig](state, rargs, types) + else + state:forceThrow("No such method defined at runtime: " .. full_sig) + end + end, fn_data.returns and (fn_data.returns[1] ~= "" and fn_data.returns[1] or nil) end - else - for parameterIndex = 2, #Args + 1 do - local parameterExpression = runtimeArgs[parameterIndex] - local parameterValue = parameterExpression[1](self, parameterExpression) - parameterValues[parameterIndex - 1] = parameterValue + elseif fn_data.attrs["legacy"] then + local fn, largs = fn_data.op, { [nargs + 3] = types, [2] = { [1] = meta } } + for k = 1, nargs do + largs[k + 2] = { [1] = args[k] } end - end - local OldScopes = self:SaveScopes() - self:InitScope() - self:PushScope() + return function(state) ---@param state RuntimeContext + return fn(state, largs) + end, fn_data.returns and fn_data.returns[1] + else + local fn = fn_data.op + return function(state) ---@param state RuntimeContext + local rargs = { meta(state) } + for k = 1, nargs do + rargs[k + 1] = args[k](state) + end - for parameterIndex = 1, #Args do - local parameterName = Args[parameterIndex][1] - local parameterValue = parameterValues[parameterIndex] - self.Scope[parameterName] = parameterValue + return fn(state, rargs, types) + end, fn_data.returns and fn_data.returns[1] end + end, - self.func_rv = nil - local ok, err = pcall(Stmt[1], self, Stmt) + ---@param data { [1]: Node, [2]: Node[], [3]: Token? } + [NodeVariant.ExprStringCall] = function (self, trace, data) + local expr = self:CompileExpr(data[1]) - local msg = err - if istable(err) then - msg = err.msg + local args, arg_types = {}, {} + for i, arg in ipairs(data[2]) do + args[i], arg_types[i] = self:CompileExpr(arg) end - self:PopScope() - self:LoadScopes(OldScopes) - - -- a "C stack overflow" error will probably just confuse E2 users more than a "tick quota" error. - if not ok and msg:find( "C stack overflow" ) then error( "tick quota exceeded", -1 ) end - - if not ok and msg == "return" then return self.func_rv end - - if not ok then error(err, 0) end + local type_sig = table.concat(arg_types) + local arg_sig = "(" .. type_sig .. ")" + local meta_arg_sig = #arg_types >= 1 and ("(" .. arg_types[1] .. ":" .. table.concat(arg_types, "", 2) .. ")") or "()" - if Return ~= "" then - local argNames = {} - local offset = methodType == "" and 0 or 1 + local ret_type = data[3] and self:CheckType(data[3]) - for k, v in ipairs(Args) do - argNames[k - offset] = v[1] + local nargs = #args + return function(state) ---@param state RuntimeContext + local rargs = {} + for k = 1, nargs do + rargs[k] = args[k](state) end - error("Function " .. E2Lib.generate_signature(Sig, nil, argNames) .. - " executed and didn't return a value - expecting a value of type " .. - E2Lib.typeName(Return), 0) - end - end - - return { self:GetOperator(args, "function", {})[1], Sig, body } -end + local fn_name = expr(state) + local sig, meta_sig = fn_name .. arg_sig, fn_name .. meta_arg_sig -function Compiler:InstrRETURN(args) - -- args = { "return", trace, return expression or nil } - local enclosingFunction = self.EnclosingFunctions[#self.EnclosingFunctions] - if enclosingFunction == nil then - self:Error("Return may not exist outside of a function", args) - end - - local expectedType = assert(enclosingFunction.ReturnType) - local value, actualType - if args[3] then - value, actualType = self:Evaluate(args, 1) - else - actualType = "" - end + local fn = state.funcs[sig] or state.funcs[meta_sig] + if fn then -- first check if user defined any functions that match signature + local r = state.funcs_ret[sig] + if r ~= ret_type then + state:forceThrow( "Mismatching return types. Got " .. (r or "void") .. ", expected " .. (ret_type or "void")) + end - if actualType ~= expectedType then - self:Error("Return type mismatch: " .. tps_pretty(expectedType) .. " expected, got " .. tps_pretty(actualType), args) - end + return fn(state, rargs, arg_types) + else -- no user defined functions, check builtins + fn = wire_expression2_funcs[sig] or wire_expression2_funcs[meta_sig] + if fn then + local r = fn[2] + if r ~= ret_type and not (ret_type == nil and r == "") then + state:forceThrow( "Mismatching return types. Got " .. (r or "void") .. ", expected " .. (ret_type or "void")) + end - self.Scope._dead = true - return { self:GetOperator(args, "return", {})[1], value, actualType } -end + if fn.attributes.legacy then + local largs = { [1] = {}, [nargs + 2] = arg_types } + for i = 1, nargs do + largs[i + 1] = { [1] = function() return rargs[i] end } + end + return fn[3](state, largs, arg_types) + else + return fn[3](state, rargs, arg_types) + end + else -- none found, check variadic builtins + for i = nargs, 0, -1 do + local varsig = fn_name .. "(" .. type_sig:sub(1, i) .. "...)" + local fn = wire_expression2_funcs[varsig] + if fn then + local r = fn[2] + if r ~= ret_type and not (ret_type == nil and r == "") then + state:forceThrow("Mismatching return types. Got " .. (r or "void") .. ", expected " .. (ret_type or "void")) + end + + if fn.attributes.legacy then + local largs = { [1] = {}, [nargs + 2] = arg_types } + for i = 1, nargs do + largs[i + 1] = { [1] = function() return rargs[i] end } + end + return fn[3](state, largs, arg_types) + elseif varsig == "array(...)" then -- Need this since can't enforce compile time argument type restrictions on string calls. Woop. Array creation should not be a function.. + local i = 1 + while i <= #arg_types do + local ty = arg_types[i] + if BLOCKED_ARRAY_TYPES[ty] then + table.remove(rargs, i) + table.remove(arg_types, i) + state:forceThrow("Cannot use type " .. ty .. " for argument #" .. i .. " in stringcall array creation") + else + i = i + 1 + end + end + end + + return fn[3](state, rargs, arg_types) + else + local varsig = fn_name .. "(" .. type_sig:sub(1, i) .. "..r)" + local fn = state.funcs[varsig] + + if fn then + for _, ty in ipairs(arg_types) do -- Just block them entirely. Current method of finding variadics wouldn't allow a proper solution that works with x types. Would need to rewrite all of this which I don't think is worth it when already nobody is going to use this functionality. + if BLOCKED_ARRAY_TYPES[ty] then + state:forceThrow("Cannot pass array into variadic array function") + end + end + + return fn(state, rargs, arg_types) + else + local varsig = fn_name .. "(" .. type_sig:sub(1, i) .. "..t)" + local fn = state.funcs[varsig] + if fn then + return fn(state, rargs, arg_types) + end + end + end + end -function Compiler:InstrKVTABLE(args) - -- args = { "kvtable", trace, { key expression = value expression... } } - local s = {} - local stypes = {} - - local exprs = args[3] - for k, v in pairs(exprs) do - local key, type = self:CallInstruction(k[1], k) - if type == "s" or type == "n" then - local value, type = self:CallInstruction(v[1], v) - s[key] = value - stypes[key] = type - else - self:Error("String or number expected, got " .. tps_pretty(type), k) + state:forceThrow("No such function: " .. fn_name .. arg_sig) + end + end + end, ret_type + end, + + ---@param data { [1]: Token, [2]: Parameter[], [3]: Node } + [NodeVariant.Event] = function (self, trace, data) + self:AssertW(self.scope:IsGlobalScope() or (self.include and self.scope:Depth() == 1), "Events cannot be nested inside of statements, they are compile time constructs. This will become a hard error in the future!", trace) + + ---@type string, { [1]: string, [2]: string }[] + local name, params = data[1].value, {} + for i, param in ipairs(data[2]) do + local type = param.type and self:CheckType(param.type) + if not type then + self:Warning("Use of implicit parameter type is deprecated (add :number)", param.name.trace) + type = "n" + end + params[i] = { param.name.value, type } end - end - - return { self:GetOperator(args, "kvtable", {})[1], s, stypes }, "t" -end -function Compiler:InstrKVARRAY(args) - -- args = { "kvarray", trace, { key expression = value expression... } } - local values = {} - local types = {} - - local exprs = args[3] - for k, v in pairs(exprs) do - local key, type = self:CallInstruction(k[1], k) - if type == "n" then - local value, type = self:CallInstruction(v[1], v) - if BLOCKED_ARRAY_TYPES[type] then - self:Error("Cannot have type " .. tps_pretty(type) .. " in array creation for keyvalue", v) + local event = self:Assert(E2Lib.Env.Events[name], "No such event exists: '" .. name .. "'", trace) + if #params > #event.args then + local extra_arg_types = {} + for i = #event.args + 1, #params do + -- name, type, variadic + extra_arg_types[#extra_arg_types + 1] = params[i][2] end - values[key] = value - types[key] = type - else - self:Error("Number expected, got " .. tps_pretty(type), k) + self:Error("Event '" .. name .. "' does not take arguments (" .. table.concat(extra_arg_types, ", ") .. ")", trace) end - end - return { self:GetOperator(args, "kvarray", {})[1], values, types }, "r" -end + for k, arg in ipairs(event.args) do + if not params[k] then + -- TODO: Maybe this should be a warning so that events can have extra params added without breaking old code? + self:Error("Event '" .. name .. "' missing argument #" .. k .. " of type " .. tostring(arg), trace) + end -function Compiler:InstrSWITCH(args) - -- args = { "switch", trace, value expression, { { case expression or nil, body }... } } - -- up to one case can have a nil case expression, this is the default case - self:PushPrfCounter() - local value, type = self:CallInstruction(args[3][1], args[3]) -- This is the value we are passing though the switch statment - local prf_cond = self:PopPrfCounter() - - local cases = {} - local Cases = args[4] - local default - - for i = 1, #Cases do - local case, block, prf_eq, eq = Cases[i][1], Cases[i][2], 0, nil - - self:PushScope() - if case then -- The default will not have one - self:PushPrfCounter() - local ex, tp = self:CallInstruction(case[1], case) -- This is the value we are checking against - prf_eq = self:PopPrfCounter() -- We add some pref - - if tp == "" then -- There is no value - self:Error("Function has no return value (void), cannot be part of expression or assigned", args) - elseif tp ~= type then -- Value types do not match. - self:Error("Case mismatch can not compare " .. tps_pretty(type) .. " with " .. tps_pretty(tp), args) + if arg.type ~= params[k][2] then + self:Error("Mismatched event argument: " .. arg.type .. " vs " .. tostring(params[k][2]), trace) end - eq = { self:GetOperator(args, "eq", { type, tp })[1], value, ex } -- This is the equals operator to check if values match - else - default=i end - local stmts = self:CallInstruction(block[1], block) -- This is statments that are run when Values match - self:PopScope() - - cases[i] = { eq, stmts, prf_eq } - end - - local rtswitch = self:GetOperator(args, "switch", {}) - return { rtswitch[1], prf_cond, cases, default } -end - -function Compiler:InstrINCLU(args) - -- args = { "inclu", trace, filename } - local file = args[3] - local include = self.includes[file] - - if not include or not include[1] then - self:Error("Problem including file '" .. file .. "'", args) - end - - if not include[2] then - include[2] = true -- Temporary value to prevent E2 compiling itself in itself. + if (self.registered_events[name] and self.registered_events[name][self.include or "__main__"]) then + self:Error("You can only register one event callback per file", trace) + end - local OldScopes = self:SaveScopes() - self:InitScope() -- Create a new Scope Enviroment - self:PushScope() + self.registered_events[name] = self.registered_events[name] or {} - local last_file = self.include - self.include = file + local block = self:IsolatedScope(function(scope) + for k, arg in ipairs(event.args) do + scope:DeclVar(params[k][1], { type = arg.type, initialized = true, trace_if_unused = params[k][3] }) + end - self.warnings[file] = self.warnings[file] or {} + return self:CompileStmt(data[3]) + end) - local root = include[1] - local status, script = pcall(self.CallInstruction, self, root[1], root) + self.registered_events[name][self.include or "__main__"] = function(state, args) ---@param state RuntimeContext + local s_scopes, s_scopeid, s_scope = state.Scopes, state.ScopeID, state.Scope - if not status then - local _catchable, reason = E2Lib.unpackException(script) - if reason:find("C stack overflow") then reason = "Include depth too deep" end + local scope = { vclk = {} } -- Hack in the fact that functions don't have upvalues right now. + state.Scopes = { [0] = state.GlobalScope, [1] = scope } + state.Scope = scope + state.ScopeID = 1 - if not self.IncludeError then - -- Otherwise Errors messages will be wrapped inside other error messages! - self.IncludeError = true - self:Error("include '" .. file .. "' -> " .. reason, args) - else - error(script, 0) + for i, param in ipairs(params) do + scope[param[1]] = args[i] end - else - self.include = last_file - local nwarnings = #self.warnings[file] - if nwarnings ~= 0 then - self:Warning("include '" .. file .. "' has " .. nwarnings .. " warning(s).", args) - end - end + block(state) - include[2] = script + state.Scopes, state.ScopeID, state.Scope = s_scopes, s_scopeid, s_scope + end - self:PopScope() - self:LoadScopes(OldScopes) -- Reload the old enviroment + return nil end +} +---@alias TypeSignature string - return { self:GetOperator(args, "include", {})[1], file } +local function DEFAULT_EQUALS(self, lhs, rhs) + return lhs == rhs and 1 or 0 end -function Compiler:InstrTRY(args) - -- args = { "try", trace, try_block, variable, catch_block } - self:PushPrfCounter() - local stmt = self:EvaluateStatement(args, 1) - local var_name = args[4] - self:PushScope() - if var_name ~= "_" then - self:SetLocalVariableType(var_name, "s", args, true) - end - - local stmt2 = self:EvaluateStatement(args, 3) - self:PopScope() - - local prf_cond = self:PopPrfCounter() +---@param variant string +---@param types TypeSignature[] +---@param trace Trace +---@return RuntimeOperator fn +---@return TypeSignature signature +---@return boolean legacy +---@return boolean default +function Compiler:GetOperator(variant, types, trace) + local fn = wire_expression2_funcs["op:" .. variant .. "(" .. table.concat(types) .. ")"] + if fn then + self.scope.data.ops = self.scope.data.ops + (fn[4] or 2) + (fn.attributes.legacy and 1 or 0) + return fn[3], fn[2], fn.attributes.legacy, false + elseif variant == "eq" and #types == 2 and types[1] == types[2] then + -- If no equals operator present, default to just basic lua equals. + return DEFAULT_EQUALS, "n", false, true + end - return { self:GetOperator(args, "try", {})[1], prf_cond, stmt, var_name, stmt2 } + self:Error("No such operator: " .. variant .. " (" .. table.concat(types, ", ") .. ")", trace) end -function Compiler:InstrEVENT(args) - -- args = { "event", trace, name, args, event_block } - local name, hargs = args[3], args[4] - - if not E2Lib.Env.Events[name] then - self:Error("No such event exists: '" .. name .. "'", args) +---@param name string +---@param types TypeSignature[] +---@param method? string +---@return EnvFunction? function +---@return boolean? variadic +function Compiler:GetUserFunction(name, types, method) + ---@type EnvFunction + local overloads + if method then + overloads = self.user_methods[method] + if not overloads then return end + overloads = overloads[name] + else + overloads = self.user_functions[name] end + if not overloads then return end + + local param_sig = table.concat(types) + if overloads[param_sig] then return overloads[param_sig], false end - local event = E2Lib.Env.Events[name] + for i = #types, 0, -1 do + local sig = table.concat(types, "", 1, i) - if #hargs > #event.args then - local extra_arg_types = {} - for i = #event.args + 1, #hargs do - -- name, type, variadic - extra_arg_types[#extra_arg_types + 1] = hargs[i][2] + local fn = overloads[sig .. "..r"] + if fn then + for j = i, #types do + if BLOCKED_ARRAY_TYPES[types[j]] then + self:Error("Cannot call variadic array function (" .. name .. ") with a " .. tostring(types[j]) .. " value.", trace) + end + end + return fn, true end - self:Error("Event '" .. name .. "' does not take arguments (" .. table.concat(extra_arg_types, ", ") .. ")", args) + fn = overloads[sig .. "..t"] + if fn then return fn, true end end +end - for k, arg in ipairs(event.args) do - if not hargs[k] then - -- TODO: Maybe this should be a warning so that events can have extra params added without breaking old code? - self:Error("Event '" .. name .. "' missing argument #" .. k .. " of type " .. tps_pretty(arg.type), args) - end +---@param name string +---@param types TypeSignature[] +---@param method? string +---@return EnvFunction? +---@return boolean? variadic +---@return boolean? userfunction +function Compiler:GetFunction(name, types, method) + local sig, method_prefix = table.concat(types), method and (method .. ":") or "" - local param_id = wire_expression_types[hargs[k][2]][1] + local fn = wire_expression2_funcs[name .. "(" .. method_prefix .. sig .. ")"] + if fn then return { op = fn[3], returns = { fn[2] }, args = types, cost = fn[4], attrs = fn.attributes }, false, false end - if arg.type ~= param_id then - self:Error("Mismatched event argument: " .. tps_pretty(arg) .. " vs " .. tps_pretty(param_id), args) - end - end + local fn, variadic = self:GetUserFunction(name, types, method) + if fn then return fn, variadic, true end - if (self.registered_events[name] and self.registered_events[name][self.include or "__main__"]) then - self:Error("You can only register one event callback per file", args) + for i = #sig, 0, -1 do + fn = wire_expression2_funcs[name .. "(" .. method_prefix .. sig:sub(1, i) .. "...)"] + if fn then return { op = fn[3], returns = { fn[2] }, args = types, cost = fn[4], attrs = fn.attributes }, true, false end end +end - self.registered_events[name] = self.registered_events[name] or {} +---@param node Node +---@return RuntimeOperator +---@return string expr_type +function Compiler:CompileExpr(node) + assert(node.trace, "Incomplete node: " .. tostring(node)) + local op, ty = assert(CompileVisitors[node.variant], "Unimplemented Compile Step: " .. node:instr())(self, node.trace, node.data, false) + self:Assert(ty, "Cannot use void in expression position", node.trace) + return op, ty +end - local OldScopes = self:SaveScopes() - self:InitScope() - self:PushScope() - for k, arg in ipairs(event.args) do - if not hargs[k][4] --[[ ensure it isn't a discard parameter ]] then - self:SetLocalVariableType(hargs[k][1], arg.type, args, true) - end - end +---@return RuntimeOperator +function Compiler:CompileStmt(node) + assert(node.trace, "Incomplete node: " .. tostring(node)) + return assert(CompileVisitors[node.variant], "Unimplemented Compile Step: " .. node:instr())(self, node.trace, node.data, true) +end - local block = self:EvaluateStatement(args, 3) - self:LoadScopes(OldScopes) +---@param ast Node +---@return RuntimeOperator +function Compiler:Process(ast) + for var, type in pairs(self.persist[3]) do + self.scope:DeclVar(var, { initialized = false, trace_if_unused = self.persist[5][var], type = type }) + end - self.registered_events[name][self.include or "__main__"] = function(self, args) - for i, arg in ipairs(hargs) do - local name = arg[1] - self.Scope[name] = args[i] - end + for var, type in pairs(self.inputs[3]) do + self.scope:DeclVar(var, { initialized = true, trace_if_unused = self.inputs[5][var], type = type }) + end - block[1](self, block) + for var, type in pairs(self.outputs[3]) do + self.scope:DeclVar(var, { initialized = false, type = type }) end + + return self:CompileStmt(ast) end \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/base/debug.lua b/lua/entities/gmod_wire_expression2/base/debug.lua new file mode 100644 index 0000000000..3535260465 --- /dev/null +++ b/lua/entities/gmod_wire_expression2/base/debug.lua @@ -0,0 +1,104 @@ +--[[ + Expression 2 Debugging + by Vurv +]] + +AddCSLuaFile() + +---@class Trace +---@field start_col integer +---@field end_col integer +---@field start_line integer +---@field end_line integer +local Trace = {} +Trace.__index = Trace + +---@param start_col integer +---@param start_line integer +---@param end_col integer +---@param end_line integer +---@return Trace +function Trace.new(start_line, start_col, end_line, end_col) + -- These traces define column as exclusive to the characters. So a start_col of 1, end_col of 2 would be a single character. + return setmetatable({ start_col = start_col, end_col = end_col, start_line = start_line, end_line = end_line }, Trace) +end + +function Trace:debug() + return string.format("Trace { start_col: %u, end_col: %u, start_line: %u, end_line: %u }", self.start_col, self.end_col, self.start_line, self.end_line) +end +Trace.__tostring = Trace.debug + +--- Returns the a new trace that spans both traces. +---@param other Trace +---@return Trace +function Trace:stitch(other) + return setmetatable({ start_col = self.start_col, end_col = other.end_col, start_line = self.start_line, end_line = other.end_line }, Trace) +end + +---@class Warning +---@field message string +---@field trace Trace +local Warning = {} +Warning.__index = Warning + +---@param message string +---@param trace Trace +---@return Warning +function Warning.new(message, trace) + return setmetatable({ message = message, trace = trace }, Warning) +end + +function Warning:debug() + return string.format("Warning { message = %q, trace = %s }", self.message, self.trace) +end + +function Warning:display() + return string.format("Warning at line %u, char %u: %q", self.trace.start_line, self.trace.start_col, self.message) +end + +Warning.__tostring = Warning.debug + +---@alias ErrorUserdata { catchable: boolean? } + +---@class Error +---@field message string +---@field trace Trace +---@field userdata ErrorUserdata +local Error = {} +Error.__index = Error + +---@param message string +---@param trace Trace? +---@param userdata ErrorUserdata? +---@return Error +function Error.new(message, trace, userdata) + return setmetatable({ message = message, trace = trace, userdata = userdata }, Error) +end + +function Error:debug() + return string.format("Error { message = %q, trace = %s }", self.message, self.trace) +end + +function Error:display() + if self.trace then + local first + if self.trace.start_line ~= self.trace.end_line then + first = "Error from lines " .. self.trace.start_line .. " to " .. self.trace.end_line + else + first = "Error at line " .. self.trace.start_line + end + + return string.format("%s, chars %u to %u: %q", first, self.trace.start_col, self.trace.end_col, self.message) + else + return self.message + end +end + +Error.__tostring = Error.debug + + +E2Lib.Debug = { + Warning = Warning, + Error = Error, + Trace = Trace +} \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/base/optimizer.lua b/lua/entities/gmod_wire_expression2/base/optimizer.lua deleted file mode 100644 index 66b3c53600..0000000000 --- a/lua/entities/gmod_wire_expression2/base/optimizer.lua +++ /dev/null @@ -1,190 +0,0 @@ ---[[ -An optimizer for E2 abstract syntax trees, as produced by the parser and -consumed by the compiler. - -Currently it only performs some simple peephole optimizations and constant -propagation. Ideally, we'd do type inference as much as possible before -optimizing, which would give us more useful information throughout. ---]] - -E2Lib.Optimizer = {} -local Optimizer = E2Lib.Optimizer -Optimizer.__index = Optimizer - -local optimizerDebug = CreateConVar("wire_expression2_optimizer_debug", 0, - "Print an E2's abstract syntax tree after optimization" -) - ----@return boolean ok ----@return table ast -function Optimizer.Execute(root) - local ok, result = xpcall(Optimizer.Process, E2Lib.errorHandler, root) - if ok and optimizerDebug:GetBool() then - print(E2Lib.AST.dump(result)) - end - return ok, result -end - -Optimizer.Passes = {} - ----@return table ast -function Optimizer.Process(tree) - E2Lib.AST.visitChildren(tree, Optimizer.Process) - - for _, pass in ipairs(Optimizer.Passes) do - local action = pass[tree[1]] - if action then - tree = assert(action(tree)) - end - end - tree.__instruction = true - return tree -end - -local constantPropagation = {} - -local function evaluateBinary(instruction) - -- this is a little sneaky: we use the operators previously registered with getOperator - -- to do compile-time evaluation, even though it really wasn't designed for it. - local op = wire_expression2_funcs["op:" .. instruction[1] .. "(" .. instruction[3][4] .. instruction[4][4] .. ")"] - local x, y = instruction[3][3], instruction[4][3] - - local value = op[3]({prf = 0}, {nil, {function() return x end}, {function() return y end}}) - local type = op[2] - return {"literal", instruction[2], value, type} -end - -local function evaluateUnary(instruction) - local op = wire_expression2_funcs["op:" .. instruction[1] .. "(" .. instruction[3][4] .. ")"] - local x = instruction[3][3] - - local value = op[3]({prf = 0}, {nil, {function() return x end}}) - local type = op[2] - return {"literal", instruction[2], value, type} -end - -for _, operator in pairs({ "add", "sub", "mul", "div", "mod", "exp", "eq", "neq", "geq", "leq", - "gth", "lth", "band", "band", "bor", "bxor", "bshl", "bshr" }) do - constantPropagation[operator] = function(instruction) - if instruction[3][1] ~= "literal" or instruction[4][1] ~= "literal" then return instruction end - return evaluateBinary(instruction) - end -end - -function constantPropagation.neg(instruction) - if instruction[3][1] ~= "literal" then return instruction end - return evaluateUnary(instruction) -end - -constantPropagation["not"] = function(instruction) - if instruction[3][1] ~= "literal" then return instruction end - instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]}) - return evaluateUnary(instruction) -end - -for _, operator in pairs({ "and", "or" }) do - constantPropagation[operator] = function(instruction) - if instruction[3][1] ~= "literal" or instruction[4][1] ~= "literal" then return instruction end - instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]}) - instruction[4] = evaluateUnary({"is", instruction[2], instruction[4]}) - return evaluateBinary(instruction) - end -end - -table.insert(Optimizer.Passes, constantPropagation) - - -local peephole = {} -function peephole.add(instruction) - -- (add 0 x) → x - if instruction[3][1] == "literal" and instruction[3][3] == 0 then return instruction[4] end - -- (add x 0) → x - if instruction[4][1] == "literal" and instruction[4][3] == 0 then return instruction[3] end - -- (add (neg x) (neg y)) → (neg (add x y)) - if instruction[3][1] == "neg" and instruction[4][1] == "neg" then - return {"neg", instruction[2], {"add", instruction[2], instruction[3][3], instruction[4][3], - __instruction = true}} - end - -- (add x (neg y)) → (sub x y) - if instruction[4][1] == "neg" then - return {"sub", instruction[2], instruction[3], instruction[4][3]} - end - -- (add (neg x) y) → (sub y x) - if instruction[3][1] == "neg" then - return {"sub", instruction[2], instruction[4], instruction[3][3]} - end - return instruction -end - -function peephole.sub(instruction) - -- (sub 0 x) → (neg x) - if instruction[3][1] == "literal" and instruction[3][3] == 0 then - return {"neg", instruction[2], instruction[4]} - end - -- (sub x 0) → x - if instruction[4][1] == "literal" and instruction[4][3] == 0 then return instruction[3] end - -- (sub (neg x) (neg y)) → (sub y x) - if instruction[3][1] == "neg" and instruction[4][1] == "neg" then - return {"sub", instruction[2], instruction[4][3], instruction[3][3]} - end - -- (sub x (neg y) → (add x y)) - if instruction[4][1] == "neg" then - return {"add", instruction[2], instruction[3], instruction[4][3]} - end - -- (sub (neg x) y) → (neg (add x y)) - if instruction[3][1] == "neg" then - return {"neg", instruction[2], {"add", instruction[2], instruction[3][3], instruction[4], - __instruction = true }} - end - return instruction -end - -function peephole.mul(instruction) - if instruction[4][1] == "literal" and instruction[3][1] ~= "literal" then - instruction[3], instruction[4] = instruction[4], instruction[3] - end - -- (mul 1 x) → x - if instruction[3][1] == "literal" and instruction[3][3] == 1 then return instruction[4] end - -- (mul -1 x) → (neg x) - if instruction[3][1] == "literal" and instruction[3][3] == -1 then - return {"neg", instruction[2], instruction[4]} - end - return instruction -end - -function peephole.neg(instruction) - -- (neg (neg x)) → x - if instruction[3][1] == "neg" then return instruction[3][3] end - return instruction -end - -peephole["if"] = function(instruction) - -- (if 1 x y) → x - -- (if 0 x y) → y - if instruction[3][1] == "literal" then - instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]}) - if instruction[3][3] == 1 then return instruction[4] end - if instruction[3][3] == 0 then return instruction[5] end - assert(false, "unreachable: `is` evaluation didn't return a boolean") - end - return instruction -end - -function peephole.whl(instruction) - -- (while 0 x false) → (seq) - -- (while 0 x true) → x - if instruction[3][1] == "literal" then - instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]}) - if instruction[3][3] == 0 then - if instruction[5] == false then - return {"seq", instruction[2]} - --else - --return instruction[4] - -- This optimization breaks when 'break' or 'continue' keyword appears - end - end - end - return instruction -end - -table.insert(Optimizer.Passes, peephole) diff --git a/lua/entities/gmod_wire_expression2/base/parser.lua b/lua/entities/gmod_wire_expression2/base/parser.lua index b3b6f05937..3b50050029 100644 --- a/lua/entities/gmod_wire_expression2/base/parser.lua +++ b/lua/entities/gmod_wire_expression2/base/parser.lua @@ -1,1512 +1,920 @@ --[[ - Expression 2 Parser for Garry's Mod - Andreas "Syranide" Svensson, me@syranide.com + Expression 2 Parser for Garry's Mod + + Rewritten by Vurv + Notable changes: + * Now uses Nodes and NodeVariant rather than strings (Much faster and better for intellisense) + * Removed excessive use of functions / recursion as an optimization + * This no longer does any analysis based on the environment like whether a type is valid or not since a Parser shouldn't be doing that. + * Removed PEG-style grammar. + * Condensed from 1.7k LOC -> 1k LOC ]] AddCSLuaFile() ---[[ - -The following is a description of the E2 language as a parsing -expression grammar. Note that the parser does all its semantic analysis -while parsing, forbidding certain things which this grammar allows. - -* ε is the end-of-file -* E? matches zero or one occurrences of T (and will always match one if possible) -* E* matches zero or more occurrences of T (and will always match as many as possible) -* E F matches E (and then whitespace) and then F -* E / F tries matching E, if it fails it matches F (from the start location) -* &E matches E, but does not consume any input. -* !E matches everything except E, and does not consume any input. - -Root ← Stmts - -Stmts ← Stmt1 (("," / " ") Stmt1)* ε - -Stmt1 ← ("if" Cond Block IfElseIf)? Stmt2 -Stmt2 ← ("while" Cond Block)? Stmt3 -Stmt3 ← ("for" "(" Var "=" Expr1 "," Expr1 ("," Expr1)? ")" Block)? Stmt4 -Stmt4 ← ("foreach" "(" Var "," Var ":" Fun "=" Expr1 ")" Block)? Stmt5 -Stmt5 ← ("break" / "continue")? Stmt6 -Stmt6 ← (Var ("++" / "--"))? Stmt7 -Stmt7 ← (Var ("+=" / "-=" / "*=" / "/="))? Stmt8 -Stmt8 ← "local"? (Var (&"[" Index ("=" Stmt8)? / "=" Stmt8))? Stmt9 -Stmt9 ← ("switch" "(" Expr1 ")" "{" SwitchBlock)? Stmt10 -Stmt10 ← (FunctionStmt / ReturnStmt)? Stmt11 -Stmt11 ← ("#include" String)? Stmt12 -Stmt12 ← ("try" Block "catch" "(" Var ")" Block)? Stmt13 -Stmt13 ← ("do" Block "while" Cond)? Expr1 -Stmt14 ← ("event" Fun "(" FunctionArgs Block) - -FunctionStmt ← "function" FunctionHead "(" FunctionArgs Block -FunctionHead ← (Type Type ":" Fun / Type ":" Fun / Type Fun / Fun) -FunctionArgs ← (FunctionArg ("," FunctionArg)*)? ")" -FunctionArg ← Var (":" Type)? - -ReturnStmt ← "return" ("void" / &"}" / Expr1) -IfElseIf ← "elseif" Cond Block IfElseIf / IfElse -IfElse ← "else" Block -Cond ← "(" Expr1 ")" -Block ← "{" (Stmt1 (("," / " ") Stmt1)*)? "}" -SwitchBlock ← (("case" Expr1 / "default") CaseBlock)* "}" -CaseBlock ← (Stmt1 (("," / " ") Stmt1)*)? &("case" / "default" / "}") - -Expr1 ← !(Var "=") !(Var "+=") !(Var "-=") !(Var "*=") !(Var "/=") Expr2 -Expr2 ← Expr3 (("?" Expr1 ":" Expr1) / ("?:" Expr1))? -Expr3 ← Expr4 ("|" Expr4)* -Expr4 ← Expr5 ("&" Expr5)* -Expr5 ← Expr6 ("||" Expr6)* -Expr6 ← Expr7 ("&&" Expr7)* -Expr7 ← Expr8 ("^^" Expr8)* -Expr8 ← Expr9 (("==" / "!=") Expr9)* -Expr9 ← Expr10 ((">" / "<" / ">=" / "<=") Expr10)* -Expr10 ← Expr11 (("<<" / ">>") Expr11)* -Expr11 ← Expr12 (("+" / "-") Expr12)* -Expr12 ← Expr13 (("*" / "/" / "%") Expr13)* -Expr13 ← Expr14 ("^" Expr14)* -Expr14 ← ("+" / "-" / "!") Expr15 -Expr15 ← Expr16 (MethodCallExpr / TableIndexExpr)? -Expr16 ← "(" Expr1 ")" / FunctionCallExpr / Expr17 -Expr17 ← Number / String / "~" Var / "$" Var / "->" Var / Expr18 -Expr18 ← !(Var "++") !(Var "--") Expr19 -Expr19 ← Var - -MethodCallExpr ← ":" Fun "(" (Expr1 ("," Expr1)*)? ")" -TableIndexExpr ← "[" Expr1 ("," Type)? "]" - -FunctionCallExpr ← Fun "(" KeyValueList? ")" -KeyValueList ← (KeyValue ("," KeyValue))* -KeyValue = Expr1 ("=" Expr1)? - -]] --- ---------------------------------------------------------------------------------- +local Trace, Warning, Error = E2Lib.Debug.Trace, E2Lib.Debug.Warning, E2Lib.Debug.Error +local Tokenizer = E2Lib.Tokenizer +local Token, TokenVariant = Tokenizer.Token, Tokenizer.Variant +local Keyword, Grammar, Operator = E2Lib.Keyword, E2Lib.Grammar, E2Lib.Operator ---@class Parser ----@field readtoken Token ---@field tokens Token[] +---@field ntokens integer ---@field index integer ----@field count integer ---@field warnings Warning[] +---@field traces Trace[] # Stack of traces to push and pop +---@field delta_vars table +---@field include_files string[] local Parser = {} Parser.__index = Parser -E2Lib.Parser = Parser - -local Tokenizer = E2Lib.Tokenizer -local Token, TokenVariant = Tokenizer.Token, Tokenizer.Variant -local Keyword, Grammar, Operator = E2Lib.Keyword, E2Lib.Grammar, E2Lib.Operator - -local parserDebug = CreateConVar("wire_expression2_parser_debug", 0, { FCVAR_NOTIFY, FCVAR_ARCHIVE}, - "Print an E2's abstract syntax tree after parsing" -) - ----@return boolean ok ----@return table tree ----@return table delta ----@return table includes ----@return Parser self -function Parser.Execute(...) - -- instantiate Parser - local instance = setmetatable({}, Parser) - - -- and pcall the new instance's Process method. - local ok, tree, delta, includes = xpcall(Parser.Process, E2Lib.errorHandler, instance, ...) - return ok, tree, delta, includes, instance -end - ----@param message string ----@param token Token? -function Parser:Error(message, token) - if token then - error(message .. " at line " .. token.start_line .. ", char " .. token.start_col, 0) - else - error(message .. " at line " .. self.token.start_line .. ", char " .. self.token.start_col, 0) - end +---@param tokens table? +function Parser.new(tokens) + return setmetatable({ tokens = tokens or {}, ntokens = tokens and #tokens or 0, index = 1, warnings = {}, traces = {} }, Parser) end ----@param message string ----@param token Token? -function Parser:Warning(message, token) - if token then - self.warnings[#self.warnings + 1] = { message = message, line = token.start_line, char = token.start_col } - else - self.warnings[#self.warnings + 1] = { message = message, line = self.token.start_line, char = self.token.start_col } - end -end +E2Lib.Parser = Parser ----@param tokens Token[] ----@return table tree ----@return table delta ----@return table includes -function Parser:Process(tokens, params) - self.tokens = tokens - self.index = 0 - self.count = #tokens - self.delta = {} - self.includes = {} - self.warnings = {} - - self:NextToken() - local tree = self:Root() - if parserDebug:GetBool() then - print(E2Lib.AST.dump(tree)) - end - return tree, self.delta, self.includes +---@class Node: { data: T, variant: NodeVariant, trace: Trace } +---@field variant NodeVariant +---@field trace Trace +---@field data any +local Node = {} +Node.__index = Node + +Parser.Node = Node + +---@param variant NodeVariant +---@param data any +---@param trace Trace +---@return Node +function Node.new(variant, data, trace) + return setmetatable({ variant = variant, trace = trace, data = data }, Node) end --- --------------------------------------------------------------------- - ----@return Token? -function Parser:GetToken() - return self.token +---@enum NodeVariant +local NodeVariant = { + Block = 1, + + --- Statements + If = 2, -- `if (1) {} elseif (1) {} else {}` + While = 3, -- `while (1) {}`, `do {} while(1)` + For = 4, -- `for (I = 1, 2, 3) {}` + Foreach = 5, -- `foreach(K, V = T) {}` + + Break = 6, -- break + Continue = 7, -- `continue` + Return = 8, -- `return` + + Increment = 9, -- `++` + Decrement = 10, -- `--` + CompoundArithmetic = 11, -- `+=`, `-=`, `*=`, `/=` + Assignment = 12, -- `X = Y[2, number] = Z[2] = 5` or `local X = 5` + + Switch = 13, -- `switch () { case , * default, } + Function = 14, -- `function test() {}` + Include = 15, -- #include "file" + Try = 16, -- try {} catch (Err) {} + + --- Compile time constructs + Event = 17, -- event tick() {} + + --- Expressions + ExprTernary = 18, -- `X ? Y : Z` + ExprDefault = 19, -- `X ?: Y` + ExprLogicalOp = 20, -- `|` `&` (Yes they are flipped.) + ExprBinaryOp = 21, -- `||` `&&` `^^` + ExprComparison = 22, -- `>` `<` `>=` `<=` + ExprEquals = 23, -- `==` `!=` + ExprBitShift = 24, -- `>>` `<<` + ExprArithmetic = 25, -- `+` `-` `*` `/` `^` `%` + ExprUnaryOp = 26, -- `-` `+` `!` + ExprMethodCall = 27, -- `:call()` + ExprIndex = 28, -- `[, ?]` + ExprGrouped = 29, -- () + ExprCall = 30, -- `call()` + ExprStringCall = 31, -- `""()` (Temporary until lambdas are made) + ExprUnaryWire = 32, -- `~Var` `$Var` `->Var` + ExprArray = 33, -- `array(1, 2, 3)` or `array(1 = 2, 2 = 3)` + ExprTable = 34, -- `table(1, 2, 3)` or `table(1 = 2, "test" = 3)` + ExprLiteral = 35, -- `"test"` `5e2` `4.023` `4j` + ExprIdent = 36 -- `Variable` +} + +Parser.Variant = NodeVariant + +local NodeVariantLookup = {} +for var, i in pairs(NodeVariant) do + NodeVariantLookup[i] = var end -function Parser:GetTokenData() - return self.token.value +function Node:debug() + return string.format("Node { variant = %s, data = %s, trace = %s }", NodeVariantLookup[self.variant], self.data, self.trace) end -function Parser:GetTokenTrace() - return { self.token.start_line, self.token.start_col } +--- Returns whether the node is an expression variant. +function Node:isExpr() + return self.variant >= NodeVariant.ExprTernary end - -function Parser:Instruction(trace, name, ...) - return { __instruction = true, name, trace, ... } +---@return string +function Node:instr() + return NodeVariantLookup[self.variant] end +Node.__tostring = Node.debug -function Parser:HasTokens() - return self.readtoken ~= nil +---@return boolean ok, Node|Error ast, table dvars, string[] include_files, Parser self +function Parser.Execute(tokens) + local instance = Parser.new(tokens) + local ok, ast, dvars, include_files = xpcall(Parser.Process, E2Lib.errorHandler, instance, tokens) + return ok, ast, dvars, include_files, instance end -function Parser:NextToken() - if self.index <= self.count then - if self.index > 0 then - self.token = self.readtoken - else - self.token = setmetatable({ value = "", variant = 1, whitespaced = false, start_col = 1, start_line = 1, end_col = 1, end_line = 1 }, Token) -- { "", "", false, 1, 1 } - end - - self.index = self.index + 1 - self.readtoken = self.tokens[self.index] - else - self.readtoken = nil - end -end - -function Parser:TrackBack() - self.index = self.index - 2 - self:NextToken() -end - - ----@param variant TokenVariant ----@param value? number|string|boolean ----@return boolean -function Parser:AcceptRoamingToken(variant, value) - local token = self.readtoken - if not token or token.variant ~= variant then return false end - if value ~= nil and token.value ~= value then return false end - - self:NextToken() - return true -end +function Parser:Eof() return self.index > self.ntokens end +function Parser:Peek() return self.tokens[self.index + 1] end +function Parser:At() return self.tokens[self.index] end +function Parser:Prev() return self.tokens[self.index - 1] end ---@param variant TokenVariant ---@param value? number|string|boolean -function Parser:AcceptTailingToken(variant, value) - local token = self.readtoken - if not token or token.whitespaced then return false end +---@return Token? +function Parser:Consume(variant, value) + local token = self:At() + if not token or token.variant ~= variant then return end + if value ~= nil and token.value ~= value then return end - return self:AcceptRoamingToken(variant, value) + self.index = self.index + 1 + return token end ----@param variant TokenVariant ----@param value? number|string|boolean -function Parser:AcceptLeadingToken(variant, value) - local token = self.tokens[self.index + 1] - if not token or token.whitespaced then return false end - - return self:AcceptRoamingToken(variant, value) +---@param message string +---@param trace Trace? +function Parser:Error(message, trace) + error( Error.new( message, trace or self:Prev().trace ), 2 ) end - ----@param func function ----@param tbl Operator[] -function Parser:RecurseLeft(func, tbl) - local expr = func(self) - local hit = true - - while hit do - hit = false - for _, op in ipairs(tbl) do - if self:AcceptRoamingToken(TokenVariant.Operator, op) then - local trace = self:GetTokenTrace() - - hit = true - expr = self:Instruction(trace, E2Lib.OperatorNames[op]:lower(), expr, func(self)) - break - end - end - end - - return expr +---@param message string +---@param trace Trace? +function Parser:Warning(message, trace) + self.warnings[#self.warnings + 1] = Warning.new( message, trace or self:Prev().trace ) end --- -------------------------------------------------------------------------- - -local loopdepth - -function Parser:Root() - loopdepth = 0 - return self:Stmts() +---@generic T +---@param v? T +---@param message string +---@param trace Trace? +---@return T +function Parser:Assert(v, message, trace) + if not v then error( Error.new( message, trace or self:Prev().trace ), 2 ) end + return v end +---@return Node ast, table dvars, string[] include_files +function Parser:Process(tokens) + self.index, self.tokens, self.ntokens, self.warnings, self.delta_vars, self.include_files = 1, tokens, #tokens, {}, {}, {} -function Parser:Stmts() - local trace = self:GetTokenTrace() - local stmts = self:Instruction(trace, "seq") - - if not self:HasTokens() then return stmts end + local stmts = {} + if self:Eof() then return Node.new(NodeVariant.Block, stmts, Trace.new(0, 0, 0, 0)), self.delta_vars, self.include_files end while true do - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then + if self:Consume(TokenVariant.Grammar, Grammar.Comma) then self:Error("Statement separator (,) must not appear multiple times") end - stmts[#stmts + 1] = self:Stmt1() + local stmt = self:Stmt() or self:Expr() + stmts[#stmts + 1] = stmt - if not self:HasTokens() then break end + if self:Eof() then break end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - if not self.readtoken.whitespaced then - self:Error("Statements must be separated by comma (,) or whitespace") + if not self:Consume(TokenVariant.Grammar, Grammar.Comma) then + if not self:At().whitespaced then + self:Error("Statements must be separated by comma (,) or whitespace", stmt.trace) end end end - return stmts + local trace = (#stmts ~= 0) and stmts[1].trace:stitch(stmts[#stmts].trace) or Trace.new(1, 1, 1, 1) + return Node.new(NodeVariant.Block, stmts, trace), self.delta_vars, self.include_files end +---@param variant TokenVariant +---@param value? number|string|boolean +function Parser:ConsumeTailing(variant, value) + local token = self:At() + if not token or token.whitespaced then return end -function Parser:Stmt1() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.If) then - local trace = self:GetTokenTrace() - return self:Instruction(trace, "if", self:Cond(), self:Block("if condition"), self:IfElseIf()) - end - - return self:Stmt2() + return self:Consume(variant, value) end -function Parser:Stmt2() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.While) then - local trace = self:GetTokenTrace() - loopdepth = loopdepth + 1 - local whl = self:Instruction(trace, "whl", self:Cond(), self:Block("while condition"), - false) -- Skip condition check first time? - loopdepth = loopdepth - 1 - return whl - end +---@param variant TokenVariant +---@param value? number|string|boolean +function Parser:ConsumeLeading(variant, value) + local token = self:Peek() + if not token or token.whitespaced then return end - return self:Stmt3() + return self:Consume(variant, value) end -function Parser:Stmt3() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.For) then - local trace = self:GetTokenTrace() - loopdepth = loopdepth + 1 - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Operator.LParen) then - self:Error("Left parenthesis (() must appear before condition") - end - - if not self:AcceptRoamingToken(TokenVariant.Ident) and not self:AcceptRoamingToken(TokenVariant.Discard) then - self:Error("Variable expected for the numeric index") - end +---@return Node +function Parser:Condition() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.LParen), "Left parenthesis (() expected before condition") + local expr = self:Expr() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RParen), "Right parenthesis ()) missing, to close condition") + return expr +end - local var = self:GetTokenData() +---@return Node? +function Parser:Stmt() + if self:Consume(TokenVariant.Keyword, Keyword.If) then + local cond, block = self:Condition(), self:Assert( self:Block(), "Expected block after if condition") - if not self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - self:Error("Assignment operator (=) expected to preceed variable") + ---@type { [1]: Node?, [2]: Node }[] + local chain = { {cond, block} } + while self:Consume(TokenVariant.Keyword, Keyword.Elseif) do + local cond, block = self:Condition(), self:Assert( self:Block(), "Expected block after elseif condition") + chain[#chain + 1] = { cond, block } end - local estart = self:Expr1() - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Comma (,) expected after start value") + if self:Consume(TokenVariant.Keyword, Keyword.Else) then + chain[#chain + 1] = { nil, self:Assert( self:Block(), "Expected block after else keyword") } end - local estop = self:Expr1() + return Node.new(NodeVariant.If, chain, cond.trace:stitch(self:Prev().trace)) + end - local estep - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - estep = self:Expr1() - end + if self:Consume(TokenVariant.Keyword, Keyword.While) then + local trace = self:Prev().trace + return Node.new(NodeVariant.While, { self:Condition(), self:Block(), false }, trace:stitch(self:Prev().trace)) + end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close condition") + if self:Consume(TokenVariant.Keyword, Keyword.For) then + local trace = self:Prev().trace + if not self:Consume(TokenVariant.Grammar, Grammar.LParen) then + self:Error("Left Parenthesis (() must appear before condition") end - local sfor = self:Instruction(trace, "for", var, estart, estop, estep, self:Block("for statement")) - - loopdepth = loopdepth - 1 - return sfor - end - - return self:Stmt4() -end + local var = self:Assert( self:Consume(TokenVariant.Ident), "Variable expected for numeric index" ) + self:Assert( self:Consume(TokenVariant.Operator, Operator.Ass), "Assignment operator (=) expected to preceed variable" ) -function Parser:Stmt4() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Foreach) then - local trace = self:GetTokenTrace() - loopdepth = loopdepth + 1 + local start = self:Expr() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.Comma), "Comma (,) expected after start value" ) - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis missing (() after foreach statement") - end + local stop = self:Expr() - if not self:AcceptRoamingToken(TokenVariant.Ident) and not self:AcceptRoamingToken(TokenVariant.Discard) then - self:Error("Variable expected to hold the key") + local step + if self:Consume(TokenVariant.Grammar, Grammar.Comma) then + step = self:Expr() end - local keyvar = self:GetTokenData() - local keytype - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Type expected after colon") - end + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RParen), "Right parenthesis ()) missing, to close for statement" ) - keytype = self:GetTokenData() - if keytype == "number" then keytype = "normal" end + return Node.new(NodeVariant.For, { var, start, stop, step, self:Block() }, trace:stitch(self:Prev().trace)) + end - if wire_expression_types[string.upper(keytype)] == nil then - self:Error("Unknown type: " .. keytype) - end + if self:Consume(TokenVariant.Keyword, Keyword.Foreach) then + local trace = self:Prev().trace + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.LParen), "Left parenthesis (() missing after foreach statement" ) - keytype = wire_expression_types[string.upper(keytype)][1] - end + local key = self:Assert( self:Consume(TokenVariant.Ident), "Variable expected to hold the key" ) - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Comma (,) expected after key variable") + local key_type + if self:Consume(TokenVariant.Operator, Operator.Col) then + key_type = self:Assert(self:Type(), "Type expected after colon") end - if not self:AcceptRoamingToken(TokenVariant.Ident) and not self:AcceptRoamingToken(TokenVariant.Discard) then - self:Error("Variable expected to hold the value") - end - local valvar = self:GetTokenData() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.Comma), "Comma (,) expected after key variable" ) - if not self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - self:Error("Colon (:) expected to separate type from variable") - end - - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Type expected after colon") - end + local value = self:Assert( self:Consume(TokenVariant.Ident), "Variable expected to hold the value" ) + self:Assert( self:Consume(TokenVariant.Operator, Operator.Col), "Colon (:) expected to separate type from variable" ) - local valtype = self:GetTokenData() - if valtype == "number" then valtype = "normal" end - if wire_expression_types[string.upper(valtype)] == nil then - self:Error("Unknown type: " .. valtype) - end - valtype = wire_expression_types[string.upper(valtype)][1] + local value_type = self:Assert(self:Type(), "Type expected after colon") + self:Assert( self:Consume(TokenVariant.Operator, Operator.Ass), "Equals sign (=) expected after value type to specify table" ) - if not self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - self:Error("Equals sign (=) expected after value type to specify table") - end + local table = self:Expr() - local tableexpr = self:Expr1() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RParen), "Missing right parenthesis after foreach statement" ) - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Missing right parenthesis after foreach statement") - end + return Node.new(NodeVariant.Foreach, { key, key_type, value, value_type, table, self:Block() }, trace:stitch(self:Prev().trace)) + end - local sfea = self:Instruction(trace, "fea", keyvar, keytype, valvar, valtype, tableexpr, self:Block("foreach statement")) - loopdepth = loopdepth - 1 - return sfea + if self:Consume(TokenVariant.Keyword, Keyword.Break) then + return Node.new(NodeVariant.Break, nil, self:Prev().trace) end - return self:Stmt5() -end + if self:Consume(TokenVariant.Keyword, Keyword.Continue) then + return Node.new(NodeVariant.Continue, nil, self:Prev().trace) + end -function Parser:Stmt5() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Break) then - if loopdepth > 0 then - local trace = self:GetTokenTrace() - return self:Instruction(trace, "brk") - else - self:Error("Break may not exist outside of a loop") - end - elseif self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Continue) then - if loopdepth > 0 then - local trace = self:GetTokenTrace() - return self:Instruction(trace, "cnt") + if self:Consume(TokenVariant.Keyword, Keyword.Return) then + local trace = self:Prev().trace + if self:Consume(TokenVariant.LowerIdent, "void") then + return Node.new(NodeVariant.Return, nil, trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Grammar, Grammar.RCurly) then + self.index = self.index - 1 + return Node.new(NodeVariant.Return, nil, trace) else - self:Error("Continue may not exist outside of a loop") + return Node.new(NodeVariant.Return, self:Expr(), trace:stitch(self:At().trace)) end end - return self:Stmt6() -end - -function Parser:Stmt6() - if self:AcceptRoamingToken(TokenVariant.Ident) then - local trace = self:GetTokenTrace() - local var = self:GetTokenData() - - if self:AcceptTailingToken(TokenVariant.Operator, Operator.Inc) then - return self:Instruction(trace, "inc", var) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Inc) then + local var = self:Consume(TokenVariant.Ident) + if var then + --- Increment / Decrement + if self:ConsumeTailing(TokenVariant.Operator, Operator.Inc) then + return Node.new(NodeVariant.Increment, var, var.trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Inc) then self:Error("Increment operator (++) must not be preceded by whitespace") end - if self:AcceptTailingToken(TokenVariant.Operator, Operator.Dec) then - return self:Instruction(trace, "dec", var) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Dec) then + if self:ConsumeTailing(TokenVariant.Operator, Operator.Dec) then + return Node.new(NodeVariant.Decrement, var, var.trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Dec) then self:Error("Decrement operator (--) must not be preceded by whitespace") end - self:TrackBack() - end - - return self:Stmt7() -end - -function Parser:Stmt7() - if self:AcceptRoamingToken(TokenVariant.Ident) then - local trace = self:GetTokenTrace() - local var = self:GetTokenData() - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Aadd) then - return self:Instruction(trace, "ass", var, self:Instruction(trace, "add", self:Instruction(trace, "var", var), self:Expr1())) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Asub) then - return self:Instruction(trace, "ass", var, self:Instruction(trace, "sub", self:Instruction(trace, "var", var), self:Expr1())) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Amul) then - return self:Instruction(trace, "ass", var, self:Instruction(trace, "mul", self:Instruction(trace, "var", var), self:Expr1())) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Adiv) then - return self:Instruction(trace, "ass", var, self:Instruction(trace, "div", self:Instruction(trace, "var", var), self:Expr1())) - end - - self:TrackBack() - end - - return self:Stmt8() -end - -function Parser:Index() - if self:AcceptTailingToken(TokenVariant.Grammar, Grammar.LSquare) then - local trace = self:GetTokenTrace() - local exp = self:Expr1() - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Indexing operator ([]) requires a lower case type [X,t]") - end - - local typename = self:GetTokenData() - if typename == "number" then typename = "normal" end - local type = wire_expression_types[string.upper(typename)] - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - self:Error("Right square bracket (]) missing, to close indexing operator [X,t]") - end - - if not type then - self:Error("Indexing operator ([]) does not support the type [" .. typename .. "]") - end - - return { exp, type[1], trace }, self:Index() - - elseif self:AcceptTailingToken(TokenVariant.Grammar, Grammar.RSquare) then - return { exp, nil, trace } - - else - self:Error("Indexing operator ([]) must not be preceded by whitespace") + --- Compound Assignment + if self:Consume(TokenVariant.Operator, Operator.Aadd) then + return Node.new(NodeVariant.CompoundArithmetic, { var, Operator.Add, self:Expr() }, var.trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Asub) then + return Node.new(NodeVariant.CompoundArithmetic, { var, Operator.Sub, self:Expr() }, var.trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Amul) then + return Node.new(NodeVariant.CompoundArithmetic, { var, Operator.Mul, self:Expr() }, var.trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Adiv) then + return Node.new(NodeVariant.CompoundArithmetic, { var, Operator.Div, self:Expr() }, var.trace:stitch(self:Prev().trace)) end - end -end - -function Parser:Stmt8(parentLocalized) - local localized - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Local) then - if parentLocalized ~= nil then self:Error("Assignment can't contain roaming local operator") end - localized = true + -- Didn't match anything. Might be something else. + self.index = self.index - 1 end - if self:AcceptRoamingToken(TokenVariant.Ident) then - local tbpos = self.index - local trace = self:GetTokenTrace() - local var = self:GetTokenData() - - if self:AcceptTailingToken(TokenVariant.Grammar, Grammar.LSquare) then - self:TrackBack() - local indexs = { self:Index() } - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - if localized or parentLocalized then - self:Error("Invalid operator (local).") - end - - local total = #indexs - local inst = self:Instruction(trace, "var", var) - - for i = 1, total do -- Yep, All this took me 2 hours to figure out! - local key, type, trace = indexs[i][1], indexs[i][2], indexs[i][3] - if i == total then - inst = self:Instruction(trace, "set", inst, key, self:Stmt8(false), type) - else - inst = self:Instruction(trace, "get", inst, key, type) - end - end -- Example Result: set( get( get(Var,1,table) ,1,table) ,3,"hello",string) - return inst - end - - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - if localized or parentLocalized then - return self:Instruction(trace, "assl", var, self:Stmt8(true)) + local is_local, var = self:Consume(TokenVariant.Keyword, Keyword.Local), self:Consume(TokenVariant.Ident) + if not var then + self:Assert(not is_local, "Invalid operator (local) must be used for variable declaration.") + else + local revert, prev = self.index, self.index + local assignments = { { var, is_local and {} or self:Indices(), (is_local or var).trace:stitch(self:Prev().trace) } } + while self:Consume(TokenVariant.Operator, Operator.Ass) do + local ident = self:Consume(TokenVariant.Ident) + if ident then + prev = self.index + assignments[#assignments + 1] = { ident, self:Indices(), ident.trace:stitch(self:Prev().trace) } else - return self:Instruction(trace, "ass", var, self:Stmt8(false)) + return Node.new(NodeVariant.Assignment, { is_local, assignments, self:Expr() }, (is_local or var).trace:stitch(self:Prev().trace)) end - elseif localized then - self:Error("Invalid operator (local) must be used for variable declaration.") - end - - self.index = tbpos - 2 - self:NextToken() - elseif localized then - self:Error("Invalid operator (local) must be used for variable declaration.") - end - - return self:Stmt9() -end - -function Parser:Stmt9() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Switch) then - local trace = self:GetTokenTrace() - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis (() expected before switch condition") - end - - local expr = self:Expr1() - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) expected after switch condition") end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LCurly) then - self:Error("Left curly bracket ({) expected after switch condition") + if #assignments == 1 then -- No assignment + self.index = revert - 1 + else -- Last 'assignment' is the expression. + table.remove(assignments) + self.index = prev - 1 + return Node.new(NodeVariant.Assignment, { is_local, assignments, self:Expr() }, (is_local or var).trace:stitch(self:Prev().trace)) end - - loopdepth = loopdepth + 1 - local cases, default = self:SwitchBlock() - loopdepth = loopdepth - 1 - - return self:Instruction(trace, "switch", expr, cases, default) end - return self:Stmt10() -end + -- Switch Case + if self:Consume(TokenVariant.Keyword, Keyword.Switch) then + local trace = self:Prev().trace + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.LParen), "Left parenthesis (() expected before switch condition" ) + local expr = self:Expr() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RParen), "Right parenthesis ()) expected before switch condition" ) -function Parser:Stmt10() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Function) then + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.LCurly), "Left curly bracket ({) expected after switch condition" ) - local Trace = self:GetTokenTrace() + local cases, default = {}, nil + if not self:Eof() and not self:Consume(TokenVariant.Grammar, Grammar.RParen) then + self:Assert( self:Consume(TokenVariant.Keyword, Keyword.Case) or self:Consume(TokenVariant.Keyword, Keyword.Default), "Expected case or default in switch block" ) + self.index = self.index - 1 + while true do + local case, expr = self:Consume(TokenVariant.Keyword, Keyword.Case) + if case then + expr = self:Expr() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.Comma), "Comma (,) expected after case condition" ) + end - local Name, Return, Type - local NameToken, ReturnToken, TypeToken - local Args, Temp = {}, {} + local default_ = self:Consume(TokenVariant.Keyword, Keyword.Default) + if default_ then + self:Assert(not default, "Only one default case (default:) may exist.") + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.Comma), "Comma (,) expected after default case" ) + elseif not case then + break + end + if self:Eof() then + self:Error("Case block is missing after case declaration.") + end - if self:AcceptRoamingToken(TokenVariant.LowerIdent) or self:AcceptRoamingToken(TokenVariant.Ident) then --get the name - Name = self:GetTokenData() - NameToken = self.token -- Copy the current token for error reporting + local block --[=[@type Node[]]=] = {} + while true do + if self:Consume(TokenVariant.Keyword, Keyword.Case) or self:Consume(TokenVariant.Keyword, Keyword.Default) or self:Consume(TokenVariant.Grammar, Grammar.RCurly) then + self.index = self.index - 1 + break + elseif self:Consume(TokenVariant.Grammar, Grammar.Comma) then + self:Error("Statement separator (,) must not appear multiple times") + elseif self:Consume(TokenVariant.Grammar, Grammar.RCurly) then + self:Error("Statement separator (,) must be suceeded by statement") + end - -- We check if the previous token was actualy the return not the name - if self:AcceptRoamingToken(TokenVariant.LowerIdent) or self:AcceptRoamingToken(TokenVariant.Ident) then - Return = Name - ReturnToken = NameToken + local stmt = self:Stmt() or self:Expr() + block[#block + 1] = stmt - Name = self:GetTokenData() - NameToken = self.token - end + if not self:Consume(TokenVariant.Grammar, Grammar.Comma) then + if self:Eof() then break end - -- We check if the name token is actually the type - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - if self:AcceptRoamingToken(TokenVariant.LowerIdent) or self:AcceptRoamingToken(TokenVariant.Ident) then - Type = Name - TypeToken = NameToken + if not self:At().whitespaced then + self:Error("Statements must be separated by comma (,) or whitespace", stmt.trace) + end + end + end - Name = self:GetTokenData() - NameToken = self.token - else - self:Error("Function name must appear after colon (:)") + if default_ then + local trace = (#block ~= 0) and default_.trace:stitch(block[1].trace):stitch(block[#block].trace) or default_.trace + default = Node.new(NodeVariant.Block, block, trace) + else ---@cast case Token # Know it isn't nil since (if not case then break end) above + local trace = (#block ~= 0) and case.trace:stitch(block[1].trace):stitch(block[#block].trace) or case.trace + cases[#cases + 1] = { expr, Node.new(NodeVariant.Block, block, trace) } end end end + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RCurly), "Right curly bracket (}) missing, to close switch block") - if Return and Return ~= "void" then -- Check the retun value - - if Return ~= Return:lower() then - self:Error("Function return type must be lowercased", ReturnToken) - end - - if Return == "number" then Return = "normal" end - - Return = Return:upper() - - if not wire_expression_types[Return] then - self:Error("Invalid return argument '" .. E2Lib.limitString(Return:lower(), 10) .. "'", ReturnToken) - end + return Node.new(NodeVariant.Switch, { expr, cases, default }, trace:stitch(self:Prev().trace)) + end - Return = wire_expression_types[Return][1] + -- Function definition + if self:Consume(TokenVariant.Keyword, Keyword.Function) then + local trace, type_or_name = self:Prev().trace, self:Assert( self:Consume(TokenVariant.LowerIdent), "Expected function return type or name after function keyword") - else - Return = "" + if self:Consume(TokenVariant.Operator, Operator.Col) then + -- function entity:xyz() + return Node.new(NodeVariant.Function, { nil, type_or_name, self:Assert(self:Consume(TokenVariant.LowerIdent), "Expected function name after colon (:)"), self:Parameters(), self:Block() }, trace:stitch(self:Prev().trace)) end - if Type then -- check the Type - - if Type ~= Type:lower() then self:Error("Function object type must be full lowercase", TypeToken) end - - if Type == "number" then Type = "normal" end - - if Type == "void" then self:Error("Void can not be used as function object type", TypeToken) end - - Type = Type:upper() - - if not wire_expression_types[Type] then - self:Error("Invalid data type '" .. E2Lib.limitString(Type:lower(), 10) .. "'", TypeToken) + local meta_or_name = self:Consume(TokenVariant.LowerIdent) + if meta_or_name then + if self:Consume(TokenVariant.Operator, Operator.Col) then + -- function void entity:xyz() + return Node.new(NodeVariant.Function, { type_or_name, meta_or_name, self:Assert(self:Consume(TokenVariant.LowerIdent), "Expected function name after colon (:)"), self:Parameters(), self:Block() }, trace:stitch(self:Prev().trace)) + else + -- function void test() + return Node.new(NodeVariant.Function, { type_or_name, nil, meta_or_name, self:Parameters(), self:Block() }, trace:stitch(self:Prev().trace)) end - - Temp["This"] = true - - Args[1] = { "This", Type } else - Type = "" + -- function test() + return Node.new(NodeVariant.Function, { nil, nil, type_or_name, self:Parameters(), self:Block() }, trace:stitch(self:Prev().trace)) end - - if not Name then self:Error("Function name must follow function declaration") end - - if Name[1] ~= Name[1]:lower() then self:Error("Function name must start with a lower case letter", NameToken) end - - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis (() must appear after function name") - end - - self:FunctionArgs(Temp, Args) - - local Sig = Name .. "(" - for I = 1, #Args do - local Arg = Args[I] - Sig = Sig .. (Arg[3] and ".." or "") .. wire_expression_types[Arg[2]][1] - if I == 1 and Arg[1] == "This" and Type ~= '' then - Sig = Sig .. ":" - end - end - Sig = Sig .. ")" - - if wire_expression2_funcs[Sig] then self:Error("Function '" .. Sig .. "' already exists") end - - -- Variadic signatures for lua created functions are ..., while user defined ones use ... - -- Check if ... functions exist as to not essentially override them - local lua_variadic_sig = string.gsub(Sig, "%.%.[rt]", "...") - if wire_expression2_funcs[lua_variadic_sig] then self:Error("Can't override function " .. lua_variadic_sig .. " with user defined variadic function " .. Sig) end - - local Inst = self:Instruction(Trace, "function", Sig, Return, Type, Args, self:Block("function declaration")) - - return Inst - - -- Return Statment - elseif self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Return) then - - local Trace = self:GetTokenTrace() - - if self:AcceptRoamingToken(TokenVariant.LowerIdent, "void") or (self.readtoken.variant == TokenVariant.Grammar and self.readtoken.value == Grammar.RCurly) then - return self:Instruction(Trace, "return") - end - - return self:Instruction(Trace, "return", self:Expr1()) - - -- Void Missplacement - elseif self:AcceptRoamingToken(TokenVariant.LowerIdent, "void") then - self:Error("Void may only exist after return") end - return self:Stmt11() -end - -function Parser:Stmt11() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword["#Include"]) then - - local Trace = self:GetTokenTrace() - - -- if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - -- self:Error("Left parenthesis (() must appear after include") - -- end - - if not self:AcceptRoamingToken(TokenVariant.String) then - self:Error("include path (string) expected after include") - end - - local Path = self:GetTokenData() + -- #include + if self:Consume(TokenVariant.Keyword, Keyword["#Include"]) then + local trace, path = self:Prev().trace, self:Assert( self:Consume(TokenVariant.String), "include path (string) expected after #include") - -- if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - -- self:Error("Right parenthesis ()) must appear after include path") - -- end - - self.includes[#self.includes + 1] = Path - - return self:Instruction(Trace, "inclu", Path) + self.include_files[#self.include_files + 1] = path.value + return Node.new(NodeVariant.Include, path.value, trace:stitch(path.trace)) end - return self:Stmt12() -end + -- Try catch + if self:Consume(TokenVariant.Keyword, Keyword.Try) then + local trace, stmt = self:Prev().trace, self:Block() -function Parser:Stmt12() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Try) then - local trace = self:GetTokenTrace() - local stmt = self:Block("try block") - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Catch) then - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then + if self:Consume(TokenVariant.Keyword, Keyword.Catch) then + if not self:Consume(TokenVariant.Grammar, Grammar.LParen) then self:Error("Left parenthesis (() expected after catch keyword") end - if not self:AcceptRoamingToken(TokenVariant.Ident) and not self:AcceptRoamingToken(TokenVariant.Discard) then + local err_ident = self:Consume(TokenVariant.Ident) + if not err_ident then self:Error("Variable expected after left parenthesis (() in catch statement") end - local var_name = self:GetTokenData() - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then + local ty + if self:Consume(TokenVariant.Operator, Operator.Col) then + ty = self:Assert(self:Type(), "Expected type name after : for error value", trace) + end + + if not self:Consume(TokenVariant.Grammar, Grammar.RParen) then self:Error("Right parenthesis ()) missing, to close catch statement") end - return self:Instruction(trace, "try", stmt, var_name, self:Block("catch block") ) + return Node.new(NodeVariant.Try, {stmt, err_ident, ty, self:Block()}, trace:stitch(self:Prev().trace)) else self:Error("Try block must be followed by catch statement") end end - return self:Stmt13() -end - -function Parser:Stmt13() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Do) then - local trace = self:GetTokenTrace() - - loopdepth = loopdepth + 1 - local code = self:Block("do keyword") - if not self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.While) then - self:Error("while expected after do and code block (do {...} )") - end - - local condition = self:Cond() - - - local whl = self:Instruction(trace, "whl", condition, code, - true) -- Skip condition check first time? - loopdepth = loopdepth - 1 - - return whl + -- Do while + if self:Consume(TokenVariant.Keyword, Keyword.Do) then + local trace, block = self:Prev().trace, self:Block() + self:Assert( self:Consume(TokenVariant.Keyword, Keyword.While), "while expected after do and code block (do {...} )") + return Node.new(NodeVariant.While, { self:Condition(), block }, trace:stitch(self:Prev().trace)) end - return self:Stmt14() -end - -function Parser:Stmt14() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Event) then - local trace = self:GetTokenTrace() - - local name = self:AcceptRoamingToken(TokenVariant.LowerIdent) - if not name then - self:Error("Expected event name after 'event' keyword") - end - local name = self:GetTokenData() - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis (() must appear after event name") - end - - local temp, args = {}, {} - self:FunctionArgs(temp, args) - - return self:Instruction(trace, "event", name, args, self:Block("event block")) + -- Event + if self:Consume(TokenVariant.Keyword, Keyword.Event) then + local trace, name = self:Prev().trace, self:Assert( self:Consume(TokenVariant.LowerIdent), "Expected event name after 'event' keyword") + return Node.new(NodeVariant.Event, { name, self:Parameters(), self:Block() }, trace:stitch(self:Prev().trace)) end - - return self:Expr1() end -function Parser:FunctionArgs(Temp, Args) - if self:HasTokens() and not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - while true do - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then self:Error("Argument separator (,) must not appear multiple times") end - - -- ...Array:array - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Spread) then - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) and not self:AcceptRoamingToken(TokenVariant.Ident) then - self:Error("Variable name expected after spread operator") - end - - local name = self:GetTokenData() - - if not self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - self:Error("Colon (:) expected after spread argument name") - end - - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Variable type expected after colon (:)") - end - - local type = self:GetTokenData() - if type ~= type:lower() then - self:Error("Variable type must be lowercased") - end - - type = type:upper() - - if not wire_expression_types[type] then - self:Error("Invalid type specified") - end - - if type ~= "ARRAY" and type ~= "TABLE" then - self:Error("Only array or table type is supported for spread arguments") - end - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Spread argument must be the last argument") - end - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) expected after spread argument") - end - - Temp[name] = true - Args[#Args + 1] = { name, type, true } - - return - end - - if self:AcceptRoamingToken(TokenVariant.Ident) or self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:FunctionArg(Temp, Args) - elseif self:AcceptRoamingToken(TokenVariant.Discard) then - self:FunctionArg(Temp, Args, true) - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LSquare) then - self:FunctionArgList(Temp, Args) - end - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - break - - elseif not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:NextToken() - self:Error("Right parenthesis ()) expected after function arguments") - end - end +---@return Token? +function Parser:Type() + local type = self:Consume(TokenVariant.LowerIdent) + if type and type.value == "normal" then + type.value = "number" end + return type end -function Parser:FunctionArg(Temp, Args, Discard) - local Type = "normal" - - local Name = self:GetTokenData() - - if not Name then self:Error("Variable required") end +---@alias Index { [1]: Node, [2]: Token?, [3]: Trace } +---@return Index[] +function Parser:Indices() + local indices = {} + while true do + local lsb = self:ConsumeTailing(TokenVariant.Grammar, Grammar.LSquare) + if not lsb then break end - if Name[1] ~= Name[1]:upper() then self:Error("Variable must start with uppercased letter") end + local exp = self:Expr() - if Temp[Name] then self:Error("Variable '" .. Name .. "' is already used as an argument,") end + if self:Consume(TokenVariant.Grammar, Grammar.Comma) then + local type = self:Assert(self:Type(), "Indexing operator ([]) requires a valid type [X, t]") + local rsb = self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RSquare), "Right square bracket (]) missing, to close indexing operator [X,t]" ) - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - if self:AcceptRoamingToken(TokenVariant.LowerIdent) or self:AcceptRoamingToken(TokenVariant.Ident) then - Type = self:GetTokenData() + indices[#indices + 1] = { exp, type, lsb.trace:stitch(rsb.trace) } + elseif self:ConsumeTailing(TokenVariant.Grammar, Grammar.RSquare) then + indices[#indices + 1] = { exp, nil, lsb.trace:stitch(self:Prev().trace) } else - self:Error("Type expected after colon (:)") - end - end - - if Type ~= Type:lower() then self:Error("Type must be lowercased") end - - if Type == "number" then Type = "normal" end - - Type = Type:upper() - - if not wire_expression_types[Type] then - self:Error("Invalid type specified") - end - - Temp[Name] = not Discard - Args[#Args + 1] = { Name, Type, false, Discard } -end - -function Parser:FunctionArgList(Temp, Args) - - if self:HasTokens() then - - local Vars = {} - while true do - if self:AcceptRoamingToken(TokenVariant.LowerIdent) or self:AcceptRoamingToken(TokenVariant.Ident) then - local Name = self:GetTokenData() - - if not Name then self:Error("Variable required") end - - if Name[1] ~= Name[1]:upper() then self:Error("Variable must start with uppercased letter") end - - if Temp[Name] then self:Error("Variable '" .. Name .. "' is already used as an argument") end - - Temp[Name] = true - Vars[#Vars + 1] = Name - - elseif self:AcceptRoamingToken(TokenVariant.Discard) then - Vars[#Vars + 1] = "_" - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - break - - else -- if !self:HasTokens() then - self:NextToken() - self:Error("Right square bracket (]) expected at end of argument list") - end - end - - if #Vars == 0 then - self:TrackBack() - self:TrackBack() - self:Error("Variables expected in variable list") - end - - local Type = "normal" - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - if self:AcceptRoamingToken(TokenVariant.LowerIdent) or self:AcceptRoamingToken(TokenVariant.Ident) then - Type = self:GetTokenData() - else - self:Error("Type expected after colon (:)") - end - end - - if Type ~= Type:lower() then self:Error("Type must be lowercased") end - - if Type == "number" then Type = "normal" end - - Type = Type:upper() - - if not wire_expression_types[Type] then - self:Error("Invalid type specified") - end - - for I = 1, #Vars do - Args[#Args + 1] = { Vars[I], Type, false, Vars[I] == "_" } + self:Error("Indexing operator ([]) must not be preceded by whitespace") end - - else - self:Error("Variable expected after left square bracket ([) in argument list") end -end -function Parser:IfElseIf() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Elseif) then - local trace = self:GetTokenTrace() - return self:Instruction(trace, "if", self:Cond(), self:Block("elseif condition"), self:IfElseIf()) - end - - return self:IfElse() -end - -function Parser:IfElse() - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Else) then - return self:Block("else") - end - - local trace = self:GetTokenTrace() - return self:Instruction(trace, "seq") + return indices end -function Parser:Cond() - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis (() expected before condition") - end - - local expr = self:Expr1() - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close condition") - end - - return expr -end - - -function Parser:Block(block_type) - local trace = self:GetTokenTrace() - local stmts = self:Instruction(trace, "seq") +function Parser:Block() + local lcb = self:Assert( self:Consume(TokenVariant.Grammar, Grammar.LCurly), "Left curly bracket ({) expected for block" ) - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LCurly) then - self:Error("Left curly bracket ({) expected after " .. (block_type or "condition")) + local stmts = {} + if self:Consume(TokenVariant.Grammar, Grammar.RCurly) then + return Node.new(NodeVariant.Block, stmts, lcb.trace:stitch(self:Prev().trace)) end - local token = self:GetToken() - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then - return stmts - end - - if self:HasTokens() then + if not self:Eof() then while true do - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then + if self:Consume(TokenVariant.Grammar, Grammar.Comma) then self:Error("Statement separator (,) must not appear multiple times") - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then + elseif self:Consume(TokenVariant.Grammar, Grammar.RCurly) then self:Error("Statement separator (,) must be suceeded by statement") end - stmts[#stmts + 1] = self:Stmt1() + stmts[#stmts + 1] = self:Stmt() or self:Expr() - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then - return stmts + if self:Consume(TokenVariant.Grammar, Grammar.RCurly) then + return Node.new(NodeVariant.Block, stmts, lcb.trace:stitch(self:Prev().trace)) end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - if not self:HasTokens() then break end - - if not self.readtoken.whitespaced then - self:Error("Statements must be separated by comma (,) or whitespace") - end + if not self:Consume(TokenVariant.Grammar, Grammar.Comma) then + if self:Eof() then break end + self:Assert(self:At().whitespaced, "Statements must be separated by comma (,) or whitespace") end end end - self:Error("Right curly bracket (}) missing, to close switch block", token) + self:Error("Right curly bracket (}) missing, to close block") end -function Parser:SwitchBlock() -- Shhh this is a secret. Do not tell anybody about this, Rusketh! - local cases = {} - local default - - if self:HasTokens() and not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - - if not self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Case) and not self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Default) then - self:Error("Case Operator (case) expected in case block.", self:GetToken()) - end - - self:TrackBack() - - while true do - - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Case) then - local expr = self:Expr1() +--- `type` is nil in case of the default param type. (number) +---@alias Parameter { name: Token, type: Token?, variadic: boolean } - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Comma (,) expected after case condition") - end - - cases[#cases + 1] = { expr, self:CaseBlock() } +---@return Parameter[]? +function Parser:Parameters() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.LParen), "Left parenthesis (() must appear for function parameters name") - elseif self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Default) then - - if default then - self:Error("Only one default case (default:) may exist.") - end - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Comma (,) expected after default case") - end - - default = true - cases[#cases + 1] = { nil, self:CaseBlock() } + local params = {} + if self:Consume(TokenVariant.Grammar, Grammar.RParen) then + return params + end + while true do + local variadic + if self:Consume(TokenVariant.Grammar, Grammar.LSquare) then + local temp = {} + repeat + temp[#temp + 1] = self:Assert(self:Consume(TokenVariant.Ident), "Expected parameter name") + until self:Consume(TokenVariant.Grammar, Grammar.RSquare) + + local typ, len = nil, #params + if self:Consume(TokenVariant.Operator, Operator.Col) then + typ = self:Assert( self:Type(), "Expected type after colon (:)" ) else - break + self:Warning("You should explicitly mark the type of these parameters") end - end - end - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then - self:Error("Right curly bracket (}) missing, to close statement block", self:GetToken()) - end - - return cases -end - -function Parser:CaseBlock() -- Shhh this is a secret. Do not tell anybody about this, Rusketh! - if self:HasTokens() then - local stmts = self:Instruction(self:GetTokenTrace(), "seq") - - if self:HasTokens() then - while true do - if self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Case) or self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Default) or self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then - self:TrackBack() - return stmts - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Statement separator (,) must not appear multiple times") - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then - self:Error("Statement separator (,) must be suceeded by statement") - end + for k, name in ipairs(temp) do + params[len + k] = { name = name, type = typ, variadic = false } + end + else + variadic = self:Consume(TokenVariant.Operator, Operator.Spread) - stmts[#stmts + 1] = self:Stmt1() + local name, type = self:Assert(self:Consume(TokenVariant.Ident), "Expected parameter name") + if self:Consume(TokenVariant.Operator, Operator.Col) then + type = self:Assert(self:Type(), "Expected valid parameter type") + end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - if not self:HasTokens() then break end + params[#params + 1] = { name = name, type = type, variadic = variadic ~= nil } + end - if not self.readtoken.whitespaced then - self:Error("Statements must be separated by comma (,) or whitespace") - end - end - end + if variadic then + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RParen), "Variadic parameter must be final in list" ) + return params + elseif self:Consume(TokenVariant.Grammar, Grammar.Comma) then + elseif self:Consume(TokenVariant.Grammar, Grammar.RParen) then + return params + else + self:Error("Expected comma (,) to separate parameters") end - else - self:Error("Case block is missing after case declaration.") end end -function Parser:Expr1() - self.exprtoken = self:GetToken() - - if self:AcceptRoamingToken(TokenVariant.Ident) then - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - self:Error("Assignment operator (=) must not be part of equation (Did you mean to use == ?)") +function Parser:Expr(ignore_assign) + -- Error for compound operators in expression + if self:Consume(TokenVariant.Ident) then + if not ignore_assign and self:Consume(TokenVariant.Operator, Operator.Ass) then + self:Error("Assignment operator (=) must not be part of equation ( Did you mean == ? )") end - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Aadd) then + if self:Consume(TokenVariant.Operator, Operator.Aadd) then self:Error("Additive assignment operator (+=) must not be part of equation") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Asub) then + elseif self:Consume(TokenVariant.Operator, Operator.Asub) then self:Error("Subtractive assignment operator (-=) must not be part of equation") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Amul) then + elseif self:Consume(TokenVariant.Operator, Operator.Amul) then self:Error("Multiplicative assignment operator (*=) must not be part of equation") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Adiv) then + elseif self:Consume(TokenVariant.Operator, Operator.Adiv) then self:Error("Divisive assignment operator (/=) must not be part of equation") end - self:TrackBack() + self.index = self.index - 1 end - return self:Expr2() -end + -- Ternary or Default + local cond = self:Expr2() + if self:Consume(TokenVariant.Operator, Operator.Qsm) then + local if_true = self:Expr() -function Parser:Expr2() - local expr = self:Expr3() + if not self:Consume(TokenVariant.Operator, Operator.Col) then -- perhaps we want to make sure there is space around this (method bug) + self:Error("Conditional operator (:) must appear after expression to complete conditional") + end - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Qsm) then - local trace = self:GetTokenTrace() - local exprtrue = self:Expr1() + local if_false = self:Expr() - if not self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then -- perhaps we want to make sure there is space around this (method bug) - self:Error("Conditional operator (:) must appear after expression to complete conditional", self:GetToken()) - end + return Node.new(NodeVariant.ExprTernary, { cond, if_true, if_false }, cond.trace:stitch(if_true.trace):stitch(if_false.trace)) + end - return self:Instruction(trace, "cnd", expr, exprtrue, self:Expr1()) + if self:Consume(TokenVariant.Operator, Operator.Def) then + local rhs = self:Expr() + return Node.new(NodeVariant.ExprDefault, { cond, rhs }, cond.trace:stitch(rhs.trace)) end - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Def) then - local trace = self:GetTokenTrace() + return cond +end - return self:Instruction(trace, "def", expr, self:Expr1()) +---@param func fun(self: Parser): Node +---@param variant NodeVariant +---@param tbl Operator[] +---@return Node +function Parser:RecurseLeft(func, variant, tbl) + local lhs, hit = func(self), true + while hit do + hit = false + for _, op in ipairs(tbl) do + if self:Consume(TokenVariant.Operator, op) then + local rhs = func(self) + hit, lhs = true, Node.new(variant, { lhs, op, rhs }, lhs.trace:stitch(rhs.trace)) + break + end + end end - return expr + return lhs +end + +function Parser:Expr2() + return self:RecurseLeft(self.Expr3, NodeVariant.ExprLogicalOp, { Operator.Or }) end function Parser:Expr3() - return self:RecurseLeft(self.Expr4, { Operator.Or }) + return self:RecurseLeft(self.Expr4, NodeVariant.ExprLogicalOp, { Operator.And }) end function Parser:Expr4() - return self:RecurseLeft(self.Expr5, { Operator.And }) + return self:RecurseLeft(self.Expr5, NodeVariant.ExprBinaryOp, { Operator.Bor }) end function Parser:Expr5() - return self:RecurseLeft(self.Expr6, { Operator.Bor }) + return self:RecurseLeft(self.Expr6, NodeVariant.ExprBinaryOp, { Operator.Band }) end function Parser:Expr6() - return self:RecurseLeft(self.Expr7, { Operator.Band }) + return self:RecurseLeft(self.Expr7, NodeVariant.ExprBinaryOp, { Operator.Bxor }) end function Parser:Expr7() - return self:RecurseLeft(self.Expr8, { Operator.Bxor }) + return self:RecurseLeft(self.Expr8, NodeVariant.ExprEquals, { Operator.Eq, Operator.Neq }) end function Parser:Expr8() - return self:RecurseLeft(self.Expr9, { Operator.Eq, Operator.Neq }) + return self:RecurseLeft(self.Expr9, NodeVariant.ExprComparison, { Operator.Gth, Operator.Lth, Operator.Geq, Operator.Leq }) end function Parser:Expr9() - return self:RecurseLeft(self.Expr10, { Operator.Gth, Operator.Lth, Operator.Geq, Operator.Leq }) + return self:RecurseLeft(self.Expr10, NodeVariant.ExprBitShift, { Operator.Bshr, Operator.Bshl }) end function Parser:Expr10() - return self:RecurseLeft(self.Expr11, { Operator.Bshr, Operator.Bshl }) + return self:RecurseLeft(self.Expr11, NodeVariant.ExprArithmetic, { Operator.Add, Operator.Sub }) end function Parser:Expr11() - return self:RecurseLeft(self.Expr12, { Operator.Add, Operator.Sub }) + return self:RecurseLeft(self.Expr12, NodeVariant.ExprArithmetic, { Operator.Mul, Operator.Div, Operator.Mod }) end function Parser:Expr12() - return self:RecurseLeft(self.Expr13, { Operator.Mul, Operator.Div, Operator.Mod }) + return self:RecurseLeft(self.Expr13, NodeVariant.ExprArithmetic, { Operator.Exp }) end +---@return Node function Parser:Expr13() - return self:RecurseLeft(self.Expr14, { Operator.Exp }) -end - -function Parser:Expr14() - if self:AcceptLeadingToken(TokenVariant.Operator, Operator.Add) then - return self:Expr15() - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Add) then + if self:ConsumeLeading(TokenVariant.Operator, Operator.Add) then + return self:Expr14() + elseif self:Consume(TokenVariant.Operator, Operator.Add) then self:Error("Identity operator (+) must not be succeeded by whitespace") end - if self:AcceptLeadingToken(TokenVariant.Operator, Operator.Sub) then - local trace = self:GetTokenTrace() - return self:Instruction(trace, "neg", self:Expr15()) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Sub) then + if self:ConsumeLeading(TokenVariant.Operator, Operator.Sub) then + local trace, exp = self:Prev().trace, self:Expr14() + return Node.new(NodeVariant.ExprUnaryOp, { Operator.Sub, exp }, trace:stitch(exp.trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Sub) then self:Error("Negation operator (-) must not be succeeded by whitespace") end - if self:AcceptLeadingToken(TokenVariant.Operator, Operator.Not) then - local trace = self:GetTokenTrace() - return self:Instruction(trace, "not", self:Expr14()) - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Not) then + if self:ConsumeLeading(TokenVariant.Operator, Operator.Not) then + local trace, exp = self:Prev().trace, self:Expr13() + return Node.new(NodeVariant.ExprUnaryOp, { Operator.Not, exp }, trace:stitch(exp.trace)) + elseif self:Consume(TokenVariant.Operator, Operator.Not) then self:Error("Logical not operator (!) must not be succeeded by whitespace") end - return self:Expr15() + return self:Expr14() end -function Parser:Expr15() - local expr = self:Expr16() - - while true do - if self:AcceptTailingToken(TokenVariant.Operator, Operator.Col) then - if not self:AcceptTailingToken(TokenVariant.LowerIdent) then - if self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Method operator (:) must not be preceded by whitespace") - else - self:Error("Method operator (:) must be followed by method name") - end - end - - local trace = self:GetTokenTrace() - local fun = self:GetTokenData() +---@return Node[] +function Parser:Arguments() + if not self:ConsumeTailing(TokenVariant.Grammar, Grammar.LParen) then + if self:Consume(TokenVariant.Grammar, Grammar.LParen) then + self:Error("Left parenthesis (() must not be preceded by whitespace") + else + self:Error("Left parenthesis (() must appear to start argument list") + end + end - if not self:AcceptTailingToken(TokenVariant.Grammar, Grammar.LParen) then - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis (() must not be preceded by whitespace") - else - self:Error("Left parenthesis (() must appear after method name") - end - end + local arguments = {} + if self:Consume(TokenVariant.Grammar, Grammar.RParen) then + return arguments + end - local token = self:GetToken() + repeat + arguments[#arguments + 1] = self:Expr() + until not self:Consume(TokenVariant.Grammar, Grammar.Comma) - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - expr = self:Instruction(trace, "methodcall", fun, expr, {}) - else - local exprs = { self:Expr1() } + if not self:Consume(TokenVariant.Grammar, Grammar.RParen) then + self:Error("Right parenthesis ()) missing, to close argument list") + end - while self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) do - exprs[#exprs + 1] = self:Expr1() - end + return arguments +end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close method argument list", token) - end +---@param start_bracket Grammar +---@param end_bracket Grammar +---@return { [1]: Node, [2]: Node }[]? +function Parser:ArgumentsKV(start_bracket, end_bracket) + local before = self.index + if not self:ConsumeTailing(TokenVariant.Grammar, start_bracket) then + if self:Consume(TokenVariant.Grammar, start_bracket) then + self:Error("Bracket must not be preceded by whitespace") + else + self.index = before + return + end + end - expr = self:Instruction(trace, "methodcall", fun, expr, exprs) - end - --elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - -- self:Error("Method operator (:) must not be preceded by whitespace") - elseif self:AcceptTailingToken(TokenVariant.Grammar, Grammar.LSquare) then - local trace = self:GetTokenTrace() + if self:Consume(TokenVariant.Grammar, end_bracket) then + return {} + end - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - self:Error("Indexing operator ([]) requires an index [X]") - end + local first = self:Expr(true) - local aexpr = self:Expr1() - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Indexing operator ([]) requires a lower case type [X,t]") - end + if not self:Consume(TokenVariant.Operator, Operator.Ass) then + self.index = before + return + else + local arguments = { { first, self:Expr() } } - local longtp = self:GetTokenData() + if self:Consume(TokenVariant.Grammar, end_bracket) then + return arguments + end - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - self:Error("Right square bracket (]) missing, to close indexing operator [X,t]") - end + while true do + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.Comma), "Expected comma (,) in between key value arguments" ) - if longtp == "number" then longtp = "normal" end - if wire_expression_types[string.upper(longtp)] == nil then - self:Error("Indexing operator ([]) does not support the type [" .. longtp .. "]") - end + local key = self:Expr() + self:Assert( self:Consume(TokenVariant.Operator, Operator.Ass), "Assignment operator (=) missing, to complete expression" ) + arguments[#arguments + 1] = {key, self:Expr()} - local tp = wire_expression_types[string.upper(longtp)][1] - expr = self:Instruction(trace, "get", expr, aexpr, tp) - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - expr = self:Instruction(trace, "get", expr, aexpr) - else - self:Error("Indexing operator ([]) needs to be closed with comma (,) or right square bracket (])") + if self:Consume(TokenVariant.Grammar, end_bracket) then + return arguments end - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LSquare) then - self:Error("Indexing operator ([]) must not be preceded by whitespace") - elseif self:AcceptTailingToken(TokenVariant.Grammar, Grammar.LParen) then - local trace = self:GetTokenTrace() - - local token = self:GetToken() - local exprs - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - exprs = {} - else - exprs = { self:Expr1() } + end + end +end - while self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) do - exprs[#exprs + 1] = self:Expr1() - end +---@return Node +function Parser:Expr14() + local expr = self:Expr15() - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close function argument list", token) + while true do + if self:ConsumeTailing(TokenVariant.Operator, Operator.Col) then + local fn = self:ConsumeTailing(TokenVariant.LowerIdent) + if not fn then + if self:Consume(TokenVariant.LowerIdent) then + self:Error("Method operator (:) must not be preceded by whitespace") + else + self:Error("Method operator (:) must be followed by method name") end end - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LSquare) then - if not self:AcceptRoamingToken(TokenVariant.LowerIdent) then - self:Error("Return type operator ([]) requires a lower case type [type]") - end + expr = Node.new(NodeVariant.ExprMethodCall, { expr, fn, self:Arguments() }, expr.trace:stitch(self:Prev().trace)) + else + local indices = self:Indices() + if #indices > 0 then + expr = Node.new(NodeVariant.ExprIndex, { expr, indices }, expr.trace:stitch(self:Prev().trace)) + elseif self:Consume(TokenVariant.Grammar, Grammar.LSquare) then + self:Error("Indexing operator ([]) must not be preceded by whitespace") + elseif self:ConsumeTailing(TokenVariant.Grammar, Grammar.LParen) then + self.index = self.index - 1 - local longtp = self:GetTokenData() + local args, typ = self:Arguments() - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - self:Error("Right square bracket (]) missing, to close return type operator [type]") - end + if self:Consume(TokenVariant.Grammar, Grammar.LSquare) then + typ = self:Assert(self:Type(), "Return type operator ([]) requires a lower case type [type]") - if longtp == "number" then longtp = "normal" end - if wire_expression_types[string.upper(longtp)] == nil then - self:Error("Return type operator ([]) does not support the type [" .. longtp .. "]") + if not self:Consume(TokenVariant.Grammar, Grammar.RSquare) then + self:Error("Right square bracket (]) missing, to close return type operator [type]") + end end - local stype = wire_expression_types[string.upper(longtp)][1] - - expr = self:Instruction(trace, "stringcall", expr, exprs, stype) + return Node.new(NodeVariant.ExprStringCall, { expr, args, typ }, expr.trace:stitch(self:Prev().trace)) else - expr = self:Instruction(trace, "stringcall", expr, exprs, "") + break end - else - break end end return expr end -function Parser:Expr16() - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - local token = self:GetToken() - - local expr = self:Expr1() - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close grouped equation", token) - end - +---@return Node +function Parser:Expr15() + if self:Consume(TokenVariant.Grammar, Grammar.LParen) then + local expr = self:Expr() + self:Assert( self:Consume(TokenVariant.Grammar, Grammar.RParen), "Right parenthesis ()) missing, to close grouped equation" ) return expr end - if self:AcceptRoamingToken(TokenVariant.LowerIdent) then - local trace = self:GetTokenTrace() - local fun = self:GetTokenData() - - if not self:AcceptTailingToken(TokenVariant.Grammar, Grammar.LParen) then - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LParen) then - self:Error("Left parenthesis (() must not be preceded by whitespace") - else - self:Error("Left parenthesis (() must appear after function name, variables must start with uppercase letter,") - end + local fn = self:Consume(TokenVariant.LowerIdent) + if fn then + -- Transform key value + if fn.value == "array" then + return Node.new(NodeVariant.ExprArray, self:ArgumentsKV(Grammar.LParen, Grammar.RParen) or self:Arguments(), fn.trace:stitch(self:Prev().trace)) + elseif fn.value == "table" then + return Node.new(NodeVariant.ExprTable, self:ArgumentsKV(Grammar.LParen, Grammar.RParen) or self:Arguments(), fn.trace:stitch(self:Prev().trace)) end - local token = self:GetToken() - - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - return self:Instruction(trace, "call", fun, {}) - else - - local exprs = {} - - -- Special case for "table( str=val, str=val, str=val, ... )" (or array) - if fun == "table" or fun == "array" then - local kvtable = false - - local key = self:Expr1() - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Expression expected, got right paranthesis ())", self:GetToken()) - end - - exprs[key] = self:Expr1() - - kvtable = true - else -- If it isn't a "table( str=val, ...)", then it's a "table( val,val,val,... )" - exprs = { key } - end - - if kvtable then - while self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) do - local key = self:Expr1() - local token = self:GetToken() - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - if self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Expression expected, got right paranthesis ())", self:GetToken()) - end - - exprs[key] = self:Expr1() - else - self:Error("Assignment operator (=) missing, to complete expression", token) - end - end - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close function argument list", self:GetToken()) - end - - return self:Instruction(trace, "kv" .. fun, exprs) - end - else - exprs = { self:Expr1() } - end - - while self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) do - exprs[#exprs + 1] = self:Expr1() - end - - if not self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) missing, to close function argument list", token) - end - - return self:Instruction(trace, "call", fun, exprs) - end + return Node.new(NodeVariant.ExprCall, { fn, self:Arguments() }, fn.trace:stitch(self:Prev().trace)) end - return self:Expr17() -end - -function Parser:Expr17() - -- Basic lua supported numeric literals (decimal, hex, binary) - if self:AcceptRoamingToken(TokenVariant.Decimal) or self:AcceptRoamingToken(TokenVariant.Hexadecimal) or self:AcceptRoamingToken(TokenVariant.Binary) then - return self:Instruction(self:GetTokenTrace(), "literal", self:GetTokenData(), "n") + -- Decimal / Hexadecimal / Binary numbers + local num = self:Consume(TokenVariant.Decimal) or self:Consume(TokenVariant.Hexadecimal) or self:Consume(TokenVariant.Binary) + if num then + return Node.new(NodeVariant.ExprLiteral, { "n", num.value }, num.trace) end - if self:AcceptRoamingToken(TokenVariant.Complex) or self:AcceptRoamingToken(TokenVariant.Quat) then - local trace = self:GetTokenTrace() - local tokendata = self:GetTokenData() - - local num, suffix = tokendata:match("^([-+e0-9.]*)(.*)$") - num = assert(tonumber(num), "unparseable numeric literal") + -- Complex / Quaternion numbers + local adv_num = self:Consume(TokenVariant.Complex) or self:Consume(TokenVariant.Quat) + if adv_num then + local num, suffix = adv_num.value:match("^([-+e0-9.]*)(.*)$") + num = self:Assert(tonumber(num), "Malformed numeric literal") local value, type if suffix == "" then value, type = num, "n" @@ -1517,181 +925,137 @@ function Parser:Expr17() elseif suffix == "k" then value, type = {0, 0, 0, num}, "q" else - error("unrecognized numeric suffix " .. suffix) - end - return self:Instruction(trace, "literal", value, type) - end - - if self:AcceptRoamingToken(TokenVariant.String) then - local trace = self:GetTokenTrace() - local str = self:GetTokenData() - - return self:Instruction(trace, "literal", str, "s") - end - - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Trg) then - local trace = self:GetTokenTrace() - - if not self:AcceptTailingToken(TokenVariant.Ident) then - if self:AcceptRoamingToken(TokenVariant.Ident) then - self:Error("Triggered operator (~) must not be succeeded by whitespace") - else - self:Error("Triggered operator (~) must be preceded by variable") - end + self:Error("unrecognized numeric suffix " .. suffix) end - local var = self:GetTokenData() - return self:Instruction(trace, "trg", var) + return Node.new(NodeVariant.ExprLiteral, { type, value }, adv_num.trace) end - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Dlt) then - local trace = self:GetTokenTrace() - - if not self:AcceptTailingToken(TokenVariant.Ident) then - if self:AcceptRoamingToken(TokenVariant.Ident) then - self:Error("Delta operator ($) must not be succeeded by whitespace") - else - self:Error("Delta operator ($) must be preceded by variable") - end - end - - local var = self:GetTokenData() - self.delta[var] = true - - return self:Instruction(trace, "dlt", var) + -- String + local str = self:Consume(TokenVariant.String) + if str then + return Node.new(NodeVariant.ExprLiteral, { "s", str.value }, str.trace) end - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Imp) then - local trace = self:GetTokenTrace() + -- Unary Wiremod Operators + for _, v in ipairs { { "~", Operator.Trg }, { "$", Operator.Dlt }, { "->", Operator.Imp } } do + local op = self:Consume(TokenVariant.Operator, v[2]) + if op then + local ident = self:ConsumeTailing(TokenVariant.Ident) + if not ident then + if self:Consume(TokenVariant.Ident) then + self:Error("Operator (" .. v[1] .. ") must not be succeeded by whitespace") + else + self:Error("Operator (" .. v[1] .. ") must be followed by variable") + end + end ---@cast ident Token # Know it isn't nil from above check - if not self:AcceptTailingToken(TokenVariant.Ident) then - if self:AcceptRoamingToken(TokenVariant.Ident) then - self:Error("Connected operator (->) must not be succeeded by whitespace") - else - self:Error("Connected operator (->) must be preceded by variable") - end + return Node.new(NodeVariant.ExprUnaryWire, { v[2], ident }, op.trace:stitch(ident.trace)) end - - local var = self:GetTokenData() - - return self:Instruction(trace, "iwc", var) end - return self:Expr18() -end - -function Parser:Expr18() - if self:AcceptRoamingToken(TokenVariant.Ident) then - if self:AcceptTailingToken(TokenVariant.Operator, Operator.Inc) then + -- Increment/Decrement + if self:Consume(TokenVariant.Ident) then + if self:ConsumeTailing(TokenVariant.Operator, Operator.Inc) then self:Error("Increment operator (++) must not be part of equation") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Inc) then + elseif self:Consume(TokenVariant.Operator, Operator.Inc) then self:Error("Increment operator (++) must not be preceded by whitespace") end - if self:AcceptTailingToken(TokenVariant.Operator, Operator.Dec) then + if self:ConsumeTailing(TokenVariant.Operator, Operator.Dec) then self:Error("Decrement operator (--) must not be part of equation") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Dec) then + elseif self:Consume(TokenVariant.Operator, Operator.Dec) then self:Error("Decrement operator (--) must not be preceded by whitespace") end - self:TrackBack() + self.index = self.index - 1 end - return self:Expr19() -end - -function Parser:Expr19() - if self:AcceptRoamingToken(TokenVariant.Ident) then - local trace = self:GetTokenTrace() - local var = self:GetTokenData() - return self:Instruction(trace, "var", var) + -- Variables + local ident = self:Consume(TokenVariant.Ident) + if ident then + return Node.new(NodeVariant.ExprIdent, ident, ident.trace) end - return self:ExprError() -end - -function Parser:ExprError() - if self:HasTokens() then - if self:AcceptRoamingToken(TokenVariant.Operator, Operator.Add) then - self:Error("Addition operator (+) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Sub) then -- can't occur (unary minus) - self:Error("Subtraction operator (-) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Mul) then - self:Error("Multiplication operator (*) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Div) then - self:Error("Division operator (/) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Mod) then - self:Error("Modulo operator (%) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Exp) then - self:Error("Exponentiation operator (^) must be preceded by equation or value") - - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Ass) then - self:Error("Assignment operator (=) must be preceded by variable") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Aadd) then - self:Error("Additive assignment operator (+=) must be preceded by variable") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Asub) then - self:Error("Subtractive assignment operator (-=) must be preceded by variable") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Amul) then - self:Error("Multiplicative assignment operator (*=) must be preceded by variable") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Adiv) then - self:Error("Divisive assignment operator (/=) must be preceded by variable") - - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.And) then - self:Error("Logical and operator (&) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Or) then - self:Error("Logical or operator (|) must be preceded by equation or value") - - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Eq) then - self:Error("Equality operator (==) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Neq) then - self:Error("Inequality operator (!=) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Gth) then - self:Error("Greater than or equal to operator (>=) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Lth) then - self:Error("Less than or equal to operator (<=) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Geq) then - self:Error("Greater than operator (>) must be preceded by equation or value") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Leq) then - self:Error("Less than operator (<) must be preceded by equation or value") - - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Inc) then - self:Error("Increment operator (++) must be preceded by variable") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Dec) then - self:Error("Decrement operator (--) must be preceded by variable") - - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RParen) then - self:Error("Right parenthesis ()) without matching left parenthesis") - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LCurly) then - self:Error("Left curly bracket ({) must be part of an if/while/for-statement block") - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RCurly) then - self:Error("Right curly bracket (}) without matching left curly bracket") - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.LSquare) then - self:Error("Left square bracket ([) must be preceded by variable") - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.RSquare) then - self:Error("Right square bracket (]) without matching left square bracket") - - elseif self:AcceptRoamingToken(TokenVariant.Grammar, Grammar.Comma) then - self:Error("Comma (,) not expected here, missing an argument?") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Col) then - self:Error("Method operator (:) must not be preceded by whitespace") - elseif self:AcceptRoamingToken(TokenVariant.Operator, Operator.Spread) then - self:Error("Spread operator (...) must only be used as a function parameter") - - elseif self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.If) then - self:Error("If keyword (if) must not appear inside an equation") - elseif self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Elseif) then - self:Error("Else-if keyword (elseif) must be part of an if-statement") - elseif self:AcceptRoamingToken(TokenVariant.Keyword, Keyword.Else) then - self:Error("Else keyword (else) must be part of an if-statement") - - - elseif self:AcceptRoamingToken(TokenVariant.Discard) then - self:Error("Discard (_) can only be used to discard function parameter") + -- Error Messages + if self:Eof() then + self:Error("Further input required at end of code, incomplete expression") + end - else - self:Error("Unexpected token found (" .. self.readtoken:display() .. ")") - end + if self:Consume(TokenVariant.Operator, Operator.Add) then + self:Error("Addition operator (+) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Sub) then -- can't occur (unary minus) + self:Error("Subtraction operator (-) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Mul) then + self:Error("Multiplication operator (*) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Div) then + self:Error("Division operator (/) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Mod) then + self:Error("Modulo operator (%) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Exp) then + self:Error("Exponentiation operator (^) must be preceded by equation or value") + + elseif self:Consume(TokenVariant.Operator, Operator.Ass) then + self:Error("Assignment operator (=) must be preceded by variable") + elseif self:Consume(TokenVariant.Operator, Operator.Aadd) then + self:Error("Additive assignment operator (+=) must be preceded by variable") + elseif self:Consume(TokenVariant.Operator, Operator.Asub) then + self:Error("Subtractive assignment operator (-=) must be preceded by variable") + elseif self:Consume(TokenVariant.Operator, Operator.Amul) then + self:Error("Multiplicative assignment operator (*=) must be preceded by variable") + elseif self:Consume(TokenVariant.Operator, Operator.Adiv) then + self:Error("Divisive assignment operator (/=) must be preceded by variable") + + elseif self:Consume(TokenVariant.Operator, Operator.And) then + self:Error("Logical and operator (&) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Or) then + self:Error("Logical or operator (|) must be preceded by equation or value") + + elseif self:Consume(TokenVariant.Operator, Operator.Eq) then + self:Error("Equality operator (==) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Neq) then + self:Error("Inequality operator (!=) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Gth) then + self:Error("Greater than or equal to operator (>=) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Lth) then + self:Error("Less than or equal to operator (<=) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Geq) then + self:Error("Greater than operator (>) must be preceded by equation or value") + elseif self:Consume(TokenVariant.Operator, Operator.Leq) then + self:Error("Less than operator (<) must be preceded by equation or value") + + elseif self:Consume(TokenVariant.Operator, Operator.Inc) then + self:Error("Increment operator (++) must be preceded by variable") + elseif self:Consume(TokenVariant.Operator, Operator.Dec) then + self:Error("Decrement operator (--) must be preceded by variable") + + elseif self:Consume(TokenVariant.Grammar, Grammar.RParen) then + self:Error("Right parenthesis ()) without matching left parenthesis") + elseif self:Consume(TokenVariant.Grammar, Grammar.LCurly) then + self:Error("Left curly bracket ({) must be part of an if/while/for-statement block") + elseif self:Consume(TokenVariant.Grammar, Grammar.RCurly) then + self:Error("Right curly bracket (}) without matching left curly bracket") + elseif self:Consume(TokenVariant.Grammar, Grammar.LSquare) then + self:Error("Left square bracket ([) must be preceded by variable") + elseif self:Consume(TokenVariant.Grammar, Grammar.RSquare) then + self:Error("Right square bracket (]) without matching left square bracket") + + elseif self:Consume(TokenVariant.Grammar, Grammar.Comma) then + self:Error("Comma (,) not expected here, missing an argument?") + elseif self:Consume(TokenVariant.Operator, Operator.Col) then + self:Error("Method operator (:) must not be preceded by whitespace") + elseif self:Consume(TokenVariant.Operator, Operator.Spread) then + self:Error("Spread operator (...) must only be used as a function parameter") + + elseif self:Consume(TokenVariant.Keyword, Keyword.If) then + self:Error("If keyword (if) must not appear inside an equation") + elseif self:Consume(TokenVariant.Keyword, Keyword.Elseif) then + self:Error("Else-if keyword (elseif) must be part of an if-statement") + elseif self:Consume(TokenVariant.Keyword, Keyword.Else) then + self:Error("Else keyword (else) must be part of an if-statement") else - self:Error("Further input required at end of code, incomplete expression", self.exprtoken) + self:Error("Unexpected token found (" .. self:At():display() .. ")", self:At().trace) end -end + + error("unreachable") +end \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/base/preprocessor.lua b/lua/entities/gmod_wire_expression2/base/preprocessor.lua index 75fe70d53d..85f27ceb14 100644 --- a/lua/entities/gmod_wire_expression2/base/preprocessor.lua +++ b/lua/entities/gmod_wire_expression2/base/preprocessor.lua @@ -1,41 +1,43 @@ --[[ - Expression 2 Pre-Processor for Garry's Mod - Andreas "Syranide" Svensson, me@syranide.com + Expression 2 Pre-Processor + Andreas "Syranide" Svensson, me@syranide.com ]] AddCSLuaFile() +local Warning, Error, Trace = E2Lib.Debug.Warning, E2Lib.Debug.Error, E2Lib.Debug.Trace + ---@class PreProcessor ---@field blockcomment boolean # Whether preprocessor is inside a block comment ---@field multilinestring boolean # Whether preprocessor is inside a multiline string ---@field readline integer ---@field warnings Warning[] +---@field errors Error[] local PreProcessor = {} PreProcessor.__index = PreProcessor E2Lib.PreProcessor = PreProcessor ----@return boolean ok ----@return table? directives ----@return string? newcode ----@return PreProcessor self -function PreProcessor.Execute(...) +---@overload fun(buffer: string, directives: PPDirectives, ent: userdata?): nil, Error[] +---@overload fun(buffer: string, directives: PPDirectives, ent: userdata?): boolean, PPDirectives, string, PreProcessor +function PreProcessor.Execute(buffer, directives, ent) -- instantiate PreProcessor local instance = setmetatable({}, PreProcessor) -- and pcall the new instance's Process method. - local ok, directives, newcode = xpcall(instance.Process, E2Lib.errorHandler, instance, ...) - return ok, directives, newcode, instance + local directives, newcode = instance:Process(buffer, directives, ent) + local ok = #instance.errors == 0 + return ok, ok and directives or instance.errors, newcode, instance end function PreProcessor:Error(message, column) - error(message .. " at line " .. self.readline .. ", char " .. (column or 1), 0) + self.errors[#self.errors + 1] = Error.new(message, Trace.new(self.readline, column or 1, self.readline, column or 1)) end ---@param message string ---@param column integer? function PreProcessor:Warning(message, column) - self.warnings[#self.warnings + 1] = { message = message, line = self.readline, char = column or 1 } + self.warnings[#self.warnings + 1] = Warning.new(message, Trace.new(self.readline, self.readline, column or 1, column or 1)) end local type_map = { @@ -204,7 +206,7 @@ local function handleIO(name) ports[1][index] = key -- Index: Name ports[2][index] = retval[2][i] -- Index: Type ports[3][key] = retval[2][i] -- Name: Type - ports[5][key] = { lines[i], columns[i] } -- Name: { Line, Column } + ports[5][key] = Trace.new(lines[i], columns[i], lines[i], columns[i]) -- Name: Trace end end end @@ -317,11 +319,19 @@ function PreProcessor:ParseDirectives(line) return "" end + +---@alias IODirective { [1]: string[], [2]: TypeSignature[], [3]: table, [4]: table, [5]: table } +---@alias PPDirectives { inputs: IODirective, outputs: IODirective, persist: IODirective, name: string?, model: string?, trigger: { [1]: boolean?, [2]: table }, strict: boolean? } + +---@param buffer string +---@param directives PPDirectives +---@param ent userdata? +---@return PPDirectives directives, string buf function PreProcessor:Process(buffer, directives, ent) -- entity is needed for autoupdate self.ent = ent self.ifdefStack = {} - self.warnings = {} + self.warnings, self.errors = {}, {} local lines = string.Explode("\n", buffer) @@ -332,7 +342,6 @@ function PreProcessor:Process(buffer, directives, ent) inputs = { {}, {}, {}, {}, {} }, -- 1: names, 2: types, 3: names=types lookup, 4: descriptions, 5: names={line, column} lookup outputs = { {}, {}, {}, {}, {} }, -- 1: names, 2: types, 3: names=types lookup, 4: descriptions, 5: names={line, column} lookup persist = { {}, {}, {}, nil, {} }, - delta = { {}, {}, {} }, trigger = { nil, {} }, } else @@ -357,7 +366,7 @@ function PreProcessor:Process(buffer, directives, ent) if self.directives.trigger[1] == nil then self.directives.trigger[1] = true end if not self.directives.name then self.directives.name = "" end - return self.directives, string.Implode("\n", lines) + return self.directives, table.concat(lines, "\n") end function PreProcessor:ParsePorts(ports, startoffset) @@ -365,12 +374,14 @@ function PreProcessor:ParsePorts(ports, startoffset) -- Preprocess [Foo Bar]:entity into [Foo,Bar]:entity so we don't have to deal with split-up multi-variable definitions in the main loop ports = ports:gsub("%[.-%]", function(s) - return s:gsub(" ", ",") + return string.Replace(s, " ", ",") end) - for column, key in ports:gmatch("()([^ ]+)") do + for column, key in ports:gmatch("()(%S+)") do + ---@cast column integer + ---@cast key string + column = startoffset + column - key = key:Trim() -------------------------------- variable names -------------------------------- @@ -385,19 +396,29 @@ function PreProcessor:ParsePorts(ports, startoffset) if not i then -- no -> malformed variable name self:Error("Variable name (" .. E2Lib.limitString(key, 10) .. ") must start with an uppercase letter", column) - end - -- yes -> add all variables. - for column2, var in namestring:gmatch("()([^,]+)") do - column2 = column + column2 - var = string.Trim(var) - -- skip empty entries - if var ~= "" then - -- error on malformed variable names - if not var:match("^[A-Z]") then self:Error("Variable name (" .. E2Lib.limitString(var, 10) .. ") must start with an uppercase letter", column2) end - local errcol = var:find("[^A-Za-z0-9_]") - if errcol then self:Error("Variable declaration (" .. E2Lib.limitString(var, 10) .. ") contains invalid characters", column2 + errcol - 1) end - -- and finally add the variable. - names[#names + 1] = var + goto cont + else + -- yes -> add all variables. + for column2, var in namestring:gmatch("()([^,]+)") do + column2 = column + column2 + var = string.Trim(var) + -- skip empty entries + if var ~= "" then + -- error on malformed variable names + if not var:match("^[A-Z]") then + self:Error("Variable name (" .. E2Lib.limitString(var, 10) .. ") must start with an uppercase letter", column2) + goto cont + else + local errcol = var:find("[^A-Za-z0-9_]") + if errcol then + self:Error("Variable declaration (" .. E2Lib.limitString(var, 10) .. ") contains invalid characters", column2 + errcol - 1) + goto cont + else + -- and finally add the variable. + names[#names + 1] = var + end + end + end end end end @@ -412,31 +433,34 @@ function PreProcessor:ParsePorts(ports, startoffset) if vtype ~= vtype:lower() then self:Error("Variable type [" .. E2Lib.limitString(vtype, 10) .. "] must be lowercase", column + i + 1) - end - - if vtype == "normal" then + goto cont + elseif vtype == "number" then + vtype = "normal" + elseif vtype == "normal" then self:Warning("Variable type [normal] is deprecated (use number instead)", column + i + 1) - else - if vtype == "number" then vtype = "normal" end - - if not wire_expression_types[vtype:upper()] then - self:Error("Unknown variable type [" .. E2Lib.limitString(vtype, 10) .. "] specified for variable(s) (" .. E2Lib.limitString(namestring, 10) .. ")", column + i + 1) - end end elseif character == "" then - -- type is not specified -> default to NORMAL - vtype = "NORMAL" + -- type is not specified -> default to number + vtype = "normal" else -- invalid -> raise an error self:Error("Variable declaration (" .. E2Lib.limitString(key, 10) .. ") contains invalid characters", column + i) + goto cont end -- fill in the missing types for i = #types + 1, #names do - types[i] = vtype:upper() - columns[i] = column - lines[i] = self.readline + local ty = wire_expression_types[vtype:upper()] + if ty then + types[i] = ty[1] + columns[i] = column + lines[i] = self.readline + else + self:Error("Unknown variable type [" .. E2Lib.limitString(vtype, 10) .. "]", column + i + 1) + end end + + ::cont:: end return { names, types }, columns, lines diff --git a/lua/entities/gmod_wire_expression2/base/tokenizer.lua b/lua/entities/gmod_wire_expression2/base/tokenizer.lua index 7979dd3c31..9f63785dfb 100644 --- a/lua/entities/gmod_wire_expression2/base/tokenizer.lua +++ b/lua/entities/gmod_wire_expression2/base/tokenizer.lua @@ -9,10 +9,13 @@ * boolean literals are reserved for later use. * internal representations are stored as enums for faster computation (operators, keywords, grammar) * emmylua annotations + * skips past errors ]] AddCSLuaFile() +local Trace, Warning, Error = E2Lib.Debug.Trace, E2Lib.Debug.Warning, E2Lib.Debug.Error + ---@class Tokenizer ---@field pos integer ---@field col integer @@ -22,30 +25,36 @@ AddCSLuaFile() local Tokenizer = {} Tokenizer.__index = Tokenizer +function Tokenizer.new() + return setmetatable({}, Tokenizer) +end + E2Lib.Tokenizer = Tokenizer ---@enum TokenVariant local TokenVariant = { - Hexadecimal = 1, - Binary = 2, - Decimal = 3, - Quat = 4, -- quat number (4j, 4k) - Complex = 5, -- complex number literal (4i) + Whitespace = 1, -- Used internally, won't be given to the parser. - String = 6, -- "foo" + Hexadecimal = 2, + Binary = 3, + Decimal = 4, + Quat = 5, -- quat number (4j, 4k) + Complex = 6, -- complex number literal (4i) - Boolean = 7, -- true, false + String = 7, -- "foo" - Grammar = 8, -- [] () {} , - Operator = 9, -- += + / * + Boolean = 8, -- true, false - Keyword = 10, - LowerIdent = 11, -- function_name + Grammar = 9, -- [] () {} , + Operator = 10, -- += + / * - Ident = 12, -- VariableName - Discard = 13, -- _ + Keyword = 11, + LowerIdent = 12, -- function_name - Constant = 14, -- _CONST + Ident = 13, -- VariableName + Discard = 14, -- _ + + Constant = 15, -- _CONST } Tokenizer.Variant = TokenVariant @@ -58,14 +67,11 @@ end Tokenizer.VariantLookup = VariantLookup ----@class Token +---@class Token: { value: T, variant: TokenVariant, whitespaced: boolean, trace: Trace } ---@field variant TokenVariant ----@field value number|string|boolean ---@field whitespaced boolean ----@field start_line integer ----@field end_line integer ----@field start_col integer ----@field end_col integer +---@field trace Trace +---@field value any local Token = {} Token.__index = Token @@ -73,9 +79,10 @@ Tokenizer.Token = Token --- Creates a new (partially filled) token --- Line, column and whitespaced need to be added manually. +---@generic T ---@param variant TokenVariant ----@param value number|string|boolean ----@return Token +---@param value T +---@return Token function Token.new(variant, value) return setmetatable({ variant = variant, value = value }, Token) end @@ -84,15 +91,16 @@ end ---@return string function Token:debug() if self.variant == TokenVariant.Operator then - return "Token { variant = " .. (VariantLookup[self.variant] or "nil") .. ", value = " .. (E2Lib.OperatorNames[self.value] or "nil") .. "}" + return string.format("Token { variant = %s, value = %s, trace = %s }", VariantLookup[self.variant], E2Lib.OperatorNames[self.value], self.trace) elseif self.variant == TokenVariant.Keyword then - return "Token { variant = " .. (VariantLookup[self.variant] or "nil") .. ", value = " .. (E2Lib.KeywordNames[self.value] or "nil") .. "}" + return string.format("Token { variant = %s, value = %s, trace = %s }", VariantLookup[self.variant], E2Lib.KeywordNames[self.value], self.trace) elseif self.variant == TokenVariant.Grammar then - return "Token { variant = " .. (VariantLookup[self.variant] or "nil") .. ", value = " .. (E2Lib.GrammarNames[self.value] or "nil") .. "}" + return string.format("Token { variant = %s, value = %s, trace = %s }", VariantLookup[self.variant], E2Lib.GrammarNames[self.value], self.trace) else - return "Token { variant = " .. (VariantLookup[self.variant] or "nil") .. ", value = " .. (self.value or "nil") .. "}" + return string.format("Token { variant = %s, value = %s, trace = %s }", VariantLookup[self.variant], self.value, self.trace) end end +Token.__tostring = Token.debug --- Returns the 'name' of a token for passing to a user. ---@return string @@ -109,35 +117,28 @@ function Token:display() end ---@return boolean ok ----@return Token[] +---@return Token[]|Error[] tokens_or_errors ---@return Tokenizer self function Tokenizer.Execute(code) - -- instantiate Tokenizer - local instance = setmetatable({}, Tokenizer) + local instance = Tokenizer.new() + local tokens = instance:Process(code) - -- and pcall the new instance's Process method. - local ok, tokens = xpcall(Tokenizer.Process, E2Lib.errorHandler, instance, code) - return ok, tokens, instance -end - -function Tokenizer:Reset() - self.pos = 1 - self.col = 1 - self.line = 1 - self.code = nil - self.warnings = {} + local ok = #instance.errors == 0 + return ok, ok and tokens or instance.errors, instance end ---@param message string ----@param offset integer? -function Tokenizer:Error(message, offset) - error(message .. " at line " .. self.line .. ", char " .. (self.col + (offset or 0)), 0) +---@param trace Trace? +---@return boolean false +function Tokenizer:Error(message, trace) + self.errors[#self.errors + 1] = Error.new( message, trace or self:GetTrace() ) + return false end ---@param message string ----@param offset integer? -function Tokenizer:Warning(message, offset) - self.warnings[#self.warnings + 1] = { message = message, line = self.line, char = (self.col + (offset or 0)) } +---@param trace Trace? +function Tokenizer:Warning(message, trace) + self.warnings[#self.warnings + 1] = Warning.new( message, trace or self:GetTrace() ) end local Escapes = { @@ -152,44 +153,64 @@ local Escapes = { ['v'] = '\v' } ----@return Token? +---@return Token|nil|boolean # Either a token, `nil` for unexpected character, or `false` for error. function Tokenizer:Next() - local match = self:ConsumePattern("^0x[0-9A-F]+") + local match = self:ConsumePattern("^%s+", true) + if match then + return Token.new(TokenVariant.Whitespace, match) + end + match = self:ConsumePattern("^0x") if match then - local val = tonumber(match) or self:Error("Invalid number format (" .. E2Lib.limitString(match, 10) .. ")") - return Token.new(TokenVariant.Hexadecimal, val) + local nums = self:ConsumePattern("^%x+") + if nums then + local val = tonumber(nums, 16) + if val then + return Token.new(TokenVariant.Hexadecimal, val) + end + end + + return self:Error("Malformed hexadecimal number") end - match = self:ConsumePattern("^0b[0-1]+") + match = self:ConsumePattern("^0b") if match then - local val = tonumber( match:sub(3), 2 ) or self:Error("Invalid number format (" .. E2Lib.limitString(match, 10) .. ")") - return Token.new(TokenVariant.Binary, val) + local nums = self:ConsumePattern("^[0-1]+") + if nums then + return Token.new(TokenVariant.Binary, tonumber(nums, 2)) + elseif self:ConsumePattern("^%w+") then + return self:Error("Malformed binary number") + else + return self:Error("No valid digits found for number") + end end match = self:ConsumePattern("^[0-9]+%.?[0-9]*[eE][+-]?[0-9]+") if match then -- Decimal number with exponent part - local val = tonumber(match) or self:Error("Invalid number format (" .. E2Lib.limitString(match, 10) .. ")") - return Token.new(TokenVariant.Decimal, val) + local val = tonumber(match) + if val then + return Token.new(TokenVariant.Decimal, val) + end + + return self:Error("Malformed decimal number") end match = self:ConsumePattern("^[0-9]+%.?[0-9]*[jk]") if match then -- Quaternion number - local badmatch = self:ConsumePattern("^[a-zA-Z_]") - if badmatch then - self:Error("Invalid number format (" .. E2Lib.limitString(match .. badmatch, 10) .. ")") + if self:ConsumePattern("^[a-zA-Z_]") then + self:Error("Malformed quaternion literal") end + return Token.new(TokenVariant.Quat, match) end match = self:ConsumePattern("^[0-9]+%.?[0-9]*i") if match then -- Complex number - local badmatch = self:ConsumePattern("^[a-zA-Z_]") - if badmatch then - self:Error("Invalid number format (" .. E2Lib.limitString(match .. badmatch, 10) .. ")") + if self:ConsumePattern("^[a-zA-Z_]") then + self:Error("Malformed complex number literal") end return Token.new(TokenVariant.Complex, match) end @@ -197,8 +218,12 @@ function Tokenizer:Next() match = self:ConsumePattern("^[0-9]+%.?[0-9]*") if match then -- Decimal number - local val = tonumber(match) or self:Error("Invalid number format (" .. E2Lib.limitString(match, 10) .. ")") - return Token.new(TokenVariant.Decimal, val) + local val = tonumber(match) + if val then + return Token.new(TokenVariant.Decimal, val) + end + + self:Error("Malformed decimal number") end match = self:ConsumePattern("^[a-z#][a-zA-Z0-9_]*") @@ -210,13 +235,7 @@ function Tokenizer:Next() return Token.new(TokenVariant.Boolean, true) elseif match == "false" then return Token.new(TokenVariant.Boolean, false) - elseif match == "k" or match == "j" then - self:Warning("Avoid using quaternion literal '" .. match .. "' on its own. (Use 1" .. match .. " instead)") - return Token.new(TokenVariant.Quat, "1" .. match) - elseif match == "i" then - -- self:Warning("Avoid using complex literal 'i' on its own. (Use 1i instead)") - return Token.new(TokenVariant.Complex, "1i") - else + elseif match:sub(1, 1) ~= "#" then return Token.new(TokenVariant.LowerIdent, match) end end @@ -231,12 +250,12 @@ function Tokenizer:Next() -- Constant value local value = wire_expression2_constants[match] - if isnumber(value) then + if type(value) == "number" then return Token.new(TokenVariant.Decimal, value) - elseif isstring(value) then + elseif type(value) == "string" then return Token.new(TokenVariant.String, value) else - self:Error("Constant (" .. match .. ") has invalid data type (" .. type(value) .. ")") + return self:Error("Constant (" .. match .. ") has invalid data type (" .. type(value) .. ")") end end @@ -244,7 +263,7 @@ function Tokenizer:Next() -- A discard is used to signal intent that something is intentionally not used. -- This is mainly to avoid warnings for unused variables from events or functions. -- You are not allowed to actually use the discard anywhere but in a signature, since you can have multiple in the signature. - return Token.new(TokenVariant.Discard, "_") + return Token.new(TokenVariant.Ident, "_") end if self:At() == "\"" then @@ -276,7 +295,8 @@ function Tokenizer:Next() buffer[nbuffer] = c end else - self:Error("Missing \" to end string") + self:ConsumePattern("^.*", true) + return self:Error("Missing \" to end string") end end @@ -312,6 +332,11 @@ function Tokenizer:At() return self.code:sub(self.pos, self.pos) end +---@return string? +function Tokenizer:Prev() + return self.code:sub(self.pos - 1, self.pos - 1) +end + ---@return string? function Tokenizer:PeekChar() return self.code:sub(self.pos + 1, self.pos + 1) @@ -369,34 +394,47 @@ end ---@return Token[] function Tokenizer:Process(code) - self:Reset() + self.pos, self.col, self.line, self.code, self.warnings, self.errors = 1, 1, 1, code, {}, {} local length = #code - local tokens, ntok = {}, 0 - self.code = code + local tokens, ntok, error, nerror = {}, 0, {}, 0 - local line, col - while self.pos <= length do - local whitespaced = self:ConsumePattern("^%s+", true) ~= nil - line, col = self.line, self.col + local line, col, whitespaced = 1, 1, false - if self.pos > length then - break - end + function self:GetTrace() + return Trace.new(line, col, self.line, self.col) + end + while self.pos <= length do local tok = self:Next() - if not tok then - self:Error("Failed to parse token") - end - tok.start_line, tok.start_col = line, col - tok.end_line, tok.end_col = self.line, self.col - tok.whitespaced = whitespaced + if tok == nil then + nerror = nerror + 1 + error[nerror] = self:Prev() + elseif tok ~= false then + if nerror ~= 0 then + self:Error("Unexpected symbol '" .. table.concat(error, '', 1, nerror) .. "'", Trace.new(line, col, line, col + nerror)) + nerror = 0 + end + + if tok.variant == TokenVariant.Whitespace then + line, col, whitespaced = self.line, self.col, true + else + tok.trace = Trace.new(line, col, self.line, self.col) + tok.whitespaced = whitespaced + + line, col = self.line, self.col - line, col = self.line, self.col + ntok = ntok + 1 + tokens[ntok] = tok + + whitespaced = false + end + end + end - ntok = ntok + 1 - tokens[ntok] = tok + if nerror ~= 0 then + self:Error("Unexpected symbol '" .. table.concat(error, '', 1, nerror) .. "'", Trace.new(line, col, line, col + nerror)) end return tokens diff --git a/lua/entities/gmod_wire_expression2/cl_init.lua b/lua/entities/gmod_wire_expression2/cl_init.lua index d9280b471e..c6c60e9438 100644 --- a/lua/entities/gmod_wire_expression2/cl_init.lua +++ b/lua/entities/gmod_wire_expression2/cl_init.lua @@ -1,79 +1,91 @@ include('shared.lua') +local Trace, Error = E2Lib.Debug.Trace, E2Lib.Debug.Error + +---@param e2 string +---@param directives PPDirectives +---@param includes table +---@param scripts table +---@return Error[]? local function Include(e2, directives, includes, scripts) if scripts[e2] then return end + local errors = {} local code = file.Read("expression2/" .. e2 .. ".txt") - -- removed CLIENT as this is client file - -- local code - -- if CLIENT the code = file.Read("expression2/" .. e2 .. ".txt") if not code then - return false, "Could not find include '" .. e2 .. ".txt'" + return { Error.new("Could not find include '" .. e2 .. ".txt'") } end local status, err, buffer = E2Lib.PreProcessor.Execute(code, directives) if not status then - return "include '" .. e2 .. "' -> " .. err + table.Add(errors, err) end local status, tokens = E2Lib.Tokenizer.Execute(buffer) if not status then - return "include '" .. e2 .. "' -> " .. tokens + table.Add(errors, tokens) end local status, tree, dvars, files = E2Lib.Parser.Execute(tokens) if not status then - return "include '" .. e2 .. "' -> " .. tree + table.insert(errors, tree) + return errors end includes[e2] = code scripts[e2] = { tree } - for i = 1, #files do - local error = Include(files[i], directives, includes, scripts) - if error then return error end + for i, file in ipairs(files) do + local ierrors = Include(file, directives, includes, scripts) + if ierrors then table.Add(errors, ierrors) end end + + if #errors ~= 0 then return errors end end -function wire_expression2_validate(buffer) - if not e2_function_data_received then return "Loading extensions. Please try again in a few seconds..." end +---@param buffer string +---@return Error[]?, table[]?, Warning[]? +function E2Lib.Validate(buffer) + if not e2_function_data_received then return { Error.new("Loading extensions. Please try again in a few seconds...") } end - ---@type Warning[] - local warnings = {} + ---@type Warning[], Error[] + local warnings, errors = {}, {} -- invoke preprocessor local status, directives, buffer, preprocessor = E2Lib.PreProcessor.Execute(buffer) - if not status then return directives end + if not status then table.Add(errors, directives) end + ---@cast directives PPDirectives table.Add(warnings, preprocessor.warnings) -- decompose directives - local inports, outports, persists = directives.inputs, directives.outputs, directives.persist RunConsoleCommand("wire_expression2_scriptmodel", directives.model or "") -- invoke tokenizer (=lexer) local status, tokens, tokenizer = E2Lib.Tokenizer.Execute(buffer) - if not status then return tokens end + if not status then table.Add(errors, tokenizer.errors) return errors end table.Add(warnings, tokenizer.warnings) -- invoke parser local status, tree, dvars, files, parser = E2Lib.Parser.Execute(tokens) - if not status then return tree end + if not status then table.insert(errors, tree) return errors end table.Add(warnings, parser.warnings) -- prepare includes - local includes, scripts = {}, {} - for i = 1, #files do - local error = Include(files[i], directives, includes, scripts) - if error then return error end + local includes, scripts = {}, {} ---@type table, Node[] + for i, file in ipairs(files) do + local ierrors = Include(file, directives, includes, scripts) + if ierrors then table.Add(errors, ierrors) end end + if not table.IsEmpty(errors) then return errors end + -- invoke compiler - local status, script, compiler = E2Lib.Compiler.Execute(tree, inports, outports, persists, dvars, scripts) - if not status then return script end + local status, script, compiler = E2Lib.Compiler.Execute(tree, directives, dvars, scripts) + if not status then table.insert(errors, script) return errors end -- Need to do this manually since table.Add loses its mind with non-numeric keys (and compiler can emit warnings per include file) (should be refactored out at some point to just having warnings separated per include) local nwarnings = #warnings @@ -81,7 +93,7 @@ function wire_expression2_validate(buffer) warnings[nwarnings + k] = warning end - return nil, includes, #warnings ~= 0 and warnings + return nil, includes, #warnings ~= 0 and warnings, compiler end -- string.GetTextSize shits itself if the string is both wide and tall, diff --git a/lua/entities/gmod_wire_expression2/core/angle.lua b/lua/entities/gmod_wire_expression2/core/angle.lua index 1d801683a5..0e5147431f 100644 --- a/lua/entities/gmod_wire_expression2/core/angle.lua +++ b/lua/entities/gmod_wire_expression2/core/angle.lua @@ -40,75 +40,40 @@ e2function angle ang(vector rv1) return Angle(rv1[1], rv1[2], rv1[3]) end -/******************************************************************************/ - -registerOperator("ass", "a", "a", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if !lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - -/******************************************************************************/ - -e2function number operator_is(angle rv1) - if rv1[1] ~= 0 or rv1[2] ~= 0 or rv1[3] ~= 0 - then return 1 else return 0 end +e2function number operator_is(angle this) + return this:IsZero() and 0 or 1 end -__e2setcost(3) - -e2function number operator==(angle rv1, angle rv2) - if rv1[1] - rv2[1] <= delta and rv2[1] - rv1[1] <= delta and - rv1[2] - rv2[2] <= delta and rv2[2] - rv1[2] <= delta and - rv1[3] - rv2[3] <= delta and rv2[3] - rv1[3] <= delta - then return 1 else return 0 end -end - -e2function number operator!=(angle rv1, angle rv2) - if rv1[1] - rv2[1] > delta or rv2[1] - rv1[1] > delta or - rv1[2] - rv2[2] > delta or rv2[2] - rv1[2] > delta or - rv1[3] - rv2[3] > delta or rv2[3] - rv1[3] > delta - then return 1 else return 0 end -end +__e2setcost(1) -e2function number operator>=(angle rv1, angle rv2) - if rv2[1] - rv1[1] <= delta and - rv2[2] - rv1[2] <= delta and - rv2[3] - rv1[3] <= delta - then return 1 else return 0 end +e2function number operator>=(angle lhs, angle rhs) + return (lhs[1] >= rhs[1] + and lhs[2] >= rhs[2] + and lhs[3] >= rhs[3]) + and 1 or 0 end -e2function number operator<=(angle rv1, angle rv2) - if rv1[1] - rv2[1] <= delta and - rv1[2] - rv2[2] <= delta and - rv1[3] - rv2[3] <= delta - then return 1 else return 0 end +e2function number operator<=(angle lhs, angle rhs) + return (lhs[1] <= rhs[1] + and lhs[2] <= rhs[2] + and lhs[3] <= rhs[3]) + and 1 or 0 end -e2function number operator>(angle rv1, angle rv2) - if rv1[1] - rv2[1] > delta and - rv1[2] - rv2[2] > delta and - rv1[3] - rv2[3] > delta - then return 1 else return 0 end +e2function number operator>(angle lhs, angle rhs) + return (lhs[1] > rhs[1] + and lhs[2] > rhs[2] + and lhs[3] > rhs[3]) + and 1 or 0 end -e2function number operator<(angle rv1, angle rv2) - if rv2[1] - rv1[1] > delta and - rv2[2] - rv1[2] > delta and - rv2[3] - rv1[3] > delta - then return 1 else return 0 end +e2function number operator<(angle lhs, angle rhs) + return (lhs[1] < rhs[1] + and lhs[2] < rhs[2] + and lhs[3] < rhs[3]) + and 1 or 0 end -/******************************************************************************/ - __e2setcost(2) e2function angle operator_neg(angle rv1) @@ -164,15 +129,14 @@ e2function angle operator/(angle rv1, angle rv2) return Angle( rv1[1] / rv2[1], rv1[2] / rv2[2], rv1[3] / rv2[3] ) end -e2function number angle:operator[](index) +registerOperator("indexget", "an", "n", function(state, this, index) return this[floor(math.Clamp(index, 1, 3) + 0.5)] -end +end) -e2function number angle:operator[](index, value) +registerOperator("indexset", "ann", "", function(state, this, index, value) this[floor(math.Clamp(index, 1, 3) + 0.5)] = value - self.GlobalScope.vclk[this] = true - return value -end + state.GlobalScope.vclk[this] = true +end) e2function string operator+(string lhs, angle rhs) self.prf = self.prf + #lhs * 0.01 diff --git a/lua/entities/gmod_wire_expression2/core/array.lua b/lua/entities/gmod_wire_expression2/core/array.lua index 051797185f..6ceec659d9 100644 --- a/lua/entities/gmod_wire_expression2/core/array.lua +++ b/lua/entities/gmod_wire_expression2/core/array.lua @@ -55,46 +55,12 @@ e2function array array(...args) return args end -registerOperator( "kvarray", "", "r", function( self, args ) - local ret = {} - local values = args[2] - - for k, v in pairs( values ) do - local key = k[1]( self, k ) - local value = v[1]( self, v ) - - ret[key] = value - - self.prf = self.prf + 1/3 - end - - return ret -end) - --------------------------------------------------------------------------------- --- = operator --------------------------------------------------------------------------------- -registerOperator("ass", "r", "r", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if !lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - - - -------------------------------------------------------------------------------- -- IS operator -------------------------------------------------------------------------------- -e2function number operator_is(array arr) - return istable(arr) and 1 or 0 + +e2function number operator_is(array this) + return istable(this) and 1 or 0 end -------------------------------------------------------------------------------- @@ -117,7 +83,7 @@ registerCallback( "postinit", function() -- Get functions -- value = R[N,type], and value = R:(N) -------------------------------------------------------------------------------- - __e2setcost(5) + __e2setcost(1) local function getter( self, array, index, doremove ) if (!array or !index) then return fixDefault( default ) end -- Make sure array and index are value @@ -132,13 +98,24 @@ registerCallback( "postinit", function() return ret end - registerOperator("idx", id.."=rn", id, function(self,args) - local op1, op2 = args[2], args[3] - local array, index = op1[1](self,op1), op2[1](self,op2) - return getter( self, array, index ) - end) + if typecheck then + registerOperator("indexget", "rn" .. id, id, function(self, array, index) + local ret = array[floor(index)] + if typecheck(ret) then + return fixDefault(default) + end + + return ret + end) + else + registerOperator("indexget", "rn" .. id, id, function(self, array, index) + return array[floor(index)] + end) + end + + __e2setcost(5) - registerFunction( name, "r:n", id, function(self,args) + registerFunction( name, "r:n", id, function(self, args) local op1, op2 = args[2], args[3] local array, index = op1[1](self,op1), op2[1](self,op2) return getter( self, array, index ) @@ -162,11 +139,21 @@ registerCallback( "postinit", function() return value end - registerOperator("idx", id.."=rn"..id, id, function(self,args) - local op1, op2, op3 = args[2], args[3], args[4] - local array, index, value = op1[1](self,op1), op2[1](self,op2), op3[1](self,op3) - return setter( self, array, index, value ) - end) + if typecheck then + registerOperator("indexset", "rn" .. id, id, function(self, array, index, value) + if typecheck(value) then + return fixDefault(default) + end + + array[floor(index)] = value + self.GlobalScope.vclk[array] = true + end, 2) + else + registerOperator("indexset", "rn" .. id, id, function(self, array, index, value) + array[floor(index)] = value + self.GlobalScope.vclk[array] = true + end, 1) + end registerFunction("set" .. nameupperfirst, "r:n"..id, id, function(self,args) local op1, op2, op3 = args[2], args[3], args[4] @@ -245,35 +232,16 @@ registerCallback( "postinit", function() -------------------------------------------------------------------------------- __e2setcost(0) - registerOperator("fea", "n" .. id .. "r", "", function(self, args) - local keyname, valname = args[2], args[3] - - local tbl = args[4] - tbl = tbl[1](self, tbl) - - local statement = args[5] - - for key, value in pairs(tbl) do - if not typecheck(value) then - self:PushScope() - - self.prf = self.prf + 3 - - self.Scope.vclk[keyname] = true - self.Scope.vclk[valname] = true - - self.Scope[keyname] = key - self.Scope[valname] = value - - local ok, msg = pcall(statement[1], self, statement) - - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end + local function iter(tbl, i) + local v = tbl[i + 1] + if not typecheck(v) then + return i + 1, v + end + end - self:PopScope() - end + registerOperator("iter", "n" .. id .. "=r", "", function(state, array) + return function() + return iter, array, 0 end end) diff --git a/lua/entities/gmod_wire_expression2/core/bone.lua b/lua/entities/gmod_wire_expression2/core/bone.lua index aa0919c15a..5906167b6f 100644 --- a/lua/entities/gmod_wire_expression2/core/bone.lua +++ b/lua/entities/gmod_wire_expression2/core/bone.lua @@ -94,27 +94,8 @@ registerType("bone", "b", nil, __e2setcost(1) --- if (B) -e2function number operator_is(bone b) - if isValidBone(b) then return 1 else return 0 end -end - ---- B = B -registerOperator("ass", "b", "b", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - ---- B == B -e2function number operator==(bone lhs, bone rhs) - if lhs == rhs then return 1 else return 0 end -end - ---- B != B -e2function number operator!=(bone lhs, bone rhs) - if lhs ~= rhs then return 1 else return 0 end +e2function number operator_is(bone this) + return isValidBone(this) and 1 or 0 end --[[************************************************************************]]-- @@ -260,7 +241,7 @@ e2function number bone:elevation(vector pos) pos = this:WorldToLocal(Vector(pos[1],pos[2],pos[3])) local len = pos:Length() - if len < delta then return 0 end + if len < 0 then return 0 end return rad2deg*asin(pos.z / len) end @@ -275,7 +256,7 @@ e2function angle bone:heading(vector pos) -- elevation local len = pos:Length()--sqrt(x*x + y*y + z*z) - if len < delta then return Angle(0, bearing, 0) end + if len < 0 then return Angle(0, bearing, 0) end local elevation = rad2deg*asin(pos.z / len) return Angle(elevation, bearing, 0) diff --git a/lua/entities/gmod_wire_expression2/core/complex.lua b/lua/entities/gmod_wire_expression2/core/complex.lua index 5fddbf5c39..87de8cb4af 100644 --- a/lua/entities/gmod_wire_expression2/core/complex.lua +++ b/lua/entities/gmod_wire_expression2/core/complex.lua @@ -18,18 +18,18 @@ local atan2 = math.atan2 local function format(value) local dbginfo - if abs(value[1]) < delta then - if abs(value[2]) < delta then + if abs(value[1]) < 0 then + if abs(value[2]) < 0 then dbginfo = "0" else dbginfo = Round(value[2]*1000)/1000 .. "i" end else - if value[2] > delta then + if value[2] > 0 then dbginfo = Round(value[1]*1000)/1000 .. "+" .. Round(value[2]*1000)/1000 .. "i" - elseif abs(value[2]) <= delta then + elseif abs(value[2]) <= 0 then dbginfo = Round(value[1]*1000)/1000 - elseif value[2] < -delta then + elseif value[2] < 0 then dbginfo = Round(value[1]*1000)/1000 .. Round(value[2]*1000)/1000 .. "i" end end @@ -66,7 +66,7 @@ local function clog(x,y) l = x*x+y*y - if l < delta then return {-1e+100, 0} end + if l < 0 then return {-1e+100, 0} end r = log(sqrt(l)) @@ -89,59 +89,26 @@ end __e2setcost(2) -registerOperator("ass", "c", "c", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - self.Scopes[scope][lhs] = rhs - self.Scopes[scope].vclk[lhs] = true - return rhs -end) - -e2function number operator_is(complex z) - if (z[1]==0) and (z[2]==0) then return 0 else return 1 end +e2function number operator_is(complex this) + return (this[1] ~= 0 or this[2] ~= 0) and 1 or 0 end e2function number operator==(complex lhs, complex rhs) - if abs(lhs[1]-rhs[1])<=delta and - abs(lhs[2]-rhs[2])<=delta then - return 1 - else return 0 end + return (lhs[1] == rhs[1] + and lhs[2] == rhs[2]) + and 1 or 0 end e2function number operator==(complex lhs, number rhs) - if abs(lhs[1]-rhs)<=delta and - abs(lhs[2])<=delta then - return 1 - else return 0 end + return (lhs[1] == rhs + and lhs[2] == 0) + and 1 or 0 end e2function number operator==(number lhs, complex rhs) - if abs(lhs-rhs[1])<=delta and - abs(rhs[2])<=delta then - return 1 - else return 0 end -end - -e2function number operator!=(complex lhs, complex rhs) - if abs(lhs[1]-rhs[1])>delta or - abs(lhs[2]-rhs[2])>delta then - return 1 - else return 0 end -end - -e2function number operator!=(complex lhs, number rhs) - if abs(lhs[1]-rhs)>delta or - abs(lhs[2])>delta then - return 1 - else return 0 end -end - -e2function number operator!=(number lhs, complex rhs) - if abs(lhs-rhs[1])>delta or - abs(rhs[2])>delta then - return 1 - else return 0 end + return (lhs == rhs[1] + and rhs[2] == 0) + and 1 or 0 end /******************************************************************************/ diff --git a/lua/entities/gmod_wire_expression2/core/core.lua b/lua/entities/gmod_wire_expression2/core/core.lua index 5e6c9d17a0..8cecb13c3a 100644 --- a/lua/entities/gmod_wire_expression2/core/core.lua +++ b/lua/entities/gmod_wire_expression2/core/core.lua @@ -2,247 +2,11 @@ -- Core language support -------------------------------------------------------------------------------- -local delta = wire_expression2_delta - -__e2setcost(1) -- approximation - -local fix_default = E2Lib.fixDefault -registerOperator("dat", "", "", function(self, args) - return fix_default(args[2]) -end) - -__e2setcost(2) -- approximation - -registerOperator("var", "", "", function(self, args) - local op1, scope = args[2], args[3] - return self.Scopes[scope][op1] -end) - --------------------------------------------------------------------------------- - -__e2setcost(0) - -registerOperator("seq", "", "", function(self, args) - self.prf = self.prf + args[2] - - if self.prf > e2_tickquota then error("perf", 0) end - - local n = #args - if n == 2 then return end - - for i = 3, n-1 do - local op = args[i] - self.trace = op.Trace - op[1](self, op) - end - - local op = args[n] - self.trace = op.Trace - return op[1](self, op) -end) - --------------------------------------------------------------------------------- - -__e2setcost(0) -- approximation - -registerOperator("whl", "", "", function(self, args) - local op1, op2 = args[2], args[3] - local skipCond = args[5] -- skipCondFirstTime - - self.prf = self.prf + args[4] + 3 - while skipCond or (op1[1](self, op1) ~= 0) do - self:PushScope() - skipCond = false - - local ok, msg = pcall(op2[1], self, op2) - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end - - self.prf = self.prf + args[4] + 3 - self:PopScope() - end -end) - -registerOperator("for", "", "", function(self, args) - local var, op1, op2, op3, op4 = args[2], args[3], args[4], args[5], args[6] - - local rstart, rend, rstep - rstart = op1[1](self, op1) - rend = op2[1](self, op2) - local rdiff = rend - rstart - local rdelta = delta - - if op3 then - rstep = op3[1](self, op3) - - if rdiff > -delta then - if rstep < delta and rstep > -delta then return end - elseif rdiff < delta then - if rstep > -delta then return end - else - return - end - - if rstep < 0 then - rdelta = -delta - end - else - if rdiff > -delta then - rstep = 1 - else - return - end - end - - self.prf = self.prf + 3 - for I=rstart,rend+rdelta,rstep do - self:PushScope() - self.Scope[var] = I - self.Scope.vclk[var] = true - - local ok, msg = pcall(op4[1], self, op4) - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end - - self.prf = self.prf + 3 - self:PopScope() - end - -end) - -__e2setcost(2) -- approximation - -registerOperator("brk", "", "", function(self, args) - error("break", 0) -end) - -registerOperator("cnt", "", "", function(self, args) - error("continue", 0) -end) - --------------------------------------------------------------------------------- - -__e2setcost(3) -- approximation - -registerOperator("if", "n", "", function(self, args) - local op1 = args[3] - self.prf = self.prf + args[2] - - local ok, result - - if op1[1](self, op1) ~= 0 then - self:PushScope() - local op2 = args[4] - ok, result = pcall(op2[1],self, op2) - else - self:PushScope() -- for else statments, elseif staments will run the if opp again - local op3 = args[5] - ok, result = pcall(op3[1],self, op3) - end - - self:PopScope() - if not ok then - error(result,0) - end -end) - -registerOperator("def", "n", "", function(self, args) - local op1 = args[2] - local op2 = args[3] - local rv2 = op2[1](self, op2) - - -- sets the argument for the DAT-operator - op1[2][2] = rv2 - local rv1 = op1[1](self, op1) - - if rv1 ~= 0 then - return rv2 - else - self.prf = self.prf + args[5] - local op3 = args[4] - return op3[1](self, op3) - end -end) - -registerOperator("cnd", "n", "", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - if rv1 ~= 0 then - self.prf = self.prf + args[5] - local op2 = args[3] - return op2[1](self, op2) - else - self.prf = self.prf + args[6] - local op3 = args[4] - return op3[1](self, op3) - end -end) - ------------------------------------------------------------------------- - -__e2setcost(1) -- approximation - -registerOperator("trg", "", "n", function(self, args) - local op1 = args[2] - return self.triggerinput == op1 and 1 or 0 -end) - - -registerOperator("iwc", "", "n", function(self, args) - local op1 = args[2] - return IsValid(self.entity.Inputs[op1].Src) and 1 or 0 -end) - -registerOperator("owc","","n",function(self,args) - local op1 = args[2] - local tbl = self.entity.Outputs[op1].Connected - local ret = #tbl - for i=1,ret do if (not IsValid(tbl[i].Entity)) then ret = ret - 1 end end - return ret -end) - - --------------------------------------------------------------------------------- - __e2setcost(0) -- cascaded -registerOperator("is", "n", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - return rv1 ~= 0 and 1 or 0 -end) - -__e2setcost(1) -- approximation - -registerOperator("not", "n", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - return rv1 == 0 and 1 or 0 -end) - -registerOperator("and", "nn", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - if rv1 == 0 then return 0 end - - local op2 = args[3] - local rv2 = op2[1](self, op2) - return rv2 ~= 0 and 1 or 0 -end) - -registerOperator("or", "nn", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - if rv1 ~= 0 then return 1 end - - local op2 = args[3] - local rv2 = op2[1](self, op2) - return rv2 ~= 0 and 1 or 0 -end) +e2function number operator_is(number this) + return (this ~= 0) and 1 or 0 +end -------------------------------------------------------------------------------- @@ -337,23 +101,26 @@ end __e2setcost(2) -- approximation e2function void exit() + self.Scope, self.ScopeID, self.Scopes = self.GlobalScope, 0, { [0] = self.GlobalScope } error("exit", 0) end do - local raise = E2Lib.raiseException - [noreturn] e2function void error( string reason ) - raise(reason, 2, self.trace) + self:forceThrow(reason) end e2function void assert(condition) - if condition == 0 then raise("assert failed", 2, self.trace) end + if condition == 0 then + self:forceThrow("assert failed") + end end e2function void assert(condition, string reason) - if condition == 0 then raise(reason, 2, self.trace) end + if condition == 0 then + self:forceThrow(reason) + end end end @@ -363,6 +130,8 @@ __e2setcost(100) -- approximation [noreturn] e2function void reset() + self.Scope, self.ScopeID, self.Scopes = self.GlobalScope, 0, { [0] = self.GlobalScope } + if self.data.last or self.entity.first then error("exit", 0) end if self.entity.last_reset and self.entity.last_reset == CurTime() then @@ -371,6 +140,7 @@ e2function void reset() self.entity.last_reset = CurTime() self.data.reset = true + error("exit", 0) end @@ -504,93 +274,3 @@ registerCallback("postinit", function() end, 5, { "index", "argument1" }) end end) - --------------------------------------------------------------------------------- - -__e2setcost(3) -- approximation - -registerOperator("switch", "", "", function(self, args) - local cases, startcase = args[3], args[4] - - - for i=1, #cases do -- We figure out what we can run. - local case = cases[i] - local op1 = case[1] - - self.prf = self.prf + case[3] - if self.prf > e2_tickquota then error("perf", 0) end - - if (op1 and op1[1](self, op1) == 1) then -- Equals operator - startcase = i - break - end - end - - if startcase then - for i=startcase, #cases do - local stmts = cases[i][2] - self:PushScope() - local ok, msg = pcall(stmts[1], self, stmts) - if not ok then - if msg == "break" then - self:PopScope() - break - elseif msg ~= "continue" then - self:PopScope() - error(msg, 0) - end - end - self:PopScope() - end - end -end) - -registerOperator("include", "", "", function(self, args) - local Include = self.includes[ args[2] ] - - if Include and Include[2] then - local Script = Include[2] - - local OldScopes = self:SaveScopes() - self:InitScope() -- Create a new Scope Enviroment - self:PushScope() - - local ok, msg = pcall(Script[1], self, Script) - - self:PopScope() - self:LoadScopes(OldScopes) - - if not ok then - error(msg, 0) - end - end -end) - -local unpackException = E2Lib.unpackException -registerOperator("try", "", "", function(self, args) - local prf, stmt, var_name, stmt2 = args[2], args[3], args[4], args[5] - self.prf = self.prf + prf - if self.prf > e2_tickquota then error("perf", 0) end - - self:PushScope() - local ok, msg = pcall(stmt[1], self, stmt) - self:PopScope() - - if not ok then - local catchable, msg = unpackException(msg) - if not catchable then - -- Anything other than context:throw / e2's error is not catchable. - error(msg, 0) - end - self:PushScope() - self.Scope[var_name] = isstring(msg) and msg or "" -- isstring check if we want to be paranoid about the sandbox. - self.Scope.vclk[var_name] = true - - local ok, msg = pcall(stmt2[1], self, stmt2) - self:PopScope() - - if not ok then - error(msg, 0) - end - end -end) \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/core/custom/effects.lua b/lua/entities/gmod_wire_expression2/core/custom/effects.lua index d3fd54598a..1e8c6d26d9 100644 --- a/lua/entities/gmod_wire_expression2/core/custom/effects.lua +++ b/lua/entities/gmod_wire_expression2/core/custom/effects.lua @@ -48,15 +48,6 @@ registerType("effect", "xef", nil, __e2setcost(1) -registerOperator("ass", "xef", "xef", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - self.Scopes[scope][lhs] = rhs - self.Scopes[scope].vclk[lhs] = true - return rhs -end) - e2function effect effect() return EffectData() end diff --git a/lua/entities/gmod_wire_expression2/core/damage.lua b/lua/entities/gmod_wire_expression2/core/damage.lua index 8fb6516a5b..cb60455d29 100644 --- a/lua/entities/gmod_wire_expression2/core/damage.lua +++ b/lua/entities/gmod_wire_expression2/core/damage.lua @@ -14,15 +14,6 @@ registerType("damage", "xdm", nil, end ) -registerOperator("ass", "xdm", "xdm", function(self, args) -- todo: remove with new compiler - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - self.Scopes[scope][lhs] = rhs - self.Scopes[scope].vclk[lhs] = true - return rhs -end) - E2Lib.registerConstant("DMG_GENERIC", DMG_GENERIC) E2Lib.registerConstant("DMG_CRUSH", DMG_CRUSH) E2Lib.registerConstant("DMG_BULLET", DMG_BULLET) diff --git a/lua/entities/gmod_wire_expression2/core/debug.lua b/lua/entities/gmod_wire_expression2/core/debug.lua index 828228fd0d..fbb4185647 100644 --- a/lua/entities/gmod_wire_expression2/core/debug.lua +++ b/lua/entities/gmod_wire_expression2/core/debug.lua @@ -100,6 +100,8 @@ hook.Add("PlayerDisconnected", "e2_print_delays_player_dc", function(ply) printD /******************************************************************************/ +__e2setcost(2) + -- Returns whether or not the next print-message will be printed or omitted by antispam e2function number playerCanPrint() if not checkOwner(self) then return end @@ -109,11 +111,15 @@ end local function repr(self, value, typeid) local fn = wire_expression2_funcs["toString(" .. typeid ..")"] or wire_expression2_funcs["toString(" .. typeid .. ":)"] - if fn and fn[2] == "s" then -- I need the compiler rewrite merged for this to not be garbage + if fn and fn[2] == "s" then self.prf = self.prf + (fn[4] or 20) - return fn[3](self, { [2] = { function() return value end } }) + if fn.attributes.legacy then + return fn[3](self, { [2] = { function() return value end } }) + else + return fn[3](self, { value }) + end elseif typeid == "s" then -- special case for string - return string.format("%q", value) + return value else return wire_expression_types2[typeid][1] end @@ -149,10 +155,7 @@ e2function void print(...args) end end ---- Posts to the chat area. (deprecated due to print(...)) ---e2 function void print(string text) --- self.player:ChatPrint(text) ---end +__e2setcost(30) --- Posts a string to the chat of 's driver. Returns 1 if the text was printed, 0 if not. e2function number entity:printDriver(string text) @@ -170,6 +173,8 @@ end /******************************************************************************/ +__e2setcost(30) + --- Displays a hint popup with message for seconds ( being clamped between 0.7 and 7). e2function void hint(string text, duration) if not IsValid(self.player) then return end @@ -199,6 +204,8 @@ for _,cname in ipairs({ "HUD_PRINTCENTER", "HUD_PRINTCONSOLE", "HUD_PRINTNOTIFY" E2Lib.registerConstant(cname, value) end +__e2setcost(30) + --- Same as print(), but can make the text show up in different places. can be one of the following: _HUD_PRINTCENTER, _HUD_PRINTCONSOLE, _HUD_PRINTNOTIFY, _HUD_PRINTTALK. e2function void print(print_type, string text) if not checkOwner(self) then return end @@ -275,6 +282,8 @@ do end end +__e2setcost(150) + --- Prints an array like the lua function [[G.PrintTable|PrintTable]] does, except to the chat area. e2function void printTable(array arr) if not checkOwner(self) then return end @@ -289,7 +298,7 @@ end /******************************************************************************/ -__e2setcost(100) +__e2setcost(150) util.AddNetworkString("wire_expression2_printColor") util.AddNetworkString("wire_expression2_print") @@ -302,17 +311,16 @@ local printColor_typeids = { e = function(e) return IsValid(e) and e:IsPlayer() and e or "" end, } -local function printColorVarArg(chip, ply, console, typeids, ...) +local function printColorVarArg(chip, ply, console, typeids, vararg) if not IsValid(ply) then return end if not checkDelay(ply) then return end - local send_array = { ... } local i = 1 for i,tp in ipairs(typeids) do if printColor_typeids[tp] then - send_array[i] = printColor_typeids[tp](send_array[i]) + vararg[i] = printColor_typeids[tp](vararg[i]) else - send_array[i] = "" + vararg[i] = "" end if i == 256 then break end i = i + 1 @@ -321,7 +329,7 @@ local function printColorVarArg(chip, ply, console, typeids, ...) net.Start("wire_expression2_printColor") net.WriteEntity(chip) net.WriteBool(console) - net.WriteTable(send_array) + net.WriteTable(vararg) net.Send(ply) end @@ -366,8 +374,8 @@ end --- Works like [[chat.AddText]](...). Parameters can be any amount and combination of numbers, strings, player entities, color vectors (both 3D and 4D). -e2function void printColor(...) - printColorVarArg(nil, self.player, false, typeids, ...) +e2function void printColor(...args) + printColorVarArg(nil, self.player, false, typeids, args) end --- Like printColor(...), except taking an array containing all the parameters. @@ -376,8 +384,8 @@ e2function void printColor(array arr) end --- Works like MsgC(...). Parameters can be any amount and combination of numbers, strings, player entities, color vectors (both 3D and 4D). -e2function void printColorC(...) - printColorVarArg(nil, self.player, true, typeids, ...) +e2function void printColorC(...args) + printColorVarArg(nil, self.player, true, typeids, args) end --- Like printColorC(...), except taking an array containing all the parameters. @@ -386,7 +394,7 @@ e2function void printColorC(array arr) end --- Like printColor(...), except printing in 's driver's chat area instead of yours. -e2function void entity:printColorDriver(...) +e2function void entity:printColorDriver(...args) if not checkVehicle(self, this) then return end local driver = this:GetDriver() @@ -394,7 +402,7 @@ e2function void entity:printColorDriver(...) if not checkDelay( driver ) then return end - printColorVarArg(self.entity, driver, false, typeids, ...) + printColorVarArg(self.entity, driver, false, typeids, args) end --- Like printColor(R), except printing in 's driver's chat area instead of yours. diff --git a/lua/entities/gmod_wire_expression2/core/e2lib.lua b/lua/entities/gmod_wire_expression2/core/e2lib.lua index 7fc727dca1..35215adb66 100644 --- a/lua/entities/gmod_wire_expression2/core/e2lib.lua +++ b/lua/entities/gmod_wire_expression2/core/e2lib.lua @@ -1,8 +1,39 @@ AddCSLuaFile() +---@class EnvEvent +---@field name string +---@field args { placeholder: string, type: TypeSignature }[] +---@field constructor fun(ctx: RuntimeContext)? +---@field destructor fun(ctx: RuntimeContext)? +---@field listening table + +---@class EnvType +---@field name string +---@field id string + +---@class EnvConstant: { name: string, type: TypeSignature, value: any } + +---@class EnvOperator +---@field args TypeSignature[] +---@field returns TypeSignature[] +---@field op RuntimeOperator +---@field cost integer + +---@class EnvFunction: EnvOperator +---@field attrs table +---@field const boolean? # Whether the function can be overridden at runtime. Optimzation. Only present in user functions. + +---@class EnvMethod: EnvFunction +---@field meta TypeSignature + +---@class EnvLibrary +---@field Constants table +---@field Functions table +---@field Methods table> + E2Lib = { Env = { - ---@type { name: string, args: { [1]: string, [2]: string }[], constructor: fun(t: table)?, destructor: fun(t: table)?, listening: table } + ---@type EnvEvent[] Events = {} } } @@ -13,18 +44,8 @@ local function checkargtype(argn, value, argtype) end -- -------------------------- Helper functions ----------------------------- -local unpack = unpack local IsValid = IsValid --- This functions should not be used in functions that tend to be used very often, as it is slower than getting the arguments manually. -function E2Lib.getArguments(self, args) - local ret = {} - for i = 2, #args[7] + 1 do - ret[i - 1] = args[i][1](self, args[i]) - end - return unpack(ret) -end - -- Backwards compatibility E2Lib.isnan = WireLib.isnan E2Lib.clampPos = WireLib.clampPos @@ -129,6 +150,7 @@ function E2Lib.splitType(args) end i = i + 1 end + return thistype, ret end @@ -203,11 +225,6 @@ function E2Lib.getOwner(self, entity) return nil end -function E2Lib.abuse(ply) - ply:Kick("Be good and don't abuse -- sincerely yours, the E2") - error("abuse", 0) -end - -- This function gets replaced when CPPI is detected, see very end of this file function E2Lib.isFriend(owner, player) return owner == player @@ -309,12 +326,15 @@ end -- usable error message. If not, then it's an error not caused by an error in -- user code, and so we dump a stack trace to the console to help debug it. function E2Lib.errorHandler(message) - if string.match(message, " at line ") then return message end + if getmetatable(message) == E2Lib.Debug.Error then + return message + end print("Internal error - please report to https://github.com/wiremod/wire/issues") print(message) debug.Trace() - return "Internal error, see console for more details" + + return E2Lib.Debug.Error.new("Internal error, see console for more details") end E2Lib.optable_inv = { @@ -556,6 +576,7 @@ local Operator = { E2Lib.Operator = Operator +---@type table local OperatorNames = {} for name, val in pairs(Operator) do OperatorNames[val] = name @@ -586,12 +607,23 @@ end E2Lib.OperatorChars = OperatorChars -E2Lib.blocked_array_types = { +E2Lib.blocked_array_types = { -- todo: fix casing ["t"] = true, ["r"] = true, ["xgt"] = true } +--- Types that will trigger their I/O connections on assignment/index change. +E2Lib.IOTableTypes = { + ARRAY = true, ["r"] = true, + TABLE = true, ["t"] = true, + VECTOR = true, ["v"] = true, + VECTOR2 = true, ["xv2"] = true, + VECTOR4 = true, ["xv4"] = true, + ANGLE = true, ["a"] = true, + QUATERNION = true, ["q"] = true +} + -- ------------------------------ string stuff --------------------------------- -- limits the given string to the given length and adds "..." to the end if too long. @@ -973,63 +1005,233 @@ function E2Lib.isValidFileWritePath(path) if ext then return file_extensions[string.lower(ext)] end end --- Different from Context:throw, which does not error the chip if --- @strict is not enabled and instead returns a default value. --- This is what Context:throw calls internally if @strict --- By default E2 can catch these errors. -function E2Lib.raiseException(msg, level, trace, can_catch) - error({ - catchable = (can_catch == nil) and true or can_catch, - msg = msg, - trace = trace - }, level) +--- Deprecated. +--- Superceded by RuntimeContext:forceThrow(msg) / RuntimeContext:throw(msg, default?) +---@deprecated +---@param message string +---@param level integer +---@param trace Trace +---@param can_catch boolean? +function E2Lib.raiseException(message, level, trace, can_catch) + error(E2Lib.Debug.Error.new( + message, + trace, + { catchable = (can_catch == nil) and true or can_catch } + ), level) end --- Unpacks either an exception object as seen above or an error string. +---@param struct string|Error ---@return boolean catchable ----@return string msg ----@return TokenTrace? trace +---@return string message +---@return Trace? trace function E2Lib.unpackException(struct) - if isstring(struct) then + if type(struct) == "string" then return false, struct, nil end - return struct.catchable, struct.msg, struct.trace + return struct.userdata and struct.userdata.catchable or false, struct.message, struct.trace end +---@class RuntimeScope: table +---@field vclk table +---@field parent RuntimeScope? + +--- Context of an Expression 2 at runtime. +---@class RuntimeContext +--- +---@field Scope RuntimeScope +---@field Scopes RuntimeScope[] +---@field ScopeID integer +---@field GlobalScope RuntimeScope | { lookup: table } +--- +---@field prf integer +---@field prfcount integer +---@field prfbench integer +--- +---@field time integer +---@field timebench integer +--- +---@field entity userdata +---@field player userdata +---@field uid integer +--- +---@field trace Trace +---@field __break__ boolean +---@field __continue__ boolean +---@field __return__ boolean +---@field __returnval__ any +--- +---@field funcs table +---@field funcs_ret table # dumb stringcall thing delete soon please +---@field includes table +--- +---@field data table # Userdata +---@field throw fun(self: RuntimeContext, msg: string, value: any?) +local RuntimeContext = {} +RuntimeContext.__index = RuntimeContext + +function RuntimeContext:__tostring() + return "RuntimeContext" +end + +E2Lib.RuntimeContext = RuntimeContext + +---@class RuntimeContextBuilder: RuntimeContext +local RuntimeContextBuilder = {} +RuntimeContextBuilder.__index = RuntimeContextBuilder + +---@return RuntimeContextBuilder +function RuntimeContext.builder() + local global = { vclk = {}, lookup = {}, parent = nil } + return setmetatable({ + GlobalScope = global, + Scopes = { [0] = global }, + ScopeID = 0, + Scope = global, ---- Mimics an E2 Context as if it were really on an entity. ---- This code can probably be deduplicated but that'd needlessly complicate things, and I've made this compact enough. ----@param owner GEntity? # Owner, or assumes world ----@return ScopeManager? # Context or nil if failed -local function makeContext(owner) - local ctx = setmetatable({ - data = {}, vclk = {}, funcs = {}, funcs_ret = {}, - entity = owner, player = owner, uid = IsValid(owner) and owner:UniqueID() or "World", prf = 0, prfcount = 0, prfbench = 0, - time = 0, timebench = 0, includes = {} - }, E2Lib.ScopeManager) + time = 0, timebench = 0, stackdepth = 0, - ctx:InitScope() + entity = game.GetWorld(), player = game.GetWorld(), uid = "World", - -- Construct the context to run code. - -- If not done, - local ok, why = pcall(wire_expression2_CallHook, "construct", ctx) - if not ok then - pcall(wire_expression2_CallHook, "destruct", ctx) + trace = nil, -- Should be set at runtime + __break__ = false, __continue__ = false, __return__ = false, + + funcs = {}, funcs_ret = {}, includes = {}, data = {} + }, RuntimeContextBuilder) +end + +---@param ply userdata +function RuntimeContextBuilder:withOwner(ply) + self.player = assert(ply) + self.uid = (self.player.UniqueID and self.player:UniqueID()) or "World" + return self +end + +---@param chip userdata +function RuntimeContextBuilder:withChip(chip) + self.entity = assert(chip) + return self +end + +---@param prf integer +---@param prfcount integer +---@param prfbench integer +function RuntimeContextBuilder:withPrf(prf, prfcount, prfbench) + self.prf, self.prfcount, self.prfbench = assert(prf), assert(prfcount), assert(prfbench) + return self +end + +---@param time integer +---@param timebench integer +function RuntimeContextBuilder:withTime(time, timebench) + self.time, self.timebench = assert(time), assert(timebench) + return self +end + +---@param functions table +---@param rets table +function RuntimeContextBuilder:withUserFunctions(functions, rets) + self.funcs = assert(functions) + self.funcs_ret = rets or self.funcs_ret + return self +end + +---@param includes table +function RuntimeContextBuilder:withIncludes(includes) + self.includes = assert(includes) + return self +end + +---@param strict boolean? +function RuntimeContextBuilder:withStrict(strict) + self.strict = strict == true + return self +end + +---@param inputs table +function RuntimeContextBuilder:withInputs(inputs) + for k, v in pairs(inputs) do + self.GlobalScope[k] = E2Lib.fixDefault(wire_expression_types2[v][2]) + end + return self +end + +---@param outputs table +function RuntimeContextBuilder:withOutputs(outputs) + for k, v in pairs(outputs) do + self.GlobalScope[k] = E2Lib.fixDefault(wire_expression_types2[v][2]) + self.GlobalScope.vclk[k] = true + end + return self +end + +---@param persists table +function RuntimeContextBuilder:withPersists(persists) + for k, v in pairs(persists) do + self.GlobalScope[k] = E2Lib.fixDefault(wire_expression_types2[v][2]) end + return self +end - return ctx +--- Registers delta variables in the context. +--- **MUST** register all persists/inputs/outputs BEFORE calling this. +---@param vars table +function RuntimeContextBuilder:withDeltaVars(vars) + for k, _ in pairs(vars) do + self.GlobalScope["$" .. k] = self.GlobalScope[k] + end + return self +end + +---@return RuntimeContext +function RuntimeContextBuilder:build() + if not self.strict then + function self:throw(_msg, variable) + return variable + end + end + + return setmetatable(self, RuntimeContext) +end + +local DEF_USERDATA = { catchable = true } + +--- If @strict, raises an error with message. +--- Otherwise, returns given value. +---@generic T +---@param message string +---@param _default T? +---@return T? +function RuntimeContext:throw(message, _default) + local err = E2Lib.Debug.Error.new(message, self.trace, DEF_USERDATA) + error(err, 2) +end + +--- Same as RuntimeContext:throw, except always throws the error regardless of @strict being disabled. +RuntimeContext.forceThrow = RuntimeContext.throw + +function RuntimeContext:PushScope() + local scope = { vclk = {} } + self.Scope, self.ScopeID = scope, self.ScopeID + 1 + self.Scopes[self.ScopeID] = scope +end + +function RuntimeContext:PopScope() + self.Scopes[self.ScopeID] = nil + self.ScopeID = self.ScopeID - 1 + self.Scope = self.Scopes[self.ScopeID] end --- Compiles an E2 script without an entity owning it. --- This doesn't have 1:1 behavior with an actual E2 chip existing, but is useful for testing. ---@param code string E2 Code to compile. ----@param owner GEntity? 'Owner' entity, default world. +---@param owner userdata? 'Owner' entity, default world. ---@return boolean success If ran successfully ---@return string|function compiled Compiled function, or error message if not success -function E2Lib.compileScript(code, owner, run) +function E2Lib.compileScript(code, owner) local status, directives, code = E2Lib.PreProcessor.Execute(code) - if not status then return false, directives end -- Preprocessor failed. + if not status then return false, directives end local status, tokens = E2Lib.Tokenizer.Execute(code) if not status then return false, tokens end @@ -1037,23 +1239,17 @@ function E2Lib.compileScript(code, owner, run) local status, tree, dvars = E2Lib.Parser.Execute(tokens) if not status then return false, tree end - status,tree = E2Lib.Optimizer.Execute(tree) - if not status then return false, tree end - - local status, script, inst = E2Lib.Compiler.Execute(tree, directives.inputs, directives.outputs, directives.persist, dvars, {}) + local status, script, inst = E2Lib.Compiler.Execute(tree, directives, dvars, {}) if not status then return false, script end - local ctx = makeContext(owner or game.GetWorld()) - if directives.strict then - local err = E2Lib.raiseException - function ctx:throw(msg) - err(msg, 2, self.trace) - end - else - function ctx:throw(_msg, variable) - return variable - end - end + local ctx = RuntimeContext.builder() + :withOwner(owner or game.GetWorld()) + :withStrict(directives.strict) + :withInputs(directives.inputs[3]) + :withOutputs(directives.outputs[3]) + :withPersists(directives.persist[3]) + :withDeltaVars(dvars) + :build() return true, function(ctx2) ctx = ctx2 or ctx @@ -1067,9 +1263,10 @@ function E2Lib.compileScript(code, owner, run) ctx.entity.GlobalScope, ctx.entity._vars = ctx.GlobalScope, ctx.GlobalScope end - ctx:PushScope() - local success, why = pcall( script[1], ctx, script ) - ctx:PopScope() + local success, why = pcall(wire_expression2_CallHook, "construct", ctx) + if success then + success, why = pcall( script, ctx ) + end -- Cleanup so hooks like runOnTick won't run after this call pcall(wire_expression2_CallHook, "destruct", ctx) @@ -1087,7 +1284,7 @@ function E2Lib.compileScript(code, owner, run) local _, why, trace = E2Lib.unpackException(why) if trace then - return false, "Runtime error: '" .. why .. "' at line " .. trace[1] .. ", col " .. trace[2] + return false, "Runtime error: '" .. why .. "' at line " .. trace.start_line .. ", col " .. trace.start_col else return false, why end diff --git a/lua/entities/gmod_wire_expression2/core/e2tests.lua b/lua/entities/gmod_wire_expression2/core/e2tests.lua index ccf5371e32..613ec5e696 100644 --- a/lua/entities/gmod_wire_expression2/core/e2tests.lua +++ b/lua/entities/gmod_wire_expression2/core/e2tests.lua @@ -20,7 +20,7 @@ end local function runE2Test(path, name) local source = file.Read(path, "GAME") - local ok, err_or_func = E2Lib.compileScript(source, nil, true) + local ok, err_or_func = E2Lib.compileScript(source) local should, step = source:match("^## SHOULD_(%w+):(%w+)") local function msgf(...) @@ -99,6 +99,6 @@ concommand.Add("e2test", function(ply) if IsValid(ply) then ply:PrintMessage(2, msg) else - print(#passed .. "/" .. (#passed + #failed) .. " tests passed") + print(msg) end end) \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/core/entity.lua b/lua/entities/gmod_wire_expression2/core/entity.lua index 34dd6d4acc..ef52836cc9 100644 --- a/lua/entities/gmod_wire_expression2/core/entity.lua +++ b/lua/entities/gmod_wire_expression2/core/entity.lua @@ -57,26 +57,10 @@ end __e2setcost(5) -- temporary -registerOperator("ass", "e", "e", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - --[[******************************************************************************]] -e2function number operator_is(entity ent) - if IsValid(ent) then return 1 else return 0 end -end - -e2function number operator==(entity lhs, entity rhs) - if lhs == rhs then return 1 else return 0 end -end - -e2function number operator!=(entity lhs, entity rhs) - if lhs ~= rhs then return 1 else return 0 end +e2function number operator_is(entity this) + return IsValid(this) and 1 or 0 end --[[******************************************************************************]] @@ -287,7 +271,7 @@ e2function number entity:elevation(vector pos) pos = this:WorldToLocal(pos) local len = pos:Length() - if len < delta then return 0 end + if len < 0 then return 0 end return rad2deg*asin(pos.z / len) end @@ -302,7 +286,7 @@ e2function angle entity:heading(vector pos) -- elevation local len = pos:Length()--sqrt(x*x + y*y + z*z) - if len < delta then return Angle(0, bearing, 0) end + if len < 0 then return Angle(0, bearing, 0) end local elevation = rad2deg * asin(pos.z / len) return Angle(elevation, bearing, 0) @@ -998,31 +982,30 @@ local function cleanEntsTbls(ent) end registerCallback("postinit",function() - for k,v in pairs( wire_expression_types ) do + for k, v in pairs( wire_expression_types ) do if not non_allowed_types[v[1]] then if k == "NORMAL" then k = "NUMBER" end k = upperfirst(k) __e2setcost(5) - local function getf( self, args ) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if not IsValid(rv1) or not rv2 or not rawget(enttbls, self.uid) or not rawget(enttbls[self.uid], rv1) then return fixDefault( v[2] ) end - return enttbls[self.uid][rv1][rv2] or fixDefault( v[2] ) + local function getf(self, ent, key) + if IsValid(ent) and key and rawget(enttbls, self.uid) and rawget(enttbls[self.uid], ent) then + return enttbls[self.uid][ent][key] or fixDefault( v[2] ) + end + + return fixDefault( v[2] ) end - local function setf( self, args ) - local op1, op2, op3 = args[2], args[3], args[4] - local rv1, rv2, rv3 = op1[1](self, op1), op2[1](self, op2), op3[1](self, op3) - if not IsValid(rv1) or not rv2 or not rv3 then return end - rv1:CallOnRemove("E2_ClearEntTbls", cleanEntsTbls) - enttbls[self.uid][rv1][rv2] = rv3 - return rv3 + local function setf(self, ent, key, value) + if IsValid(ent) and key and value ~= nil then + ent:CallOnRemove("E2_ClearEntTbls", cleanEntsTbls) + enttbls[self.uid][ent][key] = value + end end - registerOperator("idx", v[1].."=es", v[1], getf) - registerOperator("idx", v[1].."=es"..v[1], v[1], setf) + registerOperator("indexget", "es" .. v[1], v[1], getf) + registerOperator("indexset", "es" .. v[1], v[1], setf) end -- allowed check end -- loop end) -- postinit diff --git a/lua/entities/gmod_wire_expression2/core/extloader.lua b/lua/entities/gmod_wire_expression2/core/extloader.lua index 6769a0daa8..f22f13a4cd 100644 --- a/lua/entities/gmod_wire_expression2/core/extloader.lua +++ b/lua/entities/gmod_wire_expression2/core/extloader.lua @@ -31,7 +31,18 @@ if ENT then chip.script = nil end + + _Msg("Reloading Expression 2 internals.") + include("entities/gmod_wire_expression2/core/e2lib.lua") + include("entities/gmod_wire_expression2/base/debug.lua") + include("entities/gmod_wire_expression2/base/preprocessor.lua") + include("entities/gmod_wire_expression2/base/tokenizer.lua") + include("entities/gmod_wire_expression2/base/parser.lua") + include("entities/gmod_wire_expression2/base/compiler.lua") + _Msg( "Reloading Expression 2 extensions." ) + include("entities/gmod_wire_expression2/core/init.lua") + ENT = wire_expression2_ENT wire_expression2_is_reload = true include( "entities/gmod_wire_expression2/core/extloader.lua" ) @@ -77,10 +88,8 @@ end -- parses and executes an extension local function e2_include_pass2(name, luaname, contents) - local preprocessedSource = E2Lib.ExtPP.Pass2(contents) - + local preprocessedSource = E2Lib.ExtPP.Pass2(contents, luaname) E2Lib.currentextension = string.StripExtension( string.GetFileFromFilename(name) ) - if not preprocessedSource then return include(name) end local func = CompileString(preprocessedSource, luaname) @@ -156,7 +165,6 @@ e2_include("custom.lua") e2_include("datasignal.lua") e2_include("egpfunctions.lua") e2_include("functions.lua") -e2_include("strfunc.lua") e2_include("steamidconv.lua") e2_include("easings.lua") e2_include("damage.lua") diff --git a/lua/entities/gmod_wire_expression2/core/extpp.lua b/lua/entities/gmod_wire_expression2/core/extpp.lua index 2e499cb020..22c6c2746e 100644 --- a/lua/entities/gmod_wire_expression2/core/extpp.lua +++ b/lua/entities/gmod_wire_expression2/core/extpp.lua @@ -1,217 +1,137 @@ -AddCSLuaFile() - --- some constants -- - -local p_typename = "[a-z][a-z0-9]*" -local p_typeid = "[a-z][a-z0-9]?[a-z0-9]?[a-z0-9]?[a-z0-9]?" -local p_argname = "[a-zA-Z][a-zA-Z0-9_]*" -local p_funcname = "[a-z][a-zA-Z0-9]*" -local p_func_operator = "[-a-zA-Z0-9+*/%%^=!><&|$_%[%]]*" - -local OPTYPE_FUNCTION -local OPTYPE_NORMAL = 0 -local OPTYPE_DONT_FETCH_FIRST = 1 -local OPTYPE_ASSIGN = 2 -local OPTYPE_APPEND_RET = 3 - -local optable = { - ["operator+"] = "add", - ["operator++"] = { "inc", OPTYPE_DONT_FETCH_FIRST }, - ["operator-"] = "sub", - ["operator--"] = { "dec", OPTYPE_DONT_FETCH_FIRST }, - ["operator*"] = "mul", - ["operator/"] = "div", - ["operator%"] = "mod", - ["operator^"] = "exp", - ["operator="] = { "ass", OPTYPE_ASSIGN }, - ["operator=="] = "eq", - ["operator!"] = "not", - ["operator!="] = "neq", - ["operator>"] = "gth", - ["operator>="] = "geq", - ["operator<"] = "lth", - ["operator<="] = "leq", - ["operator&"] = "and", - ["operator&&"] = "and", - ["operator|"] = "or", - ["operator||"] = "or", - ["operator[]"] = "idx", -- typeless op[] - ["operator[T]"] = { "idx", OPTYPE_APPEND_RET }, -- typed op[] - - ["operator_is"] = "is", - ["operator_neg"] = "neg", - ["operator_band"] = "band", - ["operator_bor"] = "bor", - ["operator_bxor"] = "bxor", - ["operator_bshl"] = "bshl", - ["operator_bshr"] = "bshr", +local p_typename = "%l[%l%d]*" +local p_typeid = "%l[%l%d]?[%l%d]?" +local p_argname = "%a[%w_]*" +local p_funcname = "%l[%w_]*" +local p_func_operator = "[%-%+%*%%%/%^%=%!%>%<%&%|%$%[%]%w_]*" + +local Operators = { + ["operator+"] = "add", ["operator-"] = "sub", + ["operator*"] = "mul", ["operator/"] = "div", + ["operator%"] = "mod", ["operator^"] = "exp", + ["operator=="] = "eq", ["operator>"] = "gth", + ["operator>="] = "geq", ["operator<"] = "lth", + ["operator<="] = "leq", ["operator_neg"] = "neg", + ["operator_band"] = "band", ["operator_bor"] = "bor", + ["operator_bxor"] = "bxor", ["operator_bshl"] = "bshl", + ["operator_bshr"] = "bshr", ["operator_is"] = "is" } --- This is an array for types that were parsed from all E2 extensions. -local preparsed_types +local RemovedOperators = { + ["operator!"] = true, ["operator!="] = true, -- These now use operator_is and operator== internally + ["operator&"] = true, ["operator&&"] = true, -- Despite && being binary AND in E2, the preprocessor used to handle this as logical AND. + ["operator|"] = true, ["operator||"] = true, -- Despite || being binary OR in E2, the preprocessor used to handle this as logical OR. + ["operator++"] = true, ["operator--"] = true, -- Now use + and - internally. + ["operator[]"] = true, ["operator[T]"] = true, -- indexget and indexset now. + ["operator="] = true, -- Assignment "ass" operator. +} + +local ValidAttributes = { -- Expose to E2Lib? + ["deprecated"] = true, + ["nodiscard"] = true, + ["noreturn"] = true +} E2Lib.ExtPP = {} --- This function initialized extpp's dynamic fields +---@type table +local preparsed_types + function E2Lib.ExtPP.Init() - -- We initialize the array of preparsed types with an alias "number" for "normal". + -- The 'n' type is actually 'normal'. Make an alias for number -> normal preparsed_types = { ["NUMBER"] = "n" } end +function E2Lib.ExtPP.Pass1(contents) + -- look for registerType lines and fill preparsed_types with them + for typename, typeid in string.gmatch("\n" .. contents, '%WregisterType%(%s*"(' .. p_typename .. ')"%s*,%s*"(' .. p_typeid .. ')"') do + preparsed_types[string.upper(typename)] = typeid + end +end + -- This function checks whether its argument is a valid type id. local function isValidTypeId(typeid) return (typeid:match("^[a-wy-zA-WY-Z]$") or typeid:match("^[xX][a-wy-zA-WY-Z0-9][a-wy-zA-WY-Z0-9]$")) and true end -- Returns the typeid associated with the given typename -local function getTypeId(typename) - local n = string.upper(typename) - - -- was the type registered with E-2? - if wire_expression_types[n] then return wire_expression_types[n][1] end +---@return string? +local function getTypeId(ty) + local upper = ty:upper() - -- was the name found when looking for registerType lines? - if preparsed_types[n] then return preparsed_types[n] end - - -- is the type name a valid typeid? use the type name as the typeid - if isValidTypeId(typename) then return typename end + return (wire_expression_types[upper] and wire_expression_types[upper][1]) -- Already registered. + or preparsed_types[upper] -- Preparsed from registerType + or (isValidTypeId(ty) and ty) -- It is a type id. Weird. end ----@class ArgsKind -local ArgsKind = { - None = 0, - Static = 1, - Variadic = 2, - VariadicTbl = 3 -} - --- parses an argument list ----@return { typeids: string[], argnames: string[] }, integer, string? -local function parseArgs(args) - local argtable = { typeids = {}, argnames = {} } - if args:find("%S") == nil then return argtable, ArgsKind.None end -- no arguments - - local args = args:Split(",") - local len = #args - - for k, arg in ipairs(args) do - -- is this argument an ellipsis? - if arg:match( "^%s*%.%.%.%s*$") then +-- Parses list of parameters +---@param raw string +---@param trace string +---@return { [1]: string, [2]: string }[] parameters, boolean variadic, string? variadic_tbl +local function parseParameters(raw, trace) + if not raw:match("%S") then return {}, false end + + local parsed, split = {}, raw:Split(",") + local len = #split + + for k, raw_param in ipairs(split) do + local name = raw_param:match("^%s*%.%.%.(%w+)%s*$") + if name then -- Variadic table parameter + assert(k == len, "PP syntax error: Ellipses table (..." .. name .. ") must be the last argument.") + return parsed, true, name + elseif raw_param:match("^%s*%.%.%.%s*$") then -- Variadic lua parameter assert(k == len, "PP syntax error: Ellipses (...) must be the last argument.") - return argtable, ArgsKind.Variadic + ErrorNoHalt("Warning: Use of variadic parameter with ExtPP is not recommended and deprecated. Instead use ... (which passes a table) or the `args` variable " .. trace .. "\n") + return parsed, true else - local name = arg:match("^%s*%.%.%.(%w+)%s*$") - if name then - assert(k == len, "PP syntax error: Ellipses table (..." .. name .. ") must be the last argument.") - - -- assert(name ~= "args" and name ~= "typeids" and name ~= "self", "PP syntax error: Variadic table name shadows internal variable (" .. name .. ")") - - return argtable, ArgsKind.VariadicTbl, name + local typename, argname = string.match(raw_param, "^%s*(" .. p_typename .. ")%s+(" .. p_argname .. ")%s*$") + if not typename then -- Implicit 'number' type + argname, typename = string.match(raw_param, "^%s*(" .. p_argname .. ")%s*$"), "number" end - end - - -- assume a type name was given and split up the argument into type name and argument name. - local typename, argname = string.match(arg, "^%s*(" .. p_typename .. ")%s+(" .. p_argname .. ")%s*$") - -- the assumption failed - if not typename then - -- try looking for a argument name only and defaulting the type name to "number" - argname = string.match(arg, "^%s*(" .. p_argname .. ")%s*$") - typename = "number" + parsed[k] = { + assert(argname, "PP syntax error: Invalid function parameter syntax. " .. trace), + assert(getTypeId(typename), "PP syntax error: Invalid parameter type '" .. typename .. "' for argument '" .. argname .. "'." .. trace) + } end - - -- this failed as well? give up and print an error - assert(argname, "PP syntax error: Invalid function parameter syntax.") - - local typeid = assert(getTypeId(typename), "PP syntax error: Invalid parameter type '" .. typename .. "' for argument '" .. argname .. "'.") - - argtable.typeids[k] = typeid - argtable.argnames[k] = argname end - return argtable, ArgsKind.Static -end - -local function mangle(name, arg_typeids, op_type) - if op_type then name = "operator_" .. name end - local ret = "e2_" .. name - if arg_typeids == "" then return ret end - return ret .. "_" .. arg_typeids:gsub("[:=]", "_") -end - -local function linenumber(s, i) - local c = 1 - local line = 0 - while c and c < i do - line = line + 1 - c = s:find("\n", c + 1, true) - end - return line -end - --- returns a name and a register function for the given name --- also optionally returns a flag signaling how to treat the operator in question. -local function handleop(name) - local operator = optable[name] - - if operator then - local op_type = OPTYPE_NORMAL - - -- special treatment is needed for some operators. - if istable(operator) then operator, op_type = unpack(operator) end - - -- return everything. - return operator, "registerOperator", op_type - elseif name:find("^" .. p_funcname .. "$") then - return name, "registerFunction", OPTYPE_FUNCTION - else - error("PP syntax error: Invalid character in function name.", 0) - end + return parsed, false end -local function makestringtable(tbl, i, j) - if #tbl == 0 then return "{}" end - if not i then i = 1 - elseif i < 0 then i = #tbl + i + 1 - elseif i < 1 then i = 1 - elseif i > #tbl then i = #tbl - end - - if not j then j = #tbl - elseif j < 0 then j = #tbl + j + 1 - elseif j < 1 then j = 1 - elseif j > #tbl then j = #tbl - end +---@param attributes string +---@param trace string +---@return table? +local function parseAttributes(attributes, trace) + -- Parse attributes in order to pass to registerFunction/registerOperator + if attributes ~= "" and attributes:sub(1, 1) == "[" and attributes:sub(-1, -1) == "]" then + local attrs = { legacy = "false" } -- extpp can generate functions abiding by the new compiler. + for _, tag in ipairs(attributes:sub(2, -2):Split(",")) do + local k = tag:lower():Trim() + + if k:find("=", 1, true) then + -- [xyz = 567, event = "Tick"] + -- e2function number foo() + local key, value = unpack(k:Split("="), 1, 2) + attrs[key:lower():Trim()] = value:Trim() + elseif not ValidAttributes[k] then + ErrorNoHalt("Invalid attribute fed to ExtPP: " .. k .. " " .. trace .. "\n") + else + attrs[tag:lower():Trim()] = "true" + end + end - --return string.format("{"..string.rep("%q,", math.max(0,j-i+1)).."}", unpack(tbl, i, j)) - local ok, ret = pcall(string.format, "{" .. string.rep("%q,", math.max(0, j - i + 1)) .. "}", unpack(tbl, i, j)) - if not ok then - print(i, j, #tbl, "{" .. string.rep("%q,", math.max(0, j - i + 1)) .. "}") - error(ret) + return attrs end - return ret end -function E2Lib.ExtPP.Pass1(contents) - -- look for registerType lines and fill preparsed_types with them - for typename, typeid in string.gmatch("\n" .. contents, '%WregisterType%(%s*"(' .. p_typename .. ')"%s*,%s*"(' .. p_typeid .. ')"') do - preparsed_types[string.upper(typename)] = typeid - end -end - -local fmt = string.format --- Compact lua code to a single line to avoid changing lua's tracebacks. local function compact(lua) - return ( lua:gsub("\n\t*", " ") ) + return (lua:Trim():gsub("\n\t*", " ")) end -local valid_attributes = { - ["deprecated"] = true, - ["nodiscard"] = true, - ["noreturn"] = true -} - -function E2Lib.ExtPP.Pass2(contents) +---@param contents string +---@param filename string +function E2Lib.ExtPP.Pass2(contents, filename) -- We add some stuff to both ends of the string so we can look for %W (non-word characters) at the ends of the patterns. local prelude = "local tempcosts, registeredfunctions = {}, {};" contents = ("\n" .. prelude .. contents .. "\n ") @@ -219,7 +139,7 @@ function E2Lib.ExtPP.Pass2(contents) -- this is a list of pieces that make up the final code local output = {} -- this is a list of registerFunction lines that will be put at the end of the file. - local function_register = {} + local footer = {} -- We start from position 2, since char #1 is always the \n we added earlier local lastpos = 2 @@ -227,282 +147,165 @@ function E2Lib.ExtPP.Pass2(contents) -- This flag helps determine whether the preprocessor changed, so we can tell the environment about it. local changed = false - for a_begin, attributes, h_begin, ret, thistype, colon, name, args, whitespace, equals, h_end in contents:gmatch("()(%[?[%w,_ =\"]*%]?)[\r\n\t ]*()e2function%s+(" .. p_typename .. ")%s+([a-z0-9]-)%s*(:?)%s*(" .. p_func_operator .. ")%(([^)]*)%)(%s*)(=?)()") do - -- Convert attributes to a lookup table passed to registerFunction - attributes = attributes ~= "" and attributes or nil - -- attributes = attributes ~= "" - local attributes_str - - if attributes and attributes:sub(1, 1) == "[" and attributes:sub(-1, -1) == "]" then - attributes_str = attributes - attributes = attributes:sub(2, -2) -- Remove surrounding brackets - -- [deprecated, nodiscard] - -- e2function void test() - - attributes = attributes:Split(",") - - local lookup = {} - for _, tag in ipairs(attributes) do - local k = tag:lower():Trim() - - if k:find("=", 1, true) then - -- [xyz = 567, event = "Tick"] - -- e2function number foo() - local key, value = unpack( k:Split("="), 1, 2 ) - key, value = key:lower():Trim(), tonumber(value:match("%d+")) or value:Trim() - - lookup[key] = value - else - if not valid_attributes[k] then - ErrorNoHalt("Invalid attribute fed to ExtPP: " .. k) - end - lookup[ tag:lower():Trim() ] = "true" - end - end - - local buf = "{" - for attr, val in pairs(lookup) do - buf = buf .. "['" .. attr .. "'] = " .. val .. "," - end - - attributes = buf .. "}" + for a_begin, attributes, h_begin, ret, thistype, colon, name, args, whitespace, equals, h_end in contents:gmatch("()(%[?[%w,_ =\"]*%]?)[\r\n\t ]*()e2function%s+(" .. p_typename .. ")%s+([a-z0-9]-)%s*(:?)%s*(" .. p_func_operator .. ")%(([^)]*)%)(%s*)(=?)%s*()") do + local _, line = contents:sub(1, h_begin):gsub("\n", "") + + local trace = "(at line " .. line .. ")" .. (E2Lib.currentextension and (" @" .. filename) or "") + + if contents:sub(h_begin - 1, h_begin - 1):match("%w") then + error("PP syntax error: Must not have characters before 'e2function' " .. trace) + elseif not name:find("^" .. p_funcname .. "$") and not Operators[name] and not RemovedOperators[name] then + error("PP syntax error: Invalid function name format '" .. name .. "' " .. trace) + elseif thistype ~= "" and colon == "" then + error("PP syntax error: Function names may not start with a number. " .. trace) + elseif thistype == "" and colon ~= "" then + error("PP syntax error: No type for 'this' given." .. trace) + elseif thistype ~= "" and not getTypeId(thistype) then + error("PP syntax error: Invalid type for 'this': '" .. thistype .. "' " .. trace) + elseif thistype:match("^%d") then + error("PP syntax error: Type names may not start with a number." .. trace) + elseif equals ~= "" and aliaspos then + error("PP syntax error: Malformed alias definition. " .. trace) + elseif ret ~= "void" and not getTypeId(ret) then + error("PP syntax error: Invalid return type: '" .. ret .. "' " .. trace) + elseif RemovedOperators[name] then -- Old operator that no longer is needed. + ErrorNoHalt("Warning: Operator " .. name .. " is now redundant. Ignoring registration. " .. trace .. "\n") + local pivot = parseAttributes(attributes, trace) and a_begin - 1 or h_begin - 1 + table.insert(output, contents:sub(lastpos, pivot)) -- Insert code from before header. + changed, lastpos = true, h_end -- Mark as changed and remove function header. + table.insert(output, "local _ = function() ") -- Insert dummy lambda function to substitute for function declaration. else - attributes = "{}" - end + changed = true -- Mark as changed - changed = true - - local function handle_function() - if contents:sub(h_begin - 1, h_begin - 1):match("%w") then return end - local aliasflag = nil - if equals == "" then - if aliaspos then - if contents:sub(aliaspos, h_begin - 1):find("%S") then error("PP syntax error: Malformed alias definition.", 0) end - -- right hand side of an alias assignment - aliasflag = 2 - aliaspos = nil - end - else - if aliaspos then error("PP syntax error: Malformed alias definition.", 0) end - -- left hand side of an alias assignment + local aliasflag + if equals == "" and aliaspos then -- Alias right hand side + assert(not contents:sub(aliaspos, h_begin - 1):find("%S"), "PP syntax error: Malformed alias definition. " .. trace) + aliasflag, aliaspos = 2, nil + elseif equals ~= "" then -- Left hand side of alias aliasflag = 1 aliaspos = h_end end - -- check for some obvious errors - if thistype ~= "" and colon == "" then error("PP syntax error: Function names may not start with a number.", 0) end - if thistype == "" and colon ~= "" then error("PP syntax error: No type for 'this' given.", 0) end - if thistype:match("^[0-9]") then error("PP syntax error: Type names may not start with a number.", 0) end + local is_operator = false + if Operators[name] then + name, is_operator = Operators[name], true + end + + local params, has_vararg, vartbl_name = parseParameters(args, trace) + + local attributes = parseAttributes(attributes, trace) + + local attr_str + if attributes then + attributes.legacy = "false" + + attr_str = "{" + for k, v in pairs(attributes) do + attr_str = attr_str .. k .. "=" .. v .. "," + end + attr_str = attr_str .. "}" - -- append everything since the last function to the output. - if attributes_str then table.insert(output, contents:sub(lastpos, a_begin - 1)) - table.insert(output, "--" .. attributes_str .. "\n") + table.insert(output, "-- attributes: " .. attr_str .. "\n") -- Add line for annotations else - table.insert(output, contents:sub(lastpos, h_begin - 1)) + attr_str = "{ legacy = false }" + table.insert(output, contents:sub(lastpos, h_begin - 1)) -- Append stuff in between functions end - -- advance lastpos to the end of the function header - lastpos = h_end - - -- this table contains the arguments in the following form: - -- argtable.argname[n] = "" - -- argtable.typeids[n] = "" - local argtable, args_kind, args_varname = parseArgs(args) - - -- take care of operators: give them a different name and register function - -- op_type is nil if we register a function and a number if it as operator - local name, regfn, op_type = handleop(name) - - -- return type (void means "returns nothing", i.e. "" in registerFunctionese) - local ret_typeid = (ret == "void") and "" or getTypeId(ret) - - -- return type not found => throw an error - if not ret_typeid then error("PP syntax error: Invalid return type: '" .. ret .. "'", 0) end - - -- if "typename:" was found in front of the function name - if thistype ~= "" then - -- evaluate the type name - local this_typeid = getTypeId(thistype) - - -- the type was not found? - if this_typeid == nil then - -- is the type name a valid typeid? - if isValidTypeId(thistype) then - -- use the type name as the typeid - this_typeid = thistype - else - -- type is not found and not a valid typeid => error - error("PP syntax error: Invalid type for 'this': '" .. thistype .. "'", 0) - end - end + lastpos = h_end -- Advance to end of function header - -- prepend a "this" argument to the list, with the parsed type - if op_type then - -- allow pseudo-member-operators. example: e2function matrix:operator*(factor) - table.insert(argtable.typeids, 1, this_typeid) + if thistype ~= "" then -- prepend a "this" argument to the list, with the parsed type + if is_operator then -- allow pseudo-member-operators. example: e2function matrix:operator*(factor) + table.insert(params, 1, {"this", getTypeId(thistype)}) else - table.insert(argtable.typeids, 1, this_typeid .. ":") + table.insert(params, 1, {"this", getTypeId(thistype) .. ":"}) end - table.insert(argtable.argnames, 1, "this") - end -- if thistype ~= "" - - -- add a sub-table for flagging arguments as "no opfetch" - argtable.no_opfetch = {} - - if op_type == OPTYPE_ASSIGN then -- assignment - -- the assignment operator is registered with only argument typeid, hence we need a special case. - -- we need to make sure the two types match: - if argtable.typeids[1] ~= argtable.typeids[2] then error("PP syntax error: operator= needs two arguments of the same type.", 0) end - - -- remove the typeid of one of the arguments from the list - argtable.typeids[1] = "" - - -- mark the argument as "no opfetch" - argtable.no_opfetch[1] = true - elseif op_type == OPTYPE_DONT_FETCH_FIRST then -- delta/increment/decrement - -- mark the argument as "no opfetch" - argtable.no_opfetch[1] = true - elseif op_type == OPTYPE_APPEND_RET then - table.insert(argtable.typeids, 1, ret_typeid .. "=") end - -- -- prepare some variables needed to generate the function header and the registerFunction line -- -- - - -- concatenated typeids. example: "s:nn" - local arg_typeids = table.concat(argtable.typeids) - - -- generate a mangled name, which serves as the function's Lua name - local mangled_name = mangle(name, arg_typeids, op_type) - - if aliasflag then - if aliasflag == 1 then - -- left hand side of an alias definition - aliasdata = { regfn, name, arg_typeids, ret_typeid, attributes } - elseif aliasflag == 2 then - -- right hand side of an alias definition - regfn, name, arg_typeids, ret_typeid, attributes = unpack(aliasdata) - table.insert(function_register, - string.format( - 'if registeredfunctions.%s then %s(%q, %q, %q, registeredfunctions.%s, tempcosts[%q], %s, %s) end\n', - mangled_name, regfn, name, arg_typeids, ret_typeid, mangled_name, mangled_name, makestringtable(argtable.argnames, (thistype ~= "") and 2 or 1), attributes - ) + local sig = {} + for i, param in ipairs(params) do + sig[i] = param[2] + end + + local param_sig = table.concat(sig) + local ret_typeid = getTypeId(ret) or "" + + local mangled = "e2_" .. (is_operator and ("operator_" .. name) or name) .. "_" .. param_sig:gsub("[:=]", "_") + + local param_names, param_names_quot = {}, {} + for i, param in ipairs(params) do + param_names[i], param_names_quot[i] = param[1], '"' .. param[1] .. '"' + end + + if aliasflag == 1 then + aliasdata = { is_operator, name, param_sig, ret_typeid, attr_str } + elseif aliasflag == 2 then -- Override information with alias information. + is_operator, name, param_sig, ret_typeid, attr_str = aliasdata[1], aliasdata[2], aliasdata[3], aliasdata[4], aliasdata[5] + end + + table.insert(footer, compact([[ + if registeredfunctions.]] .. mangled .. [[ then + ]] .. (is_operator and "registerOperator" or "registerFunction") .. [[( + "]] .. name .. [[", + "]] .. param_sig .. (has_vararg and "..." or "") .. [[", + "]] .. ret_typeid .. [[", + registeredfunctions.]] .. mangled .. [[, + tempcosts.]] .. mangled .. [[, + ]] .. "{" .. table.concat(param_names_quot, ",", thistype ~= "" and 2 or 1) .. "}" .. [[, + ]] .. attr_str .. [[ ) end - else - -- save tempcost - table.insert(output, string.format("tempcosts[%q]=__e2getcost() ", mangled_name)) - if args_kind == ArgsKind.Variadic then - -- generate a registerFunction line - table.insert(function_register, - string.format( - 'if registeredfunctions.%s then %s(%q, %q, %q, registeredfunctions.%s, tempcosts[%q], %s) end\n', - mangled_name, regfn, name, arg_typeids .. "...", ret_typeid, mangled_name, mangled_name, makestringtable(argtable.argnames, (thistype ~= "") and 2 or 1), attributes - ) - ) + ]])) + if aliasflag then -- Add single newline, since aliasing only does anything in the footer. + table.insert(output, "\n") + else + table.insert(output, compact([[ + tempcosts.]] .. mangled .. [[ = __e2getcost() + ]])) + + if #param_names == 0 then -- No parameters, simple case. + if has_vararg then + table.insert(output, compact([[ + function registeredfunctions.]] .. mangled .. [[(self, args, typeids]] .. ((has_vararg and not vartbl_name) and ", ..." or "") .. [[) + ]] .. (vartbl_name and ("local " .. vartbl_name .. " = args") or "") .. [[ + ]] .. ((has_vararg and not vartbl_name) and ("if not ... then return registeredfunctions." .. mangled .. "(self, args, typeids, unpack(args)) end") or "") .. [[ + ]])) + else -- No varargs either, simplest case + table.insert(output, [[function registeredfunctions.]] .. mangled .. [[(self, args, typeids)]]) + end + elseif is_operator then -- Operators are directly passed the arguments, since they're known at compile time. table.insert(output, compact([[ - function registeredfunctions.]] .. mangled_name .. [[(self, args, typeids, ...) - if not typeids then - local arr, typeids, source_typeids, tmp = {}, {}, args[#args] - for i = ]] .. 2 + #argtable.typeids .. [[, #args - 1 do - tmp = args[i] - - arr[i - ]] .. 1 + #argtable.typeids .. [[] = tmp[1](self, tmp) - typeids[i - ]] .. 1 + #argtable.typeids .. [[] = source_typeids[i - ]] .. (thistype ~= "" and 2 or 1) .. [[] - end - return registeredfunctions.]] .. mangled_name .. [[(self, args, typeids, unpack(arr)) - end + function registeredfunctions.]] .. mangled .. [[(self, ]] .. table.concat(param_names, ", ") .. [[) ]])) - elseif args_kind == ArgsKind.VariadicTbl then - -- generate a registerFunction line - table.insert(function_register, - string.format( - 'if registeredfunctions.%s then %s(%q, %q, %q, registeredfunctions.%s, tempcosts[%q], %s, %s) end\n', - mangled_name, regfn, name, arg_typeids .. "...", ret_typeid, mangled_name, mangled_name, makestringtable(argtable.argnames, (thistype ~= "") and 2 or 1), attributes - ) - ) + else + local param_get = {} + for i = 1, #param_names do + param_get[i] = "args[" .. i .. "]" + end + local pivot = #param_names + 1 - -- Using __varargs_priv to avoid shadowing variables like `args` and breaking this implementation. table.insert(output, compact([[ - function registeredfunctions.]] .. mangled_name .. [[(self, args, typeids, __varargs_priv) - if not typeids then - __varargs_priv, typeids = {}, {} - local source_typeids, tmp = args[#args] - for i = ]] .. 2 + #argtable.typeids .. [[, #args - 1 do - tmp = args[i] - __varargs_priv[i - ]] .. 1 + #argtable.typeids .. [[] = tmp[1](self, tmp) - typeids[i - ]] .. 1 + #argtable.typeids .. [[] = source_typeids[i - ]] .. (thistype ~= "" and 2 or 1) .. [[] - end - end - - ]] .. (#argtable.argnames == 0 and ("local " .. args_varname .. " = __varargs_priv") or "") .. [[ + function registeredfunctions.]] .. mangled .. [[(self, args, typeids]] .. ((has_vararg and not vartbl_name) and ", ..." or "") .. [[) + ]] .. (#param_names ~= 0 and ("local " .. table.concat(param_names, ", ") .. "=" .. table.concat(param_get, ",")) or "") .. [[ + ]] .. (vartbl_name and ("local " .. vartbl_name .. " = { unpack(args, " .. pivot .. ") }") or "") .. [[ + ]] .. (has_vararg and ("local typeids = { unpack(typeids, " .. pivot - (thistype == "" and 0 or 1) .. ") }" ) or "") .. [[ + ]] .. ((has_vararg and not vartbl_name) and ("if not ... then return registeredfunctions." .. mangled .. "(self, args, typeids, unpack(args, " .. pivot .. ")) end") or "") .. [[ ]])) - else - -- generate a registerFunction line - table.insert(function_register, - string.format( - 'if registeredfunctions.%s then %s(%q, %q, %q, registeredfunctions.%s, tempcosts[%q], %s, %s) end\n', - mangled_name, regfn, name, arg_typeids, ret_typeid, mangled_name, mangled_name, makestringtable(argtable.argnames, (thistype ~= "") and 2 or 1), attributes - ) - ) - - -- generate a new function header and append it to the output - table.insert(output, 'function registeredfunctions.' .. mangled_name .. '(self, args)') end - -- if the function has arguments, insert argument fetch code - if #argtable.argnames ~= 0 then - local argfetch, opfetch_l, opfetch_r = '', '', '' - for i, name in ipairs(argtable.argnames) do - if not argtable.no_opfetch[i] then - -- generate opfetch code if not flagged as "no opfetch" - opfetch_l = string.format('%s%s, ', opfetch_l, name) - opfetch_r = string.format('%s%s[1](self, %s), ', opfetch_r, name, name) - end - argfetch = string.format('%sargs[%d], ', argfetch, i + 1) - end - - -- remove the trailing commas - argfetch = argfetch:sub(1, -3) - opfetch_l = opfetch_l:sub(1, -3) - opfetch_r = opfetch_r:sub(1, -3) - - -- fetch the rvs from the args - table.insert(output, string.format(' local %s = %s %s = %s', - table.concat(argtable.argnames, ', '), - argfetch, - opfetch_l, - opfetch_r)) - - -- Workaround if someone names their variadic args an internally used variable - if args_kind == ArgsKind.VariadicTbl then - table.insert(output, " local " .. args_varname .. " = __varargs_priv") - end - end -- if #argtable.argnames ~= 0 - end -- if aliasflag - table.insert(output, whitespace) - end - - -- use pcall, so we can add line numbers to all errors - local ok, msg = pcall(handle_function) - if not ok then - if msg:sub(1, 2) == "PP" then - error(":" .. linenumber(contents, h_begin) .. ": " .. msg, 0) - else - error(": PP internal error: " .. msg, 0) + table.insert(output, whitespace) end end end -- for contents:gmatch(e2function) - -- did the preprocessor change anything? if changed then -- yes => sweep everything together into a neat pile of hopefully valid lua code - return table.concat(output) .. contents:sub(lastpos, -6) .. table.concat(function_register) + return table.concat(output) .. contents:sub(lastpos, -6) .. table.concat(footer, "\n") else -- no => tell the environment about it, so it can include() the source file instead. return false end -end +end \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/core/gametick.lua b/lua/entities/gmod_wire_expression2/core/gametick.lua index 5e6b9e9930..79cd51acca 100644 --- a/lua/entities/gmod_wire_expression2/core/gametick.lua +++ b/lua/entities/gmod_wire_expression2/core/gametick.lua @@ -12,7 +12,7 @@ end) __e2setcost(1) --- If != 0 the expression will execute once every game tick -[nodiscard, deprecated = "Use the tick event instead"] +[deprecated = "Use the tick event instead"] e2function void runOnTick(activate) if activate ~= 0 then registered_chips[self.entity] = true diff --git a/lua/entities/gmod_wire_expression2/core/globalvars.lua b/lua/entities/gmod_wire_expression2/core/globalvars.lua index 61f367042a..940ffd3d17 100644 --- a/lua/entities/gmod_wire_expression2/core/globalvars.lua +++ b/lua/entities/gmod_wire_expression2/core/globalvars.lua @@ -32,22 +32,8 @@ registerType( "gtable", "xgt", {}, __e2setcost(1) -registerOperator("ass", "xgt", "xgt", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if !lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - -e2function number operator_is( gtable tbl ) - return istable(tbl) and 1 or 0 +e2function number operator_is(gtable this) + return istable(this) and 1 or 0 end ------------------------------------------------ @@ -146,6 +132,8 @@ local non_allowed_types = { -- If anyone can think of any other types that shoul xgt = true, } +local fixDefault = E2Lib.fixDefault + registerCallback("postinit",function() for k,v in pairs( wire_expression_types ) do if (!non_allowed_types[v[1]]) then @@ -155,28 +143,26 @@ registerCallback("postinit",function() __e2setcost(5) -- Table[index,type] functions - local function getf( self, args ) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if isnumber(rv2) then rv2 = tostring(rv2) end - local val = rv1[v[1]..rv2] - if (val) then -- If the var exists + local function getf(state, gtable, key) + if isnumber(key) then key = tostring(key) end + local val = gtable[v[1]..key] + if val then -- If the var exists return val -- return it + else + return fixDefault(v[2]) end - return E2Lib.fixDefault(v[2]) end - local function setf( self, args ) - local op1, op2, op3 = args[2], args[3], args[4] - local rv1, rv2, rv3 = op1[1](self, op1), op2[1](self, op2), op3[1](self, op3) - if isnumber(rv2) then rv2 = tostring(rv2) end - rv1[v[1]..rv2] = rv3 - return rv3 + + local function setf(state, gtable, key, value) + if isnumber(key) then key = tostring(key) end + gtable[v[1] .. key] = value + return value end - registerOperator("idx", v[1].."=xgts", v[1], getf) -- G[S,type] - registerOperator("idx", v[1].."=xgts"..v[1], v[1], setf) -- G[S,type] - registerOperator("idx", v[1].."=xgtn", v[1], getf) -- G[N,type] (same as G[N:toString(),type]) - registerOperator("idx", v[1].."=xgtn"..v[1], v[1], setf) -- G[N,type] (same as G[N:toString(),type]) + registerOperator("indexget", "xgts" .. v[1], v[1], getf) -- G[S,type] + registerOperator("indexset", "xgts".. v[1], "", setf) -- G[S,type] + registerOperator("indexget", "xgtn" .. v[1], v[1], getf) -- G[N,type] (same as G[N:toString(),type]) + registerOperator("indexset", "xgtn".. v[1], "", setf) -- G[N,type] (same as G[N:toString(),type]) ------ --gRemove* -- Remove the variable at the specified index and return it @@ -224,36 +210,18 @@ registerCallback("postinit",function() -------------------------------------------------------------------------------- __e2setcost(1) - registerOperator("fea", "s" .. v[1] .. "xgt", "", function(self, args) - local keyname, valname = args[2], args[3] - - local tbl = args[4] - tbl = tbl[1](self, tbl) - - local statement = args[5] - local len = #v[1] + local len = #v[1] - for key, value in pairs(tbl) do - if key:sub(1, len) == v[1] then - self:PushScope() - - self.prf = self.prf + 3 - - self.Scope.vclk[keyname] = true - self.Scope.vclk[valname] = true - - self.Scope[keyname] = key:sub(len + 1) - self.Scope[valname] = value - - local ok, msg = pcall(statement[1], self, statement) - - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end + local function iter(tbl, i) + local key, value = next(tbl, i) + if key and key:sub(1, len) == v[1] then + return key, value + end + end - self:PopScope() - end + registerOperator("iter", "s" .. v[1] .. "=xgt", "", function(state, gtable) + return function() + return iter, gtable end end) diff --git a/lua/entities/gmod_wire_expression2/core/init.lua b/lua/entities/gmod_wire_expression2/core/init.lua index 2c555a0a03..21ac2de546 100644 --- a/lua/entities/gmod_wire_expression2/core/init.lua +++ b/lua/entities/gmod_wire_expression2/core/init.lua @@ -5,9 +5,6 @@ AddCSLuaFile() Andreas "Syranide" Svensson, me@syranide.com ]] -wire_expression2_delta = 0.0000001000000 -delta = wire_expression2_delta - -- functions to type-check function return values. local wire_expression2_debug = CreateConVar("wire_expression2_debug", 0, 0) @@ -172,14 +169,70 @@ function __e2getcost() return tempcost end -function registerOperator(name, pars, rets, func, cost, argnames) +---@param args string +---@return string?, table +local function getArgumentTypeIds(args) + local thistype, nargs = args:match("^([^:]+):(.*)$") + if nargs then args = nargs end + + local out, ptr = {}, 1 + while ptr <= #args do + local c = args:sub(ptr, ptr) + if c == "x" then + out[#out + 1] = args:sub(ptr, ptr + 2) + ptr = ptr + 3 + elseif args:sub(ptr) == "..." then + out[#out + 1] = "..." + ptr = ptr + 3 + elseif c:match("^%w") then + out[#out + 1] = c + ptr = ptr + 1 + else + error("Invalid signature: " .. args) + end + end + + return thistype, out +end + +local EnforcedTypings = { + ["is"] = "n" +} + +---@param name string +---@param pars string +---@param rets string +---@param func fun(state: RuntimeContext, ...): any +---@param cost integer? +---@param argnames string[]? +---@param attributes table? +function registerOperator(name, pars, rets, func, cost, argnames, attributes) + if attributes and attributes.legacy == nil then + -- can explicitly mark "false" (used by extpp) + attributes.legacy = true + elseif not attributes then + attributes = { legacy = true } + end + + local enforced = EnforcedTypings[name] + if enforced and rets ~= enforced then + error("Registering invalid operator '" .. name .. "' (must return type " .. enforced .. ")") + end + local signature = "op:" .. name .. "(" .. pars .. ")" - wire_expression2_funcs[signature] = { signature, rets, func, cost or tempcost, argnames = argnames } + wire_expression2_funcs[signature] = { signature, rets, func, cost or tempcost, argnames = argnames, attributes = attributes } if wire_expression2_debug:GetBool() then makecheck(signature) end end function registerFunction(name, pars, rets, func, cost, argnames, attributes) + if attributes and attributes.legacy == nil then + -- can explicitly mark "false" (used by extpp) + attributes.legacy = true + elseif not attributes then + attributes = { legacy = true } + end + local signature = name .. "(" .. pars .. ")" wire_expression2_funcs[signature] = { signature, rets, func, cost or tempcost, argnames = argnames, extension = E2Lib.currentextension, attributes = attributes } @@ -188,10 +241,8 @@ function registerFunction(name, pars, rets, func, cost, argnames, attributes) if wire_expression2_debug:GetBool() then makecheck(signature) end end -function E2Lib.registerConstant(name, value, literal) +function E2Lib.registerConstant(name, value) if name:sub(1, 1) ~= "_" then name = "_" .. name end - if not value and not literal then value = _G[name] end - wire_expression2_constants[name] = value end @@ -312,13 +363,14 @@ if SERVER then } end - miscdata = { {}, wire_expression2_constants, events_sanitized } - - functiondata = {} + local types = {} for typename, v in pairs(wire_expression_types) do - miscdata[1][typename] = v[1] -- typeid (s) + types[typename] = v[1] -- typeid (s) end + miscdata = { types, wire_expression2_constants, events_sanitized } + functiondata = {} + for signature, v in pairs(wire_expression2_funcs) do functiondata[signature] = { v[2], v[4], v.argnames, v.extension, v.attributes } -- ret (s), cost (n), argnames (t), extension (s), attributes (t) end @@ -418,7 +470,7 @@ elseif CLIENT then end end - ---@param events table + ---@param events table local function insertMiscData(types, constants, events) wire_expression2_reset_extensions() diff --git a/lua/entities/gmod_wire_expression2/core/matrix.lua b/lua/entities/gmod_wire_expression2/core/matrix.lua index 1d22e5b17c..f51b0604ff 100644 --- a/lua/entities/gmod_wire_expression2/core/matrix.lua +++ b/lua/entities/gmod_wire_expression2/core/matrix.lua @@ -2,8 +2,6 @@ Matrix support \******************************************************************************/ -local delta = wire_expression2_delta - local function clone(a) local b = {} for k,v in ipairs(a) do @@ -86,41 +84,20 @@ e2function matrix2 identity2() 0, 1 } end -/******************************************************************************/ - -registerOperator("ass", "xm2", "xm2", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - -/******************************************************************************/ -// Comparison - -e2function number operator_is(matrix2 rv1) - if rv1[1] > delta or -rv1[1] > delta or - rv1[2] > delta or -rv1[2] > delta or - rv1[3] > delta or -rv1[3] > delta or - rv1[4] > delta or -rv1[4] > delta - then return 1 else return 0 end +e2function number operator_is(matrix2 this) + return (this[1] ~= 0 + or this[2] ~= 0 + or this[3] ~= 0) + and 1 or 0 end -e2function number operator==(matrix2 rv1, matrix2 rv2) - if rv1[1] - rv2[1] <= delta and rv2[1] - rv1[1] <= delta and - rv1[2] - rv2[2] <= delta and rv2[2] - rv1[2] <= delta and - rv1[3] - rv2[3] <= delta and rv2[3] - rv1[3] <= delta and - rv1[4] - rv2[4] <= delta and rv2[4] - rv1[4] <= delta - then return 1 else return 0 end -end -e2function number operator!=(matrix2 rv1, matrix2 rv2) - if rv1[1] - rv2[1] > delta and rv2[1] - rv1[1] > delta and - rv1[2] - rv2[2] > delta and rv2[2] - rv1[2] > delta and - rv1[3] - rv2[3] > delta and rv2[3] - rv1[3] > delta and - rv1[4] - rv2[4] > delta and rv2[4] - rv1[4] > delta - then return 1 else return 0 end +e2function number operator==(matrix2 rv1, matrix2 rv2) + return (rv1[1] == rv2[1] + and rv1[2] == rv2[2] + and rv1[3] == rv2[3] + and rv1[4] == rv2[4]) + and 1 or 0 end /******************************************************************************/ @@ -473,56 +450,25 @@ e2function matrix identity() 0, 0, 1 } end -/******************************************************************************/ - -registerOperator("ass", "m", "m", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - -/******************************************************************************/ -// Comparison - -e2function number operator_is(matrix rv1) - if rv1[1] > delta or -rv1[1] > delta or - rv1[2] > delta or -rv1[2] > delta or - rv1[3] > delta or -rv1[3] > delta or - rv1[4] > delta or -rv1[4] > delta or - rv1[5] > delta or -rv1[5] > delta or - rv1[6] > delta or -rv1[6] > delta or - rv1[7] > delta or -rv1[7] > delta or - rv1[8] > delta or -rv1[8] > delta or - rv1[9] > delta or -rv1[9] > delta - then return 1 else return 0 end +e2function number operator_is(matrix this) + return ( + this[1] ~= 0 or this[2] ~= 0 or this[3] ~= 0 + or this[4] ~= 0 or this[5] ~= 0 or this[6] ~= 0 + or this[7] ~= 0 or this[8] ~= 0 or this[9] ~= 0 + ) and 1 or 0 end e2function number operator==(matrix rv1, matrix rv2) - if rv1[1] - rv2[1] <= delta and rv2[1] - rv1[1] <= delta and - rv1[2] - rv2[2] <= delta and rv2[2] - rv1[2] <= delta and - rv1[3] - rv2[3] <= delta and rv2[3] - rv1[3] <= delta and - rv1[4] - rv2[4] <= delta and rv2[4] - rv1[4] <= delta and - rv1[5] - rv2[5] <= delta and rv2[5] - rv1[5] <= delta and - rv1[6] - rv2[6] <= delta and rv2[6] - rv1[6] <= delta and - rv1[7] - rv2[7] <= delta and rv2[7] - rv1[7] <= delta and - rv1[8] - rv2[8] <= delta and rv2[8] - rv1[8] <= delta and - rv1[9] - rv2[9] <= delta and rv2[9] - rv1[9] <= delta - then return 1 else return 0 end -end - -e2function number operator!=(matrix rv1, matrix rv2) - if rv1[1] - rv2[1] > delta and rv2[1] - rv1[1] > delta and - rv1[2] - rv2[2] > delta and rv2[2] - rv1[2] > delta and - rv1[3] - rv2[3] > delta and rv2[3] - rv1[3] > delta and - rv1[4] - rv2[4] > delta and rv2[4] - rv1[4] > delta and - rv1[5] - rv2[5] > delta and rv2[5] - rv1[5] > delta and - rv1[6] - rv2[6] > delta and rv2[6] - rv1[6] > delta and - rv1[7] - rv2[7] > delta and rv2[7] - rv1[7] > delta and - rv1[8] - rv2[8] > delta and rv2[8] - rv1[8] > delta and - rv1[9] - rv2[9] > delta and rv2[9] - rv1[9] > delta - then return 1 else return 0 end + return (rv1[1] == rv2[1] + and rv1[2] == rv2[2] + and rv1[3] == rv2[3] + and rv1[4] == rv2[4] + and rv1[5] == rv2[5] + and rv1[6] == rv2[6] + and rv1[7] == rv2[7] + and rv1[8] == rv2[8] + and rv1[9] == rv2[9]) + and 1 or 0 end /******************************************************************************/ @@ -896,7 +842,7 @@ e2function matrix mRotation(vector rv1, rv2) local vec local len = rv1:Length() if len == 1 then vec = rv1 - elseif len > delta then vec = Vector(rv1[1] / len, rv1[2] / len, rv1[3] / len) + elseif len > 0 then vec = Vector(rv1[1] / len, rv1[2] / len, rv1[3] / len) else return { 0, 0, 0, 0, 0, 0, 0, 0, 0 } @@ -1003,77 +949,33 @@ e2function matrix4 identity4() 0, 0, 0, 1 } end -/******************************************************************************/ - -registerOperator("ass", "xm4", "xm4", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - -/******************************************************************************/ -// Comparison - -e2function number operator_is(matrix4 rv1) - if rv1[1] > delta or -rv1[1] > delta or - rv1[2] > delta or -rv1[2] > delta or - rv1[3] > delta or -rv1[3] > delta or - rv1[4] > delta or -rv1[4] > delta or - rv1[5] > delta or -rv1[5] > delta or - rv1[6] > delta or -rv1[6] > delta or - rv1[7] > delta or -rv1[7] > delta or - rv1[8] > delta or -rv1[8] > delta or - rv1[9] > delta or -rv1[9] > delta or - rv1[10] > delta or -rv1[10] > delta or - rv1[11] > delta or -rv1[11] > delta or - rv1[12] > delta or -rv1[12] > delta or - rv1[13] > delta or -rv1[13] > delta or - rv1[14] > delta or -rv1[14] > delta or - rv1[15] > delta or -rv1[15] > delta or - rv1[16] > delta or -rv1[16] > delta - then return 1 else return 0 end +e2function number operator_is(matrix4 this) + return ( + this[1] ~= 0 or this[2] ~= 0 or this[3] ~= 0 or this[4] ~= 0 + or this[5] ~= 0 or this[6] ~= 0 or this[7] ~= 0 or this[8] ~= 0 + or this[9] ~= 0 or this[10] ~= 0 or this[11] ~= 0 or this[12] ~= 0 + or this[13] ~= 0 or this[14] ~= 0 or this[15] ~= 0 or this[16] ~= 0 + ) and 1 or 0 end e2function number operator==(matrix4 rv1, matrix4 rv2) - if rv1[1] - rv2[1] <= delta and rv2[1] - rv1[1] <= delta and - rv1[2] - rv2[2] <= delta and rv2[2] - rv1[2] <= delta and - rv1[3] - rv2[3] <= delta and rv2[3] - rv1[3] <= delta and - rv1[4] - rv2[4] <= delta and rv2[4] - rv1[4] <= delta and - rv1[5] - rv2[5] <= delta and rv2[5] - rv1[5] <= delta and - rv1[6] - rv2[6] <= delta and rv2[6] - rv1[6] <= delta and - rv1[7] - rv2[7] <= delta and rv2[7] - rv1[7] <= delta and - rv1[8] - rv2[8] <= delta and rv2[8] - rv1[8] <= delta and - rv1[9] - rv2[9] <= delta and rv2[9] - rv1[9] <= delta and - rv1[10] - rv2[10] <= delta and rv2[10] - rv1[10] <= delta and - rv1[11] - rv2[11] <= delta and rv2[11] - rv1[11] <= delta and - rv1[12] - rv2[12] <= delta and rv2[12] - rv1[12] <= delta and - rv1[13] - rv2[13] <= delta and rv2[13] - rv1[13] <= delta and - rv1[14] - rv2[14] <= delta and rv2[14] - rv1[14] <= delta and - rv1[15] - rv2[15] <= delta and rv2[15] - rv1[15] <= delta and - rv1[16] - rv2[16] <= delta and rv2[16] - rv1[16] <= delta - then return 1 else return 0 end -end - -e2function number operator!=(matrix4 rv1, matrix4 rv2) - if rv1[1] - rv2[1] > delta and rv2[1] - rv1[1] > delta and - rv1[2] - rv2[2] > delta and rv2[2] - rv1[2] > delta and - rv1[3] - rv2[3] > delta and rv2[3] - rv1[3] > delta and - rv1[4] - rv2[4] > delta and rv2[4] - rv1[4] > delta and - rv1[5] - rv2[5] > delta and rv2[5] - rv1[5] > delta and - rv1[6] - rv2[6] > delta and rv2[6] - rv1[6] > delta and - rv1[7] - rv2[7] > delta and rv2[7] - rv1[7] > delta and - rv1[8] - rv2[8] > delta and rv2[8] - rv1[8] > delta and - rv1[9] - rv2[9] > delta and rv2[9] - rv1[9] > delta and - rv1[10] - rv2[10] > delta and rv2[10] - rv1[10] > delta and - rv1[11] - rv2[11] > delta and rv2[11] - rv1[11] > delta and - rv1[12] - rv2[12] > delta and rv2[12] - rv1[12] > delta and - rv1[13] - rv2[13] > delta and rv2[13] - rv1[13] > delta and - rv1[14] - rv2[14] > delta and rv2[14] - rv1[14] > delta and - rv1[15] - rv2[15] > delta and rv2[15] - rv1[15] > delta and - rv1[16] - rv2[16] > delta and rv2[16] - rv1[16] > delta - then return 1 else return 0 end + return (rv1[1] == rv2[1] + and rv1[2] == rv2[2] + and rv1[3] == rv2[3] + and rv1[4] == rv2[4] + and rv1[5] == rv2[5] + and rv1[6] == rv2[6] + and rv1[7] == rv2[7] + and rv1[8] == rv2[8] + and rv1[9] == rv2[9] + and rv1[10] == rv2[10] + and rv1[11] == rv2[11] + and rv1[12] == rv2[12] + and rv1[13] == rv2[13] + and rv1[14] == rv2[14] + and rv1[15] == rv2[15] + and rv1[16] == rv2[16]) + and 1 or 0 end /******************************************************************************/ diff --git a/lua/entities/gmod_wire_expression2/core/number.lua b/lua/entities/gmod_wire_expression2/core/number.lua index 78e8a44011..bf24dc6db9 100644 --- a/lua/entities/gmod_wire_expression2/core/number.lua +++ b/lua/entities/gmod_wire_expression2/core/number.lua @@ -1,6 +1,4 @@ -- these upvalues (locals in an enclosing scope) are faster to access than globals. -local delta = wire_expression2_delta - local math = math local random = math.random local pi = math.pi @@ -50,119 +48,59 @@ E2Lib.registerConstant("PHI", (1+sqrt(5))/2) --[[************************************************************************]]-- -__e2setcost(2) - -registerOperator("ass", "n", "n", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - __e2setcost(1.5) -registerOperator("inc", "n", "", function(self, args) - local op1, scope = args[2], args[3] - self.Scopes[scope][op1] = self.Scopes[scope][op1] + 1 - self.Scopes[scope].vclk[op1] = true -end) - -registerOperator("dec", "n", "", function(self, args) - local op1, scope = args[2], args[3] - self.Scopes[scope][op1] = self.Scopes[scope][op1] - 1 - self.Scopes[scope].vclk[op1] = true -end) - --[[************************************************************************]]-- -__e2setcost(1.5) - -registerOperator("eq", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rvd = op1[1](self, op1) - op2[1](self, op2) - if rvd <= delta and -rvd <= delta - then return 1 else return 0 end -end) - -registerOperator("neq", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rvd = op1[1](self, op1) - op2[1](self, op2) - if rvd > delta or -rvd > delta - then return 1 else return 0 end -end) - -__e2setcost(1.25) - -registerOperator("geq", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rvd = op1[1](self, op1) - op2[1](self, op2) - if -rvd <= delta - then return 1 else return 0 end -end) - -registerOperator("leq", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] +registerOperator("geq", "nn", "n", function(state, lhs, rhs) + return lhs >= rhs and 1 or 0 +end, 1, nil, { legacy = false }) - local rvd = op1[1](self, op1) - op2[1](self, op2) - if rvd <= delta - then return 1 else return 0 end -end) +registerOperator("leq", "nn", "n", function(state, lhs, rhs) + return lhs <= rhs and 1 or 0 +end, 1, nil, { legacy = false }) -registerOperator("gth", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rvd = op1[1](self, op1) - op2[1](self, op2) - if rvd > delta - then return 1 else return 0 end -end) +registerOperator("gth", "nn", "n", function(state, lhs, rhs) + return lhs > rhs and 1 or 0 +end, 1, nil, { legacy = false }) -registerOperator("lth", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rvd = op1[1](self, op1) - op2[1](self, op2) - if -rvd > delta - then return 1 else return 0 end -end) +registerOperator("lth", "nn", "n", function(state, lhs, rhs) + return lhs < rhs and 1 or 0 +end, 1, nil, { legacy = false }) --[[************************************************************************]]-- __e2setcost(0.5) -- approximation -registerOperator("neg", "n", "n", function(self, args) - local op1 = args[2] - return -op1[1](self, op1) -end) +registerOperator("neg", "n", "n", function(state, num) + return -num +end, 1, nil, { legacy = false }) __e2setcost(1) -registerOperator("add", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - return op1[1](self, op1) + op2[1](self, op2) -end) +registerOperator("add", "nn", "n", function(state, lhs, rhs) + return lhs + rhs +end, 1, nil, { legacy = false }) -registerOperator("sub", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - return op1[1](self, op1) - op2[1](self, op2) -end) +registerOperator("sub", "nn", "n", function(state, lhs, rhs) + return lhs - rhs +end, 1, nil, { legacy = false }) -registerOperator("mul", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - return op1[1](self, op1) * op2[1](self, op2) -end) +registerOperator("mul", "nn", "n", function(state, lhs, rhs) + return lhs * rhs +end, 1, nil, { legacy = false }) -registerOperator("div", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - return op1[1](self, op1) / op2[1](self, op2) -end) +registerOperator("div", "nn", "n", function(state, lhs, rhs) + return lhs / rhs +end, 1, nil, { legacy = false }) -registerOperator("exp", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - return op1[1](self, op1) ^ op2[1](self, op2) -end) +registerOperator("exp", "nn", "n", function(state, lhs, rhs) + return lhs ^ rhs +end, 1, nil, { legacy = false }) -registerOperator("mod", "nn", "n", function(self, args) - local op1, op2 = args[2], args[3] - return op1[1](self, op1) % op2[1](self, op2) -end) +registerOperator("mod", "nn", "n", function(state, lhs, rhs) + return lhs % rhs +end, 1, nil, { legacy = false }) --[[************************************************************************]]-- -- TODO: select, average @@ -327,12 +265,10 @@ e2function number lerp(number from, number to, number fraction) end registerFunction("sign", "n", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - if rv1 > delta then return 1 - elseif rv1 < -delta then return -1 + if args[1] > 0 then return 1 + elseif args[1] < 0 then return -1 else return 0 end -end) +end, 2, nil, { legacy = false }) --[[************************************************************************]]-- diff --git a/lua/entities/gmod_wire_expression2/core/quaternion.lua b/lua/entities/gmod_wire_expression2/core/quaternion.lua index bc2bb8840e..50a6c2debf 100644 --- a/lua/entities/gmod_wire_expression2/core/quaternion.lua +++ b/lua/entities/gmod_wire_expression2/core/quaternion.lua @@ -98,7 +98,7 @@ local function qlog(q) local u = { q[1]/l, q[2]/l, q[3]/l, q[4]/l } local a = acos(u[1]) local m = sqrt(u[2]*u[2] + u[3]*u[3] + u[4]*u[4]) - if abs(m) > delta then + if abs(m) > 0 then return { log(l), a*u[2]/m, a*u[3]/m, a*u[4]/m } else return { log(l), 0, 0, 0 } --when m is 0, u[2], u[3] and u[4] are 0 too @@ -247,20 +247,6 @@ end __e2setcost(2) -registerOperator("ass", "q", "q", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if !lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - /******************************************************************************/ // TODO: define division as multiplication with (1/x), or is it not useful? @@ -448,39 +434,23 @@ e2function quaternion operator^(quaternion lhs, number rhs) return qexp({ l[1]*rhs, l[2]*rhs, l[3]*rhs, l[4]*rhs }) end +registerOperator("indexget", "qn", "n", function(state, this, index) + return this[math.Round(math.Clamp(index, 1, 4))] +end) -e2function number quaternion:operator[](index) - index = math.Round(math.Clamp(index,1,4)) - return this[index] -end - -e2function number quaternion:operator[](index, value) - index = math.Round(math.Clamp(index,1,4)) - this[index] = value +registerOperator("indexset", "qnn", "", function(state, this, index, value) + this[math.Round(math.Clamp(index, 1, 4))] = value self.GlobalScope.vclk[this] = true - return value -end - -/******************************************************************************/ +end) __e2setcost(6) e2function number operator==(quaternion lhs, quaternion rhs) - local rvd1, rvd2, rvd3, rvd4 = lhs[1] - rhs[1], lhs[2] - rhs[2], lhs[3] - rhs[3], lhs[4] - rhs[4] - if rvd1 <= delta and rvd1 >= -delta and - rvd2 <= delta and rvd2 >= -delta and - rvd3 <= delta and rvd3 >= -delta and - rvd4 <= delta and rvd4 >= -delta - then return 1 else return 0 end -end - -e2function number operator!=(quaternion lhs, quaternion rhs) - local rvd1, rvd2, rvd3, rvd4 = lhs[1] - rhs[1], lhs[2] - rhs[2], lhs[3] - rhs[3], lhs[4] - rhs[4] - if rvd1 > delta or rvd1 < -delta or - rvd2 > delta or rvd2 < -delta or - rvd3 > delta or rvd3 < -delta or - rvd4 > delta or rvd4 < -delta - then return 1 else return 0 end + return (lhs[1] == rhs[1] + and lhs[2] == rhs[2] + and lhs[3] == rhs[3] + and lhs[4] == rhs[4]) + and 1 or 0 end /******************************************************************************/ diff --git a/lua/entities/gmod_wire_expression2/core/ranger.lua b/lua/entities/gmod_wire_expression2/core/ranger.lua index b0c5285e97..5a42b0e071 100644 --- a/lua/entities/gmod_wire_expression2/core/ranger.lua +++ b/lua/entities/gmod_wire_expression2/core/ranger.lua @@ -17,22 +17,10 @@ registerType("ranger", "xrd", nil, __e2setcost(1) -- temporary ---- RD = RD -registerOperator("ass", "xrd", "xrd", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - self.Scopes[scope][lhs] = rhs - self.Scopes[scope].vclk[lhs] = true - return rhs -end) - -e2function number operator_is(ranger walker) - if walker then return 1 else return 0 end +e2function number operator_is(ranger this) + return this and 1 or 0 end -/******************************************************************************/ - E2Lib.RegisterExtension("ranger", true, "Lets E2 chips trace rays and check for collisions.") ------------------- diff --git a/lua/entities/gmod_wire_expression2/core/selfaware.lua b/lua/entities/gmod_wire_expression2/core/selfaware.lua index 219afbadf3..e13ce1709b 100644 --- a/lua/entities/gmod_wire_expression2/core/selfaware.lua +++ b/lua/entities/gmod_wire_expression2/core/selfaware.lua @@ -53,27 +53,8 @@ e2function entity ioInputEntity( string input ) if (self.entity.Inputs[input] and self.entity.Inputs[input].Src and IsValid(self.entity.Inputs[input].Src)) then return self.entity.Inputs[input].Src end end -local function setOutput( self, args, Type ) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self,op1), op2[1](self,op2) - if (self.entity.Outputs[rv1] and self.entity.Outputs[rv1].Type == Type) then - self.GlobalScope[rv1] = rv2 - self.GlobalScope.vclk[rv1] = true - end -end - local fixDefault = E2Lib.fixDefault -local function getInput( self, args, default, Type ) - local op1 = args[2] - local rv1 = op1[1](self,op1) - default = fixDefault(default) - if (self.entity.Inputs[rv1] and self.entity.Inputs[rv1].Type == Type) then - return self.GlobalScope[rv1] or default - end - return default -end - local excluded_types = { xgt = true, } @@ -88,8 +69,21 @@ registerCallback("postinit",function() for k,v in pairs( wire_expression_types ) do local short = v[1] if (!excluded_types[short]) then - registerFunction("ioSetOutput","s"..short,""..short,function(self,args) return setOutput(self,args,k) end) - registerFunction("ioGetInput"..upperfirst(k == "NORMAL" and "NUMBER" or k),"s",short,function(self,args) return getInput(self,args,v[2],k) end) + registerFunction("ioSetOutput","s"..short,""..short,function(self, args) + local rv1, rv2 = args[1], args[2] + if self.entity.Outputs[rv1] and self.entity.Outputs[rv1].Type == k then + self.GlobalScope[rv1] = rv2 + self.GlobalScope.vclk[rv1] = true + end + end, 3, nil, { legacy = false }) + + registerFunction("ioGetInput"..upperfirst(k == "NORMAL" and "NUMBER" or k),"s",short,function(self, args) + local rv1, default = args[1], fixDefault(v[2]) + if self.entity.Inputs[rv1] and self.entity.Inputs[rv1].Type == k then + return self.GlobalScope[rv1] or default + end + return default + end, 3, nil, { legacy = false }) end end end) @@ -154,16 +148,16 @@ registerCallback("construct", function(self) self.data.changed = {} end) -__e2setcost(1) +__e2setcost(5) -- This is the prototype for everything that can be compared using the == operator [nodiscard] e2function number changed(value) local chg = self.data.changed - if value == chg[args] then return 0 end + if value == chg[typeids] then return 0 end - chg[args] = value + chg[typeids] = value return 1 end @@ -171,18 +165,11 @@ end e2function number changed(vector value) local chg = self.data.changed - local this_chg = chg[args] - if not this_chg then - chg[args] = value - return 1 + if chg[typeids] == value then + return 0 end - if this_chg - and value[1] == this_chg[1] - and value[2] == this_chg[2] - and value[3] == this_chg[3] - then return 0 end - chg[args] = value + chg[typeids] = value return 1 end @@ -191,20 +178,22 @@ end e2function number changed(vector4 value) local chg = self.data.changed - local this_chg = chg[args] + local this_chg = chg[typeids] if not this_chg then - chg[args] = value + chg[typeids] = value return 1 end for i,v in pairs(value) do if v ~= this_chg[i] then - chg[args] = value + chg[typeids] = value return 1 end end return 0 end +__e2setcost(1) + local excluded_types = { n = true, v = true, @@ -231,9 +220,9 @@ registerCallback("postinit", function() for typeid,_ in pairs(wire_expression_types2) do if not excluded_types[typeid] then if comparable_types[typeid] then - registerFunction("changed", typeid, "n", registeredfunctions.e2_changed_n) + registerFunction("changed", typeid, "n", registeredfunctions.e2_changed_n, 5, nil, { legacy = false }) else - registerFunction("changed", typeid, "n", registeredfunctions.e2_changed_xv4) + registerFunction("changed", typeid, "n", registeredfunctions.e2_changed_xv4, 5, nil, { legacy = false }) end end end diff --git a/lua/entities/gmod_wire_expression2/core/strfunc.lua b/lua/entities/gmod_wire_expression2/core/strfunc.lua deleted file mode 100644 index 5fed3d1045..0000000000 --- a/lua/entities/gmod_wire_expression2/core/strfunc.lua +++ /dev/null @@ -1,158 +0,0 @@ -local function nicename( word ) - local ret = word:lower() - if ret == "normal" then return "number" end - return ret -end - -local function checkFuncName( self, funcname ) - if self.funcs[funcname] then - return self.funcs[funcname], self.funcs_ret[funcname], true - elseif wire_expression2_funcs[funcname] then - return wire_expression2_funcs[funcname][3], wire_expression2_funcs[funcname][2] - end -end - -registerCallback("construct", function(self) self.strfunc_cache = {{}, {}} end) - -local insert = table.insert -local concat = table.concat -local function findFunc( self, funcname, typeids, typeids_str ) - local func, func_return_type, func_custom - local cache = self.strfunc_cache[1] - - local str = funcname .. "(" .. typeids_str .. ")" - - if cache[str] then - if cache[str][4] then self.prf = self.prf + 15 end - return cache[str][1], cache[str][2] - end - - local typeIDsLength = #typeids - self.prf = self.prf + 5 - - if typeIDsLength > 0 then - if not func then - func, func_return_type, func_custom = checkFuncName( self, str ) - end - - if not func then - func, func_return_type, func_custom = checkFuncName( self, funcname .. "(" .. typeids[1] .. ":" .. concat(typeids,"",2) .. ")" ) - end - - if not func then - for i = typeIDsLength, 1, -1 do - local sig_prefix = funcname .. "(" .. concat(typeids,"",1,i) - func, func_return_type, func_custom = checkFuncName( self, sig_prefix .. "...)" ) - if func then break end - - -- Try to find user-variadic functions. - -- Only do a single table op unless function is discovered as an optimization - func = self.funcs[sig_prefix .. "..r)"] - if func then - func_return_type = self.funcs_ret[sig_prefix .. "..r)"] - func_custom = true - - break - else - func = self.funcs[sig_prefix .. "..t)"] - if func then - func_return_type = self.funcs_ret[sig_prefix .. "..t)"] - func_custom = true - - break - end - end - end - - if not func then - func = self.funcs[funcname .. "(..r)"] - if func then - func_return_type = self.funcs_ret[funcname .. "(..r)"] - func_custom = true - else - func = self.funcs[funcname .. "(..t)"] - if func then - func_return_type = self.funcs_ret[funcname .. "(..t)"] - func_custom = true - else - func, func_return_type, func_custom = checkFuncName( self, funcname .. "(...)" ) - end - end - end - end - - if not func then - for i = typeIDsLength, 2, -1 do - func, func_return_type, func_custom = checkFuncName( self, funcname .. "(" .. typeids[1] .. ":" .. concat(typeids,"",2,i) .. "...)" ) - if func then break end - end - - if not func then - func, func_return_type, func_custom = checkFuncName( self, funcname .. "(" .. typeids[1] .. ":...)" ) - end - end - else - func, func_return_type, func_custom = checkFuncName( self, funcname .. "()" ) - end - - if func then - self.prf = self.prf + 20 - if self.funcs[str] then self.prf = self.prf + 15 end - - local limiter = self.strfunc_cache[2] - local limiterLength = #limiter + 1 - - cache[str] = { func, func_return_type, limiterLength, func_custom } - insert( limiter, 1, str ) - - if limiterLength == 101 then - self.strfunc_cache[1][ limiter[101] ] = nil - self.strfunc_cache[2][101] = nil - end - end - - return func, func_return_type -end - -__e2setcost(5) - -local BLOCKED_ARRAY_TYPES = E2Lib.blocked_array_types - -registerOperator( "stringcall", "", "", function(self, args) - local op1, funcargs, typeids, typeids_str, returntype = args[2], args[3], args[4], args[5], args[6] - local funcname = op1[1](self,op1) - - local func, func_return_type = findFunc( self, funcname, typeids, typeids_str ) - - if not func then E2Lib.raiseException( "No such function: " .. funcname .. "(" .. tps_pretty( typeids_str ) .. ")", 0 ) end - - if returntype ~= "" and func_return_type ~= returntype then - error( "Mismatching return types. Got " .. nicename(wire_expression_types2[returntype][1]) .. ", expected " .. nicename(wire_expression_types2[func_return_type][1] ), 0 ) - end - - if returntype ~= "" then - if funcname == "array" then - local types = funcargs[#funcargs] - - local k = 1 - - local ty = types[k] - while ty do - if BLOCKED_ARRAY_TYPES[ty] then - table.remove(types, k) - table.remove(funcargs, k + 1) - - self:throw("Cannot use type " .. tps_pretty(ty) .. " for argument #" .. k .. " in stringcall array creation") - else - k = k + 1 - end - - ty = types[k] - end - end - local ret = func( self, funcargs ) - return ret - else - func( self, funcargs ) - end -end) \ No newline at end of file diff --git a/lua/entities/gmod_wire_expression2/core/string.lua b/lua/entities/gmod_wire_expression2/core/string.lua index 100a92a741..de6fd63bbf 100644 --- a/lua/entities/gmod_wire_expression2/core/string.lua +++ b/lua/entities/gmod_wire_expression2/core/string.lua @@ -25,74 +25,34 @@ registerType("string", "s", "", __e2setcost(3) -- temporary -registerOperator("ass", "s", "s", function(self, args) - local op1, op2, scope = args[2], args[3], args[4] - local rv2 = op2[1](self, op2) - self.Scopes[scope][op1] = rv2 - self.Scopes[scope].vclk[op1] = true - return rv2 -end) - ---[[******************************************************************************]]-- - -registerOperator("fea", "nss", "", function(self, args) - local keyname, valname = args[2], args[3] - local str = args[4] - str = str[1](self, str) - - local statement = args[5] - - for key=1, #str do - local value = string_sub(str, key, key) - self:PushScope() - - self.prf = self.prf + 1 - - self.Scope.vclk[keyname] = true - self.Scope.vclk[valname] = true - self.Scope[keyname] = key - self.Scope[valname] = value +local string_sub, string_byte = string.sub, string.byte - local ok, msg = pcall(statement[1], self, statement) +local function iterc(str, i) + i = i + 1 + if i <= #str then + return i, string_sub(str, i, i) + end +end - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end +local function iterb(str, i) + i = i + 1 + if i <= #str then + return i, string_byte(str, i, i) + end +end - self:PopScope() +registerOperator("iter", "ns=s", "", function(state, str) + state.prf = state.prf + #str + return function() + return iterc, str, 0 end end) -registerOperator("fea", "nns", "", function(self, args) - local keyname, valname = args[2], args[3] - - local str = args[4] - str = str[1](self, str) - - local statement = args[5] - - for key=1, #str do - local value = string_byte(str,key,key) - self:PushScope() - - self.prf = self.prf + 1 - - self.Scope.vclk[keyname] = true - self.Scope.vclk[valname] = true - - self.Scope[keyname] = key - self.Scope[valname] = value - - local ok, msg = pcall(statement[1], self, statement) - - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end - - self:PopScope() +registerOperator("iter", "nn=s", "", function(state, str) + state.prf = state.prf + #str + return function() + return iterb, str, 0 end end) @@ -102,14 +62,6 @@ e2function number operator_is(string this) return this ~= "" and 1 or 0 end -e2function number operator==(string lhs, string rhs) - return lhs == rhs and 1 or 0 -end - -e2function number operator!=(string lhs, string rhs) - return lhs ~= rhs and 1 or 0 -end - e2function number operator>=(string lhs, string rhs) self.prf = self.prf + math.min(#lhs, #rhs) / 10 return lhs >= rhs and 1 or 0 @@ -225,9 +177,9 @@ e2function string string:sub(start) return string_sub(this, start) end -e2function string string:operator[](index) +registerOperator("indexget", "sn", "s", function(state, this, index) return string_sub(this, index, index) -end +end) e2function string string:upper() return this:upper() @@ -386,11 +338,11 @@ end __e2setcost(3) --- Formats a values exactly like Lua's [http://www.lua.org/manual/5.1/manual.html#pdf-string.format string.format]. Any number and type of parameter can be passed through the "...". Prints errors to the chat area. -e2function string format(string fmt, ...) - self.prf = self.prf + select("#", ...) * 2 +e2function string format(string fmt, ...args) + self.prf = self.prf + #args * 2 -- TODO: call toString for table-based types - local ok, ret = pcall(string_format, fmt, ...) + local ok, ret = pcall(string_format, fmt, unpack(args)) if not ok then return self:throw(ret, "") end diff --git a/lua/entities/gmod_wire_expression2/core/table.lua b/lua/entities/gmod_wire_expression2/core/table.lua index d836e409fc..8c176435c1 100644 --- a/lua/entities/gmod_wire_expression2/core/table.lua +++ b/lua/entities/gmod_wire_expression2/core/table.lua @@ -252,70 +252,12 @@ end -- Operators -------------------------------------------------------------------------------- -__e2setcost(5) - -registerOperator("ass", "t", "t", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if not lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - __e2setcost(1) -e2function number operator_is( table tbl ) - return (tbl.size > 0) and 1 or 0 -end - -e2function number operator==( table rv1, table rv2 ) - return (rv1 == rv2) and 1 or 0 -end - -e2function number operator!=( table rv1, table rv2 ) - return (rv1 ~= rv2) and 1 or 0 +e2function number operator_is(table this) + return (this.size > 0) and 1 or 0 end -__e2setcost(nil) - -registerOperator( "kvtable", "", "t", function( self, args ) - local ret = newE2Table() - - local types = args[3] - - local s, stypes, n, ntypes = {}, {}, {}, {} - - local size = 0 - for k,v in pairs( args[2] ) do - if not blocked_types[types[k]] then - local key = k[1]( self, k ) - - if isstring(key) then - s[key] = v[1]( self, v ) - stypes[key] = types[k] - elseif isnumber(key) then - n[key] = v[1]( self, v ) - ntypes[key] = types[k] - end - size = size + 1 - end - end - - self.prf = self.prf + size * opcost - ret.size = size - ret.s = s - ret.stypes = stypes - ret.n = n - ret.ntypes = ntypes - return ret -end) - -------------------------------------------------------------------------------- -- Common functions -------------------------------------------------------------------------------- @@ -1044,6 +986,8 @@ registerCallback( "postinit", function() for k,v in pairs( wire_expression_types ) do local name = k local id = v[1] + local default = v[2] + local typecheck = v[6] if (not blocked_types[id]) then -- blocked check start @@ -1051,46 +995,72 @@ registerCallback( "postinit", function() -- Set/Get functions, t[index,type] syntax -------------------------------------------------------------------------------- - __e2setcost(5) + __e2setcost(3) -- Getters - registerOperator("idx", id.."=ts" , id, function(self,args) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if (not rv1.s[rv2] or rv1.stypes[rv2] ~= id) then return fixDefault(v[2]) end - if (v[6] and v[6](rv1.s[rv2])) then return fixDefault(v[2]) end -- Type check - return rv1.s[rv2] - end) + if typecheck then -- If there's a type check + registerOperator("indexget", "ts" .. id, id, function(self, tbl, key) + if not tbl.s[key] or tbl.stypes[key] ~= id then + return fixDefault(default) + end - registerOperator("idx", id.."=tn" , id, function(self,args) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if (not rv1.n[rv2] or rv1.ntypes[rv2] ~= id) then return fixDefault(v[2]) end - if (v[6] and v[6](rv1.n[rv2])) then return fixDefault(v[2]) end -- Type check - return rv1.n[rv2] - end) + if typecheck(tbl.s[key]) then + return fixDefault(default) + end + + return tbl.s[key] + end) + + registerOperator("indexget", "tn" .. id, id, function(self, tbl, key) + if not tbl.n[key] or tbl.ntypes[key] ~= id then + return fixDefault(default) + end + + if typecheck(tbl.n[key]) then + return fixDefault(default) + end + + return tbl.n[key] + end, 2) + else + registerOperator("indexget", "ts" .. id, id, function(self, tbl, key) + if not tbl.s[key] or tbl.stypes[key] ~= id then + return fixDefault(default) + end + + return tbl.s[key] + end, 1) + + registerOperator("indexget", "tn" .. id, id, function(self, tbl, key) + if not tbl.n[key] or tbl.ntypes[key] ~= id then + return fixDefault(default) + end + + return tbl.n[key] + end, 1) + end -- Setters - registerOperator("idx", id.."=ts"..id , id, function( self, args ) - local op1, op2, op3, scope = args[2], args[3], args[4], args[5] - local rv1, rv2, rv3 = op1[1](self, op1), op2[1](self, op2), op3[1](self, op3) - if (rv1.s[rv2] == nil and rv3 ~= nil) then rv1.size = rv1.size + 1 - elseif (rv1.n[rv2] ~= nil and rv3 == nil) then rv1.size = rv1.size - 1 end - rv1.s[rv2] = rv3 - rv1.stypes[rv2] = id - self.GlobalScope.vclk[rv1] = true - return rv3 + registerOperator("indexset", "ts" .. id , "", function(self, tbl, key, value) + if tbl.s[key] == nil and value ~= nil then + tbl.size = tbl.size + 1 + elseif tbl.s[key] ~= nil and value == nil then + tbl.size = tbl.size - 1 + end + + tbl.s[key], tbl.stypes[key] = value, id + self.GlobalScope.vclk[tbl] = true end) - registerOperator("idx", id.."=tn"..id, id, function(self,args) - local op1, op2, op3, scope = args[2], args[3], args[4], args[5] - local rv1, rv2, rv3 = op1[1](self, op1), op2[1](self, op2), op3[1](self, op3) - if (rv1.n[rv2] == nil and rv3 ~= nil) then rv1.size = rv1.size + 1 - elseif (rv1.n[rv2] ~= nil and rv3 == nil) then rv1.size = rv1.size - 1 end - rv1.n[rv2] = rv3 - rv1.ntypes[rv2] = id - self.GlobalScope.vclk[rv1] = true - return rv3 + registerOperator("indexset", "tn" .. id, "", function(self, tbl, key, value) + if tbl.n[key] == nil and value ~= nil then + tbl.size = tbl.size + 1 + elseif tbl.n[key] ~= nil and value == nil then + tbl.size = tbl.size - 1 + end + + tbl.n[key], tbl.ntypes[key] = value, id + self.GlobalScope.vclk[tbl] = true end) @@ -1191,69 +1161,33 @@ registerCallback( "postinit", function() -------------------------------------------------------------------------------- -- Foreach operators -------------------------------------------------------------------------------- - __e2setcost(nil) - - registerOperator("fea", "s" .. id .. "t", "", function(self, args) - local keyname, valname = args[2], args[3] - - local tbl = args[4] - tbl = tbl[1](self, tbl) - - local statement = args[5] - - for key, value in pairs(tbl.s) do - if tbl.stypes[key] == id then - self:PushScope() - - self.prf = self.prf + 3 + __e2setcost(0) - self.Scope.vclk[keyname] = true - self.Scope.vclk[valname] = true - - self.Scope[keyname] = key - self.Scope[valname] = value - - local ok, msg = pcall(statement[1], self, statement) + local function iteri(tbl, i) + i = i + 1 + local v = tbl.n[i] + if tbl.ntypes[i] == id then + return i, v + end + end - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end + local next = next + local function iter(tbl, i) + local key, value = next(tbl.s, i) + if tbl.stypes[key] == id then + return key, value + end + end - self:PopScope() - end + registerOperator("iter", "s" .. id .. "=t", "", function(state, table) + return function() + return iter, table end end) - registerOperator("fea", "n" .. id .. "t", "", function(self, args) - local keyname, valname = args[2], args[3] - - local tbl = args[4] - tbl = tbl[1](self, tbl) - - local statement = args[5] - - for key, value in pairs(tbl.n) do - if tbl.ntypes[key] == id then - self:PushScope() - - self.prf = self.prf + 3 - - self.Scope.vclk[keyname] = true - self.Scope.vclk[valname] = true - - self.Scope[keyname] = key - self.Scope[valname] = value - - local ok, msg = pcall(statement[1], self, statement) - - if not ok then - if msg == "break" then self:PopScope() break - elseif msg ~= "continue" then self:PopScope() error(msg, 0) end - end - - self:PopScope() - end + registerOperator("iter", "n" .. id .. "=t", "", function(state, table) + return function() + return iteri, table, 0 end end) @@ -1267,7 +1201,7 @@ end) -------------------------------------------------------------------------------- -- these postexecute and construct hooks handle changes to both tables and arrays. -registerCallback("postexecute", function(self) +registerCallback("postexecute", function(self) --- @param self RuntimeContext local Scope = self.GlobalScope local vclk, lookup = Scope.vclk, Scope.lookup @@ -1297,16 +1231,6 @@ registerCallback("postexecute", function(self) end end) -local tbls = { - ARRAY = true, - TABLE = true, - VECTOR = true, - VECTOR2 = true, - VECTOR4 = true, - ANGLE = true, - QUATERNION = true, -} - registerCallback("construct", function(self) local Scope = self.GlobalScope Scope.lookup = {} @@ -1314,7 +1238,7 @@ registerCallback("construct", function(self) for k,v in pairs( Scope ) do if k ~= "lookup" then local datatype = self.entity.outports[3][k] - if (tbls[datatype]) then + if (E2Lib.IOTableTypes[datatype]) then if (not Scope.lookup[v]) then Scope.lookup[v] = {} end Scope.lookup[v][k] = true end diff --git a/lua/entities/gmod_wire_expression2/core/vector.lua b/lua/entities/gmod_wire_expression2/core/vector.lua index 167e1a55a2..ba9387275a 100644 --- a/lua/entities/gmod_wire_expression2/core/vector.lua +++ b/lua/entities/gmod_wire_expression2/core/vector.lua @@ -2,8 +2,6 @@ -- Vector support -- -------------------------------------------------------------------------------- -local delta = wire_expression2_delta - local random = math.random local Vector = Vector local sqrt = math.sqrt @@ -93,45 +91,14 @@ end -------------------------------------------------------------------------------- -registerOperator("ass", "v", "v", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if not lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = { [lhs] = true } end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - --------------------------------------------------------------------------------- - -e2function number vector:operator_is() - if this[1] > delta or -this[1] > delta or - this[2] > delta or -this[2] > delta or - this[3] > delta or -this[3] > delta - then return 1 else return 0 end -end - -e2function number vector:operator==( vector other ) - if this[1] - other[1] <= delta and other[1] - this[1] <= delta and - this[2] - other[2] <= delta and other[2] - this[2] <= delta and - this[3] - other[3] <= delta and other[3] - this[3] <= delta - then return 1 else return 0 end -end - -e2function number vector:operator!=( vector other ) - if this[1] - other[1] > delta or other[1] - this[1] > delta or - this[2] - other[2] > delta or other[2] - this[2] > delta or - this[3] - other[3] > delta or other[3] - this[3] > delta - then return 1 else return 0 end +e2function number operator_is(vector this) + return this:IsZero() and 0 or 1 end -------------------------------------------------------------------------------- +__e2setcost(1) + e2function vector operator_neg(vector v) return -v end @@ -185,15 +152,14 @@ e2function vector operator/(vector lhs, vector rhs) return Vector( lhs[1] / rhs[1], lhs[2] / rhs[2], lhs[3] / rhs[3] ) end -e2function number vector:operator[](index) +registerOperator("indexget", "vn", "n", function(state, this, index) return this[floor(math.Clamp(index, 1, 3) + 0.5)] -end +end) -e2function number vector:operator[](index, value) +registerOperator("indexset", "vnn", "", function(state, this, index, value) this[floor(math.Clamp(index, 1, 3) + 0.5)] = value - self.GlobalScope.vclk[this] = true - return value -end + state.GlobalScope.vclk[this] = true +end) e2function string operator+(string lhs, vector rhs) self.prf = self.prf + #lhs * 0.01 @@ -262,45 +228,34 @@ end -------------------------------------------------------------------------------- -__e2setcost(5) +__e2setcost(2) e2function number vector:length() - return (this[1] * this[1] + this[2] * this[2] + this[3] * this[3]) ^ 0.5 + return this:Length() end e2function number vector:length2() - return this[1] * this[1] + this[2] * this[2] + this[3] * this[3] + return this:LengthSqr() end e2function number vector:distance(vector other) - local dx, dy, dz = this[1] - other[1], this[2] - other[2], this[3] - other[3] - return (dx * dx + dy * dy + dz * dz) ^ 0.5 + return this:Distance(other) end e2function number vector:distance2( vector other ) - local dx, dy, dz = this[1] - other[1], this[2] - other[2], this[3] - other[3] - return dx * dx + dy * dy + dz * dz + return this:DistToSqr(other) end e2function vector vector:normalized() - local len = (this[1] * this[1] + this[2] * this[2] + this[3] * this[3]) ^ 0.5 - if len > delta then - return Vector(this[1] / len, this[2] / len, this[3] / len ) - else - return Vector(0, 0, 0) - end + return this:GetNormalized() end e2function number vector:dot( vector other ) - return this[1] * other[1] + this[2] * other[2] + this[3] * other[3] + return this:Dot(other) end e2function vector vector:cross( vector other ) - return Vector( - this[2] * other[3] - this[3] * other[2], - this[3] * other[1] - this[1] * other[3], - this[1] * other[2] - this[2] * other[1] - ) + return this:Cross(other) end __e2setcost(10) @@ -708,7 +663,7 @@ end e2function number elevation(vector originpos, angle originangle, vector pos) pos = WorldToLocal(pos, ANG_ZERO, originpos, originangle) local len = pos:Length() - if (len < delta) then return 0 end + if len < 0 then return 0 end return rad2deg * asin(pos.z / len) end @@ -718,7 +673,7 @@ e2function angle heading(vector originpos,angle originangle, vector pos) local bearing = rad2deg*-atan2(pos.y, pos.x) local len = pos:Length() - if (len < delta) then return Angle(0, bearing, 0) end + if len < 0 then return Angle(0, bearing, 0) end return Angle(rad2deg*asin(pos.z / len), bearing, 0) end diff --git a/lua/entities/gmod_wire_expression2/core/vector2.lua b/lua/entities/gmod_wire_expression2/core/vector2.lua index a13d5a71c0..7e06fd7b9b 100644 --- a/lua/entities/gmod_wire_expression2/core/vector2.lua +++ b/lua/entities/gmod_wire_expression2/core/vector2.lua @@ -2,8 +2,6 @@ 2D Vector support \******************************************************************************/ -local delta = wire_expression2_delta - local floor = math.floor local ceil = math.ceil local random = math.random @@ -57,47 +55,15 @@ registerFunction("vec2", "xv4", "xv2", function(self, args) return { rv1[1], rv1[2] } end) -/******************************************************************************/ - -registerOperator("ass", "xv2", "xv2", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) +registerOperator("is", "xv2", "n", function(self, this) + return (this[1] ~= 0 and this[2] ~= 0) and 1 or 0 +end, 2, nil, { legacy = false }) - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if !lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end - - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - -/******************************************************************************/ - -registerOperator("is", "xv2", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - if rv1[1] > delta or -rv1[1] > delta or - rv1[2] > delta or -rv1[2] > delta - then return 1 else return 0 end -end) - -registerOperator("eq", "xv2xv2", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if rv1[1] - rv2[1] <= delta and rv2[1] - rv1[1] <= delta and - rv1[2] - rv2[2] <= delta and rv2[2] - rv1[2] <= delta - then return 1 else return 0 end -end) - -registerOperator("neq", "xv2xv2", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if rv1[1] - rv2[1] > delta or rv2[1] - rv1[1] > delta or - rv1[2] - rv2[2] > delta or rv2[2] - rv1[2] > delta - then return 1 else return 0 end -end) +registerOperator("eq", "xv2xv2", "n", function(self, lhs, rhs) + return (lhs[1] == rhs[1] + and lhs[2] == rhs[2]) + and 1 or 0 +end, 2, nil, { legacy = false }) /******************************************************************************/ @@ -167,15 +133,14 @@ registerOperator("div", "xv2xv2", "xv2", function(self, args) return { rv1[1] / rv2[1], rv1[2] / rv2[2] } end) -e2function number vector2:operator[](index) +registerOperator("indexget", "xv2n", "n", function(state, this, index) return this[floor(math.Clamp(index, 1, 2) + 0.5)] -end +end) -e2function number vector2:operator[](index, value) +registerOperator("indexset", "xv2nn", "", function(state, this, index, value) this[floor(math.Clamp(index, 1, 2) + 0.5)] = value - self.GlobalScope.vclk[this] = true - return value -end + state.GlobalScope.vclk[this] = true +end) /******************************************************************************/ @@ -211,7 +176,7 @@ registerFunction("normalized", "xv2:", "xv2", function(self, args) local op1 = args[2] local rv1 = op1[1](self, op1) local len = (rv1[1] * rv1[1] + rv1[2] * rv1[2] ) ^ 0.5 - if len > delta then + if len > 0 then return { rv1[1] / len, rv1[2] / len } else return { 0, 0 } @@ -610,51 +575,17 @@ end) /******************************************************************************/ -registerOperator("ass", "xv4", "xv4", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - - local Scope = self.Scopes[scope] - local lookup = Scope.lookup - if !lookup then lookup = {} Scope.lookup = lookup end - if lookup[rhs] then lookup[rhs][lhs] = true else lookup[rhs] = {[lhs] = true} end +registerOperator("is", "xv4", "n", function(self, this) + return (this[1] ~= 0 or this[2] ~= 0 or this[3] ~= 0 or this[4] ~= 0) and 1 or 0 +end, 2, nil, { legacy = false }) - Scope[lhs] = rhs - Scope.vclk[lhs] = true - return rhs -end) - -/******************************************************************************/ - -registerOperator("is", "xv4", "n", function(self, args) - local op1 = args[2] - local rv1 = op1[1](self, op1) - if rv1[1] > delta or -rv1[1] > delta or - rv1[2] > delta or -rv1[2] > delta or - rv1[3] > delta or -rv1[3] > delta or - rv1[4] > delta or -rv1[4] > delta - then return 1 else return 0 end -end) - -registerOperator("eq", "xv4xv4", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if rv1[1] - rv2[1] <= delta and rv2[1] - rv1[1] <= delta and - rv1[2] - rv2[2] <= delta and rv2[2] - rv1[2] <= delta and - rv1[3] - rv2[3] <= delta and rv2[3] - rv1[3] <= delta and - rv1[4] - rv2[4] <= delta and rv2[4] - rv1[4] <= delta - then return 1 else return 0 end -end) - -registerOperator("neq", "xv4xv4", "n", function(self, args) - local op1, op2 = args[2], args[3] - local rv1, rv2 = op1[1](self, op1), op2[1](self, op2) - if rv1[1] - rv2[1] > delta or rv2[1] - rv1[1] > delta or - rv1[2] - rv2[2] > delta or rv2[2] - rv1[2] > delta or - rv1[3] - rv2[3] > delta or rv2[3] - rv1[3] > delta or - rv1[4] - rv2[4] > delta or rv2[4] - rv1[4] > delta - then return 1 else return 0 end -end) +registerOperator("eq", "xv4xv4", "n", function(self, lhs, rhs) + return (lhs[1] == rhs[1] + and lhs[2] == rhs[2] + and lhs[3] == rhs[3] + and lhs[4] == rhs[4]) + and 1 or 0 +end, 2, nil, { legacy = false }) /******************************************************************************/ @@ -724,17 +655,14 @@ registerOperator("div", "xv4xv4", "xv4", function(self, args) return { rv1[1] / rv2[1], rv1[2] / rv2[2], rv1[3] / rv2[3], rv1[4] / rv2[4] } end) -e2function number vector4:operator[](index) +registerOperator("indexget", "xv4n", "n", function(state, this, index) return this[floor(math.Clamp(index, 1, 4) + 0.5)] -end +end) -e2function number vector4:operator[](index, value) +registerOperator("indexset", "xv4nn", "", function(state, this, index, value) this[floor(math.Clamp(index, 1, 4) + 0.5)] = value self.GlobalScope.vclk[this] = true - return value -end - -/******************************************************************************/ +end) __e2setcost(7) @@ -788,7 +716,7 @@ registerFunction("normalized", "xv4:", "xv4", function(self, args) local op1 = args[2] local rv1 = op1[1](self, op1) local len = (rv1[1] * rv1[1] + rv1[2] * rv1[2] + rv1[3] * rv1[3] + rv1[4] * rv1[4]) ^ 0.5 - if len > delta then + if len > 0 then return { rv1[1] / len, rv1[2] / len, rv1[3] / len, rv1[4] / len } else return { 0, 0, 0, 0 } diff --git a/lua/entities/gmod_wire_expression2/core/wirelink.lua b/lua/entities/gmod_wire_expression2/core/wirelink.lua index 2842eaf498..d0d704fcc2 100644 --- a/lua/entities/gmod_wire_expression2/core/wirelink.lua +++ b/lua/entities/gmod_wire_expression2/core/wirelink.lua @@ -137,27 +137,10 @@ registerType("wirelink", "xwl", nil, __e2setcost(2) -- temporary -registerOperator("ass", "xwl", "xwl", function(self, args) - local lhs, op2, scope = args[2], args[3], args[4] - local rhs = op2[1](self, op2) - self.Scopes[scope][lhs] = rhs - self.Scopes[scope].vclk[lhs] = true - return rhs -end) - /******************************************************************************/ -e2function number operator_is(wirelink value) - if not validWirelink(self, value) then return 0 end - return 1 -end - -e2function number operator==(wirelink lhs, wirelink rhs) - if lhs == rhs then return 1 else return 0 end -end - -e2function number operator!=(wirelink lhs, wirelink rhs) - if lhs ~= rhs then return 1 else return 0 end +e2function number operator_is(wirelink this) + return validWirelink(self, this) and 1 or 0 end /******************************************************************************/ @@ -192,8 +175,6 @@ end /******************************************************************************/ registerCallback("postinit", function() - - local getf, setf -- generate getters and setters for all types for typename, v in pairs( wire_expression_types ) do local id = v[1] @@ -208,47 +189,23 @@ registerCallback("postinit", function() -- for T:setNumber() etc local setter = "set"..fname:sub(1,1):upper()..fname:sub(2):lower() + local getf, setf if input_serializer then - if istable(zero) and not next(zero) then - -- table/array - function getf(self, args) - local this, portname = args[2], args[3] - this, portname = this[1](self, this), portname[1](self, portname) - - if not validWirelink(self, this) then return {} end - - portname = mapOutputAlias(this, portname) - - if not this.Outputs then return {} end - if not this.Outputs[portname] then return {} end - if this.Outputs[portname].Type ~= typename then return {} end + -- all other types with input serializers + function getf(self, this, portname) + if not validWirelink(self, this) then return input_serializer(self, zero) end - return input_serializer(self, this.Outputs[portname].Value) - end - else - -- all other types with input serializers - function getf(self, args) - local this, portname = args[2], args[3] - this, portname = this[1](self, this), portname[1](self, portname) - - if not validWirelink(self, this) then return input_serializer(self, zero) end - - portname = mapOutputAlias(this, portname) - - if not this.Outputs then return input_serializer(self, zero) end - if not this.Outputs[portname] then return input_serializer(self, zero) end - if this.Outputs[portname].Type ~= typename then return input_serializer(self, zero) end + portname = mapOutputAlias(this, portname) + if not this.Outputs then return input_serializer(self, zero) end + if not this.Outputs[portname] then return input_serializer(self, zero) end + if this.Outputs[portname].Type ~= typename then return input_serializer(self, zero) end - return input_serializer(self, this.Outputs[portname].Value) - end + return input_serializer(self, this.Outputs[portname].Value) end else -- all types without an input serializer -- a check for {} is not needed here, since array and table both have input serializers and are thus handled in the above branch. - function getf(self, args) - local this, portname = args[2], args[3] - this, portname = this[1](self, this), portname[1](self, portname) - + function getf(self, this, portname) if not validWirelink(self, this) then return zero end portname = mapOutputAlias(this, portname) @@ -262,10 +219,7 @@ registerCallback("postinit", function() end if output_serializer then - function setf(self, args) - local this, portname, value = args[2], args[3], args[4] - this, portname, value = this[1](self, this), portname[1](self, portname), value[1](self, value) - + function setf(self, this, portname, value) if not validWirelink(self, this) then return value end if not this.Inputs then return value end @@ -275,10 +229,7 @@ registerCallback("postinit", function() return value end else - function setf(self, args) - local this, portname, value = args[2], args[3], args[4] - this, portname, value = this[1](self, this), portname[1](self, portname), value[1](self, value) - + function setf(self, this, portname, value) if not validWirelink(self, this) then return value end if not this.Inputs then return value end @@ -289,10 +240,16 @@ registerCallback("postinit", function() end end - registerFunction(getter, "xwl:s", id, getf, 5) - registerOperator("idx", id.."=xwls", id, getf, 5) - registerFunction(setter, "xwl:s"..id, id, setf, 5) - registerOperator("idx", id.."=xwls"..id, id, setf, 5) + registerOperator("indexget", "xwls" .. id, id, getf, 5) + registerFunction(getter, "xwl:s", id, function(state, args) + return getf(state, args[1], args[2]) + end, 15, nil, { deprecated = true, legacy = false }) + + registerOperator("indexset", "xwls" .. id, id, setf, 5) + + registerFunction(setter, "xwl:s" .. id, id, function(state, args) + return setf(state, args[1], args[2], args[3]) + end, 15, nil, { deprecated = true, legacy = false }) end end) @@ -420,52 +377,59 @@ e2function array wirelink:readArray(start, size) return ret end -e2function number wirelink:operator[](address, value) - if not validWirelink(self, this) then return value end +registerOperator("indexset", "xwlnn", "", function(state, this, address, value) + if not validWirelink(state, this) then return end - if not this.WriteCell then return value end - this:WriteCell(address, value) - return value -end -e2function number wirelink:operator[](address) = e2function number wirelink:readCell(address) + if this.WriteCell then + this:WriteCell(address, value) + end +end, 3) -/******************************************************************************/ +registerOperator("indexget", "xwln", "n", function(state, this, address) + if not validWirelink(state, this) then return 0 end -__e2setcost(20) -- temporary + if not this.ReadCell then return 0 end + return this:ReadCell(address) or 0 +end, 3) -e2function vector wirelink:operator[T](address, vector value) - if not validWirelink(self, this) then return value end +/******************************************************************************/ - if not this.WriteCell then return value end - this:WriteCell(address, value[1]) - this:WriteCell(address+1, value[2]) - this:WriteCell(address+2, value[3]) - return value -end +__e2setcost(20) -- temporary -e2function vector wirelink:operator[T](address) - if not validWirelink(self, this) then return Vector(0, 0, 0) end +registerOperator("indexset", "xwlnv", "", function(state, this, address, value) + if not validWirelink(state, this) then return end - if not this.ReadCell then return 0 end - return Vector( - this:ReadCell(address) or 0, - this:ReadCell(address+1) or 0, - this:ReadCell(address+2) or 0 - ) -end + if this.WriteCell then + this:WriteCell(address, value[1]) + this:WriteCell(address + 1, value[2]) + this:WriteCell(address + 2, value[3]) + end +end, 20) + +registerOperator("indexget", "xwlnv", "v", function(state, this, address, value) + if not validWirelink(state, this) then return end + + if this.ReadCell then + return Vector( + this:ReadCell(address) or 0, + this:ReadCell(address+1) or 0, + this:ReadCell(address+2) or 0 + ) + else + return Vector(0, 0, 0) + end +end, 20) -e2function string wirelink:operator[T](address, string value) - if not validWirelink(self, this) or not this.WriteCell then return "" end +registerOperator("indexset", "xwlns", "", function(state, this, address, value) + if not validWirelink(state, this) or not this.WriteCell then return "" end WriteStringZero(this, address, value) - return value -end +end, 20) -e2function string wirelink:operator[T](address) - if not validWirelink(self, this) or not this.ReadCell then return "" end +registerOperator("indexget", "xwlns", "s", function(state, this, address) + if not validWirelink(state, this) or not this.ReadCell then return "" end return ReadStringZero(this, address) -end +end, 20) -/******************************************************************************/ __e2setcost(20) -- temporary diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index fb4a368064..cfb7dc6918 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -39,42 +39,6 @@ end local fixDefault = E2Lib.fixDefault - -local ScopeManager = {} -ScopeManager.__index = ScopeManager -E2Lib.ScopeManager = ScopeManager - -function ScopeManager:InitScope() - self.Scopes = {} - self.ScopeID = 0 - self.Scopes[0] = self.GlobalScope or { vclk = {} } -- for creating new enviroments - self.Scope = self.Scopes[0] - self.GlobalScope = self.Scope -end - -function ScopeManager:PushScope() - self.Scope = { vclk = {} } - self.ScopeID = self.ScopeID + 1 - self.Scopes[self.ScopeID] = self.Scope -end - -function ScopeManager:PopScope() - self.ScopeID = self.ScopeID - 1 - self.Scope = self.Scopes[self.ScopeID] - self.Scopes[self.ScopeID] = self.Scope - return table.remove(self.Scopes, self.ScopeID + 1) -end - -function ScopeManager:SaveScopes() - return { self.Scopes, self.ScopeID, self.Scope } -end - -function ScopeManager:LoadScopes(Scopes) - self.Scopes = Scopes[1] - self.ScopeID = Scopes[2] - self.Scope = Scopes[3] -end - function ENT:UpdateOverlay(clear) if clear then self:SetOverlayData( { @@ -118,13 +82,15 @@ local SysTime = SysTime function ENT:Destruct() self:PCallHook("destruct") - for evt in pairs(self.registered_events) do - if E2Lib.Env.Events[evt].destructor then - -- If the event has a destructor to run when the E2 is removed and listening to the event. - E2Lib.Env.Events[evt].destructor(self.context) - end + if self.registered_events then + for evt in pairs(self.registered_events) do + if E2Lib.Env.Events[evt].destructor then + -- If the event has a destructor to run when the E2 is removed and listening to the event. + E2Lib.Env.Events[evt].destructor(self.context) + end - E2Lib.Env.Events[evt].listening[self] = nil + E2Lib.Env.Events[evt].listening[self] = nil + end end end @@ -150,7 +116,6 @@ function ENT:Execute() self:PCallHook('preexecute') - self.context:PushScope() self.context.stackdepth = self.context.stackdepth + 1 if self.context.stackdepth >= 150 then @@ -159,7 +124,7 @@ function ENT:Execute() local bench = SysTime() - local ok, msg = pcall(self.script[1], self.context, self.script) + local ok, msg = pcall(self.script, self.context) if not ok then local _catchable, msg, trace = E2Lib.unpackException(msg) @@ -167,19 +132,19 @@ function ENT:Execute() if msg == "exit" then self:UpdatePerf() elseif msg == "perf" then + local trace = self.context.trace self:UpdatePerf() - self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded", "tick quota exceeded") + self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded (at line " .. trace.start_line .. ", char " .. trace.start_col .. ")", "tick quota exceeded") elseif trace then - self:Error("Expression 2 (" .. self.name .. "): Runtime error '" .. msg .. "' at line " .. trace[1] .. ", char " .. trace[2], "script error") + self:Error("Expression 2 (" .. self.name .. "): Runtime error '" .. msg .. "' at line " .. trace.start_line .. ", char " .. trace.start_col, "script error") else - self:Error("Expression 2 (" .. self.name .. "): " .. msg, "script error") + local trace = self.context.trace + self:Error("Expression 2 (" .. self.name .. "): Internal error '" .. msg .. "' at line " .. trace.start_line .. ", char " .. trace.start_col, "script error") end end self.context.time = self.context.time + (SysTime() - bench) - self.context.stackdepth = self.context.stackdepth - 1 - self.context:PopScope() local forceTriggerOutputs = self.first or self.duped self.first = false -- if hooks call execute @@ -206,7 +171,8 @@ function ENT:Execute() end if self.context.prfcount + self.context.prf - e2_softquota > e2_hardquota then - self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded", "hard quota exceeded") + local trace = self.context.trace + self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded (at line " .. trace.start_line .. ", char " .. trace.start_col .. ")", "hard quota exceeded") end if self.error then @@ -226,7 +192,6 @@ function ENT:ExecuteEvent(evt, args) self:PCallHook("preexecute") for name, handler in pairs(handlers) do - self.context:PushScope() self.context.stackdepth = self.context.stackdepth + 1 if self.context.stackdepth >= 150 then @@ -242,18 +207,19 @@ function ENT:ExecuteEvent(evt, args) if msg == "exit" then self:UpdatePerf() elseif msg == "perf" then + local trace = self.context.trace self:UpdatePerf() - self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded", "tick quota exceeded") + self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded (at line " .. trace.start_line .. ", char " .. trace.start_col .. ")", "tick quota exceeded") elseif trace then - self:Error("Expression 2 (" .. self.name .. "): Runtime error '" .. msg .. "' at line " .. trace[1] .. ", char " .. trace[2], "script error") + self:Error("Expression 2 (" .. self.name .. "): Runtime error '" .. msg .. "' at line " .. trace.start_line .. ", char " .. trace.start_col, "script error") else - self:Error("Expression 2 (" .. self.name .. "): " .. msg, "script error") + local trace = self.context.trace + self:Error("Expression 2 (" .. self.name .. "): Internal error '" .. msg .. "' at line " .. trace.start_line .. ", char " .. trace.start_col, "script error") end end - self.context.time = self.context.time + (SysTime() - bench) + self.context.time = self.context.time + (SysTime() - bench) self.context.stackdepth = self.context.stackdepth - 1 - self.context:PopScope() end @@ -268,7 +234,8 @@ function ENT:ExecuteEvent(evt, args) end if self.context.prfcount + self.context.prf - e2_softquota > e2_hardquota then - self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded", "hard quota exceeded") + local trace = self.context.trace + self:Error("Expression 2 (" .. self.name .. "): tick quota exceeded (at line " .. trace.start_line .. ", char " .. trace.start_col .. ")", "hard quota exceeded") end if self.error then @@ -368,20 +335,16 @@ function ENT:CompileCode(buffer, files, filepath) if not self:PrepareIncludes(files) then return end - status,tree = E2Lib.Optimizer.Execute(tree) - if not status then self:Error(tree) return end - - local status, script, inst = E2Lib.Compiler.Execute(tree, self.inports, self.outports, self.persists, dvars, self.includes) + local status, script, inst = E2Lib.Compiler.Execute(tree, directives, dvars, self.includes) if not status then self:Error(script) return end self.script = script self.registered_events = inst.registered_events - self.dvars = inst.dvars - self.funcs = inst.funcs - self.funcs_ret = inst.funcs_ret - self.globvars_mut = table.Copy(inst.GlobalScope) -- table.Copy because we will mutate this - self.globvars = inst.GlobalScope + self.dvars = dvars + self.funcs = inst.user_functions + self.globvars_mut = table.Copy(inst.global_scope.vars) ---@type table # table.Copy because we will mutate this + self.globvars = inst.global_scope.vars self:ResetContext() end @@ -394,34 +357,27 @@ function ENT:GetCode() return self.original, self.inc_files end +---@param files table function ENT:PrepareIncludes(files) - self.inc_files = files - self.includes = {} for file, buffer in pairs(files) do local status, directives, buffer = E2Lib.PreProcessor.Execute(buffer, self.directives) - if not status then - self:Error("(" .. file .. ")" .. directives) + if not status then ---@cast directives Error[] + self:Error("(" .. file .. ") " .. directives[1].message) return end local status, tokens = E2Lib.Tokenizer.Execute(buffer) - if not status then - self:Error("(" .. file .. ")" .. tokens) + if not status then ---@cast tokens Error[] + self:Error("(" .. file .. ") " .. tokens[1].message) return end local status, tree, dvars = E2Lib.Parser.Execute(tokens) - if not status then - self:Error("(" .. file .. ")" .. tree) - return - end - - status, tree = E2Lib.Optimizer.Execute(tree) - if not status then - self:Error("(" .. file .. ")" .. tree) + if not status then ---@cast tree Error + self:Error("(" .. file .. ") " .. tree.message) return end @@ -442,45 +398,33 @@ function ENT:ResetContext() end self.lastResetOrError = CurTime() - local context = { - data = {}, - vclk = {}, - funcs = self.funcs, - funcs_ret = self.funcs_ret, - entity = self, - player = self.player, - uid = self.uid, - prf = (self.context and (self.context.prf * resetPrfMult)) or 0, - prfcount = (self.context and (self.context.prfcount * resetPrfMult)) or 0, - prfbench = (self.context and (self.context.prfbench * resetPrfMult)) or 0, - time = (self.context and (self.context.time * resetPrfMult)) or 0, - timebench = (self.context and (self.context.timebench * resetPrfMult)) or 0, - stackdepth = 0, - includes = self.includes - } - - -- '@strict' try/catch Error handling. - if self.directives.strict then - local err = E2Lib.raiseException - function context:throw(msg) - err(msg, 2, self.trace) - end - else - -- '@strict' is not enabled, pass the default variable. - function context:throw(_msg, variable) - return variable - end - end + local context = E2Lib.RuntimeContext.builder() + :withChip(self) + :withOwner(self.player) + :withStrict(self.directives.strict) + :withUserFunctions(self.funcs) + :withIncludes(self.includes) - setmetatable(context, ScopeManager) - context:InitScope() + if self.context then + context = context + :withPrf(self.context.prf * resetPrfMult, self.context.prfcount * resetPrfMult, self.context.prfbench * resetPrfMult) + :withTime(self.context.time * resetPrfMult, self.context.timebench * resetPrfMult) + end - self.context = context + self.context = context:build() self.GlobalScope = context.GlobalScope self._vars = self.GlobalScope -- Dupevars - self.Inputs = WireLib.AdjustSpecialInputs(self, self.inports[1], self.inports[2], self.inports[4]) - self.Outputs = WireLib.AdjustSpecialOutputs(self, self.outports[1], self.outports[2], self.outports[4]) + local conv_inputs, conv_outputs = {}, {} + for i, input in ipairs(self.inports[2]) do + conv_inputs[i] = wire_expression_types2[input][1] + end + for i, input in ipairs(self.outports[2]) do + conv_outputs[i] = wire_expression_types2[input][1] + end + + self.Inputs = WireLib.AdjustSpecialInputs(self, self.inports[1], conv_inputs, self.inports[4]) + self.Outputs = WireLib.AdjustSpecialOutputs(self, self.outports[1], conv_outputs, self.outports[4]) if self.extended then -- It was extended before the adjustment, recreate the wirelink WireLib.CreateWirelinkOutput( self.player, self, {true} ) @@ -494,21 +438,21 @@ function ENT:ResetContext() for k, v in pairs(self.inports[3]) do self._inputs[1][#self._inputs[1] + 1] = k - self._inputs[2][#self._inputs[2] + 1] = v - self.GlobalScope[k] = fixDefault(wire_expression_types[v][2]) + self._inputs[2][#self._inputs[2] + 1] = wire_expression_types2[v][1] + self.GlobalScope[k] = fixDefault(wire_expression_types2[v][2]) self.globvars_mut[k] = nil end for k, v in pairs(self.outports[3]) do self._outputs[1][#self._outputs[1] + 1] = k - self._outputs[2][#self._outputs[2] + 1] = v - self.GlobalScope[k] = fixDefault(wire_expression_types[v][2]) + self._outputs[2][#self._outputs[2] + 1] = wire_expression_types2[v][1] + self.GlobalScope[k] = fixDefault(wire_expression_types2[v][2]) self.GlobalScope.vclk[k] = true self.globvars_mut[k] = nil end for k, v in pairs(self.persists[3]) do - self.GlobalScope[k] = fixDefault(wire_expression_types[v][2]) + self.GlobalScope[k] = fixDefault(wire_expression_types2[v][2]) self.globvars_mut[k] = nil end @@ -621,8 +565,9 @@ function ENT:TriggerInput(key, value) local t = self.inports[3][key] self.GlobalScope["$" .. key] = self.GlobalScope[key] - if wire_expression_types[t][3] then - self.GlobalScope[key] = wire_expression_types[t][3](self.context, value) + local iowrap = wire_expression_types2[t][3] + if iowrap then + self.GlobalScope[key] = iowrap(self.context, value) else self.GlobalScope[key] = value end @@ -639,8 +584,8 @@ end function ENT:TriggerOutputs(force) for key, t in pairs(self.outports[3]) do if self.GlobalScope.vclk[key] or force then - if wire_expression_types[t][4] then - WireLib.TriggerOutput(self, key, wire_expression_types[t][4](self.context, self.GlobalScope[key])) + if wire_expression_types2[t][4] then + WireLib.TriggerOutput(self, key, wire_expression_types2[t][4](self.context, self.GlobalScope[key])) else WireLib.TriggerOutput(self, key, self.GlobalScope[key]) end @@ -654,7 +599,8 @@ function ENT:ApplyDupeInfo(ply, ent, info, GetEntByID, GetConstByID) if not self.error then for k, v in pairs(self.dupevars) do -- Backwards compatibility to fix dupes with the old {n, n, n} angle and vector types - local vartype = self.globvars[k] and self.globvars[k].type + -- $ check is for delta variables stored in dupevars. ugly one liner. + local vartype = self.globvars[k] and self.globvars[k].type or (k:sub(1, 1) == "$" and (self.globvars[k:sub(2)] and self.globvars[k:sub(2)].type)) if vartype == "a" then self.GlobalScope[k] = istable(v) and Angle(v[1], v[2], v[3]) or v elseif vartype == "v" then diff --git a/lua/entities/gmod_wire_expression2/shared.lua b/lua/entities/gmod_wire_expression2/shared.lua index 65a227feee..fd53d42fc4 100644 --- a/lua/entities/gmod_wire_expression2/shared.lua +++ b/lua/entities/gmod_wire_expression2/shared.lua @@ -1,8 +1,8 @@ DEFINE_BASECLASS("base_wire_entity") ENT.PrintName = "Wire Expression 2" -ENT.Author = "Syranide" -ENT.Contact = "me@syranide.com" +ENT.Author = "" +ENT.Contact = "" ENT.Purpose = "" ENT.Instructions = "" @@ -15,12 +15,10 @@ CreateConVar("wire_expression2_quotatick", "25000", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotatime", "-1", {FCVAR_REPLICATED}, "Time in (ms) the e2 can consume before killing (-1 is infinite)") include("core/e2lib.lua") -include("base/ast.lua") +include("base/debug.lua") include("base/preprocessor.lua") include("base/tokenizer.lua") include("base/parser.lua") -if SERVER then - include("base/optimizer.lua") -end + include("base/compiler.lua") -include('core/init.lua') +include("core/init.lua") diff --git a/lua/wire/client/text_editor/issue_viewer.lua b/lua/wire/client/text_editor/issue_viewer.lua index c9120f2676..23225bc9fe 100644 --- a/lua/wire/client/text_editor/issue_viewer.lua +++ b/lua/wire/client/text_editor/issue_viewer.lua @@ -251,6 +251,8 @@ function PANEL:SetBGColor(r, g, b, a) self:SetValidationColors(Color(r, g, b, a)) end +---@param errors Error[] +---@param warnings Warning[], function PANEL:Update(errors, warnings, header_text, header_color) self.ValidationText = header_text or self.ValidationText if header_color ~= nil then self:SetValidationColors(header_color) end @@ -266,10 +268,10 @@ function PANEL:Update(errors, warnings, header_text, header_color) if warnings ~= nil and not table.IsEmpty(warnings) then for k, v in ipairs(warnings) do if v.message ~= nil then - local node = tree:AddNode(v.message .. (v.line ~= nil and string.format(" [%d:%d]", v.line, v.char) or "")) + local node = tree:AddNode(v.message .. (v.trace ~= nil and string.format(" [line %u, char %u]", v.trace.start_line, v.trace.start_col) or "")) node:SetIcon("icon16/error.png") - node.line = v.line - node.char = v.char + node.line = v.trace and v.trace.start_line + node.char = v.trace and v.trace.start_col end end failed = true @@ -277,10 +279,10 @@ function PANEL:Update(errors, warnings, header_text, header_color) if errors ~= nil and not table.IsEmpty(errors) then for k, v in ipairs(errors) do - local node = tree:AddNode(v.message .. (v.line ~= nil and string.format(" [%d:%d]", v.line, v.char) or "")) + local node = tree:AddNode(v.message .. (v.trace ~= nil and string.format(" [line %u, char %u]", v.trace.start_line, v.trace.start_col) or "")) node:SetIcon("icon16/cancel.png") - node.line = v.line - node.char = v.char + node.line = v.trace and v.trace.start_line + node.char = v.trace and v.trace.start_col end failed = true end diff --git a/lua/wire/client/text_editor/modes/e2.lua b/lua/wire/client/text_editor/modes/e2.lua index f1559ffa67..4097617e54 100644 --- a/lua/wire/client/text_editor/modes/e2.lua +++ b/lua/wire/client/text_editor/modes/e2.lua @@ -97,10 +97,18 @@ local function addToken(tokenname, tokendata) end end -local function AcceptIdent(self) +local function acceptIdent(self) return self:NextPattern("^[A-Z][a-zA-Z0-9_]*") or self:NextPattern("^_") end +local function addOptional(self, pattern, tokendata) + local s = self:SkipPattern(pattern) + if s then + self.tokendata = "" + addToken(tokendata, s) + end +end + function EDITOR:CommentSelection(removecomment) local sel_start, sel_caret = self:MakeSelection( self:Selection() ) local mode = self:GetParent().BlockCommentStyleConVar:GetInt() @@ -365,7 +373,7 @@ function EDITOR:SyntaxColorLine(row) addToken( "operator", self.tokendata ) self.tokendata = "" - while AcceptIdent(self) do -- If we found a variable + while acceptIdent(self) do -- If we found a variable addToken( "variable", self.tokendata ) self.tokendata = "" @@ -377,7 +385,7 @@ function EDITOR:SyntaxColorLine(row) addToken( "operator", "]" ) self.tokendata = "" end - elseif AcceptIdent(self) then -- If we found a variable + elseif acceptIdent(self) then -- If we found a variable -- Color the variable addToken( "variable", self.tokendata ) self.tokendata = "" @@ -447,7 +455,7 @@ function EDITOR:SyntaxColorLine(row) addToken( "operator", self.tokendata ) self.tokendata = "" - while AcceptIdent(self) do -- If we found a variable + while acceptIdent(self) do -- If we found a variable addToken( "variable", self.tokendata ) self.tokendata = "" @@ -459,7 +467,7 @@ function EDITOR:SyntaxColorLine(row) addToken( "operator", "]" ) self.tokendata = "" end - elseif AcceptIdent(self) then -- If we found a variable + elseif acceptIdent(self) then -- If we found a variable -- Color the variable addToken( "variable", self.tokendata ) self.tokendata = "" @@ -493,6 +501,42 @@ function EDITOR:SyntaxColorLine(row) end end + local found = self:SkipPattern("(} *)") + if found then + addToken("operator", found) + self.tokendata = "" + end + + local found = self:SkipPattern("( *catch)") + if found then + addToken("keyword", found) + self.tokendata = "" + addOptional(self, " *", "comment") + + if self:NextPattern("%(") then + addToken("operator", self.tokendata) + self.tokendata = "" + addOptional(self, " *", "comment") + + if acceptIdent(self) then + addToken("variable", self.tokendata) + self.tokendata = "" + addOptional(self, " *", "comment") + + if self:NextPattern(":") then + addToken("operator", self.tokendata) + self.tokendata = "" + addOptional(self, " *", "comment") + self.tokendata = "" + + if self:NextPattern("[a-z][a-zA-Z0-9_]*") then + addToken("typename", self.tokendata) + end + end + end + end + end + while self.character do local tokenname = "" self.tokendata = "" @@ -555,7 +599,7 @@ function EDITOR:SyntaxColorLine(row) elseif wire_expression2_funclist[sstr] then tokenname = "function" - elseif self.e2fs_functions[sstr] then + elseif self.e2fs_functions[sstr] or self.e2fs_methods[sstr] then tokenname = "userfunction" else @@ -584,7 +628,7 @@ function EDITOR:SyntaxColorLine(row) self.tokendata = spaces end - elseif AcceptIdent(self) then + elseif acceptIdent(self) then if self.tokendata == "This" then tokenname = "typename" else diff --git a/lua/wire/client/text_editor/texteditor.lua b/lua/wire/client/text_editor/texteditor.lua index 68e360628d..46d4267f58 100644 --- a/lua/wire/client/text_editor/texteditor.lua +++ b/lua/wire/client/text_editor/texteditor.lua @@ -87,6 +87,9 @@ function EDITOR:Init() self.LastClick = 0 self.e2fs_functions = {} + self.e2fs_methods = {} + + self.e2_functionsig_lookup = {} self.Colors = { dblclickhighlight = Color(0, 100, 0), @@ -2183,7 +2186,7 @@ end -- Adds all matching functions to the suggestions table -------------------- -local function GetTableForFunction() +local function GetTableForFunction(udf) return { nice_str = function( t ) return t.data[2] end, str = function( t ) return t.data[1] end, @@ -2196,7 +2199,9 @@ local function GetTableForFunction() return ret..(has_bracket and "" or "()"), #ret+1 end, others = function( t ) return t.data[3] end, - description = function( t ) + description = udf and function(t) + return "A userfunction\n" + end or function( t ) if t.data[4] and E2Helper.Descriptions[t.data[4]] then return E2Helper.Descriptions[t.data[4]] end @@ -2206,8 +2211,8 @@ local function GetTableForFunction() end, data = {}, - selected_color = AC_COLOR_FUNCTION_SELECTED, - color = AC_COLOR_FUNCTION + selected_color = udf and AC_COLOR_USERFUNCTION_SELECTED or AC_COLOR_FUNCTION_SELECTED, + color = udf and AC_COLOR_USERFUNCTION or AC_COLOR_FUNCTION } end @@ -2221,9 +2226,9 @@ local function FindFunctions( self, has_colon, word ) local suggested = {} local suggestions = {} - for func_id,_ in pairs( wire_expression2_funcs ) do - if wordl == func_id:lower():sub(1,len) then -- Check if the beginning of the word matches - local name, types = func_id:match( "(.+)(%b())" ) -- Get the function name and types + local function handle(id, udf) + if wordl == id:lower():sub(1, len) then -- Check if the beginning of the word matches + local name, types = id:match( "(.+)(%b())" ) -- Get the function name and types local first_type, colon, other_types = types:match( "%((%w*)(:?)(.*)%)" ) -- Sort the function types if (colon == ":") == has_colon then -- If they both have colons (or not) first_type = first_type:upper() @@ -2234,12 +2239,12 @@ local function FindFunctions( self, has_colon, word ) -- Add to suggestions if colon == ":" then - local t = GetTableForFunction() - t.data = { name, first_type .. ":" .. name .. "(" .. other_types .. ")", {}, func_id } + local t = GetTableForFunction(udf) + t.data = { name, first_type .. ":" .. name .. "(" .. other_types .. ")", {}, id } suggestions[count] = t else - local t = GetTableForFunction() - t.data = { name, name .. "(" .. first_type .. ")", {}, func_id } + local t = GetTableForFunction(udf) + t.data = { name, name .. "(" .. first_type .. ")", {}, id } suggestions[count] = t end else -- If it has already been suggested @@ -2249,18 +2254,27 @@ local function FindFunctions( self, has_colon, word ) -- Add it to the end of the list if colon == ":" then - local t = GetTableForFunction() - t.data = { name, first_type .. ":" .. name .. "(" .. other_types .. ")", nil, func_id } + local t = GetTableForFunction(udf) + t.data = { name, first_type .. ":" .. name .. "(" .. other_types .. ")", nil, id } others[i] = t else - local t = GetTableForFunction() - t.data = { name, name .. "(" .. first_type .. ")", nil, func_id } + local t = GetTableForFunction(udf) + t.data = { name, name .. "(" .. first_type .. ")", nil, id } others[i] = t end end end end end + + for id in pairs( wire_expression2_funcs ) do + handle(id) + end + + for id in pairs( self.e2_functionsig_lookup ) do + handle(id, true) + end + return suggestions end diff --git a/lua/wire/client/text_editor/wire_expression2_editor.lua b/lua/wire/client/text_editor/wire_expression2_editor.lua index a8e5217720..7a90db9e1d 100644 --- a/lua/wire/client/text_editor/wire_expression2_editor.lua +++ b/lua/wire/client/text_editor/wire_expression2_editor.lua @@ -1649,14 +1649,43 @@ function Editor:OpenOldTabs() end end +-- On a successful validation run, will call this with the compiler object +function Editor:SetValidateData(compiler) + -- Set methods and functions from all includes for syntax highlighting. + local editor = self:GetCurrentEditor() + editor.e2fs_functions = compiler.user_functions + + local function_sigs = {} + for name, overloads in pairs(compiler.user_functions) do + for args in pairs(overloads) do + function_sigs[name .. "(" .. args .. ")"] = true + end + end + + local allkeys = {} + for meta, names in pairs(compiler.user_methods) do + for name, overloads in pairs(names) do + allkeys[name] = true + for args in pairs(overloads) do + function_sigs[name .. "(" .. meta .. ":" .. args .. ")"] = true + end + end + end + + editor.e2fs_methods = allkeys + editor.e2_functionsig_lookup = function_sigs +end + function Editor:Validate(gotoerror) local header_color, header_text = nil, nil local problems_errors, problems_warnings = {}, {} if self.EditorType == "E2" then - local errors, _, warnings = wire_expression2_validate(self:GetCode()) + local errors, _, warnings, compiler = E2Lib.Validate(self:GetCode()) + + if not errors then ---@cast compiler -? + self:SetValidateData(compiler) - if not errors then if warnings then header_color = Color(163, 130, 64, 255) @@ -1665,7 +1694,8 @@ function Editor:Validate(gotoerror) if gotoerror and self.ScrollToWarning:GetBool() then header_text = "Warning (1/" .. nwarnings .. "): " .. warning.message - self:GetCurrentEditor():SetCaret { warning.line, warning.char } + + self:GetCurrentEditor():SetCaret { warning.trace.start_line, warning.trace.start_col } else header_text = "Validated with " .. nwarnings .. " warning(s)." end @@ -1676,17 +1706,20 @@ function Editor:Validate(gotoerror) end else header_color = Color(110, 0, 20, 255) - header_text = ("" .. errors) - local row, col = errors:match("at line ([0-9]+), char ([0-9]+)$") - if not row then - row, col = errors:match("at line ([0-9]+)$"), 1 - end - problems_errors = {{message = string.Explode(" at line", errors)[1], line = row, char = col}} + local nerrors, error = #errors, errors[1] if gotoerror then - if row then self:GetCurrentEditor():SetCaret({ tonumber(row), tonumber(col) }) end + header_text = "Error (1/" .. nerrors .. "): " .. error.message + + if error.trace then + self:GetCurrentEditor():SetCaret { error.trace.start_line, error.trace.start_col } + end + else + header_text = "Failed to compile with " .. nerrors .. " errors(s)." end + + problems_errors = errors end elseif self.EditorType == "CPU" or self.EditorType == "GPU" or self.EditorType == "SPU" then diff --git a/lua/wire/stools/expression2.lua b/lua/wire/stools/expression2.lua index 6680d6d120..3e9dc5c6af 100644 --- a/lua/wire/stools/expression2.lua +++ b/lua/wire/stools/expression2.lua @@ -713,9 +713,9 @@ elseif CLIENT then local err, includes, warnings if e2_function_data_received then - err, includes, warnings = wire_expression2_validate(code) - if err then - WireLib.AddNotify(err, NOTIFY_ERROR, 7, NOTIFYSOUND_ERROR1) + err, includes, warnings = E2Lib.Validate(code) + if err and err[1] then + WireLib.AddNotify(err[1].message, NOTIFY_ERROR, 7, NOTIFYSOUND_ERROR1) return end else