├── .gitignore ├── .prettierignore ├── README.md ├── demos.ts ├── docs └── getting-started.md ├── monaco-vim.d.ts ├── package-lock.json ├── package.json ├── playground.html ├── playground.ts ├── src ├── colors.ts ├── complete.test.ts ├── err.ts ├── gen.ts ├── grammar.ne ├── grammar.ts ├── ir.ts ├── lexer.test.ts ├── lexer.ts ├── nodes.ts ├── parser.test.ts ├── runner │ ├── draws.ts │ └── runner.ts ├── test.helpers.ts ├── test.parser.ts ├── test.programs.ts ├── test.setup.ts ├── translation.test.ts ├── typeinfo.ts ├── typetypes.ts ├── typing.test.ts ├── typing.ts ├── typinghelpers.ts └── util.ts ├── testsuite.html ├── testsuite.ts ├── testtsconfig.json ├── tsconfig.json ├── tsconfig.notests.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist 4 | bundle 5 | bundle.js 6 | *-bundle 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/grammar.ts 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tinsl 2 | 3 | language for multi-pass texture-to-texture effects 4 | 5 | ## [live coding environment!](https://bandaloo.fun/playground) 6 | 7 | ## overview of the language 8 | 9 | If you know GLSL, you know a lot of tinsl already. We'll go over some of the 10 | things that tinsl has that GLSL doesn't. (The opposite is true too; tinsl lacks 11 | or restricts things you can do with GLSL fragment shaders. We'll also cover 12 | that.) 13 | 14 | ### type inference 15 | 16 | tinsl can infer function return types and variable declaration types. 17 | 18 | ```c 19 | // the GLSL way with no type inference (also valid tinsl) 20 | vec2 glsl_style_rotate2d(vec2 v, float angle) { 21 | mat2 m = mat2(cos(angle), -sin(angle), 22 | sin(angle), cos(angle)); 23 | return m * v; 24 | } 25 | 26 | // with type inference 27 | fn tinsl_rotate2d(vec2 v, float angle) { 28 | m := mat2(cos(angle), -sin(angle), 29 | sin(angle), cos(angle)); 30 | return m * v; // tinsl knows that a mat2 * vec2 -> vec2 31 | } 32 | ``` 33 | 34 | Variable declarations have a few differences from GLSL: 35 | 36 | - When you declare a variable, you must also give it an initial value. This is 37 | the case with both styles. Instead of `int x;` you must be explicit and write 38 | `int x = 0;`. Using `:=` makes this a little bit easier to remember. 39 | - You cannot use assignments like expressions. They are only statements, like in 40 | Go and Python (well, before Python 3.8). 41 | - You cannot declare multiple variables in the same statement with a comma. 42 | Separate each declaration by semicolons. The `:=` operator makes declaration 43 | succinct enough that multiple related declarations can fit on one line, e.g. 44 | `x := 1; y := 2; z := 3;`. 45 | - Variable declarations must be inside function bodies. Because of this 46 | restriction, all functions are naturally "pure" functions; they cannot have 47 | side effects. This means that a `void` return type is meaningless, so it's not 48 | in tinsl. 49 | - Variables declared with `:=` are "final" by default. If you want the variable 50 | to be mutable, you must write `mut x := 42;`. If you want a variable declared 51 | with the GLSL-style syntax to be "final", you can write `final x = 42;`. (GLSL 52 | lacks this; `const` can only be used for compile time constants.) 53 | - Precision qualifiers are not in tinsl. 54 | 55 | ### default arguments and named arguments 56 | 57 | tinsl allows for default arguments by putting an expression after `=` in a 58 | function definition, similar to JavaScript. 59 | 60 | ```c 61 | fn godrays ( 62 | vec4 col = frag, // we haven't talked about frag yet 63 | float exposure = 1., 64 | float decay = 1., 65 | float density = 1., 66 | float weight = 0.01, 67 | vec2 light_pos = vec2(.5, .5), 68 | int num_samples = 100, 69 | int channel = -1 70 | ) { 71 | // imagine a function body here that returns a vec4 72 | } 73 | ``` 74 | 75 | If you have a lot of parameters like the above function, it is mighty convenient 76 | to call the function with named arguments, and let all the other arguments 77 | default. Say you just wanted to change `weight` and `num_samples`. You could 78 | call the function like this: 79 | 80 | ```c 81 | // we also haven't talked about what this arrow syntax is. 82 | 0 -> { godrays(num_samples: 50, weight: 0.02); } -> 0 83 | ``` 84 | 85 | There are a few things to note about named and default arguments: 86 | 87 | - An implication of adding in optional arguments is that it that the best way to 88 | handle function overloads becomes a bit unclear. For now, function overloads 89 | are not allowed in tinsl. 90 | - Default arguments must be trailing (e.g. `fn foo (int x = 1, int y)` is not 91 | allowed but `fn foo (int x, int y = 2)` is fine. 92 | - You cannot mix the syntax for named arguments and ordered arguments in a 93 | function call. 94 | - For now, you need an explicit parameter type even if you provide a default 95 | value. (The plan is to get rid of the need for this.) 96 | - You cannot have `in`, `out` or `inout` parameters like in GLSL. 97 | - Function parameters are immutable. 98 | 99 | ### color strings 100 | 101 | tinsl provides some syntactic sugar for specifying colors. The expression 102 | `"cornflower blue"` is syntactic sugar for `vec4(0.392, 0.584, 0.929)`. The 140 103 | HTML5 named colors are included, and are insensitive to white space and casing. 104 | You can use single quotes if you prefer. To include an alpha value, you can type 105 | `"cornflower blue"4`. You can also use hex numbers for colors of length 3, 4, 6 106 | or 8, e.g., `#f00`, `#f00f`, `#C0FFEE` or `#DEADBEEF`. Hex codes that include an 107 | alpha value, like `#f00f` or `#ff0000ff`, will evaluate to a `vec4`, while hex 108 | codes of length 3 or 6 will evaluate to a `vec3`. You could also do `"#ff0000"4` 109 | to make the alpha value 1. 110 | 111 | ### render-to-texture 112 | 113 | The best way to demonstrate what "render blocks" in tinsl are is through 114 | example. This is the simplest tinsl program: 115 | 116 | ```c 117 | 0 -> { frag; } 118 | ``` 119 | 120 | It's essentially a tinsl no-op. It samples from the first texture (the one the 121 | video is on in the playground), and renders out to the screen since it's the 122 | last render block in the program. We could have also written it like this: 123 | 124 | ```c 125 | { frag0; } 126 | ``` 127 | 128 | This makes it more explicit that we're sampling from texture 0. We could have 129 | also written it like this: 130 | 131 | ```c 132 | { frag(0); } 133 | ``` 134 | 135 | This is useful inside a function (or procedure, which we'll get to later) since 136 | you can pass in an argument into frag. 137 | 138 | ```c 139 | fn foo(int channel) { return frag(channel); } 140 | 141 | { foo(0); } 142 | ``` 143 | 144 | Let's create an effect that requires texture-to-texture rendering. A good 145 | example of this is a pixel-accumulation motion blur. 146 | 147 | ```c 148 | { frag0 * 0.1 + frag1 * 0.9; } -> 1 149 | { frag1; } 150 | ``` 151 | 152 | That's the whole program. Run it in the playground and move your head around. 153 | Some games use this to simulate drunkenness and it makes a lot of sense why. 154 | Let's go all the way and add double vision: 155 | 156 | ```c 157 | { frag0 * 0.1 + frag1 * 0.9; } -> 1 158 | // how many fingers am i holding up? 159 | { 0.5 * (frag1(npos + vec2(0.05, 0.)) + frag1(npos + vec2(-0.05, 0.))); 160 | ``` 161 | 162 | As you can see, we can pass in a `vec2` to choose where to sample from. In this 163 | case, we sample twice by a constant offset from `npos` which is how you get the 164 | current normalized position in tinsl. 165 | 166 | This inebriation simulator became a convenient segue into blurriness. The 167 | fastest way to perform a gaussian blur is to first blur horizontally, and then 168 | take that blurred image and perform the same operation vertically. This is 169 | something that cannot be done in a single fragment shader normally; we'll see 170 | how tinsl lets us do this in a concise way. 171 | 172 | This is an efficient function that lets us do a linear blur. Paste this in at 173 | the top of your file. 174 | 175 | ```c 176 | fn blur(vec2 dir, int channel = -1) { 177 | uv := pos / res; 178 | mut col := vec4(0.); 179 | off1 := vec2(1.411764705882353) * dir; 180 | off2 := vec2(3.2941176470588234) * dir; 181 | off3 := vec2(5.176470588235294) * dir; 182 | col += frag(channel, npos) * 0.1964825501511404; 183 | col += frag(channel, npos + (off1 / res)) * 0.2969069646728344; 184 | col += frag(channel, npos - (off1 / res)) * 0.2969069646728344; 185 | col += frag(channel, npos + (off2 / res)) * 0.09447039785044732; 186 | col += frag(channel, npos - (off2 / res)) * 0.09447039785044732; 187 | col += frag(channel, npos + (off3 / res)) * 0.010381362401148057; 188 | col += frag(channel, npos - (off3 / res)) * 0.010381362401148057; 189 | return col; 190 | } 191 | ``` 192 | 193 | We set the default value for `channel` to `-1`. In tinsl, this means use the "in 194 | number" of the surrounding render block. The "in number" is the number to the 195 | left of the first arrow. We could explicitly pass in `0` since we'll be doing 196 | the blur on the `0` texture, but this makes things a little nicer. 197 | 198 | ```c 199 | 0 -> loop 3 { 200 | blur(vec2(1., 0.)); 201 | refresh; 202 | blur(vec2(0., 1.)); 203 | } -> 0 204 | ``` 205 | 206 | We can loop an operation by typing `loop ` right before a 207 | render block. We need to do this in two passes; if we did not have `refresh`, 208 | the vertical blur would overwrite the horizontal blur. Before the next blur, we 209 | must render out to a texture. We could have also written it like this by 210 | creating nested render blocks: 211 | 212 | ```c 213 | 0 -> loop 3 { 214 | 0 -> { blur(vec2(1., 0.)); } -> 0 215 | 0 -> { blur(vec2(0., 1.)); } -> 0 216 | } -> 0 217 | ``` 218 | 219 | You might notice there are a lot of zeros. This is redundant. We can leave off 220 | the zeros in the inner render blocks; it will use the "in number" and "out 221 | number" of the outer render block, which are both zero. 222 | 223 | ```c 224 | 0 -> loop 3 { 225 | { blur(vec2(1., 0.)); } // implicit 0 -> { ... } -> 0 226 | { blur(vec2(0., 1.)); } // implicit 0 -> { ... } -> 0 227 | } -> 0 228 | ``` 229 | 230 | In fact, when you leave off the "in number" and "out number" on a render block 231 | at the top level, it defaults to zero, so we could have left the outer numbers 232 | off too. 233 | 234 | Look at the 'club' example in the playground to see many of these techniques 235 | in action. 236 | 237 | ### some notes about types, operators and built-ins 238 | 239 | For completion's sake, tinsl includes `bvec`s, `ivec`s, `uvec`s and `uint`s, on 240 | top of the more familiar `vec` and `mat` types. You can make arrays of all the 241 | included types. The way you call constructors is the same as GLSL, although it 242 | has _just_ come to my attention that you can construct a `mat2x3` with 243 | `mat2x3(vec2, float, vec2, float)`. (This just seems confusing in my opinion.) 244 | As it stands in tinsl, you have to choose all floats or all column vectors. 245 | Additionally, the constructors for `vec`s and `mat`s do not accept `int` types 246 | --- only floats. One note about array constructors is that, while in GLSL you 247 | have some choice about where the square brackets go, in tinsl they always go 248 | right after the type name. Using `:=` means you don't have to think about this 249 | though. 250 | 251 | ```c 252 | // (these statements are valid in a function body) 253 | // BAD!! doesn't work in tinsl 254 | float a[3] = float[3](1., 2., 3.); // BAD!! doesn't compile 255 | float b[] = float[](1., 2., 3.); // BAD!! doesn't compile 256 | 257 | // okay! works in tinsl 258 | float[3] c = float[3](1., 2., 3.); 259 | float[] d = float[](1., 2., 3.); 260 | e := float[](1., 2., 3.); 261 | ``` 262 | 263 | tinsl also includes every operator down to `^=` (did you know that existed?) 264 | except for `,`, the "sequence" or "comma" operator. There is no sampler type; 265 | this is handled by `frag`. 266 | 267 | Every builtin function in GLSL ES 300 is included, except for `modf` because one 268 | of its parameters is an `out` parameter. All of these are listed in section 8, 269 | printed page 84, PDF page 91, of this 270 | [beach read](https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf). 271 | 272 | tinsl does not include structs like GLSL does. There is no particular reason 273 | for this other than it would have been more work and this is the first time 274 | I've written anything you could almost call a compiler. Similarly, `for` is 275 | the only looping construct. 276 | 277 | ### errors 278 | 279 | If you manage to write a tinsl program that generates invalid GLSL, 280 | congratulations! This is supposed to be impossible, so please copy paste your 281 | program into a [new issue](https://github.com/bandaloo/tinsl). This is really a 282 | big help. 283 | 284 | tinsl syntax and compiler errors will show up directly in the playground editor. 285 | The line that the error is on is underlined in red, and the character at the 286 | reported column will have a dark red background. The exact column the error is 287 | on might be slightly inaccurate. 288 | -------------------------------------------------------------------------------- /demos.ts: -------------------------------------------------------------------------------- 1 | interface Demos { 2 | [name: string]: string; 3 | } 4 | 5 | export const demos: Demos = { 6 | club: ` 7 | /****************************************************************************** 8 | welcome to the tinsl playground! right now you're looking at a comprehensive 9 | code example that shows off many features of the language. if you want 10 | something less overwhelming, try "one liners" (select that option from the 11 | bottom right menu). click the run button at the bottom to run the program. 12 | 13 | everything you see is a work in progress so feedback and github issues are 14 | welcome. check out the readme on the github page for more details about the 15 | language: https://github.com/bandaloo/tinsl 16 | ******************************************************************************/ 17 | 18 | // uniforms (in playground only!) that match the pattern ^fft[0-9]+$ will 19 | // automatically be updated with the FFT frequency data from your mic. 20 | // fft0 is the lowest and fft127 is the highest 21 | uniform float fft0; 22 | 23 | // some colors for our blinking lights 24 | def colors vec4[]( 25 | 'magenta'4, 26 | 'cornflower blue'4, 27 | 'crimson'4, 28 | 'green yellow'4, 29 | 'aquamarine'4, 30 | 'orange red'4 31 | ) 32 | 33 | // converts seconds to milliseconds 34 | def seconds time / 1000. 35 | 36 | // define to easily index the color array by time 37 | def chosen_color colors[int(seconds) % 6] 38 | 39 | // fast gaussian blur 40 | // from https://github.com/Jam3/glsl-fast-gaussian-blur/blob/master/9.glsl 41 | fn blur(vec2 direction, int channel = -1) { 42 | uv := npos; 43 | mut color := vec4(0.0); 44 | off1 := vec2(1.3846153846) * direction; 45 | off2 := vec2(3.2307692308) * direction; 46 | color += frag(channel, uv) * 0.2270270270; 47 | color += frag(channel, uv + (off1 / res)) * 0.3162162162; 48 | color += frag(channel, uv - (off1 / res)) * 0.3162162162; 49 | color += frag(channel, uv + (off2 / res)) * 0.0702702703; 50 | color += frag(channel, uv - (off2 / res)) * 0.0702702703; 51 | return color; 52 | } 53 | 54 | // procedure to do horizontal then vertical blur 55 | pr two_pass_blur(float size, int reps, int channel = -1) { 56 | loop reps { 57 | blur(vec2(size, 0.), channel); 58 | refresh; 59 | blur(vec2(0., size), channel); 60 | } 61 | } 62 | 63 | // prime the accumulation texture only one time 64 | // (this is so it won't flash white when starting) 65 | 0 -> once { chosen_color * frag; } -> 1 66 | 67 | 0 -> { 68 | frag; // set the color to frag0 (0 implied by \`0 ->\` before this block) 69 | chosen_color * prev; // multiply by previous color (in this case, it's \`frag\`) 70 | prev * .1 + frag1 * .97; // blend with the accumulation buffer; oversaturate a bit 71 | } -> 1 72 | 73 | 1 -> { 74 | // darken the edges 75 | mix(frag, 'black'4, 1.5 * length(npos - 0.5)); 76 | // blend with black based on fft analysis 77 | // we do some simple filtering to drop to zero if below 0.1 and scale by 0.7 78 | mix(prev, 'black'4, fft0 < 0.1 ? 0.: fft0 * 0.7); 79 | } -> 0 80 | 81 | // blur the edges 82 | 0 -> { @two_pass_blur(2. * length(npos - 0.5), 3); } -> 0 83 | `, 84 | thermalish: ` 85 | def threshold 0.3 // luma threshold for 1 bit color 86 | 87 | // from https://github.com/hughsk/glsl-hsv2rgb/blob/master/index.glsl 88 | // most regular glsl works in tinsl just fine 89 | vec3 hsv2rgb(vec3 c) { 90 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 91 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 92 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 93 | } 94 | 95 | // gets the luma of a color 96 | fn luma(vec4 color) { 97 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 98 | } 99 | 100 | // convert to 1 bit color 101 | 0 -> { vec4(vec3(step(threshold, luma(frag))), 1.); } -> 0 102 | // do some pixel accumulation to get a motion blur trail 103 | 0 -> { 0.03 * frag + 0.97 * frag1; } -> 1 104 | // take the luma and convert that to hue 105 | 1 -> { vec4(hsv2rgb(vec3(luma(frag) / 2., .5, 1.)), 1.); } -> 0`, 106 | "one liners": ` 107 | def seconds time / 1000. 108 | 109 | // gets the luma of a color 110 | fn luma(vec4 color) { 111 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 112 | } 113 | 114 | // uncomment each individually to test these one-liners 115 | 116 | // monochrome 117 | { vec4(vec3(luma(frag)), 1.); } 118 | 119 | // sepia 120 | //{ '#B17C66FF' * vec4(vec3(luma(frag)), 1.); } 121 | 122 | // darken edges 123 | //{ mix(frag, 'black'4, 1.5 * length(npos - 0.5)); } 124 | 125 | // one bit color 126 | //{ vec4(vec3(step(0.3, luma(frag))), 1.); } 127 | 128 | // mitosis 129 | //{ frag(npos + vec2(sin(npos.x * 2. + seconds), 0.)); } 130 | 131 | // offset by the red channel 132 | //{ frag(npos + frag.r / 3.); } 133 | 134 | // mirror 135 | //{ frag(vec2(1. - npos.x, npos.y)); } 136 | 137 | // colorize 138 | //{ 'aquamarine'4 * frag; } 139 | 140 | // sierpinski 141 | //{ frag * vec4(vec3((float(int(pos.x) & int(pos.y + time / 9.)) > 1. ? 1. : 0.)), 1.); } 142 | `, 143 | fft: ` 144 | // make some noise!!! 145 | // pound your desk or play some music with a lot of bass 146 | 147 | // 0 is the lowest frequency; can instead go all the way up to fft127 148 | uniform float fft0; 149 | 150 | // offset the colors by the fft; do some simple filtering for values below 0.1 151 | def epsilon 0.01 * (fft0 < 0.1 ? 0. : fft0) 152 | 153 | // helper function to get the luma 154 | fn luma(vec4 color) { 155 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 156 | } 157 | 158 | 0 -> { vec4(vec3(1. - step(0.2, luma(frag))), 1.); } -> 0 159 | 160 | 0 -> { frag * 'lime'4 + frag(npos + epsilon) * 'red'4 + frag(npos - epsilon) * 'blue'4; } 161 | `, 162 | noop: ` 163 | { frag; } 164 | `, 165 | life: ` 166 | fn rand(vec2 n) { return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453); } 167 | 168 | once { vec4(vec3(rand(npos) > .5 ? 1. : 0.), 1.); } -> 2 169 | 170 | fn get(float x, float y) { return frag(2, npos + vec2(x, y) / res).r; } 171 | 172 | fn process() { 173 | sum := get(-1., -1.) + get(-1., 0.) + get(-1., 1.) + get(0., -1.) 174 | + get(0., 1.) + get(1., -1.) + get(1., 0.) + get(1., 1.); 175 | if (sum == 3.) { 176 | return vec4(1.); 177 | } else if (sum == 2.) { 178 | return vec4(vec3(get(0., 0.)), 1.); 179 | } 180 | return vec4(0., 0., 0., 1.); 181 | } 182 | 183 | 2 -> { process(); } -> 1 // simulate 184 | 1 -> { frag; } -> 2 // swap 185 | { frag0 * frag2; } // render to screen 186 | `, 187 | "reaction diffusion": ` 188 | fn get(float x, float y) { return frag(2, npos + vec2(x, y) / res); } 189 | 190 | def f .046 def k .064 def dA 1.1 def dB 0.39 // tweak these very slightly! 191 | 192 | fn luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); } 193 | 194 | once { luma(frag) > 0.3 ? vec4(0., 0.5, 0., 1.) : vec4(1., 0., 0., 1.); } -> 2 195 | 196 | vec4 process() { 197 | mut state := get(0., 0.); 198 | a := state.r; 199 | b := state.g; 200 | mut sumA := a * -1.; 201 | mut sumB := b * -1.; 202 | sums := vec4[](get(-1.,0.) * .2, get(-1.,-1.) * .05, get(0.,-1.) * .2, get(1.,-1.) * .05, 203 | get(1.,0.) * .2, get(1.,1.) * .05, get(0.,1.) * .2, get(-1.,1.) * .05); 204 | for (int i = 0; i < 8; i++) { sumA += sums[i].r; } 205 | for (int i = 0; i < 8; i++) { sumB += sums[i].g; } 206 | state.r = a + dA * sumA - a * b * b + f * (1. - a); 207 | state.g = b + dB * sumB + a * b * b - ((k+f) * b); 208 | return state; 209 | } 210 | 211 | 2 -> { process(); } -> 1 // simulate 212 | 1 -> { frag; } -> 2 // swap 213 | { frag0 * vec4(vec3(frag2.g), 1.); } // render to screen 214 | `, 215 | }; 216 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # getting started with tinsl 2 | 3 | If you intend to fill out the survey by the end of the tutorial, visit the 4 | [survey link](https://docs.google.com/forms/d/e/1FAIpQLSclVAb-okgpAFoR3XdrZJ8xagOtWVW5eVBxr8ij5oY09yCoIA/viewform?usp=sf_link) 5 | now in order to read and agree to the informed consent form before 6 | proceeding. Keep the survey open and return to it by the end of the tutorial. 7 | Thank you for your participation! 8 | 9 | tinsl is a language for creating post-processing effects that can run in real 10 | time. It would be helpful to know a little bit about fragment shaders for this 11 | tutorial, however, if you are completely new to this, that is okay too. If you 12 | want to learn more about fragment shaders, [The Book of 13 | Shaders](https://thebookofshaders.com/) is a fun way to get started. 14 | 15 | Follow along in this tutorial with the [tinsl 16 | playground](https://bandaloo.fun/playground). Chrome is the recommended browser 17 | for this. Firefox should also work, but use Chrome if you have the option. 18 | Resize your window to make it wide enough so that nothing is cut off. You can 19 | enable your webcam and microphone for this. The microphone is used for 20 | audio-reactive components that are not required for this tutorial. However, if 21 | you do not have a webcam or you do not want to give permission to use it, that 22 | is okay too. You will get a test image that you can use to complete the 23 | tutorial. 24 | 25 | What separates tinsl from a shading language? (After all, the acronym for tinsl 26 | claims that it's not one.) tinsl allows you to specify a rendering pipeline 27 | using special semantics to render to off-screen textures. This is not as 28 | complicated as it sounds. Let's see how that works: 29 | 30 | ```c 31 | { vec4(1., 0., 0., 1.); } -> 0 // render to texture 0 (red) 32 | { vec4(0., 0., 1., 1.); } -> 1 // render to texture 1 (blue) 33 | { frag0 + frag1; } // add texture 0 and texture 1, render to screen (magenta) 34 | ``` 35 | 36 | What you see in the previous code example is a series of three "render blocks". 37 | Render blocks are a series of expressions that evaluate to a `vec4` (a four 38 | component floating point vector) separated by semicolons, enclosed by curly 39 | brackets. In this particular example, we only have one such expression statement 40 | in each block. Each component of the `vec4` corresponds to red, green, blue and 41 | alpha (transparency) in that order. In the first line, we create a vector to 42 | represent solid red, with an instruction to render the final output of that 43 | block to texture 0. 44 | 45 | In the next line, we do the same thing, but we make texture 1 blue. In the final 46 | render block, we sample from the two textures we wrote to and add them together. 47 | Red and blue make magenta, so that's what we see. 48 | 49 | (Note: in the playground, the lowest texture number used in the program is the 50 | texture that the video feed is on. We'll use zero for all these examples, but if 51 | you ignore texture 0 by commenting out a line of code, the video feed will be on 52 | the next lowest texture number. Keep this in mind! We're overwriting the video 53 | texture in this example because we don't care about it.) 54 | 55 | Perhaps you don't find a pink screen particularly compelling. Tough, but fair. 56 | Let's create a feedback effect that simulates motion blur. We do this by using 57 | an extra texture texture for color accumulation. Delete everything and paste 58 | this in: 59 | 60 | ```c 61 | once { frag0; } -> 1 // prime our color accumulation texture just once! 62 | { frag0 * 0.03 + frag1 * 0.97; } -> 1 // accumulate colors 63 | { frag1; } // render to the screen 64 | ``` 65 | 66 | Run this program in the playground and wave your hand around. Many video games 67 | use this kind of effect to simulate drunkenness and I think it's pretty apparent 68 | why. (Motion blur in modern video games is not often done with color 69 | accumulation anymore. Instead, objects are blurred based on their velocity; this 70 | method allows camera movement to be removed from the motion blur equation, which 71 | is just way nicer for actual gameplay.) 72 | 73 | The first line is a bit of a nitpick. If you got rid of it, the image would 74 | slowly fade in, but with this small addition we can copy the contents of our 75 | video stream texture on the first draw call. `once` lets us do this, well, once. 76 | If we left off the `once`, we'd be overwriting our accumulation texture each 77 | draw call, which we don't want! This would result in no blur at all. 78 | 79 | The next line blends a bit of our video stream (3% of it) with a lot of our 80 | accumulation texture (97% of it). You can bump these coefficients around to see 81 | how this changes the final effect. (If they don't add up to 1 the feedback loop 82 | might blow up and go to white!) The values give us a very pronounced effect. 83 | 84 | The last line renders the accumulation texture to the screen. If we forgot this 85 | line (try it, comment it out) we'll still see an image but we don't get a motion 86 | blur. This is because the last render block in a tinsl program always goes to 87 | the screen. The `-> 1` of the previous block is ignored, and texture 1 never 88 | gets used as an accumulation texture. Keep this in mind! If you think this is a 89 | footgun and overall design flaw, I'm inclined to agree. However, this is how it 90 | is for now. 91 | 92 | Okay, let's do another effect. Delete everything! In Tinsl, we can have 93 | functions that look like GLSL. In fact, (nearly) all of the builtin functions 94 | and operators of GLSL 3.00 can be used in tinsl function definitions. Paste this 95 | in: 96 | 97 | ```c 98 | fn luma(vec4 color) { 99 | v := vec3(0.299, 0.587, 0.114); // coefficients for each color channel 100 | return dot(color.rgb, v); 101 | } 102 | ``` 103 | 104 | You can think of this function as taking in a color and returning how bright it 105 | is. Notice that we can declare variables with `:=` to get static type inference, 106 | and we don't need to specify the return type of a function either. tinsl takes 107 | care of that. We could have been explicit and written this function in the GLSL 108 | compatible way. This is helpful if you're just pasting in GLSL functions you 109 | find on the internet: 110 | 111 | ```c 112 | float luma(vec4 color) { 113 | vec3 v = vec3(0.299, 0.587, 0.114); 114 | return dot(color.rgb, v); 115 | } 116 | ``` 117 | 118 | But, the first way is nicer, don't you think? (Truthfully, there are arguments 119 | to be made for either style.) Moving on, let's use this function to turn our 120 | camera feed into a bespoke black-and-white. We'll write a simple function to do 121 | this: 122 | 123 | ```c 124 | fn black_and_white() { 125 | gray := vec3(luma(frag)); // grayscale rgb value 126 | return vec4(gray, 1.); // return a gray color with an alpha of 1. 127 | } 128 | ``` 129 | 130 | You'll notice we left off the number after `frag`. When there's no number, tinsl 131 | will sample from the "in number" of the enclosing render block. We do that by 132 | including an arrow before the render block: 133 | 134 | ```c 135 | 0 -> { black_and_white(); } 136 | ``` 137 | 138 | Run the program now to check that everything's in black and white. Let's do 139 | something that maps the domain of the image to a different coordinate space. In 140 | other words, let's turn the image upside down. Delete the render block we just 141 | wrote and replace it with this: 142 | 143 | ```c 144 | 0 -> { frag(vec2(0., 1.) - npos); } 145 | ``` 146 | 147 | Run it, and you'll see that you're now upside down (and in full color again). I 148 | mentioned earlier that you could have multiple `vec4` expression statements. 149 | Let's do that, and add back in our black and white filter. Augment the existing 150 | render block to look like this: 151 | 152 | ```c 153 | 0 -> { 154 | frag(vec2(0., 1.) - npos); 155 | black_and_white(); 156 | } 157 | ``` 158 | 159 | If we call `frag` like a function and pass in a `vec2`, we can sample from 160 | off-center. The `npos` keyword is the normalized pixel position. We invert the y 161 | component to flip the image. 162 | 163 | Run this, and wait...what happened? You're in black and white again, but you're 164 | now right side up. The issue here is that we want the color of the previous 165 | operation; we don't want to sample from the original image again, like 166 | `black_and_white` does with `frag`. The `prev` keyword lets us do this. To fix 167 | our issue, let's make `black_and_white` take in an argument. Update the function 168 | definition, and while we're at it let's just inline the luma part: 169 | 170 | ```c 171 | fn black_and_white(vec4 color) { 172 | return vec4(vec3(luma(color)), 1.); 173 | } 174 | ``` 175 | 176 | Now, update the call to `black_and_white` and pass in `prev`: 177 | 178 | ```c 179 | 0 -> { 180 | frag(vec2(0., 1.) - npos); 181 | black_and_white(prev); 182 | } 183 | ``` 184 | 185 | To summarize, we can use `frag` like a function and pass in a `vec2` to sample 186 | from a different position. This is useful in conjunction with `npos`, which 187 | allows you to transform the coordinate space. `prev` lets us chain together 188 | steps without breaking up a render block. As an aside, if we _really_ wanted to 189 | break these into separate steps, we could have broken this into two render 190 | blocks: 191 | 192 | ```c 193 | 0 -> { frag(vec2(0., 1.) - npos); } -> 0 194 | 0 -> { black_and_white(frag); } // renders to screen 195 | // don't do this if you don't have to!!! 196 | ``` 197 | 198 | This works because the `black_and_white` call is in a separate render block, and 199 | `frag` has access to a fresh new texture 0. As the code comment indicates, don't 200 | do this if you don't have to! The single render block version from earlier is 201 | more efficient. However, there are cases where breaking an operation into two 202 | passes is the most efficient option (see: Gaussian blur, which is a 203 | mathematically separable image processing filter.) We can do the same thing with 204 | the `refresh` keyword: 205 | 206 | ```c 207 | 0 -> { 208 | frag(vec2(0., 1.) - npos); 209 | refresh; // now `frag` has access to the updated fragments 210 | black_and_white(frag); 211 | } 212 | ``` 213 | 214 | ## survey code 215 | 216 | Take a look at the following block of code. It may contain syntax you are 217 | unfamiliar with, so you are not expected to accurately predict what it will do. 218 | Even so, please try to guess at the effect it will produce. Once you have made 219 | your guess, run the following code. Take a note of whether your intuition was 220 | correct; it will be asked on the survey. 221 | 222 | ```c 223 | // the same luma function in the tutorial previously 224 | fn luma(vec4 color) { 225 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 226 | } 227 | 228 | { mix('red'4, 'blue'4, luma(frag)); } 229 | ``` 230 | 231 | Thank you for going through the tutorial! Return to the survey (you should 232 | have the link open already; if not, 233 | [here it is](https://docs.google.com/forms/d/e/1FAIpQLSclVAb-okgpAFoR3XdrZJ8xagOtWVW5eVBxr8ij5oY09yCoIA/viewform?usp=sf_link)) 234 | and complete the survey. Thanks again! 235 | -------------------------------------------------------------------------------- /monaco-vim.d.ts: -------------------------------------------------------------------------------- 1 | declare module "monaco-vim"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinsl", 3 | "version": "0.1.0", 4 | "description": "language for multipass texture-to-texture effects", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npx cross-env TS_NODE_PROJECT=\"testtsconfig.json\" mocha -r ts-node/register src/test.setup.ts src/**/*.test.ts", 8 | "grammar": "npx nearleyc src/grammar.ne -o src/grammar.ts", 9 | "build": "npx tsc --project tsconfig.notests.json --outDir dist", 10 | "bundle": "npx webpack --env development --env testsuite --config webpack.config.js", 11 | "playground": "npx webpack --env development --env playground --config webpack.config.js", 12 | "playground:prod": "npx webpack --env production --env playground --config webpack.config.js", 13 | "buildwatch": "npx tsc -w --outDir dist", 14 | "bundlewatch": "npm run bundle -- --watch", 15 | "playgroundwatch": "npm run playground -- --watch", 16 | "prepublish": "npm run build" 17 | }, 18 | "keywords": [ 19 | "glsl", 20 | "webgl", 21 | "webgl2" 22 | ], 23 | "author": "Cole Granof", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@types/chai": "^4.2.13", 27 | "@types/mocha": "^8.0.3", 28 | "@types/moo": "^0.5.3", 29 | "@types/nearley": "^2.11.1", 30 | "@types/node": "^14.14.9", 31 | "chai": "^4.2.0", 32 | "chai-exclude": "^2.0.2", 33 | "cross-env": "^7.0.3", 34 | "css-loader": "^5.1.3", 35 | "file-loader": "^6.2.0", 36 | "mocha": "^8.1.3", 37 | "monaco-editor": "^0.23.0", 38 | "monaco-editor-webpack-plugin": "^3.0.1", 39 | "monaco-vim": "^0.1.12", 40 | "moo": "^0.5.1", 41 | "nearley": "^2.19.7", 42 | "style-loader": "^2.0.0", 43 | "ts-node": "^9.0.0", 44 | "typescript": "^4.1.2", 45 | "webpack": "^5.34.0", 46 | "webpack-cli": "^4.5.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 90 | 91 | 92 | tinsl playground 93 | 94 | 95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 |
105 | 106 | 107 | 108 |
109 |
110 |
111 |
112 | 113 |
114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /playground.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 2 | import { overlap, regexes, tinsl, types } from "./src/lexer"; 3 | import { Runner } from "./src/runner/runner"; 4 | import { builtIns } from "./src/typeinfo"; 5 | import { initVimMode, VimMode } from "monaco-vim"; 6 | import { demos } from "./demos"; 7 | 8 | /////////////////////////////////////////////////////////////////////////////// 9 | // utils 10 | 11 | function getQuery(variable: string, query: string) { 12 | const vars = query.split("&"); 13 | for (let i = 0; i < vars.length; i++) { 14 | let pair = vars[i].split("="); 15 | if (pair[0] == variable) { 16 | return pair[1]; 17 | } 18 | } 19 | } 20 | 21 | /////////////////////////////////////////////////////////////////////////////// 22 | // constants 23 | 24 | const enum Highlight { 25 | BuiltIn = "#FFCB1C", 26 | Number = "#FA076B", 27 | Comment = "#777777", 28 | String = "#FF853F", 29 | Ident = "#5ed1ff", 30 | Type = "#ADFF22", 31 | Keyword = "#CE29FF", 32 | Frag = "#FF72CD", 33 | } 34 | 35 | const FFT_SIZE = 256; 36 | const FFT_LENGTH = FFT_SIZE / 2; 37 | 38 | /////////////////////////////////////////////////////////////////////////////// 39 | // monaco setup 40 | 41 | const keywords = [...tinsl, ...overlap]; 42 | const typeKeywords = types; 43 | const builtInKeywords = Object.entries(builtIns).map((b) => b[0]); 44 | 45 | monaco.languages.register({ id: "tinsl-lang" }); 46 | 47 | monaco.languages.setMonarchTokensProvider("tinsl-lang", { 48 | keywords: keywords, 49 | typeKeywords: typeKeywords, 50 | builtInKeywords: builtInKeywords, 51 | 52 | tokenizer: { 53 | root: [ 54 | { include: "@whitespace" }, 55 | [regexes.frag, "tinsl-frag"], 56 | [ 57 | regexes.ident, 58 | { 59 | cases: { 60 | "@keywords": "tinsl-kw", 61 | "@typeKeywords": "tinsl-type", 62 | "@builtInKeywords": "tinsl-builtin", 63 | "@default": "tinsl-ident", 64 | }, 65 | }, 66 | ], 67 | [regexes.float, "tinsl-float"], 68 | [regexes.uint, "tinsl-uint"], 69 | [regexes.int, "tinsl-int"], 70 | [regexes.string, "tinsl-string"], 71 | //[regexes.comment, "tinsl-comment"], 72 | //[regexes.multilineComment, "tinsl-multilinecomment"], 73 | ], 74 | 75 | comment: [ 76 | [/[^\/*]+/, "tinsl-comment"], 77 | [/\/\*/, "tinsl-comment", "@push"], // nested comment 78 | ["\\*/", "tinsl-comment", "@pop"], 79 | [/[\/*]/, "tinsl-comment"], 80 | ], 81 | 82 | whitespace: [ 83 | [/[ \t\r\n]+/, "white"], 84 | [/\/\*/, "tinsl-comment", "@comment"], 85 | [/\/\/.*$/, "tinsl-comment"], 86 | ], 87 | }, 88 | }); 89 | 90 | monaco.editor.defineTheme("tinsl-theme", { 91 | base: "vs-dark", 92 | inherit: true, 93 | rules: [ 94 | { token: "tinsl-type", foreground: Highlight.Type }, // 29 95 | { token: "tinsl-kw", foreground: Highlight.Keyword }, // 27 96 | { token: "tinsl-builtin", foreground: Highlight.BuiltIn }, // 22 97 | { token: "tinsl-uint", foreground: Highlight.Number }, // 24 98 | { token: "tinsl-float", foreground: Highlight.Number }, // 24 99 | { token: "tinsl-int", foreground: Highlight.Number }, // 24 100 | { token: "tinsl-string", foreground: Highlight.String }, // 28 101 | { token: "tinsl-comment", foreground: Highlight.Comment }, // 23 102 | { token: "tinsl-frag", foreground: Highlight.Frag }, // 25 103 | { token: "tinsl-ident", foreground: Highlight.Ident }, // 26 104 | ], 105 | colors: { 106 | "editor.background": "#00000000", 107 | }, 108 | }); 109 | 110 | const editor = monaco.editor.create( 111 | document.getElementById("editor") as HTMLElement, 112 | { 113 | value: stripFirstLine( 114 | demos[ 115 | "" + 116 | getQuery("demo", window.location.search.substring(1))?.replace( 117 | "_", 118 | " " 119 | ) 120 | ] ?? demos["club"] 121 | ), 122 | language: "tinsl-lang", 123 | minimap: { 124 | enabled: false, 125 | }, 126 | theme: "tinsl-theme", 127 | contextmenu: false, 128 | tabSize: 2, 129 | } 130 | ); 131 | 132 | /////////////////////////////////////////////////////////////////////////////// 133 | // vim mode 134 | 135 | VimMode.Vim.map("jk", "", "insert"); 136 | let vimMode: any; 137 | 138 | const statusBar = document.getElementById("statusbar") as HTMLElement; 139 | 140 | const startVimMode = () => { 141 | vimMode = initVimMode(editor, statusBar); 142 | }; 143 | 144 | const endVimMode = () => { 145 | /* 146 | while (statusBar.firstChild !== null) { 147 | console.log("removing child"); 148 | statusBar.removeChild(statusBar.firstChild); 149 | } 150 | */ 151 | 152 | vimMode?.dispose(); 153 | }; 154 | 155 | const checkBox = document.getElementById("vim-mode") as HTMLInputElement; 156 | 157 | checkBox.addEventListener("change", (e) => { 158 | checkBox.checked ? startVimMode() : endVimMode(); 159 | }); 160 | 161 | /////////////////////////////////////////////////////////////////////////////// 162 | // helpers 163 | 164 | let oldDecorations: string[] = []; 165 | 166 | function parseErrorMessage(message: string): [number, number][] { 167 | const arr = message.split("\n").slice(1); 168 | return arr.map((a) => { 169 | const m = a.match(/line ([0-9]+) column ([0-9]+)/); 170 | if (m === null) throw Error("no match for lines and columns"); 171 | return [parseInt(m[1]), parseInt(m[2])]; 172 | }); 173 | } 174 | 175 | function clearErrors() { 176 | oldDecorations = editor.deltaDecorations(oldDecorations, [ 177 | { range: new monaco.Range(1, 1, 1, 1), options: {} }, 178 | ]); 179 | } 180 | 181 | function highlightErrors(linesAndColumns: [number, number][]) { 182 | const decorations: monaco.editor.IModelDeltaDecoration[] = linesAndColumns 183 | .map((lc) => { 184 | return [ 185 | { 186 | range: new monaco.Range(lc[0], lc[1], lc[0], lc[1] + 1), 187 | options: { inlineClassName: "error-char", isWholeLine: false }, 188 | }, 189 | { 190 | range: new monaco.Range(lc[0], lc[1], lc[0], lc[1] + 1), 191 | options: { inlineClassName: "error-line", isWholeLine: true }, 192 | }, 193 | ]; 194 | }) 195 | .flat(); 196 | 197 | oldDecorations = editor.deltaDecorations(oldDecorations, decorations); 198 | } 199 | 200 | function stripFirstLine(code: string) { 201 | return code.split("\n").slice(1).join("\n"); 202 | } 203 | 204 | function addDemos() { 205 | const entries = Object.entries(demos); 206 | const select = document.getElementById("demos") as HTMLSelectElement; 207 | 208 | select.addEventListener("change", (e) => { 209 | console.log("event", e); 210 | const code = demos[select.options[select.selectedIndex].value]; 211 | const stripped = stripFirstLine(code); 212 | editor.setValue(stripped); 213 | runTinslProgram(); 214 | }); 215 | 216 | console.log("select", select); 217 | for (const e of entries) { 218 | const option = document.createElement("option"); 219 | option.value = e[0]; 220 | option.innerText = e[0]; 221 | console.log(option); 222 | select.appendChild(option); 223 | } 224 | } 225 | 226 | /////////////////////////////////////////////////////////////////////////////// 227 | // canvas setup 228 | 229 | addDemos(); 230 | 231 | const glCanvas = document.getElementById("gl") as HTMLCanvasElement; 232 | const glTemp = glCanvas.getContext("webgl2"); 233 | 234 | if (glTemp === null) throw new Error("problem getting the gl context"); 235 | const gl = glTemp; 236 | 237 | async function getVideo() { 238 | const video = document.createElement("video"); 239 | try { 240 | const stream = await navigator.mediaDevices.getUserMedia({ video: true }); 241 | video.srcObject = stream; 242 | video.muted = true; 243 | video.play(); 244 | } catch { 245 | console.log("could not get video; using default animation"); 246 | return null; 247 | } 248 | 249 | return video; 250 | } 251 | 252 | function getPlaceholder() { 253 | const canvas = document.createElement("canvas"); 254 | canvas.width = 640; 255 | canvas.height = 480; 256 | return canvas; 257 | } 258 | 259 | async function getAudio() { 260 | const audio = new AudioContext(); 261 | const analyzer = audio.createAnalyser(); 262 | 263 | // TODO fix 264 | try { 265 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 266 | const source = audio.createMediaStreamSource(stream); 267 | source.connect(analyzer); 268 | 269 | analyzer.fftSize = FFT_SIZE; 270 | } catch { 271 | console.log("could not get audio; fft will not work"); 272 | return { audio: null, analyzer: null }; 273 | } 274 | /* 275 | try { 276 | navigator.mediaDevices 277 | .getUserMedia({ 278 | audio: true, 279 | }) 280 | .then((stream) => { 281 | const source = audio.createMediaStreamSource(stream); 282 | source.connect(analyzer); 283 | 284 | analyzer.fftSize = FFT_SIZE; 285 | }); 286 | } catch (e) { 287 | console.log("could not get audio"); 288 | return { audio: null, analyzer: null }; 289 | } 290 | */ 291 | 292 | return { audio, analyzer }; 293 | } 294 | 295 | /* 296 | function getMedia() { 297 | const video = document.createElement("video"); 298 | const audio = new AudioContext(); 299 | const analyzer = audio.createAnalyser(); 300 | 301 | navigator.mediaDevices 302 | .getUserMedia({ 303 | audio: true, 304 | video: true, 305 | }) 306 | .then((stream) => { 307 | video.srcObject = stream; 308 | video.muted = true; 309 | video.play(); 310 | 311 | const source = audio.createMediaStreamSource(stream); 312 | source.connect(analyzer); 313 | 314 | analyzer.fftSize = FFT_SIZE; 315 | }); 316 | 317 | return { video, audio, analyzer }; 318 | } 319 | */ 320 | 321 | //const { video, audio, analyzer } = getMedia(); 322 | const video = (await getVideo()) ?? getPlaceholder(); 323 | const context = 324 | video instanceof HTMLCanvasElement ? video.getContext("2d") : null; 325 | 326 | if (context !== null) { 327 | context.font = "bold 85px sans-serif"; 328 | context.textAlign = "center"; 329 | context.textBaseline = "middle"; 330 | } 331 | 332 | const { audio, analyzer } = await getAudio(); 333 | 334 | const analyzerCanvas = document.getElementById("fft") as HTMLCanvasElement; 335 | const analyzerTemp = analyzerCanvas.getContext("2d"); 336 | 337 | if (analyzerTemp === null) throw new Error("problem getting fft graph context"); 338 | const analyzerContext = analyzerTemp; 339 | 340 | const FFT_W = analyzerCanvas.width; 341 | const FFT_H = analyzerCanvas.height; 342 | 343 | function analyze() { 344 | if (analyzer === null) return; 345 | const dataArray = new Uint8Array(analyzer.frequencyBinCount); 346 | analyzer.getByteFrequencyData(dataArray); 347 | analyzerContext.fillStyle = "black"; 348 | analyzerContext.fillRect(0, 0, FFT_W, FFT_H); 349 | analyzerContext.fillStyle = "lime"; 350 | const width = FFT_W / dataArray.length; 351 | dataArray.forEach((d, i) => { 352 | const height = Math.round(FFT_H * ((d - 128) / 128)); 353 | analyzerContext.fillRect(i * width, FFT_H - height, width, height); 354 | }); 355 | return dataArray; 356 | } 357 | 358 | const consoleWindow = document.getElementById("console-window") as HTMLElement; 359 | 360 | const runTinslProgram = () => { 361 | if (audio !== null && audio.state !== "running") { 362 | console.log("resuming audio"); 363 | audio.resume(); 364 | } 365 | const code = monaco.editor.getModels()[0].getValue(); 366 | startup(code); 367 | }; 368 | 369 | document.getElementById("run")?.addEventListener("click", runTinslProgram); 370 | 371 | document.addEventListener("keypress", (e) => { 372 | if (e.ctrlKey) { 373 | if (e.key === "Enter") { 374 | e.preventDefault(); 375 | runTinslProgram(); 376 | } else if (e.key === "H") { 377 | e.preventDefault(); 378 | const div = document.getElementById("video") as HTMLElement; 379 | div.style.display = div.style.display !== "none" ? "none" : "block"; 380 | } 381 | } 382 | }); 383 | 384 | let request: number | undefined = undefined; 385 | 386 | const startTinsl = (code: string) => { 387 | if (request !== undefined) cancelAnimationFrame(request); 388 | let runner: Runner; 389 | runner = new Runner(gl, code, [video], { edgeMode: "wrap" }); 390 | 391 | const unifs = runner.getUnifsByPattern(/^fft[0-9]+$/); 392 | const nums = unifs.map((u) => { 393 | const m = u.match(/^fft([0-9]+)$/); 394 | if (m === null) throw new Error("fft match was null"); 395 | const num = parseInt(m[1]); 396 | if (num >= FFT_LENGTH) 397 | throw new Error( 398 | "fft number was not in range " + 399 | `(needs to be non-negative integer less than ${FFT_LENGTH})` 400 | ); 401 | return num; 402 | }); 403 | 404 | const animate = (time: number) => { 405 | if (context !== null) { 406 | context.fillStyle = "white"; 407 | context.fillRect(0, 0, 640, 480); 408 | //context.fillText("no video", 320 + 50 * Math.sin(time), 240); 409 | const f = (color: string, offset: number) => { 410 | context.fillStyle = color; 411 | const x = 320 + 50 * Math.sin(time / 1000); 412 | context.fillText("no video", x, 240 + offset); 413 | }; 414 | 415 | const spacing = 64; 416 | f("red", -spacing); 417 | f("lime", 0); 418 | f("blue", spacing); 419 | } 420 | runner.draw(time); 421 | 422 | // parse uniform names and get fft data 423 | const data = analyze(); 424 | if (data !== undefined) { 425 | unifs.forEach((u, i) => { 426 | const num = (data[nums[i]] - 128) / 128; 427 | runner.setUnif(u, num); 428 | }); 429 | } 430 | 431 | request = requestAnimationFrame(animate); 432 | }; 433 | 434 | animate(0); 435 | }; 436 | 437 | const startup = (code: string) => { 438 | consoleWindow.innerText = ""; 439 | try { 440 | clearErrors(); 441 | startTinsl(code); 442 | } catch (err) { 443 | console.log(err.message); 444 | consoleWindow.innerText = err.message; 445 | highlightErrors(parseErrorMessage(err.message)); 446 | //throw "look at the logged error message"; 447 | } 448 | }; 449 | 450 | if (video instanceof HTMLCanvasElement) runTinslProgram(); 451 | if (video instanceof HTMLVideoElement) { 452 | video.addEventListener("playing", runTinslProgram); 453 | } 454 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | interface ColorDictionary { 2 | [key: string]: string | undefined; 3 | } 4 | 5 | export const colors: ColorDictionary = Object.freeze({ 6 | black: "#000000", 7 | navy: "#000080", 8 | darkblue: "#00008B", 9 | mediumblue: "#0000CD", 10 | blue: "#0000FF", 11 | darkgreen: "#006400", 12 | green: "#008000", 13 | teal: "#008080", 14 | darkcyan: "#008B8B", 15 | deepskyblue: "#00BFFF", 16 | darkturquoise: "#00CED1", 17 | mediumspringgreen: "#00FA9A", 18 | lime: "#00FF00", 19 | springgreen: "#00FF7F", 20 | aqua: "#00FFFF", 21 | cyan: "#00FFFF", 22 | midnightblue: "#191970", 23 | dodgerblue: "#1E90FF", 24 | lightseagreen: "#20B2AA", 25 | forestgreen: "#228B22", 26 | seagreen: "#2E8B57", 27 | darkslategray: "#2F4F4F", 28 | darkslategrey: "#2F4F4F", 29 | limegreen: "#32CD32", 30 | mediumseagreen: "#3CB371", 31 | turquoise: "#40E0D0", 32 | royalblue: "#4169E1", 33 | steelblue: "#4682B4", 34 | darkslateblue: "#483D8B", 35 | mediumturquoise: "#48D1CC", 36 | indigo: "#4B0082", 37 | darkolivegreen: "#556B2F", 38 | cadetblue: "#5F9EA0", 39 | cornflowerblue: "#6495ED", 40 | rebeccapurple: "#663399", 41 | mediumaquamarine: "#66CDAA", 42 | dimgray: "#696969", 43 | dimgrey: "#696969", 44 | slateblue: "#6A5ACD", 45 | olivedrab: "#6B8E23", 46 | slategray: "#708090", 47 | slategrey: "#708090", 48 | lightslategray: "#778899", 49 | lightslategrey: "#778899", 50 | mediumslateblue: "#7B68EE", 51 | lawngreen: "#7CFC00", 52 | chartreuse: "#7FFF00", 53 | aquamarine: "#7FFFD4", 54 | maroon: "#800000", 55 | purple: "#800080", 56 | olive: "#808000", 57 | gray: "#808080", 58 | grey: "#808080", 59 | skyblue: "#87CEEB", 60 | lightskyblue: "#87CEFA", 61 | blueviolet: "#8A2BE2", 62 | darkred: "#8B0000", 63 | darkmagenta: "#8B008B", 64 | saddlebrown: "#8B4513", 65 | darkseagreen: "#8FBC8F", 66 | lightgreen: "#90EE90", 67 | mediumpurple: "#9370DB", 68 | darkviolet: "#9400D3", 69 | palegreen: "#98FB98", 70 | darkorchid: "#9932CC", 71 | yellowgreen: "#9ACD32", 72 | sienna: "#A0522D", 73 | brown: "#A52A2A", 74 | darkgray: "#A9A9A9", 75 | darkgrey: "#A9A9A9", 76 | lightblue: "#ADD8E6", 77 | greenyellow: "#ADFF2F", 78 | paleturquoise: "#AFEEEE", 79 | lightsteelblue: "#B0C4DE", 80 | powderblue: "#B0E0E6", 81 | firebrick: "#B22222", 82 | darkgoldenrod: "#B8860B", 83 | mediumorchid: "#BA55D3", 84 | rosybrown: "#BC8F8F", 85 | darkkhaki: "#BDB76B", 86 | silver: "#C0C0C0", 87 | mediumvioletred: "#C71585", 88 | indianred: "#CD5C5C", 89 | peru: "#CD853F", 90 | chocolate: "#D2691E", 91 | tan: "#D2B48C", 92 | lightgray: "#D3D3D3", 93 | lightgrey: "#D3D3D3", 94 | thistle: "#D8BFD8", 95 | orchid: "#DA70D6", 96 | goldenrod: "#DAA520", 97 | palevioletred: "#DB7093", 98 | crimson: "#DC143C", 99 | gainsboro: "#DCDCDC", 100 | plum: "#DDA0DD", 101 | burlywood: "#DEB887", 102 | lightcyan: "#E0FFFF", 103 | lavender: "#E6E6FA", 104 | darksalmon: "#E9967A", 105 | violet: "#EE82EE", 106 | palegoldenrod: "#EEE8AA", 107 | lightcoral: "#F08080", 108 | khaki: "#F0E68C", 109 | aliceblue: "#F0F8FF", 110 | honeydew: "#F0FFF0", 111 | azure: "#F0FFFF", 112 | sandybrown: "#F4A460", 113 | wheat: "#F5DEB3", 114 | beige: "#F5F5DC", 115 | whitesmoke: "#F5F5F5", 116 | mintcream: "#F5FFFA", 117 | ghostwhite: "#F8F8FF", 118 | salmon: "#FA8072", 119 | antiquewhite: "#FAEBD7", 120 | linen: "#FAF0E6", 121 | lightgoldenrodyellow: "#FAFAD2", 122 | oldlace: "#FDF5E6", 123 | red: "#FF0000", 124 | fuchsia: "#FF00FF", 125 | magenta: "#FF00FF", 126 | deeppink: "#FF1493", 127 | orangered: "#FF4500", 128 | tomato: "#FF6347", 129 | hotpink: "#FF69B4", 130 | coral: "#FF7F50", 131 | darkorange: "#FF8C00", 132 | lightsalmon: "#FFA07A", 133 | orange: "#FFA500", 134 | lightpink: "#FFB6C1", 135 | pink: "#FFC0CB", 136 | gold: "#FFD700", 137 | peachpuff: "#FFDAB9", 138 | navajowhite: "#FFDEAD", 139 | moccasin: "#FFE4B5", 140 | bisque: "#FFE4C4", 141 | mistyrose: "#FFE4E1", 142 | blanchedalmond: "#FFEBCD", 143 | papayawhip: "#FFEFD5", 144 | lavenderblush: "#FFF0F5", 145 | seashell: "#FFF5EE", 146 | cornsilk: "#FFF8DC", 147 | lemonchiffon: "#FFFACD", 148 | floralwhite: "#FFFAF0", 149 | snow: "#FFFAFA", 150 | yellow: "#FFFF00", 151 | lightyellow: "#FFFFE0", 152 | ivory: "#FFFFF0", 153 | white: "#FFFFFF", 154 | }); 155 | -------------------------------------------------------------------------------- /src/complete.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { parseAndCheck } from "./gen"; 3 | import { bloom, errBloom, godrays } from "./test.programs"; 4 | 5 | describe("complex program tests", () => { 6 | it("parses a bloom effect program", () => { 7 | expect(() => parseAndCheck(bloom)).to.not.throw(); 8 | }); 9 | 10 | it("parses a bloom effect program with multiple errors", () => { 11 | expect(() => parseAndCheck(errBloom)).to.throw("8 errors"); 12 | }); 13 | 14 | it("parses godrays effect", () => { 15 | expect(() => parseAndCheck(godrays)).to.not.throw(); 16 | }); 17 | }); 18 | 19 | // TODO move to their own test file 20 | describe("aggregate error tests", () => { 21 | it("reports multiple errors in function body alone", () => { 22 | expect(() => 23 | parseAndCheck(` 24 | fn multiple_errors () { 25 | int a = 1.; 26 | float b = 2; 27 | return 1; 28 | }`) 29 | ).to.throw("2 errors"); 30 | }); 31 | 32 | it("reports multiple errors in function params and body", () => { 33 | expect(() => 34 | parseAndCheck(` 35 | fn multiple_errors (int c = 1.) { 36 | int a = 1.; 37 | float b = 2; 38 | return 1; 39 | }`) 40 | ).to.throw("3 errors"); 41 | }); 42 | 43 | it("reports multiple errors in procedure body alone", () => { 44 | expect(() => 45 | parseAndCheck(` 46 | pr multiple_errors () { 47 | vec2(1., 1.); 48 | vec3(1., 1., 3.); 49 | }`) 50 | ).to.throw("2 errors"); 51 | }); 52 | 53 | it("doesn't use plural 'error' when reporting a single error", () => { 54 | expect(() => 55 | parseAndCheck("pr multiple_errors () { vec2(1., 1.); }") 56 | ).to.throw("1 error "); 57 | }); 58 | 59 | it("reports multiple errors in procedure definition", () => { 60 | expect(() => 61 | parseAndCheck(` 62 | pr multiple_errors () { 63 | "green"3; 64 | { "blue"3; "red"3; } -> 1 65 | }`) 66 | ).to.throw("3 errors"); 67 | }); 68 | 69 | it("reports multiple errors in condition and body of if", () => { 70 | expect(() => 71 | parseAndCheck(` 72 | fn foo () { 73 | mut a := 1; 74 | if (1 + 2) { 75 | a += "purple"; 76 | } 77 | return a; 78 | }`) 79 | ).to.throw("2 errors"); 80 | }); 81 | 82 | it("throws one error for broken function and not another for use", () => { 83 | expect(() => 84 | parseAndCheck(` 85 | fn foo () { 86 | if (false) { return 1u; } 87 | return 2; 88 | } 89 | { foo(); } 90 | `) 91 | ).to.throw("1 error"); 92 | }); 93 | 94 | it("throws only one error when performing binary op on undecided", () => { 95 | expect(() => 96 | parseAndCheck(` 97 | fn foo () { 98 | if (false) { return 1u; } 99 | return 2; 100 | } 101 | { foo() / 2.; } 102 | `) 103 | ).to.throw("1 error"); 104 | }); 105 | 106 | it("throws only one error when chained nested undecided functions", () => { 107 | expect(() => 108 | parseAndCheck(` 109 | fn foo () { 110 | if (false) { return 1u; } 111 | return 2; 112 | } 113 | fn bar () { return foo(); } 114 | { bar(); } 115 | `) 116 | ).to.throw("1 error"); 117 | }); 118 | 119 | it("undecided type params don't throw an extra error", () => { 120 | expect(() => 121 | parseAndCheck(` 122 | fn foo () { 123 | if (false) { return 1u; } 124 | return 2; 125 | } 126 | fn bar (int a) { return "blue"4; } 127 | 128 | { bar(foo()); } 129 | `) 130 | ).to.throw("1 error"); 131 | }); 132 | 133 | it("undecided default values don't throw an extra error", () => { 134 | expect(() => 135 | parseAndCheck(` 136 | fn foo () { 137 | if (false) { return 1u; } 138 | return 2; 139 | } 140 | 141 | fn bar (int a = foo()) { return "blue"4; } 142 | 143 | { bar(); } 144 | `) 145 | ).to.throw("1 error"); 146 | }); 147 | }); 148 | 149 | describe("testing pure params", () => { 150 | it("parses and checks two levels of pure params", () => { 151 | expect(() => 152 | parseAndCheck(` 153 | pr foo (int x) { x -> { "blue"4; } -> x } 154 | 155 | pr bar (int y) { @foo(y); } 156 | 157 | { @bar(0); } 158 | { @bar(1); }`) 159 | ).to.not.throw(); 160 | }); 161 | 162 | it("parses and checks two levels of pure rb params", () => { 163 | expect(() => 164 | parseAndCheck(` 165 | pr foo (int x) { x -> { "blue"4; } -> x } 166 | 167 | pr bar (int y) { @foo(y); } 168 | 169 | { @bar(0); } 170 | { @bar(1 + 1); }`) 171 | ).to.throw("atomic"); 172 | }); 173 | 174 | it("parses and checks two levels of pure frag params", () => { 175 | expect(() => 176 | parseAndCheck(` 177 | pr foo (int x) { frag(x); } 178 | 179 | pr bar (int y) { @foo(y); } 180 | 181 | { @bar(0); } 182 | { @bar(1 + 1); }`) 183 | ).to.throw("atomic"); 184 | }); 185 | 186 | it("parses and checks two levels of pure rb params with default", () => { 187 | expect(() => 188 | parseAndCheck(` 189 | pr foo (int x) { x -> { "blue"4; } -> x } 190 | 191 | pr bar (int y = 1 + 1) { @foo(y); } 192 | 193 | { @bar(); }`) 194 | ).to.throw("atomic"); 195 | }); 196 | }); 197 | // TODO what about continued error reporting for functions where return type 198 | // cannot be determined? 199 | 200 | // TODO undecided tests for top def 201 | // TODO undecided tests for var decls 202 | -------------------------------------------------------------------------------- /src/err.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "moo"; 2 | import { ExSt, LexicalScope, typeCheckExprStmts } from "./nodes"; 3 | 4 | // TODO doesn't requiring lexical scope create a circular dependency? 5 | 6 | export class TinslError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | } 11 | } 12 | 13 | export class TinslLineError extends Error { 14 | line: number; 15 | col: number; 16 | 17 | constructor(message: string, tokn: Token | { line: number; col: number }) { 18 | super(message); 19 | this.line = tokn.line; 20 | this.col = tokn.col; 21 | this.message = `at line ${tokn.line} column ${tokn.col}: ` + message; 22 | Object.setPrototypeOf(this, new.target.prototype); 23 | } 24 | } 25 | 26 | export class TinslAggregateError extends Error { 27 | errors: TinslLineError[]; 28 | 29 | constructor(errors: TinslLineError[]) { 30 | super( 31 | `tinsl: ${errors.length} error${errors.length > 1 ? "s" : ""} found:\n` + 32 | errors.map((e) => e.message).join("\n") 33 | ); 34 | this.errors = errors; 35 | } 36 | } 37 | 38 | export function wrapErrorHelper( 39 | callback: (scope: LexicalScope) => T, 40 | exSt: ExSt, 41 | scope: LexicalScope, 42 | renderLevel = false, 43 | extraExSts: ExSt[] = [], 44 | innerScope = scope 45 | ): T { 46 | let lineErr: TinslLineError | undefined = undefined; 47 | let ret: T | null = null; 48 | try { 49 | ret = callback(scope); 50 | } catch (err) { 51 | if (!(err instanceof TinslError)) throw err; 52 | lineErr = new TinslLineError(err.message, exSt.getToken()); 53 | } 54 | 55 | let aggregateErr: TinslAggregateError | undefined = undefined; 56 | try { 57 | typeCheckExprStmts(extraExSts, innerScope, renderLevel); 58 | } catch (err) { 59 | if (!(err instanceof TinslAggregateError)) throw err; 60 | aggregateErr = err; 61 | } 62 | 63 | const totalErrors = [ 64 | ...(lineErr !== undefined ? [lineErr] : []), 65 | ...(aggregateErr !== undefined ? aggregateErr.errors : []), 66 | ]; 67 | 68 | if (totalErrors.length > 0) throw new TinslAggregateError(totalErrors); 69 | 70 | if (ret === null) { 71 | throw new Error("ret was null and no throw happened before"); 72 | } 73 | 74 | return ret; 75 | } 76 | 77 | export const atomicIntHint = 78 | "e.g. `42` or `some_num` where `def some_num 42` is defined earlier. " + 79 | "these restrictions apply to expressions for source/target texture numbers " + 80 | "or loop numbers of render blocks"; 81 | 82 | export const lValueHint = (valid: string) => 83 | "invalid l-value in assignment" + 84 | (valid === "const" 85 | ? ". this is because it was declared as constant" 86 | : valid === "final" 87 | ? '. this is because the l-value was declared as "final". ' + 88 | "variables declared with := are final by default. " + 89 | 'to declare a mutable variable this way, use "mut" before ' + 90 | "the variable name, e.g. `mut foo := 42;`" 91 | : ""); 92 | -------------------------------------------------------------------------------- /src/gen.ts: -------------------------------------------------------------------------------- 1 | import nearley from "nearley"; 2 | import { TinslError } from "./err"; 3 | import grammar from "./grammar"; 4 | import { getAllUsedFuncs, IRLeaf, IRTree, renderBlockToIR } from "./ir"; 5 | import { 6 | compileTimeInt, 7 | Expr, 8 | ExSt, 9 | IdentExpr, 10 | Param, 11 | ParamScope, 12 | ParamScoped, 13 | ProcCall, 14 | Refresh, 15 | RenderBlock, 16 | SourceLeaf, 17 | SourceTree, 18 | TinslProgram, 19 | TinslTree, 20 | TopDef, 21 | Uniform, 22 | } from "./nodes"; 23 | import { NON_CONST_ID, tinslNearleyError } from "./util"; 24 | 25 | export function parse(str: string) { 26 | const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)); 27 | try { 28 | parser.feed(str); 29 | } catch (e) { 30 | throw tinslNearleyError(e); 31 | } 32 | 33 | if (parser.results.length > 1) { 34 | throw new Error("ambiguous grammar! length: " + parser.results.length); 35 | } 36 | return parser.results[0]; 37 | } 38 | 39 | export function parseAndCheck(str: string) { 40 | const res = parse(str) as ExSt[]; 41 | new TinslProgram(res).check(); 42 | return res; 43 | } 44 | 45 | // expand procs -> fill in default in/out nums -> regroup by refresh 46 | 47 | export function expandProcsInBlock(block: RenderBlock) { 48 | block.scopedBody = expandBody( 49 | block.body, 50 | [], 51 | [], 52 | new ParamScope(new Map(), null) 53 | ); 54 | return block; 55 | } 56 | 57 | // TODO move on because this finally works, but consider refactoring 58 | function expandBody( 59 | body: ExSt[], 60 | args: Expr[], 61 | params: Param[], 62 | paramScope: ParamScope 63 | ): ParamScoped[] { 64 | const fillAtomicNum = (renderNum: null | Expr | number) => { 65 | if (renderNum instanceof Expr) { 66 | if (!(renderNum instanceof IdentExpr)) { 67 | throw new Error( 68 | "render num was not ident at code gen step; " + 69 | "should have been caught by the checker" 70 | ); 71 | } 72 | 73 | const res = renderNum.cachedParam; 74 | 75 | if (res === undefined) { 76 | throw new Error("cached param was somehow undefined"); 77 | } 78 | 79 | // after resolving the ident expression, it's actually the same reference 80 | // as what is stored in params, so we can use indexOf 81 | const index = params.indexOf(res); 82 | if (index === -1) throw new Error("could not find index of param"); 83 | 84 | const arg = args[index]; 85 | 86 | const argRes = 87 | (arg instanceof IdentExpr ? arg.cachedResolve : null) ?? null; 88 | 89 | // at this point, the arg can only be an ident -> top def or int directly 90 | const int = compileTimeInt(arg, argRes); 91 | 92 | if (int !== null) return int; 93 | throw new Error( 94 | "int was null, arg could be resolved. arg: " + arg + " res: " + argRes 95 | ); 96 | } 97 | return renderNum; 98 | }; 99 | 100 | // TODO this is probably overly-defensive, but keep for now 101 | if (args.length !== params.length) { 102 | throw new Error( 103 | "args length didn't equal params length when expanding procedure" 104 | ); 105 | } 106 | 107 | const result: ParamScoped[] = []; 108 | 109 | for (const b of body) { 110 | if (b instanceof RenderBlock) { 111 | b.inNum = fillAtomicNum(b.inNum); 112 | b.outNum = fillAtomicNum(b.outNum); 113 | b.loopNum = fillAtomicNum(b.loopNum); 114 | //b.body = expandBody(b.body, args, params, paramScope); 115 | b.scopedBody = expandBody(b.body, args, params, paramScope); 116 | result.push(new ParamScoped(b, paramScope)); 117 | } else if (b instanceof ProcCall) { 118 | // fill in any arg that is an ident before passing on 119 | const newArgs: Expr[] = []; 120 | 121 | if (b.cachedProcDef === undefined) { 122 | throw new Error("cached proc def was undefined"); 123 | } 124 | 125 | const filledArgs = b.cachedProcDef.fillInNamedAndDefaults(b.args); 126 | //b.cachedProcDef.addInDefaults(filledArgs); 127 | 128 | for (const a of filledArgs) { 129 | // TODO similar logic in the above function 130 | if (!(a instanceof IdentExpr)) { 131 | newArgs.push(a); 132 | continue; 133 | } 134 | 135 | const res = a.cachedResolve; 136 | if (res === undefined) { 137 | throw new Error("arg didn't have a cached resolve"); 138 | } 139 | 140 | if (res instanceof Param) { 141 | const index = params.indexOf(res); 142 | if (index === -1) throw new Error("could not find index of param"); 143 | 144 | const newArg = args[index]; 145 | newArgs.push(newArg); 146 | continue; 147 | } 148 | 149 | if (res instanceof Uniform || res instanceof TopDef) { 150 | newArgs.push(a); 151 | continue; 152 | } 153 | 154 | throw new Error("ident arg didn't resolve to a param or uniform decl"); 155 | } 156 | if (b.cachedProcDef === undefined) throw new Error("no cached proc def"); 157 | const newBody = b.cachedProcDef.body; 158 | // TODO might be more convenient to have these be the params/args that get 159 | // set in the map 160 | const newParams = b.cachedProcDef.params; 161 | const innerParamScope = new ParamScope(new Map(), paramScope); 162 | newParams.forEach((newParam, i) => { 163 | const newArg = newArgs[i]; 164 | innerParamScope.set(newParam, newArg); 165 | }); 166 | 167 | const expandedProcBody = expandBody( 168 | newBody, 169 | newArgs, 170 | newParams, 171 | innerParamScope 172 | ); 173 | 174 | result.push(...expandedProcBody); 175 | } else { 176 | result.push(new ParamScoped(b, paramScope)); 177 | } 178 | } 179 | 180 | // TODO elements might be added redundantly but that's okay for now 181 | // TODO create this map earlier? then won't have the need for indexOf 182 | for (let i = 0; i < args.length; i++) { 183 | // TODO this is the issue. make sure mapping is not overwritten 184 | //outerBlock.paramMappings.set(params[i], args[i]); 185 | } 186 | 187 | return result; 188 | } 189 | 190 | // TODO rename 191 | export function fillInDefaults( 192 | block: RenderBlock, 193 | outer?: RenderBlock 194 | ): RenderBlock { 195 | const defaultNum = ( 196 | innerNum: number | Expr | null, 197 | outerNum: number | Expr | null 198 | ) => { 199 | if (innerNum instanceof Expr) { 200 | throw new Error( 201 | "render block number had an expression outside of a procedure" 202 | ); 203 | } 204 | if (innerNum === null || innerNum === -1) { 205 | if (outerNum === null) return 0; // at the top level 206 | return outerNum; 207 | } 208 | return innerNum; 209 | }; 210 | 211 | block.inNum = defaultNum(block.inNum, outer?.inNum ?? null); 212 | block.outNum = defaultNum(block.outNum, outer?.outNum ?? null); 213 | for (const b of block.scopedBody) { 214 | const inmost = b.inmost(); 215 | if (inmost instanceof RenderBlock) { 216 | fillInDefaults(inmost, block); 217 | } 218 | } 219 | 220 | return block; 221 | } 222 | 223 | export function regroupByRefresh(block: RenderBlock): RenderBlock { 224 | // will get replaced with new empty array once refresh 225 | //let previous: ExSt[] = []; 226 | let previous: ParamScoped[] = []; 227 | 228 | // new render block gets added to this on refresh 229 | // rest of body gets tacked on when it hits the end 230 | const regrouped: ParamScoped[] = []; 231 | 232 | let breaks = 0; 233 | 234 | const breakOff = () => { 235 | if (previous.length > 0) 236 | regrouped.push( 237 | new ParamScoped(block.innerCopy(previous), previous[0].mapping) 238 | ); 239 | previous = []; 240 | breaks++; 241 | }; 242 | 243 | for (const b of block.scopedBody) { 244 | const inmost = b.inmost(); 245 | if (inmost instanceof Refresh) { 246 | // break off, ignore refresh 247 | breakOff(); 248 | } else if (inmost instanceof RenderBlock) { 249 | // break off and push on this block separately 250 | // this avoids redundant regrouping 251 | breakOff(); 252 | 253 | const regroupedMapping = regroupByRefresh(inmost); 254 | const mapping = regroupedMapping.scopedBody[0].mapping; 255 | regrouped.push(new ParamScoped(regroupByRefresh(inmost), mapping)); 256 | } else { 257 | previous.push(b); 258 | } 259 | } 260 | 261 | // prevents nesting in redundant block 262 | if (breaks === 0) return block; 263 | 264 | breakOff(); 265 | 266 | block.scopedBody = regrouped; 267 | return block; 268 | } 269 | 270 | export function processBlocks(block: RenderBlock): IRTree | IRLeaf { 271 | return renderBlockToIR( 272 | regroupByRefresh(fillInDefaults(expandProcsInBlock(block))) 273 | ); 274 | } 275 | 276 | export function irToSourceLeaf(ir: IRLeaf): SourceLeaf { 277 | const funcs = getAllUsedFuncs(ir.exprs.map((e) => e.inmost())); 278 | 279 | // wrap a leaf together with a map to pass down into translate 280 | const sl = new SourceLeaf(ir.loopInfo.outNum, ir.loopInfo.inNum); 281 | 282 | // generate the code for the function calls 283 | const funcsList = Array.from(funcs).reverse(); 284 | const funcDefsSource = funcsList 285 | .map((f) => f.translate({ leaf: sl, map: null })) 286 | .join("\n"); 287 | 288 | let needsOneMult = ir.oneMult; 289 | 290 | // collect all required textures from functions 291 | const texNums = new Set(); 292 | for (const f of funcsList) { 293 | for (const t of f.requiredTexNums) { 294 | texNums.add(t); 295 | } 296 | // also see if any functions need a one multiplication 297 | needsOneMult ||= f.needsOneMult; 298 | } 299 | 300 | const nonConstIdDeclSource = needsOneMult 301 | ? `int int_${NON_CONST_ID} = 1;\nuint uint_${NON_CONST_ID} = 1u;\n` 302 | : ""; 303 | 304 | // collect all required textures from ir 305 | for (const t of ir.texNums) { 306 | texNums.add(t); 307 | } 308 | 309 | // input and output the webgl2 way 310 | const fragColorSource = "out vec4 fragColor;\n"; 311 | 312 | // generate the main loop (series of assignments to fragColor) 313 | let mainSource = ` 314 | void main(){\n`; 315 | for (const e of ir.exprs) { 316 | mainSource += "fragColor=" + e.translate(sl) + ";\n"; 317 | } 318 | mainSource += "}"; 319 | 320 | // generate built-in uniforms 321 | let uniformsSource = ""; 322 | if (sl.requires.time) uniformsSource += "uniform float uTime;\n"; 323 | if (sl.requires.res) uniformsSource += "uniform vec2 uResolution;\n"; 324 | 325 | // generate user-defined uniforms 326 | for (const u of sl.requires.uniforms) { 327 | uniformsSource += u.translate(); 328 | } 329 | 330 | // generate required samplers 331 | let samplersSource = ""; 332 | for (const s of sl.requires.samplers) { 333 | samplersSource += `uniform sampler2D uSampler${s};\n`; 334 | } 335 | 336 | const defaultPrecisionSource = `#version 300 es 337 | #ifdef GL_ES 338 | precision mediump float; 339 | #endif\n`; 340 | 341 | sl.source = 342 | defaultPrecisionSource + 343 | fragColorSource + 344 | nonConstIdDeclSource + 345 | samplersSource + 346 | uniformsSource + 347 | funcDefsSource + 348 | mainSource; 349 | 350 | return sl; 351 | } 352 | 353 | export function genSource(ir: IRTree | IRLeaf): SourceTree | SourceLeaf { 354 | if (ir instanceof IRTree) { 355 | const st = new SourceTree(ir.loopInfo.loopNum, ir.loopInfo.once); 356 | for (const n of ir.subNodes) { 357 | st.body.push(genSource(n)); 358 | } 359 | return st; 360 | } 361 | const leaf = irToSourceLeaf(ir); 362 | leaf.log(); // TODO get rid of this 363 | return leaf; 364 | } 365 | 366 | export function gen(source: string) { 367 | const exprs = parseAndCheck(source); 368 | const blocks = exprs.filter( 369 | (e): e is RenderBlock => e instanceof RenderBlock 370 | ); 371 | 372 | const processed = blocks.map(processBlocks).map(genSource); 373 | // TODO better place to throw this 374 | if (processed.length === 0) 375 | throw new TinslError("error: no render blocks defined"); 376 | return processed; 377 | } 378 | 379 | export function genTinsl(source: string): TinslTree { 380 | const tinslNodes = gen(source).map((s) => s.output()); 381 | // wrap in a tree 382 | const outerTree = { loop: 1, once: false, body: tinslNodes }; 383 | return outerTree; 384 | } 385 | -------------------------------------------------------------------------------- /src/grammar.ne: -------------------------------------------------------------------------------- 1 | @preprocessor typescript 2 | 3 | @{% 4 | import { 5 | RenderBlock, 6 | BinaryExpr, 7 | UnaryExpr, 8 | IntExpr, 9 | FloatExpr, 10 | SubscriptExpr, 11 | CallExpr, 12 | IdentExpr, 13 | BoolExpr, 14 | VarDecl, 15 | TypeName, 16 | ConstructorExpr, 17 | Assign, 18 | Param, 19 | FuncDef, 20 | Return, 21 | TernaryExpr, 22 | ForLoop, 23 | If, 24 | Else, 25 | Uniform, 26 | ProcDef, 27 | TopDef, 28 | Refresh, 29 | Frag, 30 | UIntExpr, 31 | ProcCall, 32 | Time, 33 | Pos, 34 | NPos, 35 | Res, 36 | Prev, 37 | ColorString, 38 | } from "./nodes"; 39 | import { lexer } from "./lexer"; 40 | 41 | const nearleyLexer = (lexer as unknown) as NearleyLexer; 42 | 43 | const bin = (d: any) => new BinaryExpr(d[0], d[2], d[4]); 44 | const pre = (d: any) => new UnaryExpr(d[0], d[2]); 45 | const post = (d: any) => new UnaryExpr(d[2], d[0], true); 46 | const sep = (d: any) => [d[0], ...d[1].map((e: any) => e[2])]; 47 | 48 | const access = (d: any, alt: string) => d[0] !== null ? d[0][0].text : alt; 49 | %} 50 | 51 | @lexer nearleyLexer 52 | 53 | # TODO disallow frag (and eventually prev) 54 | 55 | Main -> 56 | _ TopLevel (__ TopLevel):* _ 57 | {% ([, first, rest, ]: any) => [first, ...rest.map((t: any) => t[1])] %} 58 | 59 | TopLevel -> 60 | RenderBlock {% id %} 61 | | DefBlock {% id %} 62 | | Uniform {% id %} 63 | | ProcBlock {% id %} 64 | | TopDef {% id %} 65 | 66 | OptionalTypeName -> 67 | TypeName {% id %} 68 | | %kw_fn {% d => null %} 69 | 70 | DefBlock -> 71 | OptionalTypeName _ %ident _ %lparen (_ Params):? _ %rparen _ %lbrace (%lbc):* _ (FuncLine):* %rbrace 72 | {% ([typ, , id, , , params, , , , , , , body, ]: any) => new FuncDef( 73 | typ, id, params === null ? [] : params[1], body.map((e: any) => e[0]) 74 | ) 75 | %} 76 | 77 | ProcBlock -> 78 | %kw_pr _ %ident _ %lparen (_ Params):? _ %rparen _ %lbrace (%lbc):* _ (RenderLine):* %rbrace 79 | {% ([, , id, , , params, , , , , , , body, ,]: any) => new ProcDef( 80 | id, params === null ? [] : params[1], body.map((e: any) => e[0]) 81 | ) 82 | %} 83 | 84 | RenderBlock -> 85 | (Expr _ %arrow _):? (%kw_loop _ Expr _):? (%kw_once _):? %lbrace (%lbc):* _ (RenderLine):* %rbrace (_ %arrow _ Expr):? 86 | {% ([inNumBl, loopNumBl, onceBl, open, , , body, , outNumBl]: any) => { 87 | const blockHelper = (bl: null | any[], num: number) => bl !== null ? (bl[num] instanceof IntExpr ? parseInt(bl[num].getToken().text) : bl[num]) : null; 88 | return new RenderBlock( 89 | onceBl !== null && onceBl[0] !== null, 90 | body.map((e: any) => e[0]), 91 | blockHelper(inNumBl, 0), 92 | blockHelper(outNumBl, 3), 93 | blockHelper(loopNumBl, 2), 94 | open 95 | ) 96 | } 97 | %} 98 | 99 | # TODO possible ambiguous grammar with out texture expression? 100 | 101 | Uniform -> 102 | %kw_uniform _ TypeName _ %ident (%lbc):+ {% d => new Uniform(d[2], d[4]) %} 103 | 104 | # and statements/expressions allowed to appear in render block 105 | RenderLevel -> 106 | Decl {% id %} 107 | | Expr {% id %} 108 | | Refresh {% id %} 109 | | ProcCall {% id %} 110 | 111 | # statements/expressions allowed within function bodies 112 | FuncLevel -> 113 | Expr {% id %} 114 | | Decl {% id %} 115 | | Assign {% id %} 116 | | Return {% id %} 117 | 118 | FuncLine -> 119 | FuncLevel (%lbc):+ _ {% d => d[0] %} 120 | | ForLoop {% d => d[0] %} 121 | | If {% id %} 122 | 123 | RenderLine -> 124 | RenderLevel (%lbc):+ _ {% d => d[0] %} 125 | | RenderBlock _ {% d => d[0] %} 126 | 127 | Return -> 128 | %kw_return _ Expr {% d => new Return(d[2], d[0]) %} 129 | 130 | Refresh -> 131 | %kw_refresh {% d => new Refresh(d[0]) %} 132 | 133 | NormalAccess -> 134 | %kw_const {% id %} 135 | | %kw_final {% id %} 136 | 137 | DeclAccess -> 138 | %kw_const {% id %} 139 | | %kw_mut {% id %} 140 | 141 | Decl -> 142 | (NormalAccess _):? (TypeName _) (%ident _) %assignment _ Expr 143 | {% d => 144 | new VarDecl( 145 | access(d, "mut"), 146 | d[1][0], d[2][0], d[5], d[3] 147 | ) 148 | %} 149 | | (DeclAccess _):? (%ident _) %decl _ Expr 150 | {% d => 151 | new VarDecl( 152 | access(d, "final"), 153 | null, d[1][0], d[4], d[2] 154 | ) 155 | %} 156 | 157 | TopDef -> 158 | %kw_def _ %ident __ Expr {% d => new TopDef(d[2], d[4]) %} 159 | 160 | Assign -> 161 | Expr _ AssignSymbol _ Expr {% d => new Assign(d[0], d[2], d[4]) %} 162 | 163 | ForInit -> 164 | Decl {% id %} 165 | | Expr {% id %} 166 | | Assign {% id %} 167 | 168 | ForFinal -> 169 | Expr {% id %} 170 | | Assign {% id %} 171 | 172 | ForLoop -> 173 | %kw_for _ %lparen (_ ForInit):? %lbc (_ Expr):? %lbc (_ ForFinal):? _ %rparen _ BlockBody 174 | {% ([kw, , , init, , cond, , loop, , , , body]: any) => 175 | new ForLoop( 176 | init === null ? null : init[1], 177 | cond === null ? null : cond[1], 178 | loop === null ? null : loop[1], 179 | body, 180 | kw 181 | ) 182 | %} 183 | 184 | If -> 185 | %kw_if _ %lparen _ Expr _ %rparen _ BlockBody (Else):? 186 | {% ([tokn, , , , cond, , , , body, cont]: any) => 187 | new If( 188 | cond, 189 | body, 190 | tokn, 191 | cont === null ? null : cont[0] 192 | ) 193 | %} 194 | 195 | ProcCall -> 196 | %at %ident _ %lparen _ Args:? _ %rparen 197 | {% (d: any) => new ProcCall(d[3], new IdentExpr(d[1]), d[5] !== null ? d[5] : []) %} 198 | 199 | Else -> 200 | %kw_else _ BlockBody {% d => new Else(d[2], d[0]) %} 201 | 202 | BlockBody -> 203 | FuncLine {% d => [d[0]] %} 204 | | %lbrace (%lbc):* _ (FuncLine):* %rbrace (%lbc):* _ {% d => d[3].map((e: any) => e[0]) %} 205 | 206 | # order of operations 207 | Paren -> 208 | %lparen _ Expr _ %rparen {% d => d[2] %} 209 | | Atom {% id %} 210 | 211 | MiscPost -> 212 | MiscPost _ %lbracket _ Expr _ %rbracket {% (d: any) => new SubscriptExpr(d[2], d[0], d[4]) %} 213 | | MiscPost _ %lparen _ Args:? _ %rparen {% (d: any) => new CallExpr(d[2], d[0], d[4] !== null ? d[4] : []) %} 214 | # TODO is Paren correct here? 215 | | MiscPost _ %period _ Paren {% bin %} 216 | | MiscPost _ %incr {% post %} 217 | | MiscPost _ %decr {% post %} 218 | | Paren {% id %} 219 | 220 | Unary -> 221 | %incr _ Unary {% pre %} 222 | | %decr _ Unary {% pre %} 223 | | %add _ Unary {% pre %} 224 | | %sub _ Unary {% pre %} 225 | | %bnot _ Unary {% pre %} 226 | | %not _ Unary {% pre %} 227 | | MiscPost {% id %} 228 | 229 | MultDiv -> 230 | MultDiv _ %mult _ Unary {% bin %} 231 | | MultDiv _ %div _ Unary {% bin %} 232 | | MultDiv _ %modulo _ Unary {% bin %} 233 | | Unary {% id %} 234 | 235 | AddSub -> 236 | AddSub _ %add _ MultDiv {% bin %} 237 | | AddSub _ %sub _ MultDiv {% bin %} 238 | | MultDiv {% id %} 239 | 240 | BitShift -> 241 | BitShift _ %blshift _ AddSub {% bin %} 242 | | BitShift _ %brshift _ AddSub {% bin %} 243 | | AddSub {% id %} 244 | 245 | Relational -> 246 | Relational _ %lt _ BitShift {% bin %} 247 | | Relational _ %gt _ BitShift {% bin %} 248 | | Relational _ %lte _ BitShift {% bin %} 249 | | Relational _ %gte _ BitShift {% bin %} 250 | | BitShift {% id %} 251 | 252 | Equality -> 253 | Equality _ %eq _ Relational {% bin %} 254 | | Equality _ %neq _ Relational {% bin %} 255 | | Relational {% id %} 256 | 257 | BitAnd -> 258 | BitAnd _ %band _ Equality {% bin %} 259 | | Equality {% id %} 260 | 261 | BitXor -> 262 | BitXor _ %bxor _ BitAnd {% bin %} 263 | | BitAnd {% id %} 264 | 265 | BitOr -> 266 | BitOr _ %bor _ BitXor {% bin %} 267 | | BitXor {% id %} 268 | 269 | LogicAnd -> 270 | LogicAnd _ %and _ BitOr {% bin %} 271 | | BitOr {% id %} 272 | 273 | LogicXor -> 274 | LogicXor _ %xor _ LogicAnd {% bin %} 275 | | LogicAnd {% id %} 276 | 277 | LogicOr -> 278 | LogicOr _ %or _ LogicXor {% bin %} 279 | | LogicXor {% id %} 280 | 281 | # ternary is right associative unlike other operators 282 | Ternary -> 283 | LogicOr _ MiddleTernary _ Ternary {% d => new TernaryExpr(d[0], d[2].expr, d[4], d[2].tok) %} 284 | | LogicOr {% id %} 285 | 286 | Expr -> Ternary {% id %} 287 | 288 | MiddleTernary -> 289 | %question_mark _ Expr _ %colon {% d => { return { tok: d[0], expr: d[2] } } %} 290 | 291 | TypeName -> 292 | TypeWord (_ %lbracket (_ %int):? _ %rbracket):? 293 | {% d => new TypeName(d[0], d[1] === null ? null : d[1][2] === null ? 0 : parseInt(d[1][2][1])) %} 294 | 295 | TypeWord -> 296 | %kw_int {% id %} 297 | | %kw_uint {% id %} 298 | | %kw_float {% id %} 299 | | %kw_bool {% id %} 300 | | %kw_vec2 {% id %} 301 | | %kw_vec3 {% id %} 302 | | %kw_vec4 {% id %} 303 | | %kw_uvec2 {% id %} 304 | | %kw_uvec3 {% id %} 305 | | %kw_uvec4 {% id %} 306 | | %kw_ivec2 {% id %} 307 | | %kw_ivec3 {% id %} 308 | | %kw_ivec4 {% id %} 309 | | %kw_bvec2 {% id %} 310 | | %kw_bvec3 {% id %} 311 | | %kw_bvec4 {% id %} 312 | | %kw_mat2 {% id %} 313 | | %kw_mat3 {% id %} 314 | | %kw_mat4 {% id %} 315 | | %kw_mat2x2 {% id %} 316 | | %kw_mat2x3 {% id %} 317 | | %kw_mat2x4 {% id %} 318 | | %kw_mat3x2 {% id %} 319 | | %kw_mat3x3 {% id %} 320 | | %kw_mat3x4 {% id %} 321 | | %kw_mat4x2 {% id %} 322 | | %kw_mat4x3 {% id %} 323 | | %kw_mat4x4 {% id %} 324 | # generics 325 | # TODO get rid of these for now 326 | #| %kw_genType {% id %} 327 | #| %kw_genBType {% id %} 328 | #| %kw_genIType {% id %} 329 | #| %kw_genUType {% id %} 330 | #| %kw_mat {% id %} 331 | #| %kw_vec {% id %} 332 | #| %kw_bvec {% id %} 333 | #| %kw_ivec {% id %} 334 | #| %kw_uvec {% id %} 335 | 336 | Arg -> 337 | (%ident _ %colon _):? Expr 338 | {% d => d[0] === null ? d[1] : {id: d[0][0], expr: d[1]} %} 339 | 340 | Args -> 341 | Arg (%comma _ Arg):* {% sep %} 342 | 343 | Params -> 344 | Param (%comma _ Param):* {% sep %} 345 | 346 | Param -> 347 | TypeName _ %ident (_ %assignment _ Expr):? 348 | {% d => new Param(d[0], d[2], d[3] === null ? null : d[3][3]) %} 349 | 350 | Atom -> 351 | %float {% d => new FloatExpr(d[0]) %} 352 | | %int {% d => new IntExpr(d[0]) %} 353 | | %uint {% d => new UIntExpr(d[0]) %} 354 | | %ident {% d => new IdentExpr(d[0]) %} 355 | | %kw_true {% d => new BoolExpr(d[0]) %} 356 | | %kw_false {% d => new BoolExpr(d[0]) %} 357 | | %kw_time {% d => new Time(d[0]) %} 358 | | %kw_pos {% d => new Pos(d[0]) %} 359 | | %kw_npos {% d => new NPos(d[0]) %} 360 | | %kw_res {% d => new Res(d[0]) %} 361 | | %kw_prev {% d => new Prev(d[0]) %} 362 | | %frag {% d => new Frag(d[0]) %} 363 | | %string (%int):? {% d => new ColorString(d[0], d[1] === null ? null : parseInt(d[1][0].text)) %} 364 | | TypeName _ %lparen _ Args:? _ %rparen 365 | {% (d: any) => new ConstructorExpr(d[2], d[0], d[4] !== null ? d[4] : []) %} 366 | 367 | AssignSymbol -> 368 | %assignment {% id %} 369 | | %assign_add {% id %} 370 | | %assign_sub {% id %} 371 | | %assign_mult {% id %} 372 | | %assign_div {% id %} 373 | | %assign_modulo {% id %} 374 | | %assign_band {% id %} 375 | | %assign_bxor {% id %} 376 | | %assign_bor {% id %} 377 | | %assign_blshift {% id %} 378 | | %assign_brshift {% id %} 379 | 380 | _ -> (%ws | %comment | %multiline_comment):* 381 | 382 | __ -> (%ws | %comment | %multiline_comment):+ 383 | 384 | # TODO break keyword and ast node -------------------------------------------------------------------------------- /src/ir.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallExpr, 3 | Expr, 4 | FuncDef, 5 | isOnlyExpr, 6 | isOnlyRenderBlock, 7 | isOnlyScopedExpr, 8 | isOnlyScopedRenderBlock, 9 | Param, 10 | ParamScoped, 11 | RenderBlock, 12 | TopDef, 13 | } from "./nodes"; 14 | 15 | interface LoopInfo { 16 | once: boolean; 17 | inNum: number; 18 | outNum: number; 19 | loopNum: number; 20 | } 21 | 22 | // IR classes are similar to render blocks but simpler, and narrow the types 23 | 24 | export abstract class IRNode { 25 | loopInfo: LoopInfo; 26 | paramMappings: Map; 27 | 28 | constructor(loopInfo: LoopInfo, paramMappings: Map) { 29 | this.loopInfo = loopInfo; 30 | this.paramMappings = paramMappings; 31 | } 32 | //abstract print(): void; 33 | } 34 | 35 | export class IRTree extends IRNode { 36 | subNodes: (IRTree | IRLeaf)[]; 37 | 38 | constructor( 39 | loopInfo: LoopInfo, 40 | paramMappings: Map, 41 | subNodes: (IRTree | IRLeaf)[] 42 | ) { 43 | super(loopInfo, paramMappings); 44 | this.subNodes = subNodes; 45 | } 46 | 47 | // TODO get rid of this or make it better 48 | /* 49 | print(): void { 50 | for (const s of this.subNodes) { 51 | s.print(); 52 | } 53 | } 54 | */ 55 | } 56 | 57 | export class IRLeaf extends IRNode { 58 | source = ""; // TODO get rid of this? 59 | 60 | constructor( 61 | loopInfo: LoopInfo, 62 | paramMappings: Map, 63 | public exprs: ParamScoped[], 64 | public oneMult: boolean, 65 | public texNums: Set 66 | ) { 67 | super(loopInfo, paramMappings); 68 | } 69 | } 70 | 71 | export function getAllUsedFuncs( 72 | exprs: Expr[], 73 | funcs: Set = new Set() 74 | ) { 75 | for (const e of exprs) { 76 | // if it is a call expression and not a builtin or constructor, add it 77 | if (e instanceof CallExpr && e.userDefinedFuncDef !== undefined) { 78 | // get all nested function dependencies of this function, and add them 79 | funcs.add(e.userDefinedFuncDef); 80 | // tell the func def to add all of its dependencies to the set 81 | e.userDefinedFuncDef.getAllNestedFunctionDeps(funcs); 82 | } 83 | 84 | // do the same for all the sub expressions of each expression 85 | getAllUsedFuncs(e.getSubExpressions(), funcs); 86 | } 87 | 88 | return funcs; 89 | } 90 | 91 | export function renderBlockToIR(block: RenderBlock): IRTree | IRLeaf { 92 | if ( 93 | typeof block.inNum !== "number" || 94 | typeof block.outNum !== "number" || 95 | block.loopNum instanceof Expr 96 | ) { 97 | throw new Error("a render block num was not a normal number"); 98 | } 99 | 100 | const loopInfo: LoopInfo = { 101 | inNum: block.inNum, 102 | outNum: block.outNum, 103 | loopNum: block.loopNum ?? 1, 104 | once: block.once, 105 | }; 106 | 107 | if (isOnlyScopedExpr(block.scopedBody)) { 108 | const leaf = new IRLeaf( 109 | loopInfo, 110 | block.paramMappings, 111 | //block.body, 112 | block.scopedBody, 113 | block.needsOneMult, 114 | block.requiredTexNums 115 | ); 116 | 117 | if (block.loopNum !== null || block.once) { 118 | return new IRTree(loopInfo, block.paramMappings, [leaf]); 119 | } 120 | 121 | return leaf; 122 | } 123 | 124 | if (isOnlyScopedRenderBlock(block.scopedBody)) { 125 | return new IRTree( 126 | loopInfo, 127 | block.paramMappings, 128 | block.scopedBody.map((s) => s.inmost()).map(renderBlockToIR) 129 | ); 130 | } 131 | 132 | throw new Error("render block contained mix of types"); 133 | } 134 | -------------------------------------------------------------------------------- /src/lexer.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Lexer, Token } from "moo"; 3 | import { keywords, lexer } from "./lexer"; 4 | 5 | /** returns types of all tokens */ 6 | const types = (tokens: Token[]) => tokens.map((t) => t.type); 7 | 8 | /** helper function to put "ws" between all elements in array */ 9 | const separate = (arr: string[]) => 10 | arr 11 | .map((s) => [s, "ws"]) 12 | .flat() 13 | .slice(0, arr.length * 2 - 1); 14 | 15 | /** helper function to return uniform list separated by whitespace */ 16 | const uniform = (s: string, n: number) => separate([...Array(n)].map(() => s)); 17 | 18 | /** helper function to get array of all tokens from lexer */ 19 | const tokens = (lexer: Lexer, str: string) => { 20 | lexer.reset(str); 21 | return Array.from(lexer); 22 | }; 23 | 24 | describe("numbers", () => { 25 | it("lexes ints", () => { 26 | expect(types(tokens(lexer, "0 12 012 00012"))).to.deep.equal( 27 | uniform("int", 4) 28 | ); 29 | }); 30 | 31 | it("lexes uints", () => { 32 | expect(types(tokens(lexer, "0u 12u 012u 00012u"))).to.deep.equal( 33 | uniform("uint", 4) 34 | ); 35 | }); 36 | 37 | it("lexes floats left of decimal", () => { 38 | expect(types(tokens(lexer, "0. 12. 012. 00012."))).to.deep.equal( 39 | uniform("float", 4) 40 | ); 41 | }); 42 | 43 | it("lexes floats right of decimal", () => { 44 | expect(types(tokens(lexer, ".0 .12 .012 .00012"))).to.deep.equal( 45 | uniform("float", 4) 46 | ); 47 | }); 48 | 49 | it("lexes floats both side of decimal", () => { 50 | expect(types(tokens(lexer, "0.0 1.12 22.012 00123.00012"))).to.deep.equal( 51 | uniform("float", 4) 52 | ); 53 | }); 54 | 55 | it("lexes `.` as period, not number", () => { 56 | expect(types(tokens(lexer, "."))).to.deep.equal(["period"]); 57 | }); 58 | }); 59 | 60 | describe("operators", () => { 61 | it("lexes grouping operators", () => { 62 | expect(types(tokens(lexer, "(){}[]"))).to.deep.equal([ 63 | "lparen", 64 | "rparen", 65 | "lbrace", 66 | "rbrace", 67 | "lbracket", 68 | "rbracket", 69 | ]); 70 | }); 71 | 72 | it("lexes arithmetic operators", () => { 73 | expect(types(tokens(lexer, "+ - * / %"))).to.deep.equal( 74 | separate(["add", "sub", "mult", "div", "modulo"]) 75 | ); 76 | }); 77 | 78 | it("lexes relational operators", () => { 79 | expect(types(tokens(lexer, "> < >= <="))).to.deep.equal( 80 | separate(["gt", "lt", "gte", "lte"]) 81 | ); 82 | }); 83 | 84 | it("lexes assignment operators", () => { 85 | expect(types(tokens(lexer, "== !="))).to.deep.equal( 86 | separate(["eq", "neq"]) 87 | ); 88 | }); 89 | 90 | it("lexes bitwise operators", () => { 91 | expect(types(tokens(lexer, "& ^ | << >> ~"))).to.deep.equal( 92 | separate(["band", "bxor", "bor", "blshift", "brshift", "bnot"]) 93 | ); 94 | }); 95 | 96 | it("lexes logical operators", () => { 97 | expect(types(tokens(lexer, "&& || !"))).to.deep.equal( 98 | separate(["and", "or", "not"]) 99 | ); 100 | }); 101 | 102 | it("lexes assignment operators", () => { 103 | expect( 104 | types(tokens(lexer, "= += -= *= /= %= &= ^= |= <<= >>=")) 105 | ).to.deep.equal( 106 | separate([ 107 | "assignment", 108 | "assign_add", 109 | "assign_sub", 110 | "assign_mult", 111 | "assign_div", 112 | "assign_modulo", 113 | "assign_band", 114 | "assign_bxor", 115 | "assign_bor", 116 | "assign_blshift", 117 | "assign_brshift", 118 | ]) 119 | ); 120 | }); 121 | 122 | it("lexes ternary operators", () => { 123 | expect(types(tokens(lexer, "?:"))).to.deep.equal([ 124 | "question_mark", 125 | "colon", 126 | ]); 127 | }); 128 | 129 | it("lexes increment and decrement operators", () => { 130 | expect(types(tokens(lexer, "++--"))).to.deep.equal(["incr", "decr"]); 131 | }); 132 | 133 | it("lexes access operators", () => { 134 | expect(types(tokens(lexer, ". ->"))).to.deep.equal( 135 | separate(["period", "arrow"]) 136 | ); 137 | }); 138 | 139 | it("lexes keywords", () => { 140 | expect(types(tokens(lexer, keywords.join(" ")))).to.deep.equal( 141 | separate(keywords.map((s) => "kw_" + s)) 142 | ); 143 | }); 144 | }); 145 | 146 | describe("comments", () => { 147 | it("lexes single line comment", () => { 148 | expect(types(tokens(lexer, "// some comment"))).to.deep.equal(["comment"]); 149 | }); 150 | 151 | it("lexes empty single line comment", () => { 152 | expect(types(tokens(lexer, "//"))).to.deep.equal(["comment"]); 153 | }); 154 | 155 | it("lexes nested single line comment", () => { 156 | expect(types(tokens(lexer, "// some // comment"))).to.deep.equal([ 157 | "comment", 158 | ]); 159 | }); 160 | 161 | it("lexes two single line comments", () => { 162 | expect(types(tokens(lexer, "// some\n// comment"))).to.deep.equal([ 163 | "comment", 164 | "ws", 165 | "comment", 166 | ]); 167 | }); 168 | 169 | it("lexes multiline comment on one line", () => { 170 | expect(types(tokens(lexer, "/* some comment */"))).to.deep.equal([ 171 | "multiline_comment", 172 | ]); 173 | }); 174 | 175 | it("lexes empty multiline comment", () => { 176 | expect(types(tokens(lexer, "/**/"))).to.deep.equal(["multiline_comment"]); 177 | }); 178 | 179 | it("lexes multiline comment on multiple lines", () => { 180 | expect(types(tokens(lexer, "/*\nsome\ncomment\n*/"))).to.deep.equal([ 181 | "multiline_comment", 182 | ]); 183 | }); 184 | 185 | it("lexes two multiline comments", () => { 186 | expect( 187 | types(tokens(lexer, "/* some comment */\n/*\nsome\ncomment\n*/")) 188 | ).to.deep.equal(["multiline_comment", "ws", "multiline_comment"]); 189 | }); 190 | }); 191 | 192 | describe("strings", () => { 193 | it("lexes string single quote", () => { 194 | expect(types(tokens(lexer, "'hello'"))).to.deep.equal(["string"]); 195 | }); 196 | 197 | it("lexes string double quote", () => { 198 | expect(types(tokens(lexer, '"hello"'))).to.deep.equal(["string"]); 199 | }); 200 | 201 | it("lexes empty string single quote", () => { 202 | expect(types(tokens(lexer, "''"))).to.deep.equal(["string"]); 203 | }); 204 | 205 | it("lexes empty string double quote", () => { 206 | expect(types(tokens(lexer, '""'))).to.deep.equal(["string"]); 207 | }); 208 | }); 209 | 210 | describe("identifiers", () => { 211 | it("lexes an identifier", () => { 212 | expect( 213 | types(tokens(lexer, "_some_arb1traryIdentifier_123")) 214 | ).to.deep.equal(["ident"]); 215 | }); 216 | 217 | it("lexes an identifier and number", () => { 218 | expect( 219 | types(tokens(lexer, "99some_arb1traryIdentifier_123")) 220 | ).to.deep.equal(["int", "ident"]); 221 | }); 222 | 223 | it("lexes identifier containing keywords", () => { 224 | expect(types(tokens(lexer, "for_a_while"))).to.deep.equal(["ident"]); 225 | }); 226 | 227 | it("lexes single character identifier", () => { 228 | expect(types(tokens(lexer, "b"))).to.deep.equal(["ident"]); 229 | }); 230 | }); 231 | 232 | describe("semicolons", () => { 233 | it("lexes semicolon alone", () => { 234 | expect(types(tokens(lexer, ";"))).to.deep.equal(["lbc"]); 235 | }); 236 | 237 | it("lexes semicolon ws start", () => { 238 | expect(types(tokens(lexer, "\n ;"))).to.deep.equal(["lbc"]); 239 | }); 240 | 241 | it("lexes semicolon ws end", () => { 242 | expect(types(tokens(lexer, ";\n "))).to.deep.equal(["lbc", "ws"]); 243 | }); 244 | 245 | it("lexes semicolon ws both sides", () => { 246 | expect(types(tokens(lexer, "\n ;\n "))).to.deep.equal(["lbc", "ws"]); 247 | }); 248 | 249 | it("lexes multiple semicolons alone", () => { 250 | expect(types(tokens(lexer, ";;"))).to.deep.equal(["lbc", "lbc"]); 251 | }); 252 | 253 | // TODO make this test more clear 254 | it("lexes multiple semicolons ws", () => { 255 | expect(types(tokens(lexer, "\n ;;"))).to.deep.equal(["lbc", "lbc"]); 256 | expect(types(tokens(lexer, ";;\n "))).to.deep.equal(["lbc", "lbc", "ws"]); 257 | expect(types(tokens(lexer, ";\n ;"))).to.deep.equal(["lbc", "lbc"]); 258 | expect(types(tokens(lexer, "\n ;;\n "))).to.deep.equal([ 259 | "lbc", 260 | "lbc", 261 | "ws", 262 | ]); 263 | expect(types(tokens(lexer, "\n ;\n ;\n "))).to.deep.equal([ 264 | "lbc", 265 | "lbc", 266 | "ws", 267 | ]); 268 | }); 269 | }); 270 | 271 | describe("whitespace", () => { 272 | it("lexes tabs", () => { 273 | expect(types(tokens(lexer, "\t"))).to.deep.equal(["ws"]); 274 | expect(types(tokens(lexer, "\t\t\t"))).to.deep.equal(["ws"]); 275 | }); 276 | 277 | it("lexes newlines", () => { 278 | expect(types(tokens(lexer, "\n"))).to.deep.equal(["ws"]); 279 | expect(types(tokens(lexer, "\n\n\n"))).to.deep.equal(["ws"]); 280 | }); 281 | 282 | it("lexes spaces", () => { 283 | expect(types(tokens(lexer, " "))).to.deep.equal(["ws"]); 284 | expect(types(tokens(lexer, " "))).to.deep.equal(["ws"]); 285 | }); 286 | 287 | it("lexes mix", () => { 288 | expect(types(tokens(lexer, " \n\t"))).to.deep.equal(["ws"]); 289 | expect(types(tokens(lexer, " \n\n\t\t \n\n\t\t"))).to.deep.equal(["ws"]); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /src/lexer.ts: -------------------------------------------------------------------------------- 1 | import * as moo from "moo"; 2 | import { TinslError } from "./err"; 3 | 4 | // https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf p18 5 | // "The maximum length of an identifier is 1024 characters." p20 6 | 7 | export const types = [ 8 | "mat2", 9 | "mat3", 10 | "mat4", 11 | "mat2x2", 12 | "mat2x3", 13 | "mat2x4", 14 | "mat3x2", 15 | "mat3x3", 16 | "mat3x4", 17 | "mat4x2", 18 | "mat4x3", 19 | "mat4x4", 20 | "vec2", 21 | "vec3", 22 | "vec4", 23 | "ivec2", 24 | "ivec3", 25 | "ivec4", 26 | "bvec2", 27 | "bvec3", 28 | "bvec4", 29 | "uint", 30 | "uvec2", 31 | "uvec3", 32 | "uvec4", 33 | "float", 34 | "int", 35 | "bool", 36 | ] as const; 37 | 38 | export const precision = ["lowp", "mediump", "highp"] as const; 39 | 40 | // currently unused 41 | export const generics = [ 42 | "genType", 43 | "genBType", 44 | "genIType", 45 | "genUType", 46 | "mat", 47 | "vec", 48 | "bvec", 49 | "ivec", 50 | "uvec", 51 | ] as const; 52 | 53 | export const overlap = [ 54 | "const", 55 | "uniform", 56 | "continue", // TODO 57 | "break", // TODO 58 | "for", 59 | "if", 60 | "else", 61 | "true", 62 | "false", 63 | "return", 64 | ] as const; 65 | 66 | export const tinsl = [ 67 | "fn", 68 | "pr", 69 | "final", 70 | "mut", 71 | "def", 72 | "once", 73 | "loop", 74 | "refresh", 75 | "res", 76 | "pos", 77 | "npos", 78 | "time", 79 | "prev", 80 | ] as const; 81 | 82 | export const glsl = [ 83 | "layout", 84 | "centroid", 85 | "flat", 86 | "smooth", 87 | "do", 88 | "while", 89 | "switch", 90 | "case", 91 | "default", 92 | "in", 93 | "out", 94 | "inout", 95 | "void", 96 | "invariant", 97 | "discard", 98 | "precision", 99 | "sampler2D", 100 | "sampler3D", 101 | "samplerCube", 102 | "sampler2DShadow", 103 | "samplerCubeShadow", 104 | "sampler2DArray", 105 | "sampler2DArrayShadow", 106 | "isampler2D", 107 | "isampler3D", 108 | "isamplerCube", 109 | "isampler2DArray", 110 | "usampler2D", 111 | "usampler3D", 112 | "usamplerCube", 113 | "usampler2DArray", 114 | "struct", 115 | ] as const; 116 | 117 | export const future = [ 118 | "attribute", 119 | "varying", 120 | "coherent", 121 | "volatile", 122 | "restrict", 123 | "readonly", 124 | "writeonly", 125 | "resource", 126 | "atomic_uint", 127 | "noperspective", 128 | "patch", 129 | "sample", 130 | "subroutine", 131 | "common", 132 | "partition", 133 | "active", 134 | "asm", 135 | "class", 136 | "union", 137 | "enum", 138 | "typedef", 139 | "template", 140 | "this", 141 | "goto", 142 | "inline", 143 | "noinline", 144 | "volatile", 145 | "public", 146 | "static", 147 | "extern", 148 | "external", 149 | "interface", 150 | "long", 151 | "short", 152 | "double", 153 | "half", 154 | "fixed", 155 | "unsigned", 156 | "superp", 157 | "input", 158 | "output", 159 | "hvec2", 160 | "hvec3", 161 | "hvec4", 162 | "dvec2", 163 | "dvec3", 164 | "dvec4", 165 | "fvec2", 166 | "fvec3", 167 | "fvec4", 168 | "sampler3DRect", 169 | "filter", 170 | "image1D", 171 | "image2D", 172 | "image3D", 173 | "imageCube", 174 | "iimage1D", 175 | "iimage2D", 176 | "iimage3D", 177 | "iimageCube", 178 | "uimage1D", 179 | "uimage2D", 180 | "uimage3D", 181 | "uimageCube", 182 | "image1DArray", 183 | "image2DArray", 184 | "iimage1DArray", 185 | "iimage2DArray", 186 | "uimage1DArray", 187 | "uimage2DArray", 188 | "imageBuffer", 189 | "iimageBuffer", 190 | "uimageBuffer", 191 | "sampler1D", 192 | "sampler1DShadow", 193 | "sampler1DArray", 194 | "sampler1DArrayShadow", 195 | "isampler1D", 196 | "isampler1DArray", 197 | "usampler1D", 198 | "usampler1DArray", 199 | "sampler2DRect", 200 | "sampler2DRectShadow", 201 | "isampler2DRect", 202 | "usampler2DRect", 203 | "samplerBuffer", 204 | "isamplerBuffer", 205 | "usamplerBuffer", 206 | "sampler2DMS", 207 | "isampler2DMS", 208 | "usampler2DMS", 209 | "sampler2DMSArray", 210 | "isampler2DMSArray", 211 | "usampler2DMSArray", 212 | "sizeof", 213 | "cast", 214 | "namespace", 215 | "using", 216 | ] as const; 217 | 218 | const reserved = new Set([ 219 | ...precision, 220 | ...glsl, 221 | ...future, 222 | "fragColor", 223 | "int_non_const_identity", 224 | "uint_non_const_identity", 225 | ] as string[]); 226 | 227 | export const regexes = { 228 | float: /(?:[0-9]*\.[0-9]+|[0-9]+\.)/, 229 | uint: /[0-9]+u/, 230 | int: /[0-9]+/, 231 | string: /(?:".*?"|'.*?')/, 232 | comment: /\/\/.*?$/, 233 | multilineComment: /\/\*[^]*?\*\//, 234 | ident: /[_a-zA-Z][_a-zA-Z0-9]*/, 235 | frag: /frag[0-9]*/, 236 | }; 237 | 238 | /** throws when the string is an invalid identifier */ 239 | export function validIdent(str: string) { 240 | if (/^gl_*/.test(str)) { 241 | throw new TinslError("identifier cannot start with `gl_`"); 242 | } 243 | 244 | if (/__/.test(str)) { 245 | throw new TinslError("identifier cannot contain a double underscore"); 246 | } 247 | 248 | if (reserved.has(str)) { 249 | throw new TinslError(`\`${str}\` is a reserved keyword`); 250 | } 251 | 252 | if (str.length > 1024) { 253 | throw new TinslError("identifier cannot be over 1024 characters in length"); 254 | } 255 | } 256 | 257 | // TODO add fragColor 258 | export const keywords = [...tinsl, ...overlap, ...types] as const; 259 | 260 | // TODO break all these regexes out so they can be used by editor 261 | export const lexer = moo.compile({ 262 | lbc: { 263 | //match: /(?:[\t ]+\n+[\t ]+|[ \t]+\n+|\n+[\t ]+|\n+)+/, 264 | // TODO is this redundant? 265 | //match: /(?:[ \t\n]+;[ \t\n]+|[ \t\n]+;|;[ \t\n]+|;)/, 266 | match: /[ \t\n]*;/, 267 | lineBreaks: true, 268 | }, 269 | ws: { match: /[ \t\n]+/, lineBreaks: true }, 270 | //lb: { match: /\n/, lineBreaks: true }, 271 | comment: regexes.comment, 272 | string: regexes.string, 273 | multiline_comment: { match: regexes.multilineComment, lineBreaks: true }, 274 | float: regexes.float, 275 | uint: regexes.uint, 276 | int: regexes.int, 277 | assign_add: "+=", 278 | assign_sub: "-=", 279 | assign_mult: "*=", 280 | assign_div: "/=", 281 | assign_modulo: "%=", 282 | assign_band: "&=", 283 | assign_bxor: "^=", 284 | assign_bor: "|=", 285 | incr: "++", 286 | decr: "--", 287 | assign_blshift: "<<=", 288 | assign_brshift: ">>=", 289 | blshift: "<<", 290 | brshift: ">>", 291 | arrow: "->", 292 | lte: "<=", 293 | lt: "<", 294 | gte: ">=", 295 | gt: ">", 296 | eq: "==", 297 | neq: "!=", 298 | and: "&&", 299 | xor: "^^", 300 | or: "||", 301 | band: "&", 302 | bxor: "^", 303 | bor: "|", 304 | not: "!", 305 | bnot: "~", 306 | assignment: "=", 307 | lparen: "(", 308 | rparen: ")", 309 | lbrace: "{", 310 | rbrace: "}", 311 | lbracket: "[", 312 | rbracket: "]", 313 | comma: ",", 314 | add: "+", 315 | sub: "-", 316 | mult: "*", 317 | div: "/", 318 | modulo: "%", 319 | question_mark: "?", 320 | decl: ":=", 321 | colon: ":", 322 | semicolon: ";", // TODO remove this 323 | period: ".", 324 | at: "@", 325 | frag: { match: regexes.frag }, 326 | ident: { 327 | match: regexes.ident, 328 | type: moo.keywords(Object.fromEntries(keywords.map((k) => ["kw_" + k, k]))), 329 | }, 330 | }); 331 | -------------------------------------------------------------------------------- /src/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { parse, parseAndCheck } from "./gen"; 3 | import { 4 | Assign, 5 | BinaryExpr, 6 | BoolExpr, 7 | CallExpr, 8 | ColorString, 9 | ConstructorExpr, 10 | Else, 11 | Expr, 12 | ExSt, 13 | FloatExpr, 14 | ForLoop, 15 | Frag, 16 | FuncDef, 17 | IdentExpr, 18 | If, 19 | IntExpr, 20 | Param, 21 | ProcCall, 22 | ProcDef, 23 | Refresh, 24 | RenderBlock, 25 | Return, 26 | SubscriptExpr, 27 | TernaryExpr, 28 | TopDef, 29 | TypeName, 30 | UIntExpr, 31 | UnaryExpr, 32 | Uniform, 33 | VarDecl, 34 | } from "./nodes"; 35 | import { checkExpr, checkProgram, tok } from "./test.helpers"; 36 | 37 | const oneTwoThreeForward = (op1: string, op2: string) => 38 | new BinaryExpr( 39 | new BinaryExpr(new IntExpr(tok("1")), tok(op1), new IntExpr(tok("2"))), 40 | tok(op2), 41 | new IntExpr(tok("3")) 42 | ); 43 | 44 | const oneTwoThreeBackward = (op1: string, op2: string) => 45 | new BinaryExpr( 46 | new IntExpr(tok("1")), 47 | tok(op1), 48 | new BinaryExpr(new IntExpr(tok("2")), tok(op2), new IntExpr(tok("3"))) 49 | ); 50 | 51 | const logicReverse = new BinaryExpr( 52 | new BoolExpr(tok("true")), 53 | tok("||"), 54 | new BinaryExpr( 55 | new BoolExpr(tok("false")), 56 | tok("^^"), 57 | new BinaryExpr( 58 | new BoolExpr(tok("true")), 59 | tok("&&"), 60 | new BoolExpr(tok("false")) 61 | ) 62 | ) 63 | ); 64 | 65 | const bitwiseReverse = new BinaryExpr( 66 | new UIntExpr(tok("1u")), 67 | tok("|"), 68 | new BinaryExpr( 69 | new UIntExpr(tok("2u")), 70 | tok("^"), 71 | new BinaryExpr(new UIntExpr(tok("3u")), tok("&"), new UIntExpr(tok("4u"))) 72 | ) 73 | ); 74 | 75 | const oneCompareTwo = (comp: string, eq: string) => 76 | new BinaryExpr( 77 | new BinaryExpr(new IntExpr(tok("1")), tok(comp), new IntExpr(tok("2"))), 78 | tok(eq), 79 | new BoolExpr(tok("true")) 80 | ); 81 | 82 | const oneTwoThreeBitshift = new BinaryExpr( 83 | new BinaryExpr(new IntExpr(tok("1")), tok("<<"), new IntExpr(tok("2"))), 84 | tok(">>"), 85 | new IntExpr(tok("3")) 86 | ); 87 | 88 | const oneTwoThreeForwardUnary = new BinaryExpr( 89 | new BinaryExpr( 90 | new UnaryExpr(tok("+"), new IntExpr(tok("1"))), 91 | tok("<"), 92 | new BinaryExpr( 93 | new UnaryExpr(tok("-"), new IntExpr(tok("2"))), 94 | tok("+"), 95 | new UnaryExpr(tok("~"), new IntExpr(tok("3"))) 96 | ) 97 | ), 98 | tok("=="), 99 | new BinaryExpr( 100 | new UnaryExpr(tok("!"), new IntExpr(tok("true"))), 101 | tok("||"), 102 | new UnaryExpr(tok("!"), new IntExpr(tok("false"))) 103 | ) 104 | ); 105 | 106 | const vec = (...args: number[]) => 107 | new ConstructorExpr( 108 | tok("("), 109 | new TypeName(tok("vec" + args.length)), 110 | args.map((n) => new FloatExpr(tok(n + "."))) 111 | ); 112 | 113 | const mat = (redundant: boolean, ...args: number[][]) => 114 | new ConstructorExpr( 115 | tok("("), 116 | new TypeName( 117 | tok( 118 | "mat" + 119 | (!redundant && args.length === args[0].length 120 | ? args.length 121 | : args[0].length + "x" + args.length) 122 | ) 123 | ), 124 | args.flat().map((n) => new FloatExpr(tok(n + "."))) 125 | ); 126 | 127 | const assignFloat = (left: string, symbol: string, right: string) => 128 | new Assign(new IdentExpr(tok(left)), tok(symbol), new FloatExpr(tok(right))); 129 | 130 | const funcNoParams = new FuncDef( 131 | new TypeName(tok("float")), 132 | tok("foo"), 133 | [], 134 | [ 135 | new UnaryExpr(tok("+"), new FloatExpr(tok("1."))), 136 | new UnaryExpr(tok("-"), new FloatExpr(tok("2."))), 137 | new Return(new FloatExpr(tok("1.")), tok("return")), 138 | ] 139 | ); 140 | 141 | const tern = (cond: string, first: string, second: string) => 142 | new TernaryExpr( 143 | new BoolExpr(tok(cond)), 144 | new IntExpr(tok(first)), 145 | new IntExpr(tok(second)), 146 | tok("?") 147 | ); 148 | 149 | describe("order of ops", () => { 150 | it("parses in reverse precedence logical or, xor, and", () => { 151 | checkExpr("true || false ^^ true && false", logicReverse); 152 | }); 153 | 154 | it("parses in reverse precedence bitwise or, xor, and", () => { 155 | checkExpr("1u | 2u ^ 3u & 4u", bitwiseReverse); 156 | }); 157 | 158 | it("parses relational and equality", () => { 159 | checkExpr("1 < 2 == true", oneCompareTwo("<", "==")); 160 | checkExpr("1 > 2 == true", oneCompareTwo(">", "==")); 161 | checkExpr("1 <= 2 != true", oneCompareTwo("<=", "!=")); 162 | checkExpr("1 >= 2 != true", oneCompareTwo(">=", "!=")); 163 | }); 164 | 165 | it("parses bitshift", () => { 166 | checkExpr("1 << 2 >> 3", oneTwoThreeBitshift); 167 | }); 168 | 169 | it("parses arithmetic order of ops", () => { 170 | checkExpr("1 + 2 - 3", oneTwoThreeForward("+", "-")); 171 | checkExpr("1 * 2 / 3", oneTwoThreeForward("*", "/")); 172 | checkExpr("1 * 2 % 3", oneTwoThreeForward("*", "%")); 173 | checkExpr("1 + 2 * 3", oneTwoThreeBackward("+", "*")); 174 | checkExpr("1 - 2 / 3", oneTwoThreeBackward("-", "/")); 175 | checkExpr("1 - 2 % 3", oneTwoThreeBackward("-", "%")); 176 | }); 177 | 178 | it("parses arithmetic order of ops reversed with parens", () => { 179 | checkExpr("(1 + 2) * 3", oneTwoThreeForward("+", "*")); 180 | checkExpr("(1 - 2) / 3", oneTwoThreeForward("-", "/")); 181 | checkExpr("(1 - 2) % 3", oneTwoThreeForward("-", "%")); 182 | }); 183 | 184 | it("parses prefix unary expressions", () => { 185 | checkExpr("(+1 < -2 + ~3) == (!true || !false)", oneTwoThreeForwardUnary); 186 | }); 187 | 188 | it("parses ternary expressions with other ops", () => { 189 | checkExpr("true ? 1 : 2", tern("true", "1", "2")); 190 | 191 | checkExpr( 192 | "true || false ? 1 + 2 : 3 / 4", 193 | new TernaryExpr( 194 | new BinaryExpr( 195 | new BoolExpr(tok("true")), 196 | tok("||"), 197 | new BoolExpr(tok("false")) 198 | ), 199 | new BinaryExpr(new IntExpr(tok("1")), tok("+"), new BoolExpr(tok("2"))), 200 | new BinaryExpr(new IntExpr(tok("3")), tok("/"), new BoolExpr(tok("4"))), 201 | tok("?") 202 | ) 203 | ); 204 | }); 205 | 206 | it("parses nested ternary expressions testing right associativity", () => { 207 | checkExpr( 208 | "true ? true ? 1 : 2 : 3", 209 | new TernaryExpr( 210 | new BoolExpr(tok("true")), 211 | tern("true", "1", "2"), 212 | new IntExpr(tok("3")), 213 | tok("?") 214 | ) 215 | ); 216 | 217 | const ternaryAssociative = new TernaryExpr( 218 | new BoolExpr(tok("true")), 219 | tern("true", "1", "2"), 220 | tern("false", "3", "4"), 221 | tok("?") 222 | ); 223 | 224 | checkExpr("true ? true ? 1 : 2 : false ? 3 : 4", ternaryAssociative); 225 | checkExpr("true ? (true ? 1 : 2) : false ? 3 : 4", ternaryAssociative); 226 | }); 227 | }); 228 | 229 | describe("call expressions", () => { 230 | it("parses vec2 constructor call", () => { 231 | checkExpr("vec2(0., 1.)", vec(0, 1)); 232 | }); 233 | 234 | it("parses vec3 constructor call", () => { 235 | checkExpr("vec3(0., 1., 2.)", vec(0, 1, 2)); 236 | }); 237 | 238 | it("parses vec4 constructor call", () => { 239 | checkExpr("vec4(0., 1., 2., 3.)", vec(0, 1, 2, 3)); 240 | }); 241 | 242 | it("parses mat2 constructor call", () => { 243 | checkExpr("mat2(0., 1., 2., 3.)", mat(false, [0, 1], [2, 3])); 244 | }); 245 | 246 | it("parses mat3 constructor call", () => { 247 | checkExpr( 248 | "mat3(0., 1., 2., 3., 4., 5., 6., 7., 8.)", 249 | mat(false, [0, 1, 2], [3, 4, 5], [6, 7, 8]) 250 | ); 251 | }); 252 | 253 | it("parses mat4 constructor call", () => { 254 | checkExpr( 255 | "mat4(0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15.)", 256 | mat(false, [0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]) 257 | ); 258 | }); 259 | 260 | it("parses mat2x2 constructor call", () => { 261 | checkExpr("mat2x2(0., 1., 2., 3.)", mat(true, [0, 1], [2, 3])); 262 | }); 263 | 264 | it("parses mat2x3 constructor call", () => { 265 | checkExpr( 266 | "mat2x3(0., 1., 2., 3., 4., 5.)", 267 | mat(true, [0, 1], [2, 3], [4, 5]) 268 | ); 269 | }); 270 | 271 | it("parses mat2x4 constructor call", () => { 272 | checkExpr( 273 | "mat2x4(0., 1., 2., 3., 4., 5., 6., 7.)", 274 | mat(true, [0, 1], [2, 3], [4, 5], [6, 7]) 275 | ); 276 | }); 277 | 278 | it("parses mat3x2 constructor call", () => { 279 | checkExpr( 280 | "mat3x2(0., 1., 2., 3., 4., 5.)", 281 | mat(true, [0, 1, 2], [3, 4, 5]) 282 | ); 283 | }); 284 | 285 | it("parses mat3x3 constructor call", () => { 286 | checkExpr( 287 | "mat3x3(0., 1., 2., 3., 4., 5., 6., 7., 8.)", 288 | mat(true, [0, 1, 2], [3, 4, 5], [6, 7, 8]) 289 | ); 290 | }); 291 | 292 | it("parses mat3x4 constructor call", () => { 293 | checkExpr( 294 | "mat3x4(0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.)", 295 | mat(true, [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]) 296 | ); 297 | }); 298 | 299 | it("parses mat4x2 constructor call", () => { 300 | checkExpr( 301 | "mat4x2(0., 1., 2., 3., 4., 5., 6., 7.)", 302 | mat(true, [0, 1, 2, 3], [4, 5, 6, 7]) 303 | ); 304 | }); 305 | 306 | it("parses mat4x3 constructor call", () => { 307 | checkExpr( 308 | "mat4x3(0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.)", 309 | mat(true, [0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]) 310 | ); 311 | }); 312 | 313 | it("parses mat4x4 constructor call", () => { 314 | checkExpr( 315 | "mat4x4(0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15.)", 316 | mat(true, [0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]) 317 | ); 318 | }); 319 | }); 320 | 321 | describe("variable declarations", () => { 322 | const vec2Decl = new VarDecl( 323 | "const", 324 | new TypeName(tok("vec2")), 325 | tok("bar"), 326 | vec(1, 2), 327 | tok("=") 328 | ); 329 | 330 | const arr = [ 331 | new IntExpr(tok("1")), 332 | new IntExpr(tok("2")), 333 | new IntExpr(tok("3")), 334 | ]; 335 | 336 | const intArrayDecl = new VarDecl( 337 | "mut", 338 | new TypeName(tok("int"), 0), 339 | tok("arr"), 340 | new ConstructorExpr(tok("("), new TypeName(tok("int"), 0), arr), 341 | tok("=") 342 | ); 343 | 344 | it("parses non-constant variable declaration float", () => { 345 | checkExpr( 346 | "float foo = 1.", 347 | new VarDecl( 348 | "mut", 349 | new TypeName(tok("float")), 350 | tok("foo"), 351 | new FloatExpr(tok("1.")), 352 | tok("=") 353 | ) 354 | ); 355 | }); 356 | 357 | it("parses constant variable declaration vec2", () => { 358 | checkExpr("const vec2 bar = vec2(1., 2.)", vec2Decl); 359 | }); 360 | 361 | it("parses declaration with minimal whitespace", () => { 362 | checkExpr("const vec2 bar=vec2(1.,2.)", vec2Decl); 363 | }); 364 | 365 | it("parses declaration with newlines", () => { 366 | checkExpr("\nconst\nvec2\nbar\n=\nvec2(1.,2.)\n", vec2Decl); 367 | }); 368 | 369 | it("parses array declaration", () => { 370 | checkExpr("int[] arr = int[](1, 2, 3)", intArrayDecl); 371 | }); 372 | }); 373 | 374 | describe("assignment", () => { 375 | it("parses direct assignment", () => { 376 | checkExpr("foo = 1.", assignFloat("foo", "=", "1.")); 377 | }); 378 | 379 | // TODO assignment not allowed in block (change from checkExpr) 380 | it("parses all relative assignment float", () => { 381 | checkExpr("foo += 1.", assignFloat("foo", "+=", "1.")); 382 | checkExpr("foo -= 1.", assignFloat("foo", "-=", "1.")); 383 | checkExpr("foo *= 1.", assignFloat("foo", "*=", "1.")); 384 | checkExpr("foo /= 1.", assignFloat("foo", "/=", "1.")); 385 | checkExpr("foo %= 1.", assignFloat("foo", "%=", "1.")); 386 | checkExpr("foo &= 1.", assignFloat("foo", "&=", "1.")); 387 | checkExpr("foo ^= 1.", assignFloat("foo", "^=", "1.")); 388 | checkExpr("foo |= 1.", assignFloat("foo", "|=", "1.")); 389 | checkExpr("foo <<= 1.", assignFloat("foo", "<<=", "1.")); 390 | checkExpr("foo >>= 1.", assignFloat("foo", ">>=", "1.")); 391 | }); 392 | 393 | it("parses assignment vec2", () => { 394 | checkExpr( 395 | "foo.xy = vec2(1., 2.)", 396 | new Assign( 397 | new BinaryExpr( 398 | new IdentExpr(tok("foo")), 399 | tok("."), 400 | new IdentExpr(tok("xy")) 401 | ), 402 | tok("="), 403 | vec(1, 2) 404 | ) 405 | ); 406 | }); 407 | }); 408 | 409 | describe("function and procedure declarations", () => { 410 | const noDefaultParams = [ 411 | new Param(new TypeName(tok("vec2")), tok("bar")), 412 | new Param(new TypeName(tok("vec3")), tok("baz")), 413 | ]; 414 | 415 | const defaultParams = [ 416 | new Param(new TypeName(tok("float")), tok("bar"), new FloatExpr(tok(".1"))), 417 | new Param(new TypeName(tok("float")), tok("baz"), new FloatExpr(tok(".2"))), 418 | ]; 419 | 420 | it("parses function declaration two arguments no defaults", () => { 421 | checkProgram("float foo (vec2 bar, vec3 baz) { return 1.; }", [ 422 | new FuncDef(new TypeName(tok("float")), tok("foo"), noDefaultParams, [ 423 | new Return(new FloatExpr(tok("1.")), tok("return")), 424 | ]), 425 | ]); 426 | }); 427 | 428 | it("parses function declaration two arguments defaults", () => { 429 | checkProgram("float foo (float bar = .1, float baz = .2) { return 1.; }", [ 430 | new FuncDef(new TypeName(tok("float")), tok("foo"), defaultParams, [ 431 | new Return(new FloatExpr(tok("1.")), tok("return")), 432 | ]), 433 | ]); 434 | }); 435 | 436 | it("parses function declaration no args multiple statements", () => { 437 | checkProgram( 438 | `float foo () { 439 | +1.; 440 | -2.; 441 | return 1.; 442 | }`, 443 | [funcNoParams] 444 | ); 445 | }); 446 | 447 | it("parses function declaration no args with spaces in parens", () => { 448 | checkProgram( 449 | `float foo ( ) { 450 | +1.; 451 | -2.; 452 | return 1.; 453 | }`, 454 | [funcNoParams] 455 | ); 456 | }); 457 | 458 | it("parses function redundant semicolons after", () => { 459 | checkProgram( 460 | `float foo () { 461 | +1.;; 462 | -2.;;; 463 | return 1.;;; 464 | ; 465 | }`, 466 | [funcNoParams] 467 | ); 468 | }); 469 | 470 | it("parses function redundant semicolons before", () => { 471 | checkProgram( 472 | `float foo () { 473 | ; 474 | ;+1.;; 475 | -2.;;; 476 | return 1.; 477 | }`, 478 | [funcNoParams] 479 | ); 480 | }); 481 | 482 | it("parses two function declarations", () => { 483 | checkProgram( 484 | `float foo () { 485 | +1.; 486 | -2.; 487 | return 1.; 488 | } 489 | float foo () { 490 | +1.; 491 | -2.; 492 | return 1.; 493 | }`, 494 | [funcNoParams, funcNoParams] 495 | ); 496 | }); 497 | 498 | it("parses two function declarations surrounding whitespace", () => { 499 | checkProgram( 500 | `\n\n\nfloat foo () { 501 | +1.; 502 | -2.; 503 | return 1.; 504 | } 505 | float foo () { 506 | +1.; 507 | -2.; 508 | return 1.; 509 | }\n\n\n`, 510 | [funcNoParams, funcNoParams] 511 | ); 512 | }); 513 | 514 | it("parses function declaration return type array", () => { 515 | checkProgram( 516 | ` 517 | int[2] foo () { 518 | return int[](1, 2); 519 | }`, 520 | [ 521 | new FuncDef( 522 | new TypeName(tok("int"), 2), 523 | tok("foo"), 524 | [], 525 | [ 526 | new Return( 527 | new ConstructorExpr(tok("("), new TypeName(tok("int"), 0), [ 528 | new IntExpr(tok("1")), 529 | new IntExpr(tok("2")), 530 | ]), 531 | tok("return") 532 | ), 533 | ] 534 | ), 535 | ] 536 | ); 537 | }); 538 | 539 | const procDecl = (args: Param[]) => 540 | new ProcDef(tok("foo"), args, [vec(1, 0, 0, 1)]); 541 | 542 | it("parses proc decl no arguments", () => { 543 | checkProgram("pr foo() {vec4(1., 0., 0., 1.);}", [procDecl([])]); 544 | }); 545 | 546 | it("parses proc decl two arguments no default", () => { 547 | checkProgram("pr foo(vec2 bar, vec3 baz) {vec4(1., 0., 0., 1.);}", [ 548 | procDecl(noDefaultParams), 549 | ]); 550 | }); 551 | 552 | it("parses proc decl two arguments default params", () => { 553 | checkProgram( 554 | "pr foo(float bar = .1, float baz = .2) {vec4(1., 0., 0., 1.);}", 555 | [procDecl(defaultParams)] 556 | ); 557 | }); 558 | 559 | it("parses procedure excessive whitespace", () => { 560 | checkProgram( 561 | "\npr\nfoo\n(\nfloat\nbar\n=\n.1,\nfloat\nbaz\n=\n.2)\n{\nvec4(1., 0., 0., 1.);\n}\n", 562 | [procDecl(defaultParams)] 563 | ); 564 | }); 565 | }); 566 | 567 | describe("render block", () => { 568 | const empty = new RenderBlock( 569 | false, 570 | [vec(1, 2, 3, 4)], 571 | null, 572 | null, 573 | null, 574 | tok("{") 575 | ); 576 | const bl = new RenderBlock(false, [vec(1, 2, 3, 4)], null, 0, null, tok("{")); 577 | const nestedBl = new RenderBlock( 578 | false, 579 | [vec(1, 2, 3, 4), bl, vec(5, 6, 7, 8)], 580 | null, 581 | 0, 582 | null, 583 | tok("{") 584 | ); 585 | const refreshBl = new RenderBlock( 586 | false, 587 | [vec(1, 2, 3, 4), new Refresh(tok("refresh"))], 588 | null, 589 | 0, 590 | null, 591 | tok("{") 592 | ); 593 | 594 | const completeBlock = ( 595 | inNum: number | Expr = 0, 596 | outNum: number | Expr = 1, 597 | loopNum: number | Expr = 2 598 | ) => 599 | new RenderBlock( 600 | true, 601 | [vec(1, 2, 3, 4), vec(5, 6, 7, 8)], 602 | inNum, 603 | outNum, 604 | loopNum, 605 | tok("{") 606 | ); 607 | 608 | it("parses an empty render block", () => { 609 | checkProgram("{vec4(1., 2., 3., 4.);}", [empty]); 610 | }); 611 | 612 | it("parses a render block only out number minimal ws", () => { 613 | checkProgram("{vec4(1., 2., 3., 4.);}->0", [bl]); 614 | }); 615 | 616 | it("parses render block surrounding whitespace", () => { 617 | checkProgram(" \n\t{vec4(1., 2., 3., 4.);}->0 \n\t", [bl]); 618 | }); 619 | 620 | it("parses a render block with all options and multiple statements", () => { 621 | checkProgram( 622 | `0 -> loop 2 once { 623 | vec4(1., 2., 3., 4.); 624 | vec4(5., 6., 7., 8.); 625 | } -> 1`, 626 | [completeBlock()] 627 | ); 628 | }); 629 | 630 | it("parses complete render block where all numbers are identifiers", () => { 631 | checkProgram( 632 | `inNum -> loop loopNum once { 633 | vec4(1., 2., 3., 4.); 634 | vec4(5., 6., 7., 8.); 635 | } -> outNum`, 636 | [ 637 | completeBlock( 638 | new IdentExpr(tok("inNum")), 639 | new IdentExpr(tok("outNum")), 640 | new IdentExpr(tok("loopNum")) 641 | ), 642 | ] 643 | ); 644 | }); 645 | 646 | it("parses render block with redundant semicolons", () => { 647 | checkProgram( 648 | `0 -> loop 2 once { 649 | ; 650 | vec4(1., 2., 3., 4.);; 651 | vec4(5., 6., 7., 8.);; 652 | ; 653 | } -> 1`, 654 | [completeBlock()] 655 | ); 656 | }); 657 | 658 | it("parses a render block with all options minimal ws", () => { 659 | checkProgram( 660 | `0->loop 2once{vec4(1., 2., 3., 4.);vec4(5., 6., 7., 8.);}->1`, 661 | [completeBlock()] 662 | ); 663 | }); 664 | 665 | it("parses two render blocks", () => { 666 | checkProgram("{vec4(1., 2., 3., 4.);}->0\n{vec4(1., 2., 3., 4.);}->0", [ 667 | bl, 668 | bl, 669 | ]); 670 | }); 671 | 672 | it("parses nested render blocks", () => { 673 | checkProgram( 674 | ` 675 | { 676 | vec4(1., 2., 3., 4.); 677 | { 678 | vec4(1., 2., 3., 4.); 679 | } -> 0 680 | vec4(5., 6., 7., 8.); 681 | } -> 0`, 682 | [nestedBl] 683 | ); 684 | }); 685 | 686 | it("parses refresh in a renderblock", () => { 687 | checkProgram(`{vec4(1., 2., 3., 4.); refresh;}->0`, [refreshBl]); 688 | }); 689 | 690 | it("parses function decl and renderblock", () => { 691 | checkProgram( 692 | `float foo () { 693 | +1.; 694 | -2.; 695 | return 1.; 696 | } 697 | {vec4(1., 2., 3., 4.);}->0`, 698 | [funcNoParams, bl] 699 | ); 700 | }); 701 | }); 702 | 703 | describe("top level definitions", () => { 704 | const topLevel = (scalar: number) => 705 | new TopDef( 706 | tok("pi" + scalar), 707 | new BinaryExpr( 708 | new FloatExpr(tok("3.14159")), 709 | tok("*"), 710 | new FloatExpr(tok(scalar + ".")) 711 | ) 712 | ); 713 | 714 | it("parses a top level definition", () => { 715 | checkProgram("def pi2 3.14159 * 2.", [topLevel(2)]); 716 | }); 717 | 718 | it("parses a top level definition", () => { 719 | checkProgram("def pi2 3.14159 * 2.\ndef pi3 3.14159 * 3.", [ 720 | topLevel(2), 721 | topLevel(3), 722 | ]); 723 | }); 724 | 725 | it("parses a top level definition", () => { 726 | checkProgram("def\npi2\n3.14159\n*\n2.\ndef\npi3\n3.14159\n*\n3.", [ 727 | topLevel(2), 728 | topLevel(3), 729 | ]); 730 | }); 731 | }); 732 | 733 | describe("for loops", () => { 734 | const emptyForLoop = new ForLoop(null, null, null, [], tok("for")); 735 | 736 | const forLoop = (body: ExSt[]) => 737 | new ForLoop( 738 | new VarDecl( 739 | "mut", 740 | new TypeName(tok("int")), 741 | tok("i"), 742 | new IntExpr(tok("0")), 743 | tok("=") 744 | ), 745 | new BinaryExpr(new IdentExpr(tok("i")), tok("<"), new IntExpr(tok("3"))), 746 | new UnaryExpr(tok("++"), new IdentExpr(tok("i")), true), 747 | body, 748 | tok("for") 749 | ); 750 | 751 | const jUnary = new UnaryExpr(tok("++"), new IdentExpr(tok("j")), true); 752 | const kUnary = new UnaryExpr(tok("++"), new IdentExpr(tok("k")), true); 753 | 754 | const fullForLoop1 = forLoop([jUnary]); 755 | const fullForLoop2 = forLoop([jUnary, kUnary]); 756 | 757 | it("parses an empty for loop with empty body brackets", () => { 758 | checkExpr("for(;;){}", emptyForLoop, false); 759 | }); 760 | 761 | it("parses full for loop minimal whitespace", () => { 762 | checkExpr("for(int i=0;i<3;i++){j++;k++;}", fullForLoop2, false); 763 | }); 764 | 765 | it("parses full for loop natural whitespace", () => { 766 | checkExpr( 767 | `for (int i = 0; i < 3; i++) { 768 | j++; 769 | k++; 770 | }`, 771 | fullForLoop2, 772 | false 773 | ); 774 | }); 775 | 776 | it("parses full for loop excessive whitespace", () => { 777 | checkExpr( 778 | ` 779 | for 780 | ( 781 | int i = 0; 782 | i < 3; 783 | i++ 784 | ) 785 | { 786 | j++; 787 | k++; 788 | } 789 | `, 790 | fullForLoop2, 791 | false 792 | ); 793 | }); 794 | 795 | it("parses for loop no brackets various spacing", () => { 796 | checkExpr(`for(int i = 0;i < 3;i++)j++;`, fullForLoop1, false); 797 | checkExpr( 798 | `for(int i = 0; i < 3; i++) 799 | j++;`, 800 | fullForLoop1, 801 | false 802 | ); 803 | }); 804 | 805 | it("parses for loop with redundant semicolons", () => { 806 | checkExpr(`for(int i=0;i<3;i++)j++;;;;`, fullForLoop1, false); 807 | checkExpr(`for(int i=0;i<3;i++){;;j++;;;;}`, fullForLoop1, false); 808 | checkExpr(`for(int i=0;i<3;i++){;;j++;;k++;;}`, fullForLoop2, false); 809 | }); 810 | 811 | const declHelper = (str: string) => 812 | new VarDecl( 813 | "mut", 814 | new TypeName(tok("int")), 815 | tok(str), 816 | new IntExpr(tok("0")), 817 | tok("=") 818 | ); 819 | 820 | const forFunc = new FuncDef( 821 | new TypeName(tok("float")), 822 | tok("foo"), 823 | [], 824 | [ 825 | declHelper("j"), 826 | declHelper("k"), 827 | fullForLoop1, 828 | fullForLoop2, 829 | new Return(new IdentExpr(tok("k")), tok("return")), 830 | ] 831 | ); 832 | 833 | it("parses multiple for loops", () => { 834 | checkProgram( 835 | ` 836 | float foo () { 837 | int j = 0; 838 | int k = 0; 839 | 840 | for(int i = 0; i < 3; i++) 841 | j++; 842 | 843 | for(int i = 0; i < 3; i++) { 844 | j++; 845 | k++; 846 | } 847 | 848 | return k; 849 | } 850 | `, 851 | [forFunc] 852 | ); 853 | }); 854 | 855 | it("cannot parse when condition expression is declaration", () => { 856 | expect(() => 857 | parse("float foo () {for(int i = 0; int k = 0; i++) {}}") 858 | ).to.throw("unexpected"); 859 | }); 860 | 861 | it("cannot parse when condition expression is assignment", () => { 862 | expect(() => 863 | parse("float foo () {for(int i = 0; k = 0; i++) {}}") 864 | ).to.throw("unexpected"); 865 | }); 866 | 867 | it("cannot parse when final expression is declaration", () => { 868 | expect(() => 869 | parse("float foo () {for(int i = 0; i < 3; int k = 1) {}}") 870 | ).to.throw("unexpected"); 871 | }); 872 | }); 873 | 874 | describe("ifs and elses", () => { 875 | const basicIf = (cont: Else | null) => 876 | new If( 877 | new BoolExpr(tok("true")), 878 | [new Return(new BoolExpr(tok("false")), tok("return"))], 879 | tok("if"), 880 | cont 881 | ); 882 | 883 | const basicElse = new Else( 884 | [new Return(new BoolExpr(tok("true")), tok("return"))], 885 | tok("else") 886 | ); 887 | 888 | const basicElseIf = new Else([basicIf(null)], tok("else")); 889 | 890 | const basicElseIfElse = new Else([basicIf(basicElse)], tok("else")); 891 | 892 | it("parses basic if statement", () => { 893 | checkExpr("if(true)return false;", basicIf(null)); 894 | }); 895 | 896 | it("parses basic if else statement", () => { 897 | checkExpr("if(true)return false;else return true;", basicIf(basicElse)); 898 | }); 899 | 900 | it("parses basic if else if statement", () => { 901 | checkExpr( 902 | "if(true)return false;else if(true)return false;", 903 | basicIf(basicElseIf) 904 | ); 905 | }); 906 | 907 | it("parses dangling else", () => { 908 | checkExpr( 909 | "if(true)return false;else if(true)return false;else return true;", 910 | basicIf(basicElseIfElse) 911 | ); 912 | }); 913 | 914 | it("parses basic if statement curly braces", () => { 915 | checkExpr("if(true){return false;}", basicIf(null)); 916 | }); 917 | 918 | it("parses basic if else statement curly braces", () => { 919 | checkExpr("if(true){return false;}else{return true;}", basicIf(basicElse)); 920 | }); 921 | 922 | it("parses basic if else if statement curly braces", () => { 923 | checkExpr( 924 | "if(true){return false;}else if(true){return false;}", 925 | basicIf(basicElseIf) 926 | ); 927 | }); 928 | 929 | it("parses dangling else curly braces", () => { 930 | checkExpr( 931 | "if(true){return false;}else if(true){return false;}else{return true;}", 932 | basicIf(basicElseIfElse) 933 | ); 934 | }); 935 | 936 | it("parses multiple statements in if and else blocks", () => { 937 | const assignHelper = (str: string, val: number) => 938 | new Assign( 939 | new IdentExpr(tok(str)), 940 | tok("="), 941 | new FloatExpr(tok(val + ".")) 942 | ); 943 | 944 | const multiIfElse = new If( 945 | new BoolExpr(tok("true")), 946 | [assignHelper("a", 1), assignHelper("b", 2)], 947 | tok("if"), 948 | new Else([assignHelper("a", 3), assignHelper("b", 4)], tok("else")) 949 | ); 950 | checkExpr( 951 | ` 952 | if (true) { 953 | a = 1.; 954 | b = 2.; 955 | } else { 956 | a = 3.; 957 | b = 4.; 958 | } 959 | `, 960 | multiIfElse 961 | ); 962 | }); 963 | 964 | const multiIfElseFunc = new FuncDef( 965 | new TypeName(tok("float")), 966 | tok("foo"), 967 | [], 968 | [ 969 | basicIf(basicElse), 970 | basicIf(basicElse), 971 | new Return(new FloatExpr(tok("1.")), tok("return")), 972 | ] 973 | ); 974 | 975 | it("parses multiple if else statements", () => { 976 | checkProgram( 977 | ` 978 | float foo () { 979 | if (true) 980 | return false; 981 | else 982 | return true; 983 | 984 | if (true) 985 | return false; 986 | else 987 | return true; 988 | 989 | return 1.; 990 | } 991 | `, 992 | [multiIfElseFunc] 993 | ); 994 | }); 995 | }); 996 | 997 | describe("uniforms", () => { 998 | const un = (type: string, tokn: string) => 999 | new Uniform(new TypeName(tok(type)), tok(tokn)); 1000 | 1001 | it("parses a basic uniform", () => { 1002 | checkProgram("uniform float foo;", [un("float", "foo")]); 1003 | }); 1004 | 1005 | it("parses multiple uniforms", () => { 1006 | checkProgram("uniform float foo;\n\nuniform vec3 bar;", [ 1007 | un("float", "foo"), 1008 | un("vec3", "bar"), 1009 | ]); 1010 | }); 1011 | 1012 | it("parses uniform with extra whitespace", () => { 1013 | checkProgram( 1014 | `uniform 1015 | float 1016 | foo 1017 | ;`, 1018 | [un("float", "foo")] 1019 | ); 1020 | }); 1021 | }); 1022 | 1023 | describe("function calls and subscripting", () => { 1024 | const fooCall = (args: Expr[]) => 1025 | new CallExpr(tok("("), new IdentExpr(tok("foo")), args); 1026 | 1027 | const barSubscript = new SubscriptExpr( 1028 | tok("["), 1029 | new IdentExpr(tok("bar")), 1030 | new IntExpr(tok("3")) 1031 | ); 1032 | 1033 | it("parses function call no args", () => { 1034 | checkExpr("foo()", fooCall([])); 1035 | }); 1036 | 1037 | it("parses function call one arg", () => { 1038 | checkExpr("foo(1)", fooCall([new IntExpr(tok("1"))])); 1039 | }); 1040 | 1041 | it("parses function call multiple args", () => { 1042 | checkExpr( 1043 | "foo(1, 2, 3.4)", 1044 | fooCall([ 1045 | new IntExpr(tok("1")), 1046 | new IntExpr(tok("2")), 1047 | new FloatExpr(tok("3.4")), 1048 | ]) 1049 | ); 1050 | }); 1051 | 1052 | it("parses subscripting an array", () => { 1053 | checkExpr("bar[3]", barSubscript); 1054 | }); 1055 | }); 1056 | 1057 | describe("index out of range tests", () => { 1058 | it("checks index for arrays", () => { 1059 | expect(() => 1060 | parseAndCheck(` 1061 | fn foo() { return int[](1, 2, 3)[3]; }`) 1062 | ).to.throw("index"); 1063 | () => 1064 | expect( 1065 | parseAndCheck(` 1066 | fn foo() { return int[](1, 2, 3)[-1]; }`) 1067 | ).to.throw("index"); 1068 | () => 1069 | expect( 1070 | parseAndCheck(` 1071 | fn foo() { return int[](1, 2, 3)[0]; }`) 1072 | ).to.not.throw(); 1073 | }); 1074 | 1075 | it("checks index for vecs", () => { 1076 | expect(() => 1077 | parseAndCheck(` 1078 | fn foo() { return ivec3(1, 2, 3)[3]; }`) 1079 | ).to.throw("index"); 1080 | () => 1081 | expect( 1082 | parseAndCheck(` 1083 | fn foo() { return ivec3(1, 2, 3)[-1]; }`) 1084 | ).to.throw("index"); 1085 | () => 1086 | expect( 1087 | parseAndCheck(` 1088 | fn foo() { return ivec3(1, 2, 3)[0]; }`) 1089 | ).to.not.throw(); 1090 | }); 1091 | 1092 | it("checks index for matrices", () => { 1093 | expect(() => 1094 | parseAndCheck(` 1095 | fn foo() { return mat3x4(1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.)[3]; }`) 1096 | ).to.throw("index"); 1097 | () => 1098 | expect( 1099 | parseAndCheck(` 1100 | fn foo() { return mat3x4(1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.)[-1]; }`) 1101 | ).to.throw("index"); 1102 | () => 1103 | expect( 1104 | parseAndCheck(` 1105 | fn foo() { return mat3x4(1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.)[0]; }`) 1106 | ).to.not.throw(); 1107 | }); 1108 | }); 1109 | 1110 | describe("frag", () => { 1111 | it("parses a frag expression with no sampler number", () => { 1112 | const f = new Frag(tok("frag")); 1113 | expect(f.sampler).to.equal(null); 1114 | checkExpr("frag", f); 1115 | }); 1116 | 1117 | it("parses a frag expression with sampler number, no coords", () => { 1118 | const f = new Frag(tok("frag0")); 1119 | expect(f.sampler).to.equal(0); 1120 | checkExpr("frag0", f); 1121 | }); 1122 | 1123 | it("parses a frag expression with multi-digit sampler number", () => { 1124 | const f = new Frag(tok("frag10")); 1125 | expect(f.sampler).to.equal(10); 1126 | checkExpr("frag10", new Frag(tok("frag10"))); 1127 | }); 1128 | 1129 | describe("pure int mixed usage test", () => { 1130 | it("throws when param used for tex num then normally", () => { 1131 | expect(() => 1132 | parseAndCheck(` 1133 | fn foo (int tex) { 1134 | return frag(tex) / float(tex); 1135 | }`) 1136 | ).to.throw("mixed use"); 1137 | }); 1138 | }); 1139 | 1140 | it("throws when param used normally then for tex num", () => { 1141 | expect(() => 1142 | parseAndCheck(` 1143 | fn foo (int tex) { 1144 | return float(tex) * frag(tex); 1145 | }`) 1146 | ).to.throw("mixed use"); 1147 | }); 1148 | 1149 | it("usage status gets passed up through function", () => { 1150 | expect(() => 1151 | parseAndCheck(` 1152 | fn foo (int tex) { 1153 | return frag(tex); 1154 | } 1155 | 1156 | fn bar (int tex) { 1157 | return float(tex) * foo(tex); 1158 | }`) 1159 | ).to.throw("mixed use"); 1160 | }); 1161 | 1162 | it("usage status gets passed up through procedure", () => { 1163 | expect(() => 1164 | parseAndCheck(` 1165 | fn foo (int tex) { 1166 | return frag(tex); 1167 | } 1168 | 1169 | pr bar (int tex) { 1170 | float(tex) * foo(tex); 1171 | }`) 1172 | ).to.throw("mixed use"); 1173 | }); 1174 | }); 1175 | 1176 | describe("proc call", () => { 1177 | it("checks proc with two args", () => { 1178 | const proc = new ProcCall(tok("("), new IdentExpr(tok("some_proc")), [ 1179 | new IntExpr(tok("1")), 1180 | new IntExpr(tok("2")), 1181 | ]); 1182 | checkProgram("{@some_proc(1, 2);}", [ 1183 | new RenderBlock(false, [proc], null, null, null, tok("{")), 1184 | ]); 1185 | }); 1186 | // TODO more proc call tests 1187 | }); 1188 | 1189 | describe("color strings", () => { 1190 | it("parses a color string single quotes no number", () => { 1191 | checkExpr("'red'", new ColorString(tok("'red'"))); 1192 | }); 1193 | 1194 | it("parses a color string double quotes with number", () => { 1195 | checkExpr('"red"', new ColorString(tok('"red"'))); 1196 | }); 1197 | 1198 | it("parses a color string single quotes no number", () => { 1199 | checkExpr("'red'3", new ColorString(tok("'red'"), 3)); 1200 | }); 1201 | 1202 | it("parses a color string double quotes with number", () => { 1203 | checkExpr("'red'3", new ColorString(tok("'red'"), 3)); 1204 | }); 1205 | }); 1206 | 1207 | describe("calling with named arguments", () => { 1208 | it("calls a function with named argument", () => { 1209 | expect(() => 1210 | parseAndCheck(` 1211 | fn foo (int a) { return a; } 1212 | fn bar() { return foo(a: 2); }`) 1213 | ).to.not.throw(); 1214 | }); 1215 | 1216 | it("calls a function with named argument leaving off default", () => { 1217 | expect(() => 1218 | parseAndCheck(` 1219 | fn foo (int a, int b = 2) { return a + b; } 1220 | fn bar() { return foo(a: 2); }`) 1221 | ).to.not.throw(); 1222 | }); 1223 | 1224 | it("throws when calling a named argument that does not exist", () => { 1225 | expect(() => 1226 | parseAndCheck(` 1227 | fn foo (int a, int b = 2) { return a + b; } 1228 | fn bar() { return foo(c: 2); }`) 1229 | ).to.throw("does not exist"); 1230 | }); 1231 | 1232 | it("passes in named arguments out of order", () => { 1233 | expect(() => 1234 | parseAndCheck(` 1235 | fn foo (int a, int b, int c, int d = 2) { return a + b; } 1236 | fn bar() { return foo(b: 0, a: 1, c: 2); }`) 1237 | ).to.not.throw(); 1238 | }); 1239 | 1240 | it("does not name all required arguments, throws", () => { 1241 | expect(() => 1242 | parseAndCheck(` 1243 | fn foo (int a, int b, int c, int d = 2) { return a + b; } 1244 | fn bar() { return foo(a: 0, c: 2); }`) 1245 | ).to.throw("not filled in"); 1246 | }); 1247 | 1248 | it("throws too few when missing arg is trailing", () => { 1249 | expect(() => 1250 | parseAndCheck(` 1251 | fn foo (int a, int b, int c, int d = 2) { return a + b; } 1252 | fn bar() { return foo(a: 0, b: 2); }`) 1253 | ).to.throw("not filled in"); 1254 | }); 1255 | 1256 | it("throws when named argument is declared twice", () => { 1257 | expect(() => 1258 | parseAndCheck(` 1259 | fn foo (int a, int b, int c) { return a; } 1260 | fn bar() { return foo(a: 3, a: 2); }`) 1261 | ).to.throw("repeat"); 1262 | }); 1263 | 1264 | it("throws when named args and regular args are mixed", () => { 1265 | expect(() => 1266 | parseAndCheck(` 1267 | fn foo (int a, int b, int c, int d = 2) { return a + b; } 1268 | fn bar() { return foo(0, b: 2, c: 3, a: 4); }`) 1269 | ).to.throw("mix"); 1270 | }); 1271 | 1272 | it("works when all defaults and middle named is passed", () => { 1273 | expect(() => 1274 | parseAndCheck(` 1275 | fn foo (int a = 1, int b = 2, int c = 3, int d = 4) { return a + b; } 1276 | fn bar() { return foo(b: 2); }`) 1277 | ).to.not.throw(); 1278 | }); 1279 | 1280 | it("whitespace after colon allowed for named arguments", () => { 1281 | expect(() => 1282 | parseAndCheck(` 1283 | fn foo (int a, int b, int c, int d = 2) { return a + b; } 1284 | fn bar() { return foo(b\t: 0, a\n: 1, c : 2); }`) 1285 | ).to.not.throw(); 1286 | }); 1287 | }); 1288 | 1289 | describe("default parameter validation", () => { 1290 | it("throws when default parameters are not trailing", () => { 1291 | expect(() => 1292 | parseAndCheck(` 1293 | fn foo (int a, int b = 2, int c, int d = 2) { 1294 | return a + b + c + d; 1295 | }`) 1296 | ).to.throw("trailing"); 1297 | }); 1298 | 1299 | it("throws when default parameters are the wrong types", () => { 1300 | expect(() => 1301 | parseAndCheck(` 1302 | fn foo (int a, int b = 2.) { 1303 | return a + b; 1304 | }`) 1305 | ).to.throw("type"); 1306 | }); 1307 | }); 1308 | 1309 | // TODO parsing empty program 1310 | -------------------------------------------------------------------------------- /src/runner/draws.ts: -------------------------------------------------------------------------------- 1 | // dwitter sim 2 | let C = Math.cos; 3 | let S = Math.sin; 4 | let T = Math.tan; 5 | 6 | let R = (r?: any, g?: any, b?: any, a: any = 1) => 7 | `rgba(${r | 0},${g | 0},${b | 0},${a})`; 8 | 9 | export const stripes = ( 10 | t: number, 11 | frames: number, 12 | x: CanvasRenderingContext2D, 13 | c: HTMLCanvasElement 14 | ) => { 15 | if (frames === 0) { 16 | x.fillStyle = "black"; 17 | x.fillRect(0, 0, 960, 540); 18 | x.font = "99px Helvetica, sans-serif"; 19 | x.fillStyle = "white"; 20 | x.textAlign = "center"; 21 | x.textBaseline = "middle"; 22 | x.fillText("hello world", 960 / 2, 540 / 4); 23 | } 24 | const i = ~~(frames / 9); 25 | const j = ~~(i / 44); 26 | const k = i % 44; 27 | x.fillStyle = `hsl(${(k & j) * i},40%,${50 + C(i) * 10}%`; 28 | x.fillRect(k * 24, 0, 24, k + 2); 29 | x.drawImage(c, 0, k + 2); 30 | }; 31 | 32 | export const higherOrderSpiral = ( 33 | dots: [number, number, number], 34 | background: [number, number, number], 35 | num = 50, 36 | size = 1, 37 | speed = 1 38 | ) => ( 39 | t: number, 40 | frames: number, 41 | x: CanvasRenderingContext2D, 42 | c: HTMLCanvasElement 43 | ) => { 44 | x.fillStyle = R(...background); 45 | x.fillRect(0, 0, 960, 540); 46 | let d; 47 | for (let i = num; (i -= 0.5); i > 0) { 48 | x.beginPath(); 49 | d = 2 * C((2 + S(t / 99)) * 2 * i * speed); 50 | x.arc(480 + d * 10 * C(i) * i, 270 + d * 9 * S(i) * i, i * size, 0, 44 / 7); 51 | const fade = i / num; 52 | x.fillStyle = R(dots[0] * fade, dots[1] * fade, dots[2] * fade); 53 | x.fill(); 54 | } 55 | }; 56 | 57 | export const fabric = ( 58 | t: number, 59 | frames: number, 60 | x: CanvasRenderingContext2D, 61 | c: HTMLCanvasElement 62 | ) => { 63 | let h = 20 + C(frames / 30) * 9; 64 | let b = ~~(h / 8); 65 | for (let i = 240; i--; ) { 66 | x.fillStyle = `hsl(${(i ^ ~~(t * 60)) % 99},90%,${h}%)`; 67 | x.fillRect(4 * i, 0, 4, b); 68 | } 69 | x.drawImage(c, 1, b); 70 | }; 71 | 72 | export const shaderLike = (fillFunc: (x: number, y: number) => string) => { 73 | return ( 74 | t: number, 75 | frames: number, 76 | x: CanvasRenderingContext2D, 77 | c: HTMLCanvasElement 78 | ) => { 79 | for (let i = 960; i--; ) { 80 | x.fillStyle = fillFunc(i, frames); 81 | x.fillRect(i, 0, 1, 1); 82 | } 83 | x.drawImage(c, 0, 1); 84 | }; 85 | }; 86 | 87 | export const higherOrderWaves = (color: boolean) => 88 | shaderLike( 89 | color 90 | ? (x: number, y: number) => `hsl(${~~((x + y) / 20) * 100},50%,90%)` 91 | : (x: number, y: number) => 92 | R((256 / 4) * Math.round(2 + S(x / 20) + C(y / 30))) 93 | ); 94 | 95 | export const uncommonCheckerboard = shaderLike((x, y) => { 96 | y /= 60; 97 | return `hsl(${x / 9 + y * 9},40%,${ 98 | 9 + 60 * ~~((1 + C(y) + 4 * C(x / (99 + 20 * C(y / 5))) * S(y / 2)) % 2) 99 | }%)`; 100 | }); 101 | 102 | export const bitwiseGrid = () => 103 | shaderLike((x: number, y: number) => R((x & y) * 20)); 104 | 105 | export const higherOrderGoo = (color: boolean) => { 106 | const colFunc = (i: number, ti: number) => 107 | 20 * ~~(1 + S(i / 20) + T(ti + S(ti + i / 99))); 108 | const fillFunc = color 109 | ? (i: number, ti: number) => 110 | `hsl(${i / 9 + 99 * C(ti)},90%,${colFunc(i, ti)}%` 111 | : (i: number, ti: number) => R(colFunc(i, ti)); 112 | const goo = ( 113 | t: number, 114 | frames: number, 115 | x: CanvasRenderingContext2D, 116 | c: HTMLCanvasElement 117 | ) => { 118 | let ti = frames / 60; 119 | for (let i = 960; i--; ) { 120 | x.fillStyle = fillFunc(i, ti); 121 | x.fillRect(i, 0, 1, 1); 122 | } 123 | x.drawImage(c, 0, 1); 124 | }; 125 | return goo; 126 | }; 127 | 128 | export const vectorSpiral = ( 129 | t: number, 130 | frames: number, 131 | x: CanvasRenderingContext2D, 132 | c: HTMLCanvasElement 133 | ) => { 134 | x.fillStyle = "black"; 135 | x.fillRect(0, 0, 960, 540); 136 | let d; 137 | x.lineWidth = 2; 138 | for (let i = 50; (i -= 0.5); ) { 139 | x.beginPath(); 140 | x.strokeStyle = `hsl(${i * 9},50%,50%)`; 141 | d = 2 * C((2 + S(t / 99)) * 2 * i); 142 | x.arc(480 + d * 10 * C(i) * i, 270 + d * 9 * S(i) * i, i, 0, 44 / 7); 143 | x.stroke(); 144 | } 145 | }; 146 | 147 | export const pinkishHelix = ( 148 | t: number, 149 | frames: number, 150 | x: CanvasRenderingContext2D, 151 | c: HTMLCanvasElement 152 | ) => { 153 | x.fillStyle = "white"; 154 | x.fillRect(0, 0, 960, 540); 155 | let i, j; 156 | for (i = 0; i < 960; i += 32) { 157 | x.fillStyle = R(((1 + C(i)) / 2) * 255, 0, 155); 158 | for (j = 0; j < 3; j++) x.fillRect(i + j, 266 + C(i + j + t) * 50, 32, 8); 159 | } 160 | }; 161 | 162 | export const movingGrid = ( 163 | t: number, 164 | frames: number, 165 | x: CanvasRenderingContext2D, 166 | c: HTMLCanvasElement 167 | ) => { 168 | let i, j, s; 169 | c.width |= 0; 170 | for (i = 940; (i -= 20); ) 171 | for (j = 520; (j -= 20); ) 172 | (x.fillStyle = R( 173 | 6 * 174 | (s = 175 | 6 * 176 | (4 + C(t * 6) + C((C(t) * i) / 99 + t) + S((S(t) * j) / 99 + t))), 177 | 0, 178 | s + i / 9 179 | )), 180 | x.fillRect(i, j, s, s); 181 | }; 182 | 183 | export const higherOrderPerspective = (color: boolean, normalized = true) => { 184 | const layerNum = 12; 185 | const fillFunc = color 186 | ? (i: number) => `hsl(${i * 99},50%,50%)` 187 | : (i: number) => R(255 * (normalized ? 1 / (1 + i) : i / layerNum)); 188 | return ( 189 | t: number, 190 | frames: number, 191 | x: CanvasRenderingContext2D, 192 | c: HTMLCanvasElement 193 | ) => { 194 | x.fillStyle = !normalized ? R(255) : R(1, color, color); 195 | x.fillRect(0, 0, 960, 540); 196 | const d = (xp: number, yp: number, zp: number, w: number, h: number) => { 197 | x.fillRect( 198 | Math.round(480 + (xp - w / 2) / zp), 199 | Math.round(270 + (yp - h / 2) / zp), 200 | Math.round(w / zp), 201 | Math.round(h / zp) 202 | ); 203 | x.fill(); 204 | }; 205 | const offset = 200; 206 | const size = 64; 207 | const amplitude = 32; 208 | for (let i = layerNum; i > 0; i -= 0.5) { 209 | x.fillStyle = fillFunc(i); 210 | const span = 14; 211 | const spacing = 64; 212 | const f = (off: number) => { 213 | for (let j = 0; j < span; j++) { 214 | d( 215 | (j - span / 2) * spacing + spacing / 2, 216 | offset * off + amplitude * C(j + frames / 60), 217 | i, 218 | size * ((span - j) / span), 219 | size * ((j + 1) / span) 220 | ); 221 | } 222 | }; 223 | f(-1); 224 | f(C(frames / 60)); 225 | f(1); 226 | } 227 | }; 228 | }; 229 | 230 | export const higherOrderDonuts = (color = true, extra = 0) => { 231 | const rFunc = (i: number, j: number) => 232 | 255 * ~~((1 + 3 * C(i / (99 + 20 * C(j / 5))) * S(j / 2)) % 2); 233 | const fillFunc = !color 234 | ? (i: number, j: number) => { 235 | let r = 255 - rFunc(i, j); 236 | return R(r, r, r); 237 | } 238 | : (i: number, j: number) => { 239 | let r = rFunc(i, j); 240 | return r > 0 241 | ? R(r / 4, extra) 242 | : R(extra, 0, 99 * C(i / 10) * S(j / 2) + 30); 243 | }; 244 | 245 | return ( 246 | t: number, 247 | frames: number, 248 | x: CanvasRenderingContext2D, 249 | c: HTMLCanvasElement 250 | ) => { 251 | if (!frames) { 252 | x.fillStyle = "black"; 253 | x.fillRect(0, 0, 960, 540); 254 | } 255 | let j = frames / 60; 256 | for (let i = 960; i--; x.fillStyle = fillFunc(i, j)) x.fillRect(i, 0, 1, 1); 257 | x.drawImage(c, 0, 1); 258 | }; 259 | }; 260 | 261 | export const bloomTest = ( 262 | t: number, 263 | frames: number, 264 | x: CanvasRenderingContext2D, 265 | c: HTMLCanvasElement 266 | ) => { 267 | const hsize = 32; 268 | const spacing = 100; 269 | x.fillStyle = "black"; 270 | x.fillRect(0, 0, 960, 540); 271 | const num = 8; 272 | for (let i = 0; i < num; i++) { 273 | const c = 254 / (i + 1) + 1; 274 | const position = spacing * i - (spacing * (num - 1)) / 2; 275 | x.fillStyle = R(c, c, c); 276 | x.fillRect( 277 | 960 / 2 - hsize + position, 278 | 540 / 2 - hsize, 279 | hsize * 2, 280 | hsize * 2 281 | ); 282 | } 283 | }; 284 | 285 | export const celTest = ( 286 | t: number, 287 | frames: number, 288 | x: CanvasRenderingContext2D, 289 | c: HTMLCanvasElement 290 | ) => { 291 | x.fillStyle = "white"; 292 | x.fillRect(0, 0, 960, 540); 293 | const g = x.createRadialGradient(480, 270, 0, 400, 200, 200); 294 | g.addColorStop(0, "#ff0000"); 295 | g.addColorStop(1, "#330000"); 296 | x.fillStyle = g; 297 | x.beginPath(); 298 | x.arc(480, 270, 200, 0, 2 * Math.PI); 299 | x.fill(); 300 | }; 301 | -------------------------------------------------------------------------------- /src/runner/runner.ts: -------------------------------------------------------------------------------- 1 | import { genTinsl } from "../gen"; 2 | import { getAllSamplers, isTinslLeaf, TinslLeaf, TinslTree } from "../nodes"; 3 | 4 | /////////////////////////////////////////////////////////////////////////////// 5 | // constants 6 | 7 | const V_SOURCE = `#version 300 es 8 | in vec2 aPosition; 9 | void main() { 10 | gl_Position = vec4(aPosition, 0.0, 1.0); 11 | }\n`; 12 | 13 | const U_TIME = "uTime"; 14 | 15 | const U_RES = "uResolution"; 16 | 17 | const verbosity = 0; 18 | 19 | /////////////////////////////////////////////////////////////////////////////// 20 | // types 21 | 22 | /** setting for min and max texture filtering modes */ 23 | type FilterMode = "linear" | "nearest"; 24 | /** setting for clamp */ 25 | type ClampMode = "clamp" | "wrap"; 26 | 27 | /** extra texture options for the merger */ 28 | interface RunnerOptions { 29 | /** min filtering mode for the texture */ 30 | minFilterMode?: FilterMode; 31 | /** max filtering mode for the texture */ 32 | maxFilterMode?: FilterMode; 33 | /** how the edges of the texture should be handled */ 34 | edgeMode?: ClampMode; 35 | /** textures or images to use as extra channels */ 36 | channels?: (TexImageSource | WebGLTexture | null)[]; 37 | /** how much to offset the textures (useful for integration) */ 38 | offset?: number; 39 | } 40 | 41 | interface TexInfo { 42 | scratch: TexWrapper; 43 | channels: TexWrapper[]; 44 | /** maps defined sampler num to sequential sampler num */ 45 | definedNumToChannelNum: Map; 46 | } 47 | 48 | /** useful for debugging */ 49 | interface TexWrapper { 50 | name: string; 51 | tex: WebGLTexture; 52 | } 53 | 54 | interface NameToLoc { 55 | [name: string]: { type: string; loc: WebGLUniformLocation } | undefined; 56 | } 57 | 58 | interface NameToVal { 59 | [name: string]: { 60 | type: string; 61 | val: number; 62 | needsUpdate: boolean; 63 | }; 64 | } 65 | 66 | /////////////////////////////////////////////////////////////////////////////// 67 | // program loop classes 68 | 69 | class WebGLProgramTree { 70 | readonly loop: number; 71 | readonly once: boolean; 72 | readonly body: (WebGLProgramTree | WebGLProgramLeaf)[]; 73 | 74 | private ranOnce = false; 75 | 76 | constructor( 77 | gl: WebGL2RenderingContext, 78 | tree: TinslTree, 79 | vShader: WebGLShader, 80 | rightMost: boolean, 81 | texInfo: TexInfo, 82 | uniformVals: NameToVal 83 | ) { 84 | this.loop = tree.loop; 85 | this.once = tree.once; 86 | 87 | const f = (node: TinslTree | TinslLeaf, i: number) => { 88 | const innerRightMost = rightMost && i === tree.body.length - 1; 89 | if (isTinslLeaf(node)) { 90 | return new WebGLProgramLeaf( 91 | gl, 92 | node, 93 | vShader, 94 | innerRightMost, 95 | texInfo, 96 | uniformVals 97 | ); 98 | } 99 | return new WebGLProgramTree( 100 | gl, 101 | node, 102 | vShader, 103 | innerRightMost, 104 | texInfo, 105 | uniformVals 106 | ); 107 | }; 108 | 109 | this.body = tree.body.map(f); 110 | } 111 | 112 | run( 113 | texInfo: TexInfo, 114 | framebuffer: WebGLFramebuffer, 115 | last: boolean, 116 | time: number, 117 | uniformVals: NameToVal 118 | ) { 119 | if (this.once && this.ranOnce) return; 120 | for (let i = 0; i < this.loop; i++) { 121 | if (verbosity > 1) { 122 | console.log("loop iteration", i); 123 | } 124 | this.body.forEach((b, j) => { 125 | const lastInBody = j === this.body.length - 1; 126 | const lastInLoop = i === this.loop - 1; 127 | b.run( 128 | texInfo, 129 | framebuffer, 130 | last && lastInBody && lastInLoop, 131 | time, 132 | uniformVals 133 | ); 134 | }); 135 | } 136 | this.ranOnce = true; 137 | } 138 | } 139 | 140 | class WebGLProgramLeaf { 141 | readonly program: WebGLProgram; 142 | readonly locs: NameToLoc; 143 | readonly target: number; 144 | readonly samplers: number[]; 145 | readonly last: boolean; 146 | readonly definedNumToChannelNum: Map = new Map(); 147 | 148 | constructor( 149 | readonly gl: WebGL2RenderingContext, 150 | leaf: TinslLeaf, 151 | vShader: WebGLShader, 152 | rightMost: boolean, 153 | texInfo: TexInfo, 154 | uniformVals: NameToVal 155 | ) { 156 | const channelTarget = texInfo.definedNumToChannelNum.get(leaf.target); 157 | if (channelTarget === undefined) { 158 | throw new Error("channel target undefined"); 159 | } 160 | this.target = channelTarget; 161 | this.samplers = leaf.requires.samplers; 162 | this.last = rightMost; 163 | 164 | const definedNumToTexNum: Map = new Map(); 165 | 166 | // create mapping of texture num on the gpu to index in channels 167 | this.samplers.forEach((s, i) => { 168 | const channelNum = texInfo.definedNumToChannelNum.get(s); 169 | if (channelNum === undefined) { 170 | throw new Error("defined num did not exist on the map"); 171 | } 172 | this.definedNumToChannelNum.set(s, channelNum); 173 | definedNumToTexNum.set(s, i); 174 | }); 175 | 176 | [this.program, this.locs] = compileProgram( 177 | this.gl, 178 | leaf, 179 | vShader, 180 | definedNumToTexNum, 181 | uniformVals 182 | ); 183 | } 184 | 185 | run( 186 | texInfo: TexInfo, 187 | framebuffer: WebGLFramebuffer, 188 | last: boolean, 189 | time: number, 190 | uniformVals: NameToVal 191 | ) { 192 | const swap = () => { 193 | if (verbosity > 1) { 194 | console.log("swapping " + this.target + " with scratch"); 195 | } 196 | [texInfo.scratch, texInfo.channels[this.target]] = [ 197 | texInfo.channels[this.target], 198 | texInfo.scratch, 199 | ]; 200 | }; 201 | 202 | // bind all the required textures 203 | this.samplers.forEach((s, i) => { 204 | // TODO add offset 205 | this.gl.activeTexture(this.gl.TEXTURE0 + i); 206 | const channelNum = this.definedNumToChannelNum.get(s); 207 | if (channelNum === undefined) { 208 | throw new Error("sampler offset undefined"); 209 | } 210 | if (verbosity > 1) { 211 | console.log("defined num", s, "channel num", channelNum, "tex num", i); 212 | } 213 | this.gl.bindTexture(this.gl.TEXTURE_2D, texInfo.channels[channelNum].tex); 214 | }); 215 | 216 | // final swap to replace the texture 217 | this.gl.useProgram(this.program); 218 | 219 | // apply all the uniforms 220 | // TODO iterate on the other map instead 221 | for (const [k, v] of Object.entries(uniformVals)) { 222 | if (v.needsUpdate) { 223 | const loc = this.locs[k]; 224 | if (loc === undefined) continue; 225 | // TODO make this work for all types 226 | this.gl.uniform1f(loc.loc, v.val); 227 | // TODO figure out how change will work 228 | } 229 | } 230 | 231 | const uTime = this.locs[U_TIME]; 232 | if (uTime !== undefined) this.gl.uniform1f(uTime.loc, time); 233 | 234 | if (last && this.last) { 235 | // draw to the screen by setting to default framebuffer (null) 236 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); 237 | if (verbosity > 1) { 238 | console.log("last!!"); 239 | } 240 | } else { 241 | if (verbosity > 1) { 242 | console.log("not last"); 243 | } 244 | // we are not on the last pass 245 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer); 246 | if (verbosity > 1) { 247 | console.log(this.target); 248 | } 249 | this.gl.framebufferTexture2D( 250 | this.gl.FRAMEBUFFER, 251 | this.gl.COLOR_ATTACHMENT0, 252 | this.gl.TEXTURE_2D, 253 | texInfo.scratch.tex, 254 | 0 255 | ); 256 | } 257 | this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); 258 | 259 | swap(); 260 | } 261 | 262 | delete() { 263 | this.gl.deleteProgram(this.program); 264 | } 265 | } 266 | 267 | /////////////////////////////////////////////////////////////////////////////// 268 | // graphics resources management 269 | 270 | export class Runner { 271 | readonly gl: WebGL2RenderingContext; 272 | readonly options: RunnerOptions; 273 | private readonly vertexBuffer: WebGLBuffer; 274 | private readonly vShader: WebGLShader; 275 | private readonly texInfo: TexInfo; 276 | private readonly framebuffer: WebGLFramebuffer; 277 | private readonly programs: WebGLProgramTree; 278 | private readonly sources: (TexImageSource | WebGLTexture | undefined)[]; 279 | 280 | // TODO update this to be able to work for more than just floats 281 | private readonly uniformVals: NameToVal = {}; 282 | 283 | constructor( 284 | gl: WebGL2RenderingContext, 285 | code: TinslTree | string, 286 | sources: (TexImageSource | WebGLTexture | undefined)[], 287 | options: RunnerOptions 288 | ) { 289 | const tree = typeof code === "string" ? genTinsl(code) : code; 290 | 291 | this.sources = sources; 292 | 293 | if (verbosity > 1) { 294 | console.log("program tree", tree); 295 | } 296 | 297 | this.gl = gl; 298 | this.options = options; 299 | 300 | // set the viewport 301 | const [w, h] = [this.gl.drawingBufferWidth, this.gl.drawingBufferHeight]; 302 | this.gl.viewport(0, 0, w, h); 303 | 304 | // set up the vertex buffer 305 | const vertexBuffer = this.gl.createBuffer(); 306 | 307 | if (vertexBuffer === null) { 308 | throw new Error("problem creating vertex buffer"); 309 | } 310 | 311 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer); 312 | 313 | const vertexArray = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]; 314 | const triangles = new Float32Array(vertexArray); 315 | this.gl.bufferData(this.gl.ARRAY_BUFFER, triangles, this.gl.STATIC_DRAW); 316 | 317 | // save the vertex buffer reference so we can delete it later 318 | this.vertexBuffer = vertexBuffer; 319 | 320 | // compile the simple vertex shader (2 big triangles) 321 | const vShader = this.gl.createShader(this.gl.VERTEX_SHADER); 322 | if (vShader === null) { 323 | throw new Error("problem creating the vertex shader"); 324 | } 325 | 326 | this.gl.shaderSource(vShader, V_SOURCE); 327 | this.gl.compileShader(vShader); 328 | 329 | // save the vertex shader reference so we can delete it later 330 | this.vShader = vShader; 331 | 332 | // make textures 333 | const scratch = { name: "scratch", tex: makeTex(this.gl, this.options) }; 334 | const samplers = Array.from(getAllSamplers(tree)).sort((a, b) => a - b); 335 | 336 | const mapping = new Map(); 337 | 338 | for (let i = 0; i < samplers.length; i++) { 339 | mapping.set(samplers[i], i); 340 | } 341 | 342 | if (verbosity > 0) { 343 | console.log("samplers", samplers); 344 | } 345 | 346 | const channels = samplers.map((s, i) => { 347 | // over-indexing is fine because it will be undefined, meaning empty 348 | const channel = sources[i]; 349 | if (channel === undefined) { 350 | return { name: "empty " + s, tex: makeTex(this.gl, this.options) }; 351 | } else if (channel instanceof WebGLTexture) { 352 | return { name: "provided " + s, tex: channel }; 353 | } else { 354 | return { name: "img src " + s, tex: makeTex(this.gl, this.options) }; 355 | } 356 | }); 357 | 358 | console.log(channels); 359 | 360 | console.log("channels", channels); 361 | 362 | this.texInfo = { scratch, channels, definedNumToChannelNum: mapping }; 363 | console.log(this.texInfo); 364 | 365 | // create the frame buffer 366 | const framebuffer = gl.createFramebuffer(); 367 | if (framebuffer === null) { 368 | throw new Error("problem creating the framebuffer"); 369 | } 370 | 371 | this.framebuffer = framebuffer; 372 | 373 | this.programs = new WebGLProgramTree( 374 | gl, 375 | tree, 376 | vShader, 377 | true, 378 | this.texInfo, 379 | this.uniformVals 380 | ); 381 | } 382 | 383 | draw(time = 0) { 384 | //const offset = this.options.offset ?? 0; 385 | this.gl.activeTexture(this.gl.TEXTURE0); 386 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.texInfo.channels[0].tex); 387 | // TODO send to every texture that needs it 388 | sendTexture(this.gl, this.sources[0]); 389 | this.programs.run( 390 | this.texInfo, 391 | this.framebuffer, 392 | true, 393 | time, 394 | this.uniformVals 395 | ); 396 | // TODO see if we should unbind this 397 | this.gl.bindTexture(this.gl.TEXTURE_2D, null); 398 | //this.gl.activeTexture(this.gl.TEXTURE0 + offset); 399 | 400 | for (const v of Object.values(this.uniformVals)) { 401 | v.needsUpdate = false; 402 | } 403 | } 404 | 405 | getUnifsByPattern(regex: RegExp) { 406 | return Object.keys(this.uniformVals).filter((s) => regex.test(s)); 407 | } 408 | 409 | // TODO make this work for more types 410 | setUnif(str: string, val: number) { 411 | if (this.uniformVals[str] === undefined) { 412 | throw new Error(`uniform ${str} doesn't exist`); 413 | } 414 | this.uniformVals[str].val = val; 415 | this.uniformVals[str].needsUpdate = true; 416 | } 417 | } 418 | 419 | /////////////////////////////////////////////////////////////////////////////// 420 | // webgl helpers 421 | 422 | /** creates a texture given a context and options */ 423 | function makeTex(gl: WebGL2RenderingContext, options?: RunnerOptions) { 424 | const texture = gl.createTexture(); 425 | if (texture === null) { 426 | throw new Error("problem creating texture"); 427 | } 428 | 429 | // flip the order of the pixels, or else it displays upside down 430 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 431 | 432 | // bind the texture after creating it 433 | gl.bindTexture(gl.TEXTURE_2D, texture); 434 | 435 | gl.texImage2D( 436 | gl.TEXTURE_2D, 437 | 0, 438 | gl.RGBA, 439 | gl.drawingBufferWidth, 440 | gl.drawingBufferHeight, 441 | 0, 442 | gl.RGBA, 443 | gl.UNSIGNED_BYTE, 444 | null 445 | ); 446 | 447 | const filterMode = (f: undefined | FilterMode) => 448 | f === undefined || f === "linear" ? gl.LINEAR : gl.NEAREST; 449 | 450 | // how to map texture element 451 | gl.texParameteri( 452 | gl.TEXTURE_2D, 453 | gl.TEXTURE_MIN_FILTER, 454 | filterMode(options?.minFilterMode) 455 | ); 456 | gl.texParameteri( 457 | gl.TEXTURE_2D, 458 | gl.TEXTURE_MAG_FILTER, 459 | filterMode(options?.maxFilterMode) 460 | ); 461 | 462 | if (options?.edgeMode !== "wrap") { 463 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 464 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 465 | } 466 | 467 | return texture; 468 | } 469 | 470 | /** copies onto texture */ 471 | export function sendTexture( 472 | gl: WebGL2RenderingContext, 473 | // TODO consider if passing in undefined makes sense 474 | src: TexImageSource | WebGLTexture | undefined // TODO type for this 475 | ) { 476 | // if you are using textures instead of images, the user is responsible for 477 | // updating that texture, so just return 478 | if (src instanceof WebGLTexture || src === undefined) return; 479 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src); 480 | } 481 | 482 | /** compile a full program */ 483 | function compileProgram( 484 | gl: WebGL2RenderingContext, 485 | leaf: TinslLeaf, 486 | vShader: WebGLShader, 487 | definedNumToTexNum: Map, 488 | uniformVals: NameToVal 489 | ): [WebGLProgram, NameToLoc] { 490 | const uniformLocs: NameToLoc = {}; 491 | 492 | const fShader = gl.createShader(gl.FRAGMENT_SHADER); 493 | if (fShader === null) { 494 | throw new Error("problem creating fragment shader"); 495 | } 496 | 497 | gl.shaderSource(fShader, leaf.source); 498 | gl.compileShader(fShader); 499 | 500 | const program = gl.createProgram(); 501 | if (program === null) { 502 | throw new Error("problem creating program"); 503 | } 504 | 505 | gl.attachShader(program, vShader); 506 | gl.attachShader(program, fShader); 507 | 508 | const shaderLog = (name: string, shader: WebGLShader) => { 509 | const output = gl.getShaderInfoLog(shader); 510 | if (output) console.log(`${name} shader info log\n${output}`); 511 | }; 512 | 513 | shaderLog("vertex", vShader); 514 | shaderLog("fragment", fShader); 515 | 516 | gl.linkProgram(program); 517 | gl.useProgram(program); 518 | 519 | const getLocation = (name: string) => { 520 | const loc = gl.getUniformLocation(program, name); 521 | if (loc === null) throw new Error(`could not get location for "${name}"`); 522 | return loc; 523 | }; 524 | 525 | for (const unif of leaf.requires.uniforms) { 526 | const location = getLocation(unif.name); 527 | uniformLocs[unif.name] = { type: unif.type, loc: location }; 528 | uniformVals[unif.name] = { 529 | type: unif.type, 530 | val: 0, 531 | needsUpdate: false, 532 | }; 533 | } 534 | 535 | if (leaf.requires.resolution) { 536 | const uResolution = getLocation(U_RES); 537 | gl.uniform2f(uResolution, gl.drawingBufferWidth, gl.drawingBufferHeight); 538 | } 539 | 540 | if (leaf.requires.time) { 541 | const uTime = getLocation(U_TIME); 542 | uniformLocs[U_TIME] = { type: "float", loc: uTime }; 543 | } 544 | 545 | for (const s of leaf.requires.samplers) { 546 | const samplerName = "uSampler" + s; 547 | const uSampler = getLocation(samplerName); 548 | uniformLocs[samplerName] = { type: "sampler2D", loc: uSampler }; 549 | const texNum = definedNumToTexNum.get(s); 550 | if (texNum === undefined) { 551 | throw new Error("tex number is defined"); 552 | } 553 | console.log("setting", samplerName, "to TEXTURE", texNum); 554 | gl.uniform1i(uSampler, texNum); 555 | } 556 | 557 | const position = gl.getAttribLocation(program, "aPosition"); 558 | gl.enableVertexAttribArray(position); 559 | gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 560 | 561 | return [program, uniformLocs]; 562 | } 563 | -------------------------------------------------------------------------------- /src/test.helpers.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Token } from "moo"; 3 | import { Stmt } from "./nodes"; 4 | import { parse, parseAndCheck } from "./gen"; 5 | 6 | export function tok(val: string): Token { 7 | return { 8 | toString: () => val, 9 | value: val, 10 | offset: -1, 11 | text: val, 12 | lineBreaks: -1, 13 | line: -1, 14 | col: -1, 15 | }; 16 | } 17 | 18 | export function extractExpr(str: string, semicolon: boolean) { 19 | return parse(`float f () {${str}${semicolon ? ";" : ""}}`)[0].body[0]; 20 | } 21 | 22 | const excludes = ["toString", "offset", "lineBreaks", "line", "col", "type"]; 23 | 24 | // TODO rename 25 | export function checkExpr(str: string, eql: object, semicolon = true) { 26 | expect(extractExpr(str, semicolon)) 27 | .excludingEvery(excludes) 28 | .to.deep.equal(eql); 29 | } 30 | 31 | export function checkProgram(str: string, eql: object) { 32 | expect(parse(str)).excludingEvery(excludes).to.deep.equal(eql); 33 | } 34 | 35 | export const extractTopLevel = (str: string, index = 0) => 36 | parseAndCheck(str)[index] as T; 37 | -------------------------------------------------------------------------------- /src/test.parser.ts: -------------------------------------------------------------------------------- 1 | import * as nearley from "nearley"; 2 | import grammar from "./grammar"; 3 | import util from "util"; 4 | 5 | console.log("running"); 6 | const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)); 7 | 8 | //parser.feed("{(true ? true ? 1 : 2 : false) ? 3 : 4;}->0"); 9 | //parser.feed("float foo (float bar) {for(int i=0;i<3;i++)j++;;}"); 10 | //parser.feed("float foo (float bar) {if (1 < 2) {3; 4;} else 5}"); 11 | //parser.feed("{@some_proc(1, 2);}"); 12 | parser.feed(` 13 | fn foo (int a) { return a; } 14 | fn bar() { return foo(a: 2); }`); 15 | if (parser.results.length > 1) { 16 | console.error("ambiguous grammar!"); 17 | } 18 | 19 | for (let i = 0; i < parser.results.length; i++) { 20 | console.log( 21 | util.inspect( 22 | parser.results[i].map((e: any) => e.toJson()), 23 | { 24 | showHidden: false, 25 | depth: null, 26 | colors: true, 27 | } 28 | ) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/test.programs.ts: -------------------------------------------------------------------------------- 1 | export const bloom = `def threshold 0.9 2 | uniform float u_size; 3 | 4 | fn luma(vec4 color) { 5 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 6 | } 7 | 8 | fn blur5(vec2 direction, int channel) { 9 | uv := pos / res; 10 | off1 := vec2(1.3333333333333333) * direction; 11 | 12 | mut color := vec4(0.); 13 | 14 | color += frag(uv, channel) * 0.29411764705882354; 15 | color += frag(uv + (off1 / res), channel) * 0.35294117647058826; 16 | color += frag(uv - (off1 / res), channel) * 0.35294117647058826; 17 | 18 | return color; 19 | } 20 | 21 | pr two_pass_blur(float size, int reps, int channel = -1) { 22 | loop reps { 23 | blur5(vec2(size, 0.), channel); refresh; 24 | blur5(vec2(0., size), channel); refresh; 25 | } 26 | } 27 | 28 | { frag0 * step(luma(frag0), threshold); } -> 1 29 | 30 | { @two_pass_blur(size: u_size, reps: 3, channel: 1); } -> 1 31 | 32 | { frag0 + frag1; } -> 0`; 33 | 34 | export const errBloom = `def threshold 0.9 35 | 36 | fn luma(vec4 color) { 37 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 38 | } 39 | 40 | fn blur5(vec2 direction, int channel) { 41 | uv := pos / res; 42 | off1 := vec2(1.3333333333333333) * direction; 43 | 44 | color := vec4(0.); 45 | 46 | color += frag(uv, channel) * 0.29411764705882354; // err! 47 | color += frag(uv + (off1 / res), channel) * 0.35294117647058826; // err! 48 | color += frag(uv - (off1 / res), channel) * 0.35294117647058826; // err! 49 | 50 | return color; 51 | } 52 | 53 | pr two_pass_blur(float size, int reps, int channel = -1) { 54 | loop reps { 55 | blur5(vec2(size, 0.), 1 + 2); refresh; // err! 56 | blur5(vec2(0., size)); refresh; // err! 57 | } 58 | } 59 | 60 | { frag0 * step(luma(frag0), int(threshold)); } -> 1 // err! 61 | 62 | { @two_pass_blur(size: 1., reps: 3); } -> 1 + 2 // err! 63 | 64 | { (frag0 + frag1).rgb; } -> 0 // err!`; 65 | 66 | export const godrays = `fn godrays ( 67 | vec4 col = frag, 68 | float exposure = 1., 69 | float decay = 1., 70 | float density = 1., 71 | float weight = 0.01, 72 | vec2 light_pos = vec2(.5, .5), 73 | int num_samples = 100, 74 | int channel = -1 75 | ) { 76 | mut uv := pos / res; 77 | delta_uv := (uv - light_pos) / float(num_samples) * density; 78 | 79 | mut illumination_decay := 1.; 80 | 81 | mut color := col; 82 | 83 | for (int i = 0; i < num_samples; i++) { 84 | uv -= delta_uv; 85 | tex_sample := frag(channel, uv) * illumination_decay * weight; 86 | color += tex_sample; 87 | illumination_decay *= decay; 88 | } 89 | 90 | return color * exposure; 91 | }`; 92 | -------------------------------------------------------------------------------- /src/test.setup.ts: -------------------------------------------------------------------------------- 1 | import chai from "chai"; 2 | import chaiExclude from "chai-exclude"; 3 | 4 | // this needs to run before all the tests 5 | chai.use(chaiExclude); 6 | -------------------------------------------------------------------------------- /src/translation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | expandProcsInBlock, 4 | fillInDefaults, 5 | irToSourceLeaf, 6 | processBlocks, 7 | regroupByRefresh, 8 | gen, 9 | } from "./gen"; 10 | import { getAllUsedFuncs, IRLeaf, renderBlockToIR } from "./ir"; 11 | import { RenderBlock } from "./nodes"; 12 | import { extractTopLevel } from "./test.helpers"; 13 | import { bloom } from "./test.programs"; 14 | import util from "util"; 15 | 16 | describe("renderblock has refresh", () => { 17 | it("refresh at first level of render block", () => { 18 | expect( 19 | extractTopLevel( 20 | "1 -> { 'red'4; frag / 2.; refresh; } -> 0" 21 | ).containsRefresh() 22 | ).to.be.true; 23 | }); 24 | 25 | it("refresh in nested render block", () => { 26 | expect( 27 | extractTopLevel(` 28 | 1 -> { 29 | 'red'4; 30 | frag / 2.; 31 | { 'blue'4; refresh; } 32 | frag * 2.; 33 | } -> 0`).containsRefresh() 34 | ).to.be.true; 35 | }); 36 | 37 | it("refresh in nested render block", () => { 38 | expect( 39 | extractTopLevel( 40 | ` 41 | pr foo () { 42 | frag / 2.; 43 | { 'blue'4; refresh; } 44 | frag * 2.; 45 | } 46 | 47 | 1 -> { 48 | 'green'4; 49 | @foo(); 50 | 'yellow'4; 51 | } -> 0`, 52 | 1 53 | ).containsRefresh() 54 | ).to.be.true; 55 | }); 56 | 57 | it("no refresh at any level of render block", () => { 58 | expect( 59 | extractTopLevel(` 60 | 1 -> { 61 | 'red'4; 62 | frag / 2.; 63 | { 'blue'4; vec4(1., 0., 0., 1.); } 64 | frag * 2.; 65 | } -> 0`).containsRefresh() 66 | ).to.be.false; 67 | }); 68 | }); 69 | 70 | describe("fill in defaults of render block", () => { 71 | it("defaults to 0 for undefined in/out nums", () => { 72 | const defaultedBlock = fillInDefaults( 73 | extractTopLevel("{ 'blue'4; }") 74 | ); 75 | 76 | expect(defaultedBlock.inNum).to.equal(0); 77 | expect(defaultedBlock.outNum).to.equal(0); 78 | }); 79 | 80 | it("does not change defined in/out nums", () => { 81 | const defaultedBlock = fillInDefaults( 82 | extractTopLevel("2 -> { 'blue'4; } -> 3") 83 | ); 84 | 85 | expect(defaultedBlock.inNum).to.equal(2); 86 | expect(defaultedBlock.outNum).to.equal(3); 87 | }); 88 | 89 | // FIXME out of date with param scope changes 90 | /* 91 | it("changes defaults for nested blocks", () => { 92 | const defaultedBlock = fillInDefaults( 93 | extractTopLevel("2 -> { { 'blue'4; } } -> 3") 94 | ); 95 | 96 | const innerBlock = defaultedBlock.body[0] as RenderBlock; 97 | 98 | expect(innerBlock.inNum).to.equal(2); 99 | expect(innerBlock.outNum).to.equal(3); 100 | }); 101 | */ 102 | 103 | // FIXME out of date with param scope changes 104 | /* 105 | it("defaults for inner block", () => { 106 | const defaultedBlock = fillInDefaults( 107 | extractTopLevel("{ { 'blue'4; } }") 108 | ); 109 | 110 | const innerBlock = defaultedBlock.body[0] as RenderBlock; 111 | 112 | expect(innerBlock.inNum).to.equal(0); 113 | expect(innerBlock.outNum).to.equal(0); 114 | }); 115 | */ 116 | }); 117 | 118 | describe("expands procedures", () => { 119 | it("expands a simple procedure", () => { 120 | const expandedBlock = expandProcsInBlock( 121 | extractTopLevel( 122 | ` 123 | pr foo () { 'red'4; } 124 | { @foo(); }`, 125 | 1 126 | ) 127 | ); 128 | 129 | //console.log("expanded block", "" + expandedBlock); 130 | // TODO do something with this test 131 | }); 132 | 133 | it("expands a procedure with in and out num params and loop num", () => { 134 | const expandedBlock = expandProcsInBlock( 135 | extractTopLevel( 136 | ` 137 | pr foo (int x, int y, int z) { x -> loop z {'red'4; } -> y } 138 | { @foo(0, 1, 2); }`, 139 | 1 140 | ) 141 | ); 142 | 143 | //console.log("expanded block", "" + expandedBlock); 144 | // TODO do something with this test 145 | }); 146 | 147 | it("expands a procedure with in and out num, loop num and tex param", () => { 148 | const expandedBlock = expandProcsInBlock( 149 | extractTopLevel( 150 | ` 151 | pr foo (int x, int y, int z, int w) { x -> loop z { frag(w); } -> y } 152 | { @foo(0, 1, 2, 3); }`, 153 | 1 154 | ) 155 | ); 156 | 157 | //console.log("expanded block", "" + expandedBlock); 158 | // TODO do something with this test 159 | }); 160 | 161 | // FIXME out of date with param scope changes 162 | /* 163 | it("expands multiple layers of procedures", () => { 164 | const expandedBlock = expandProcsInBlock( 165 | extractTopLevel( 166 | ` 167 | pr foo (int x) { x -> { frag(x); } -> x} 168 | pr bar (int y) { @foo(y); } 169 | { @bar(1); }`, 170 | 2 171 | ) 172 | ); 173 | 174 | const rb = expandedBlock.body[0] as RenderBlock; 175 | 176 | expect(rb.inNum).to.equal(1); 177 | expect(rb.outNum).to.equal(1); 178 | }); 179 | */ 180 | }); 181 | 182 | describe("regrouping", () => { 183 | it("regroups nested", () => { 184 | const expandedBlock = regroupByRefresh( 185 | fillInDefaults( 186 | expandProcsInBlock( 187 | extractTopLevel( 188 | ` 189 | fn fake_blur(vec2 direction) { return "white"4; } 190 | 191 | loop 3 { 192 | { 193 | fake_blur(vec2(1., 0.)); refresh; 194 | fake_blur(vec2(0., 1.)); refresh; 195 | } 196 | fake_blur(vec2(0., 1.)); refresh; 197 | fake_blur(vec2(1., 0.)); refresh; 198 | }`, 199 | 1 200 | ) 201 | ) 202 | ) 203 | ); 204 | 205 | // it should look be shaped like: [[[] []] [] []] 206 | 207 | // outer render blocks 208 | expect(expandedBlock.scopedBody.length).to.equal(3); 209 | 210 | // inner render blocks 211 | expect( 212 | (expandedBlock.scopedBody[0].inmost() as RenderBlock).scopedBody.length 213 | ).to.equal(2); 214 | expect( 215 | (expandedBlock.scopedBody[1].inmost() as RenderBlock).scopedBody.length 216 | ).to.equal(1); 217 | expect( 218 | (expandedBlock.scopedBody[2].inmost() as RenderBlock).scopedBody.length 219 | ).to.equal(1); 220 | }); 221 | 222 | it("doesn't create redundant render block wrapping", () => { 223 | const expandedBlock = regroupByRefresh( 224 | fillInDefaults( 225 | expandProcsInBlock( 226 | extractTopLevel( 227 | ` 228 | loop 3 { 229 | frag0 + frag1; 230 | mix(frag, "red"4, 0.5); 231 | }` 232 | ) 233 | ) 234 | ) 235 | ); 236 | 237 | // outer render blocks 238 | expect(expandedBlock.body.length).to.equal(2); 239 | }); 240 | }); 241 | 242 | describe("getting all funcs in a leaf", () => { 243 | it("gets all used funcs in ir with no chains", () => { 244 | const ir = processBlocks( 245 | extractTopLevel( 246 | ` 247 | fn foo () { return "red"4; } 248 | fn bar () { return "blue"4; } 249 | { foo(); bar(); }`, 250 | 2 251 | ) 252 | ); 253 | 254 | if (!(ir instanceof IRLeaf)) throw new Error("ir not a leaf"); 255 | 256 | const funcSet = getAllUsedFuncs(ir.exprs.map((e) => e.inmost())); 257 | expect(funcSet.size).to.equal(2); 258 | }); 259 | 260 | it("gets all used funcs in ir with no chains", () => { 261 | const ir = processBlocks( 262 | extractTopLevel( 263 | ` 264 | fn pippo () { return "red"3 / 2.; } 265 | fn pluto () { return "blue"3 / 2.; } 266 | fn paperino () { return "green"3 / 2.; } 267 | 268 | fn baz () { return pippo() + pluto() + paperino(); } 269 | fn bar () { a := baz(); return a + baz(); } 270 | fn foo () { return bar(); } 271 | 272 | { vec4(foo().rgb, 1.); }`, 273 | 6 274 | ) 275 | ); 276 | 277 | if (!(ir instanceof IRLeaf)) throw new Error("ir not a leaf"); 278 | 279 | const funcSet = getAllUsedFuncs(ir.exprs.map((e) => e.inmost())); 280 | expect(funcSet.size).to.equal(6); 281 | }); 282 | 283 | // TODO more tests 284 | // TODO test default parameters in call expressions 285 | }); 286 | 287 | describe("converting ir leaf to source", () => { 288 | it("gets all the function definitions", () => { 289 | const ir = processBlocks( 290 | extractTopLevel( 291 | ` 292 | fn pippo (float d = 2.) { return "red"3 / d; } 293 | fn pluto (float d = 2.) { return "blue"3 / d; } 294 | fn paperino (float d = 2.) { return "green"3 / d; } 295 | 296 | fn baz () { return pippo() + pluto(d: 3.) + paperino(4.); } 297 | fn bar () { a := baz(); return a + baz(); } 298 | fn foo () { return bar(); } 299 | 300 | { vec4(foo().rgb, 1.); }`, 301 | 6 302 | ) 303 | ); 304 | 305 | // TODO more testing that the order is ok 306 | 307 | if (!(ir instanceof IRLeaf)) throw new Error("ir not a leaf"); 308 | 309 | const source = irToSourceLeaf(ir); 310 | //fullLog(source); 311 | }); 312 | }); 313 | 314 | /* 315 | describe("logging source", () => { 316 | it("gets all the function definitions", () => { 317 | const comp = gen(bloom); 318 | for (const c of comp) c.log(); 319 | }); 320 | }); 321 | */ 322 | // TODO don't let loop num be -1 323 | 324 | const fullLog = (input: any) => { 325 | console.log( 326 | util.inspect(input, { 327 | showHidden: false, 328 | depth: null, 329 | colors: true, 330 | }) 331 | ); 332 | }; 333 | 334 | // TODO try to add in set of func def instead of render block 335 | -------------------------------------------------------------------------------- /src/typeinfo.ts: -------------------------------------------------------------------------------- 1 | import { GenType, SpecType, SpecTypeSimple } from "./typetypes"; 2 | import { 3 | extractMatrixDimensions, 4 | extractVecBase, 5 | extractVecLength, 6 | isMat, 7 | isVec, 8 | matchingVecScalar, 9 | } from "./typinghelpers"; 10 | 11 | export type TotalType = GenType | SpecType; 12 | 13 | export interface TypeInfo { 14 | params: TotalType[]; 15 | ret: TotalType; 16 | } 17 | 18 | interface PrototypeDictionary { 19 | [key: string]: TypeInfo | TypeInfo[] | undefined; 20 | } 21 | 22 | const preserveScalarType = (typ: SpecType): TypeInfo[] => [ 23 | { params: ["int"], ret: typ }, 24 | { params: ["float"], ret: typ }, 25 | { params: ["bool"], ret: typ }, 26 | { params: ["uint"], ret: typ }, 27 | ]; 28 | 29 | const rep = (len: number, elem: T) => [...new Array(len)].map(() => elem); 30 | 31 | function constructorInfo(typ: SpecTypeSimple): TypeInfo[] { 32 | if (isVec(typ)) { 33 | const base = extractVecBase(typ); 34 | const num = parseInt(extractVecLength(typ)); 35 | const scalar = matchingVecScalar(typ); 36 | return [ 37 | { params: rep(num, scalar), ret: typ }, 38 | { params: rep(1, scalar), ret: typ }, 39 | ...(num > 2 40 | ? [{ params: [base + (num - 1), scalar] as SpecType[], ret: typ }] 41 | : []), 42 | ]; 43 | } 44 | 45 | if (isMat(typ)) { 46 | const [m, n] = extractMatrixDimensions(typ).map((num) => parseInt(num)); 47 | const vec = "vec" + m; 48 | // note that it doesn't simplify matrix type name 49 | return [ 50 | { params: ["float"], ret: typ }, 51 | { params: rep(m * n, "float"), ret: typ }, 52 | { params: rep(n, vec) as SpecType[], ret: typ }, 53 | ]; 54 | } 55 | 56 | throw new Error("not a vector or matrix"); 57 | } 58 | 59 | export const constructors: PrototypeDictionary = { 60 | int: preserveScalarType("int"), 61 | bool: preserveScalarType("bool"), 62 | float: preserveScalarType("float"), 63 | uint: preserveScalarType("uint"), 64 | 65 | vec2: constructorInfo("vec2"), 66 | vec3: constructorInfo("vec3"), 67 | vec4: constructorInfo("vec4"), 68 | 69 | ivec2: constructorInfo("ivec2"), 70 | ivec3: constructorInfo("ivec3"), 71 | ivec4: constructorInfo("ivec4"), 72 | 73 | uvec2: constructorInfo("uvec2"), 74 | uvec3: constructorInfo("uvec3"), 75 | uvec4: constructorInfo("uvec4"), 76 | 77 | bvec2: constructorInfo("bvec2"), 78 | bvec3: constructorInfo("bvec3"), 79 | bvec4: constructorInfo("bvec4"), 80 | 81 | mat2: constructorInfo("mat2"), 82 | mat3: constructorInfo("mat3"), 83 | mat4: constructorInfo("mat4"), 84 | 85 | mat2x2: constructorInfo("mat2x2"), 86 | mat2x3: constructorInfo("mat2x3"), 87 | mat2x4: constructorInfo("mat2x4"), 88 | 89 | mat3x2: constructorInfo("mat3x2"), 90 | mat3x3: constructorInfo("mat3x3"), 91 | mat3x4: constructorInfo("mat3x4"), 92 | 93 | mat4x2: constructorInfo("mat4x2"), 94 | mat4x3: constructorInfo("mat4x3"), 95 | mat4x4: constructorInfo("mat4x4"), 96 | }; 97 | 98 | // https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf 99 | // starting from p. 86 100 | // note: modf is skipped because it has an output parameter 101 | export const builtIns: PrototypeDictionary = { 102 | // trig 103 | radians: { params: ["genType"], ret: "genType" }, 104 | degrees: { params: ["genType"], ret: "genType" }, 105 | sin: { params: ["genType"], ret: "genType" }, 106 | cos: { params: ["genType"], ret: "genType" }, 107 | tan: { params: ["genType"], ret: "genType" }, 108 | asin: { params: ["genType"], ret: "genType" }, 109 | acos: { params: ["genType"], ret: "genType" }, 110 | atan: [ 111 | { params: ["genType", "genType"], ret: "genType" }, 112 | { params: ["genType"], ret: "genType" }, 113 | ], 114 | sinh: { params: ["genType"], ret: "genType" }, 115 | cosh: { params: ["genType"], ret: "genType" }, 116 | tanh: { params: ["genType"], ret: "genType" }, 117 | asinh: { params: ["genType"], ret: "genType" }, 118 | acosh: { params: ["genType"], ret: "genType" }, 119 | atanh: { params: ["genType"], ret: "genType" }, 120 | 121 | // exponential 122 | pow: { params: ["genType", "genType"], ret: "genType" }, 123 | exp: { params: ["genType"], ret: "genType" }, 124 | log: { params: ["genType"], ret: "genType" }, 125 | exp2: { params: ["genType"], ret: "genType" }, 126 | log2: { params: ["genType"], ret: "genType" }, 127 | sqrt: { params: ["genType"], ret: "genType" }, 128 | inversesqrt: { params: ["genType"], ret: "genType" }, 129 | 130 | // common 131 | abs: [ 132 | { params: ["genType"], ret: "genType" }, 133 | { params: ["genIType"], ret: "genIType" }, 134 | ], 135 | sign: [ 136 | { params: ["genType"], ret: "genType" }, 137 | { params: ["genIType"], ret: "genIType" }, 138 | ], 139 | floor: { params: ["genType"], ret: "genType" }, 140 | trunc: { params: ["genType"], ret: "genType" }, 141 | round: { params: ["genType"], ret: "genType" }, 142 | roundEven: { params: ["genType"], ret: "genType" }, 143 | ceil: { params: ["genType"], ret: "genType" }, 144 | fract: { params: ["genType"], ret: "genType" }, 145 | mod: [ 146 | { params: ["genType", "float"], ret: "genType" }, 147 | { params: ["genType", "genType"], ret: "genType" }, 148 | ], 149 | min: [ 150 | { params: ["genType", "genType"], ret: "genType" }, 151 | { params: ["genType", "float"], ret: "genType" }, 152 | { params: ["genIType", "genIType"], ret: "genIType" }, 153 | { params: ["genIType", "int"], ret: "genIType" }, 154 | { params: ["genUType", "genUType"], ret: "genUType" }, 155 | { params: ["genUType", "uint"], ret: "genUType" }, 156 | ], 157 | max: [ 158 | { params: ["genType", "genType"], ret: "genType" }, 159 | { params: ["genType", "float"], ret: "genType" }, 160 | { params: ["genIType", "genIType"], ret: "genIType" }, 161 | { params: ["genIType", "int"], ret: "genIType" }, 162 | { params: ["genUType", "genUType"], ret: "genUType" }, 163 | { params: ["genUType", "uint"], ret: "genUType" }, 164 | ], 165 | clamp: [ 166 | { params: ["genType", "genType", "genType"], ret: "genType" }, 167 | { params: ["genType", "float", "float"], ret: "genType" }, 168 | { params: ["genIType", "genIType", "genIType"], ret: "genIType" }, 169 | { params: ["genIType", "int", "int"], ret: "genIType" }, 170 | { params: ["genUType", "genUType", "genUType"], ret: "genUType" }, 171 | { params: ["genUType", "uint", "uint"], ret: "genUType" }, 172 | ], 173 | mix: [ 174 | { params: ["genType", "genType", "genType"], ret: "genType" }, 175 | { params: ["genType", "genType", "float"], ret: "genType" }, 176 | { params: ["genType", "genType", "genBType"], ret: "genType" }, 177 | ], 178 | step: [ 179 | { params: ["genType", "genType"], ret: "genType" }, 180 | { params: ["float", "genType"], ret: "genType" }, 181 | ], 182 | smoothstep: [ 183 | { params: ["genType", "genType", "genType"], ret: "genType" }, 184 | { params: ["float", "float", "genType"], ret: "genType" }, 185 | ], 186 | isnan: [{ params: ["genType"], ret: "genBType" }], 187 | isinf: [{ params: ["genType"], ret: "genBType" }], 188 | floatBitsToInt: [{ params: ["genType"], ret: "genIType" }], 189 | floatBitsToUint: [{ params: ["genType"], ret: "genUType" }], 190 | intBitsToFloat: [{ params: ["genIType"], ret: "genType" }], 191 | uintBitsToFloat: [{ params: ["genUType"], ret: "genType" }], 192 | 193 | // floating point pack/unpack 194 | packSnorm2x16: [{ params: ["vec2"], ret: "uint" }], // -> highp 195 | unpackSnorm2x16: [{ params: ["uint"], ret: "vec2" }], // highp -> highp 196 | packUnorm2x16: [{ params: ["vec2"], ret: "uint" }], // -> highp 197 | unpackUnorm2x16: [{ params: ["uint"], ret: "vec2" }], // highp -> highp 198 | packHalf2x16: [{ params: ["vec2"], ret: "uint" }], // mediump -> highp 199 | unpackHalf2x16: [{ params: ["uint"], ret: "vec2" }], // highp -> mediump 200 | 201 | // geometric 202 | length: { params: ["genType"], ret: "float" }, 203 | distance: { params: ["genType", "genType"], ret: "float" }, 204 | dot: { params: ["genType", "genType"], ret: "float" }, 205 | cross: { params: ["vec3", "vec3"], ret: "vec3" }, 206 | normalize: { params: ["genType"], ret: "genType" }, 207 | faceforward: { params: ["genType", "genType", "genType"], ret: "genType" }, 208 | reflect: { params: ["genType", "genType"], ret: "genType" }, 209 | refract: { params: ["genType", "genType", "float"], ret: "genType" }, 210 | 211 | // matrix 212 | matrixCompMult: { params: ["mat", "mat"], ret: "mat" }, 213 | outerProduct: [ 214 | { params: ["vec2", "vec2"], ret: "mat2" }, 215 | { params: ["vec3", "vec3"], ret: "mat3" }, 216 | { params: ["vec4", "vec4"], ret: "mat4" }, 217 | 218 | { params: ["vec3", "vec2"], ret: "mat2x3" }, 219 | { params: ["vec2", "vec3"], ret: "mat3x2" }, 220 | 221 | { params: ["vec4", "vec2"], ret: "mat2x4" }, 222 | { params: ["vec2", "vec4"], ret: "mat4x2" }, 223 | 224 | { params: ["vec4", "vec3"], ret: "mat3x4" }, 225 | { params: ["vec3", "vec4"], ret: "mat4x3" }, 226 | ], 227 | transpose: [ 228 | { params: ["mat2"], ret: "mat2" }, 229 | { params: ["mat3"], ret: "mat3" }, 230 | { params: ["mat4"], ret: "mat4" }, 231 | 232 | { params: ["mat3x2"], ret: "mat2x3" }, 233 | { params: ["mat2x3"], ret: "mat3x2" }, 234 | 235 | { params: ["mat4x2"], ret: "mat2x4" }, 236 | { params: ["mat2x4"], ret: "mat4x2" }, 237 | 238 | { params: ["mat4x3"], ret: "mat3x4" }, 239 | { params: ["mat3x4"], ret: "mat4x3" }, 240 | ], 241 | determinant: [ 242 | { params: ["mat2"], ret: "float" }, 243 | { params: ["mat3"], ret: "float" }, 244 | { params: ["mat4"], ret: "float" }, 245 | ], 246 | inverse: [ 247 | { params: ["mat2"], ret: "mat2" }, 248 | { params: ["mat3"], ret: "mat3" }, 249 | { params: ["mat4"], ret: "mat4" }, 250 | ], 251 | 252 | // vector and relational 253 | lessThan: [ 254 | { params: ["vec", "vec"], ret: "bvec" }, 255 | { params: ["ivec", "ivec"], ret: "bvec" }, 256 | { params: ["uvec", "uvec"], ret: "bvec" }, 257 | ], 258 | lessThanEqual: [ 259 | { params: ["vec", "vec"], ret: "bvec" }, 260 | { params: ["ivec", "ivec"], ret: "bvec" }, 261 | { params: ["uvec", "uvec"], ret: "bvec" }, 262 | ], 263 | greaterThan: [ 264 | { params: ["vec", "vec"], ret: "bvec" }, 265 | { params: ["ivec", "ivec"], ret: "bvec" }, 266 | { params: ["uvec", "uvec"], ret: "bvec" }, 267 | ], 268 | greaterThanEqual: [ 269 | { params: ["vec", "vec"], ret: "bvec" }, 270 | { params: ["ivec", "ivec"], ret: "bvec" }, 271 | { params: ["uvec", "uvec"], ret: "bvec" }, 272 | ], 273 | equal: [ 274 | { params: ["vec", "vec"], ret: "bvec" }, 275 | { params: ["ivec", "ivec"], ret: "bvec" }, 276 | { params: ["uvec", "uvec"], ret: "bvec" }, 277 | { params: ["bvec", "bvec"], ret: "bvec" }, 278 | ], 279 | notEqual: [ 280 | { params: ["vec", "vec"], ret: "bvec" }, 281 | { params: ["ivec", "ivec"], ret: "bvec" }, 282 | { params: ["uvec", "uvec"], ret: "bvec" }, 283 | { params: ["bvec", "bvec"], ret: "bvec" }, 284 | ], 285 | any: [{ params: ["bvec"], ret: "bool" }], 286 | all: [{ params: ["bvec"], ret: "bool" }], 287 | not: [{ params: ["bvec"], ret: "bool" }], 288 | }; 289 | -------------------------------------------------------------------------------- /src/typetypes.ts: -------------------------------------------------------------------------------- 1 | export interface ArrayType { 2 | typ: T; 3 | size: number; 4 | } 5 | export type GenTypeSimple = 6 | | "genType" 7 | | "genBType" 8 | | "genIType" 9 | | "genUType" 10 | | "mat" 11 | | "vec" 12 | | "bvec" 13 | | "ivec" 14 | | "uvec"; 15 | 16 | export type GenType = GenTypeSimple | ArrayType; 17 | 18 | export type SpecTypeSimple = 19 | | "float" 20 | | "int" 21 | | "bool" 22 | | "uint" 23 | | "vec2" 24 | | "vec3" 25 | | "vec4" 26 | | "ivec2" 27 | | "ivec3" 28 | | "ivec4" 29 | | "uvec2" 30 | | "uvec3" 31 | | "uvec4" 32 | | "bvec2" 33 | | "bvec3" 34 | | "bvec4" 35 | | "mat2" 36 | | "mat3" 37 | | "mat4" 38 | | "mat2x2" 39 | | "mat2x3" 40 | | "mat2x4" 41 | | "mat3x2" 42 | | "mat3x3" 43 | | "mat3x4" 44 | | "mat4x2" 45 | | "mat4x3" 46 | | "mat4x4" 47 | | "__undecided"; // if this shows up in an error message, that's a mistake 48 | 49 | export type SpecType = SpecTypeSimple | ArrayType; 50 | -------------------------------------------------------------------------------- /src/typing.ts: -------------------------------------------------------------------------------- 1 | import { TinslError } from "./err"; 2 | import { TotalType, TypeInfo } from "./typeinfo"; 3 | import { 4 | ArrayType, 5 | GenType, 6 | GenTypeSimple, 7 | SpecType, 8 | SpecTypeSimple, 9 | } from "./typetypes"; 10 | import { 11 | extractMatrixDimensions, 12 | extractVecBase, 13 | extractVecLength, 14 | isScalar, 15 | matchingVecScalar, 16 | } from "./typinghelpers"; 17 | import { strHasRepeats } from "./util"; 18 | 19 | export function compareTypes(left: SpecType, right: SpecType) { 20 | if (typeof left === "string" && typeof right === "string") { 21 | return left === right; 22 | } 23 | 24 | if (typeof left === "object" && typeof right === "object") { 25 | return left.typ === right.typ && left.size === right.size; 26 | } 27 | return false; 28 | } 29 | 30 | export function typeToString(typ: SpecType) { 31 | if (typeof typ === "string") return typ; 32 | return `${typ.typ}[${typ.size === 0 ? "" : typ.size}]`; 33 | } 34 | 35 | // TODO should use specific type where total type is used some places 36 | 37 | function isIntBased(typ: SpecTypeSimple) { 38 | return ["int", "uint"].includes(typ) || /^[i|u]vec/.test(typ); 39 | } 40 | 41 | function dimensionMismatch(op: string, left: TotalType, right: TotalType) { 42 | return new TinslError(`dimension mismatch: \ 43 | cannot do vector/matrix operation \`${left} ${op} ${right}\``); 44 | } 45 | 46 | function toSimpleMatrix(m: SpecTypeSimple) { 47 | return (m === "mat2x2" 48 | ? "mat2" 49 | : m === "mat3x3" 50 | ? "mat3" 51 | : m === "mat4x4" 52 | ? "mat4" 53 | : m) as SpecTypeSimple; 54 | } 55 | 56 | function validGenSpecPair(gen: GenType, spec: SpecTypeSimple) { 57 | switch (gen) { 58 | case "genType": 59 | return spec === "float" || /^vec/.test(spec); 60 | case "genBType": 61 | return spec === "bool" || /^bvec/.test(spec); 62 | case "genIType": 63 | return spec === "int" || /^ivec/.test(spec); 64 | case "genUType": 65 | return spec === "uint" || /^uvec/.test(spec); 66 | case "mat": 67 | return /^mat/.test(spec); 68 | case "vec": 69 | return /^vec/.test(spec); 70 | case "bvec": 71 | return /^bvec/.test(spec); 72 | case "ivec": 73 | return /^ivec/.test(spec); 74 | case "uvec": 75 | return /^uvec/.test(spec); 76 | default: 77 | return false; 78 | } 79 | } 80 | 81 | const parseArrayType = ( 82 | tempParam: T | ArrayType 83 | ): [T, number | null] => 84 | typeof tempParam === "object" 85 | ? [tempParam.typ, tempParam.size] 86 | : [tempParam, null]; 87 | 88 | export function callReturnType( 89 | args: SpecType[], 90 | typeInfo: TypeInfo | TypeInfo[] | undefined, 91 | funcName: string = "__foo" // default value to make tests more convenient 92 | ): SpecType { 93 | if (typeInfo === undefined) throw new Error("type info was undefined"); 94 | const infoArr = Array.isArray(typeInfo) ? typeInfo : [typeInfo]; 95 | if (args.includes("__undecided")) return "__undecided"; 96 | 97 | for (const info of infoArr) { 98 | const genMap = callTypeCheck(args, info.params); 99 | 100 | // not a match, try again 101 | if (!genMap) continue; 102 | 103 | // return type is already specific type 104 | // decide the return type, which could be generic 105 | const [ret, retSize] = parseArrayType(info.ret); 106 | const retGenMapping = genMap.get(ret as any); 107 | if (retGenMapping !== undefined) { 108 | // if return type is generic and there is no match, invalid function 109 | if (retGenMapping === null) { 110 | throw new TinslError( 111 | `function "${funcName}" has a generic return type that was never matched in the arguments` 112 | ); 113 | } 114 | return retSize === null 115 | ? retGenMapping 116 | : { typ: retGenMapping, size: retSize }; 117 | } 118 | 119 | // return type is already specific type 120 | return info.ret as SpecType; 121 | } 122 | throw new TinslError(`no matching overload for function "${funcName}"`); 123 | } 124 | 125 | type GenMap = Map; 126 | 127 | export function callTypeCheck( 128 | args: SpecType[], 129 | params: TotalType[] 130 | ): null | GenMap { 131 | const genArr: GenTypeSimple[] = [ 132 | "genType", 133 | "genBType", 134 | "genIType", 135 | "genUType", 136 | "mat", 137 | "vec", 138 | "bvec", 139 | "ivec", 140 | "uvec", 141 | ]; 142 | 143 | const genMap: GenMap = new Map(); 144 | 145 | // clear the map 146 | for (const g of genArr) { 147 | genMap.set(g, null); 148 | } 149 | 150 | // num of params and num of args don't match, so move on 151 | if (params.length !== args.length) return null; 152 | for (let i = 0; i < args.length; i++) { 153 | const [arg, argSize] = parseArrayType(args[i]); 154 | const [param, paramSize] = parseArrayType(params[i]); 155 | 156 | // if one type is an array and the other is not, or arrays are not same 157 | // size, move on 158 | if (argSize !== paramSize) { 159 | return null; 160 | } 161 | 162 | const argGenMapping = genMap.get(param as any); 163 | if (argGenMapping !== undefined) { 164 | // if it is null, this generic type already has a mapping 165 | // all future arguments must be of the same type 166 | if (argGenMapping !== null && argGenMapping !== arg) { 167 | return null; 168 | } else { 169 | // now we know the param is a generic type 170 | const genParam = param as GenTypeSimple; 171 | if (!validGenSpecPair(genParam, arg)) { 172 | return null; 173 | } 174 | genMap.set(genParam, arg); 175 | } 176 | } else { 177 | // now we know the param is a specific type 178 | if (param !== arg) { 179 | return null; 180 | } 181 | } 182 | } 183 | 184 | return genMap; 185 | } 186 | 187 | /** checks if two types in an operation can be applied without type error */ 188 | export function scalarOp( 189 | op: string, 190 | left: SpecTypeSimple, 191 | right: SpecTypeSimple 192 | ): SpecType { 193 | if (isScalar(right) && !isScalar(left)) [left, right] = [right, left]; 194 | if ( 195 | (left === "float" && (/^vec/.test(right) || /^mat/.test(right))) || 196 | (left === "int" && /^ivec/.test(right)) || 197 | (left === "uint" && /^uvec/.test(right)) 198 | ) { 199 | return right; 200 | } 201 | 202 | if (!isScalar(left) && !isScalar(right)) { 203 | throw dimensionMismatch(op, left, right); 204 | } 205 | 206 | throw new TinslError(`type mismatch: \ 207 | cannot do scalar operation \`${left} ${op} ${right}\``); 208 | } 209 | 210 | export function dimensions(typ: SpecTypeSimple, side?: "left" | "right") { 211 | if (/^vec/.test(typ)) { 212 | const size = extractVecLength(typ); 213 | if (side === undefined) throw new Error("side was undefined for vec"); 214 | if (side === "left") { 215 | return ["1", size]; 216 | } 217 | return [size, "1"]; 218 | } 219 | 220 | return extractMatrixDimensions(typ).reverse(); 221 | } 222 | 223 | export function unaryTyping(op: string, typ: SpecType): SpecType { 224 | if (typ === "__undecided") return "__undecided"; 225 | if (typeof typ === "object") 226 | throw new TinslError("cannot perform unary operation on an array"); 227 | typ = toSimpleMatrix(typ); 228 | if (["+", "-", "++", "--"].includes(op)) { 229 | // TODO we'll have to check if ++, -- value is valid l-value 230 | if (typ === "bool" || /^bvec/.test(typ)) { 231 | throw new TinslError(`unary operator ${op} can only be used on \ 232 | boolean scalars`); 233 | } 234 | return typ; 235 | } 236 | 237 | if (op === "~") { 238 | if (!isIntBased(typ)) { 239 | throw new TinslError(`unary operator ${op} cannot be used on \ 240 | floating point scalars, vectors or matrices`); 241 | } 242 | return typ; 243 | } 244 | 245 | if (op === "!") { 246 | if (typ !== "bool") { 247 | throw new TinslError(`unary operator ${op} can only be used on \ 248 | scalar booleans. for boolean vectors, use not(val)`); 249 | } 250 | return typ; 251 | } 252 | 253 | throw new Error(`"${op}" not a valid unary operator`); 254 | } 255 | 256 | export function binaryTyping( 257 | op: string, 258 | left: SpecType, 259 | right: SpecType 260 | ): SpecType { 261 | if (left === "__undecided" || right === "__undecided") return "__undecided"; 262 | 263 | if (typeof left === "object" || typeof right === "object") 264 | throw new TinslError("cannot perform binary operation on array types"); 265 | [left, right] = [left, right].map(toSimpleMatrix); 266 | 267 | if ("+-/*%&|^".includes(op)) { 268 | if ("%&|^".includes(op)) { 269 | if (!(isIntBased(left) && isIntBased(right))) { 270 | throw new TinslError(`${ 271 | op === "%" ? "mod" : "bitwise" 272 | } operator ${op} cannot be used on \ 273 | floating point scalars, vectors or matrices${ 274 | op === "%" ? ". use mod(x, y) instead" : "" 275 | }`); 276 | } 277 | } 278 | 279 | if (left === right) return left; 280 | 281 | // matrix mult 282 | const matrixMultTypeMatch = (left: SpecTypeSimple, right: SpecTypeSimple) => 283 | (/^vec/.test(left) && /^mat/.test(right)) || 284 | (/^mat/.test(left) && /^vec/.test(right)) || 285 | (/^mat/.test(left) && /^mat/.test(right)); 286 | 287 | if (op === "*" && matrixMultTypeMatch(left, right)) { 288 | // mxn * nxp -> mxp 289 | // but in GLSL row and col are reversed from linear algebra 290 | const [m, n1] = dimensions(left, "left"); 291 | const [n2, p] = dimensions(right, "right"); 292 | if (n1 !== n2) 293 | throw new TinslError("matrix and/or vector dimension mismatch"); 294 | if (m === "1" || p === "1") 295 | return `vec${Math.max(parseInt(m), parseInt(p))}` as SpecType; 296 | return `mat${p === m ? m : p + "x" + m}` as SpecType; 297 | } 298 | 299 | return scalarOp(op, left, right); 300 | } 301 | 302 | if ([">", "<", ">=", "<="].includes(op)) { 303 | if (left !== right) { 304 | throw new TinslError( 305 | `${op} relation operator can only compare values of the same type` 306 | ); 307 | } 308 | 309 | if (!(isScalar(left) && isScalar(right))) { 310 | throw new TinslError(`${op} relational operator can only be used to \ 311 | compare scalars. for vectors, use ${ 312 | op === ">" 313 | ? "greaterThan" 314 | : op === "<" 315 | ? "lessThan" 316 | : op === ">=" 317 | ? "greaterThanEqual" 318 | : "lessThanEqual" 319 | }(left, right) instead.`); 320 | } 321 | 322 | return "bool"; 323 | } 324 | 325 | if (["==", "!="].includes(op)) { 326 | if (left !== right) { 327 | throw new TinslError( 328 | `${op} equality operator can only compare expressions of the same type` 329 | ); 330 | } 331 | 332 | return "bool"; 333 | } 334 | 335 | if (["&&", "||", "^^"].includes(op)) { 336 | if (!(left === "bool" && right === "bool")) { 337 | throw new TinslError( 338 | `${op} logical operator can only operate on booleans` 339 | ); 340 | } 341 | 342 | return left; 343 | } 344 | 345 | if ([">>", "<<"].includes(op)) { 346 | // "For both operators, the operands must be signed or unsigned integers or 347 | // integer vectors. One operand can be signed while the other is unsigned." 348 | if (!isIntBased(left) || !isIntBased(right)) { 349 | throw new TinslError(`${op} bitshift operator can only operate on \ 350 | signed or unsigned integers or vectors`); 351 | } 352 | 353 | // "If the first operand is a scalar, the second operand has to be a scalar 354 | // as well." 355 | if (isScalar(left) && !isScalar(right)) { 356 | throw new TinslError(`expression to right of ${op} bitshift operator \ 357 | must be a scalar if expression to left is also a scalar`); 358 | } 359 | 360 | // "If the first operand is a vector, the second operand must be a scalar or 361 | // a vector with the same size as the first operand [...]." 362 | if ( 363 | !isScalar(left) && 364 | !(isScalar(right) || extractVecLength(left) === extractVecLength(right)) 365 | ) { 366 | throw new TinslError(`expression to right of ${op} bitshift operator \ 367 | must be a scalar or same-length vector if the expression to left is a vector`); 368 | } 369 | 370 | // "In all cases, the resulting type will be the same type as the left 371 | // operand." 372 | return left; 373 | } 374 | 375 | throw new Error(`${op} not a valid binary operator`); 376 | } 377 | 378 | export function ternaryTyping( 379 | condType: SpecType, 380 | ifType: SpecType, 381 | elseType: SpecType 382 | ): SpecType { 383 | if ([condType, ifType, elseType].includes("__undecided")) 384 | return "__undecided"; 385 | 386 | if (typeof condType === "object") { 387 | throw new TinslError( 388 | "cannot have array type as condition in ternary expression" 389 | ); 390 | } 391 | 392 | if (typeof ifType === "object" || typeof elseType === "object") { 393 | throw new TinslError( 394 | "cannot have ternary expression evaluate to an array type" 395 | ); 396 | } 397 | 398 | [ifType, elseType] = [ifType, elseType].map(toSimpleMatrix); 399 | 400 | if (condType !== "bool") { 401 | throw new TinslError(`the condition expression in a ternary expression \ 402 | must be of boolean type`); 403 | } 404 | 405 | if (ifType !== elseType) { 406 | throw new TinslError(`both ending expressions in a ternary expression \ 407 | must have the same type`); 408 | } 409 | 410 | return ifType; 411 | } 412 | 413 | export function matrixAccessTyping(mat: SpecTypeSimple) { 414 | const [_, n] = extractMatrixDimensions(mat); 415 | return ("vec" + n) as SpecTypeSimple; 416 | } 417 | 418 | export function vectorAccessTyping(comps: string, vec: SpecType) { 419 | // TODO .length property of array 420 | if (typeof vec === "object") 421 | throw new TinslError( 422 | "cannot access properties of an array with . operator (use [])" 423 | ); 424 | 425 | if (comps.length > 4) 426 | throw new TinslError( 427 | "too many components; " + 428 | "cannot access greater than 4 components on a vector" 429 | ); 430 | 431 | if (isScalar(vec)) 432 | throw new TinslError( 433 | "cannot access components of a scalar. " + 434 | "can only access components of vector" 435 | ); 436 | 437 | const base = extractVecBase(vec); 438 | const len = parseInt(extractVecLength(vec)); 439 | const sets = ["rgba", "xyzw", "stpq"]; 440 | const domains = sets.join(""); 441 | let prevSet: number | undefined = undefined; 442 | 443 | for (const c of comps) { 444 | const trueIndex = domains.lastIndexOf(c); 445 | const index = trueIndex % 4; 446 | if (index !== -1) { 447 | if (index > len - 1) 448 | throw new TinslError( 449 | `component ${c} cannot be used \ 450 | on a vector of length ${len}` 451 | ); 452 | const set = Math.floor(trueIndex / 4); 453 | if (prevSet !== undefined && prevSet !== set) { 454 | throw new TinslError( 455 | `mixed sets (${sets.join(", ")}) in components ${comps}` 456 | ); 457 | } 458 | prevSet = set; 459 | } else { 460 | throw new TinslError( 461 | `component ${c} does not belong in any set ${sets.join(", ")}` 462 | ); 463 | } 464 | } 465 | 466 | if (comps.length === 1) return matchingVecScalar(vec); 467 | return (base + comps.length) as SpecType; 468 | } 469 | 470 | // TODO length method for arrays 471 | // TODO does the grammar allow return statements inside procedures? 472 | -------------------------------------------------------------------------------- /src/typinghelpers.ts: -------------------------------------------------------------------------------- 1 | import { SpecType, SpecTypeSimple } from "./typetypes"; 2 | 3 | export function extractVecBase(typ: string) { 4 | const matches = typ.match(/^([i|u|b]?vec)/); 5 | if (matches === null) throw new Error("could not match base of vec"); 6 | return matches[1]; 7 | } 8 | 9 | export function extractVecLength(typ: string) { 10 | const matches = typ.match(/^[i|u|b]?vec(.+)/); // TODO test bvec (was missing) 11 | if (matches === null) throw new Error("could not match size of vec"); 12 | return matches[1]; 13 | } 14 | 15 | export function extractMatrixDimensions(typ: string) { 16 | const matches = typ.match(/^mat(.+)/); 17 | if (matches === null) throw new Error("no dimensions matches mat"); 18 | const dims = matches[1].split("x"); 19 | if (dims.length === 1) return [dims[0], dims[0]]; 20 | return dims; 21 | } 22 | 23 | export function isInIndexableRange(typ: SpecType, index: number) { 24 | if (typeof typ === "object") { 25 | return index >= 0 && index < typ.size; 26 | } 27 | 28 | if (isVec(typ)) { 29 | const len = parseInt(extractVecLength(typ)); 30 | return index >= 0 && index < len; 31 | } 32 | 33 | if (isMat(typ)) { 34 | const dim = parseInt(extractMatrixDimensions(typ)[0]); 35 | return index >= 0 && index < dim; 36 | } 37 | 38 | throw new Error("not an indexable type"); 39 | } 40 | 41 | export function matchingVecScalar(vec: SpecTypeSimple): SpecType { 42 | return /^vec/.test(vec) 43 | ? "float" 44 | : /^ivec/.test(vec) 45 | ? "int" 46 | : /^uvec/.test(vec) 47 | ? "uint" 48 | : "bool"; 49 | } 50 | 51 | export const isVec = (typ: SpecTypeSimple) => /^[i|u|b]?vec/.test(typ); 52 | 53 | export const isMat = (typ: SpecTypeSimple) => /^mat/.test(typ); 54 | 55 | // helpers for type checking 56 | export function isScalar(typ: SpecTypeSimple) { 57 | return ["float", "int", "uint"].includes(typ); 58 | } 59 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { TinslAggregateError, TinslError, TinslLineError } from "./err"; 2 | 3 | export function strHasRepeats(str: string) { 4 | return /(.).*\1/.test(str); 5 | } 6 | 7 | export function arrHasRepeats(arr: T[]) { 8 | return new Set(arr).size !== arr.length; 9 | } 10 | 11 | function simplifyNearleyErrorMessage(msg: string) { 12 | return msg.substr(0, msg.lastIndexOf(" Instead")); 13 | } 14 | 15 | export function increasesByOneFromZero(arr: number[]) { 16 | for (let i = 0; i < arr.length; i++) { 17 | if (arr[i] !== i) return false; 18 | } 19 | return true; 20 | } 21 | 22 | export function tinslNearleyError(e: Error) { 23 | const simple = simplifyNearleyErrorMessage(e.message); 24 | const matches = simple.match(/line ([0-9]+) col ([0-9]+)/); 25 | const index = simple.indexOf("\n\n"); 26 | if (matches === null) throw new Error("no matches in nearley error"); 27 | const line = parseInt(matches[1]); 28 | const col = parseInt(matches[2]); 29 | // strip out the arrow pointing to character 30 | let simpler = simple.substr(index, simple.length).split("\n")[4]; 31 | // lowercase u and remove period 32 | simpler = "u" + simpler.substr(0, simpler.length - 1).slice(1); 33 | return new TinslAggregateError([new TinslLineError(simpler, { line, col })]); 34 | } 35 | 36 | export function hexColorToVector(str: string) { 37 | if (str[0] !== "#") return; 38 | str = str.slice(1); // get rid of the # at the beginning 39 | if (![3, 4, 6, 8].includes(str.length)) { 40 | throw new TinslError("invalid length for hex color"); 41 | } 42 | const num = str.length === 3 || str.length === 4 ? 1 : 2; 43 | const vals = num === 2 ? str.match(/../g) : str.match(/./g); 44 | if (vals === null) throw new Error("no match in color"); 45 | const vec = vals.map((n) => parseInt(n, 16) / (num === 1 ? 7 : 255)); 46 | if (vec.includes(NaN)) throw new TinslError("not a valid color"); 47 | return vec; 48 | } 49 | 50 | export function toColorKey(str: string) { 51 | return str 52 | .toLowerCase() 53 | .split("") 54 | .filter((a) => a !== " " && a !== "\t") 55 | .join(""); 56 | } 57 | 58 | export const arrayPad = (arr: T[], len: number, elem: U): (T | U)[] => 59 | arr.concat(new Array(len - arr.length).fill(elem)); 60 | 61 | // TODO make this an invalid ident 62 | export const NON_CONST_ID = "non_const_identity"; 63 | -------------------------------------------------------------------------------- /testsuite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tinsl test suite 6 | 7 | 8 |

output

9 | 10 |

input

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /testsuite.ts: -------------------------------------------------------------------------------- 1 | import { higherOrderSpiral } from "./src/runner/draws"; 2 | import { Runner } from "./src/runner/runner"; 3 | 4 | const glCanvas = document.getElementById("gl") as HTMLCanvasElement; 5 | const gl = glCanvas.getContext("webgl2"); 6 | 7 | if (gl === null) throw new Error("problem getting the gl context"); 8 | 9 | const sourceCanvas = document.getElementById("source") as HTMLCanvasElement; 10 | const source = sourceCanvas.getContext("2d"); 11 | 12 | console.log(source); 13 | 14 | if (source === null) throw new Error("problem getting the source context"); 15 | 16 | /* 17 | const grd = source.createLinearGradient(0, 0, 960, 0); 18 | grd.addColorStop(0, "black"); 19 | grd.addColorStop(1, "white"); 20 | source.fillStyle = grd; 21 | source.fillRect(0, 0, 960, 540); 22 | source.fillStyle = "white"; 23 | source.fillRect(960 / 4, 540 / 4, 960 / 2, 540 / 2); 24 | */ 25 | 26 | const bloom = `def threshold 0.2 27 | uniform float u_size; 28 | 29 | fn luma(vec4 color) { 30 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 31 | } 32 | 33 | fn blur5(vec2 direction, int channel) { 34 | uv := pos / res; 35 | off1 := vec2(1.3333333333333333) * direction; 36 | 37 | mut color := vec4(0.); 38 | 39 | color += frag(uv, channel) * 0.29411764705882354; 40 | color += frag(uv + (off1 / res), channel) * 0.35294117647058826; 41 | color += frag(uv - (off1 / res), channel) * 0.35294117647058826; 42 | 43 | return color; 44 | } 45 | 46 | pr two_pass_blur(float size, int reps, int channel = -1) { 47 | loop reps { 48 | blur5(vec2(size, 0.), channel); refresh; 49 | blur5(vec2(0., size), channel); refresh; 50 | } 51 | } 52 | 53 | { vec4(frag.rgb * (step(1. - luma(frag0), 1. - threshold)), frag.a); } -> 1 54 | 55 | { @two_pass_blur(size: 1., reps: 3, channel: 1);} -> 1 56 | //{ vec4(1., 0., 0., 1.); } -> 1 57 | 58 | { frag0 + frag1; } -> 0 59 | `; 60 | 61 | // FIXME 62 | const blur13Func = `fn blur13(vec2 dir, int channel = -1) { 63 | uv := pos / res; 64 | mut col := vec4(0.); 65 | off1 := vec2(1.411764705882353) * dir; 66 | off2 := vec2(3.2941176470588234) * dir; 67 | off3 := vec2(5.176470588235294) * dir; 68 | col += frag(channel, uv) * 0.1964825501511404; 69 | col += frag(frag, uv + (off1 / res)) * 0.2969069646728344; 70 | col += frag(frag, uv - (off1 / res)) * 0.2969069646728344; 71 | col += frag(frag, uv + (off2 / res)) * 0.09447039785044732; 72 | col += frag(frag, uv - (off2 / res)) * 0.09447039785044732; 73 | col += frag(frag, uv + (off3 / res)) * 0.010381362401148057; 74 | col += frag(frag, uv - (off3 / res)) * 0.010381362401148057; 75 | return col; 76 | }`; 77 | 78 | const redSimple = `0 -> { frag0 * vec4(1., 0., 0., 1.); } -> 0`; 79 | 80 | const rgbTextures = ` 81 | { vec4(1., 0., 0., 1.); } -> 1 82 | { vec4(0., 1., 0., 1.); } -> 2 83 | { vec4(0., 0., 1., 1.); } -> 3 84 | { frag0 * (frag1 + frag2); } 85 | `; 86 | 87 | const ifElse = ` 88 | fn redOrBlue (int x) { 89 | if (x > 0) return vec4(1., 0., 0., 1.); 90 | else return vec4(0., 0., 1., 1.); 91 | } 92 | 93 | { redOrBlue(-1); } -> 1 94 | { redOrBlue(1); } -> 2 95 | 0 -> { frag * (frag1 + frag2); } 96 | `; 97 | 98 | const forLoop = ` 99 | fn keepDividingByTwo (float x, int reps) { 100 | mut r := 1.; 101 | for (int i = 0; i < reps; i++) { 102 | r /= float(2); 103 | } 104 | return r; 105 | } 106 | 107 | { frag * keepDividingByTwo(1., 3); } 108 | `; 109 | 110 | const sobelFunc = ` 111 | vec4 sobel(int channel = -1) { 112 | uv := pos / res; 113 | w := 1. / res.x; 114 | h := 1. / res.y; 115 | 116 | k := vec4[]( 117 | frag(channel, uv + vec2(-w, -h)), 118 | frag(channel, uv + vec2(0., -h)), 119 | frag(channel, uv + vec2(w, -h)), 120 | frag(channel, uv + vec2(-w, 0.)), 121 | 122 | frag(channel, uv + vec2(w, 0.)), 123 | frag(channel, uv + vec2(-w, h)), 124 | frag(channel, uv + vec2(0., h)), 125 | frag(channel, uv + vec2(w, h)) 126 | ); 127 | 128 | edge_h := k[2] + (2. * k[4]) + k[7] - (k[0] + (2. * k[3]) + k[5]); 129 | edge_v := k[0] + (2. * k[1]) + k[2] - (k[5] + (2. * k[6]) + k[7]); 130 | sob := sqrt(edge_h * edge_h + edge_v * edge_v); 131 | 132 | return vec4(1. - sob.rgb, 1.); 133 | }`; 134 | 135 | const godraysFunc = `fn godrays ( 136 | vec4 col = frag, 137 | float exposure = 1., 138 | float decay = 1., 139 | float density = 1., 140 | float weight = 0.01, 141 | vec2 light_pos = vec2(.5, .5), 142 | int num_samples = 100, 143 | int channel = -1 144 | ) { 145 | mut uv := pos / res; 146 | delta_uv := (uv - light_pos) / float(num_samples) * density; 147 | 148 | mut illumination_decay := 1.; 149 | 150 | vec4 color = col; 151 | 152 | for (int i = 0; i < num_samples; i++) { 153 | uv -= delta_uv; 154 | tex_sample := frag(channel, uv) * illumination_decay * weight; 155 | color += tex_sample; 156 | illumination_decay *= decay; 157 | } 158 | 159 | return color * exposure; 160 | }`; 161 | 162 | const twoPassBlurProc = ` 163 | pr two_pass_blur(float size, int reps = 2, int channel = -1) { 164 | loop reps { 165 | blur13(vec2(size, 0.), channel); refresh; 166 | blur13(vec2(0., size), channel); refresh; 167 | } 168 | }`; 169 | 170 | const identityMultTest = ` 171 | { vec4(float(int[](1)[int(cos(0.))]), 0., 0., 1.); } 172 | `; 173 | 174 | const fullTest = [ 175 | godraysFunc, 176 | blur13Func, 177 | twoPassBlurProc, 178 | sobelFunc, 179 | `0 -> { frag; } -> 1 180 | 1 -> { @two_pass_blur(reps: 10, size: 1.); } -> 1 181 | 182 | fn outline (int channel = -1) { 183 | sob := sobel(channel); 184 | avg := (sob.x + sob.y + sob.z) / 3.; 185 | return vec4(avg, avg, avg, 1.); 186 | } 187 | 188 | //{ frag1 + vec4(1. - outline().rgb, 1.); } 189 | //{ godrays(num_samples: 75); } 190 | `, 191 | ].join("\n"); 192 | 193 | const mysteriousBlur = `def threshold 0.2 194 | uniform float u_size; 195 | 196 | fn luma(vec4 color) { 197 | return dot(color.rgb, vec3(0.299, 0.587, 0.114)); 198 | } 199 | 200 | fn blur13(vec2 dir, int channel = -1) { 201 | uv := pos / res; 202 | mut col := vec4(0.); 203 | off1 := vec2(1.411764705882353) * dir; 204 | off2 := vec2(3.2941176470588234) * dir; 205 | off3 := vec2(5.176470588235294) * dir; 206 | col += frag(channel, uv) * 0.1964825501511404; 207 | col += frag(channel, uv + (off1 / res)) * 0.2969069646728344; 208 | col += frag(channel, uv - (off1 / res)) * 0.2969069646728344; 209 | col += frag(channel, uv + (off2 / res)) * 0.09447039785044732; 210 | col += frag(channel, uv - (off2 / res)) * 0.09447039785044732; 211 | col += frag(channel, uv + (off3 / res)) * 0.010381362401148057; 212 | col += frag(channel, uv - (off3 / res)) * 0.010381362401148057; 213 | return col; 214 | } 215 | 216 | pr two_pass_blur(float size, int reps, int channel = -1) { 217 | loop reps { 218 | blur13(vec2(size, 0.), channel); refresh; 219 | blur13(vec2(0., size), channel); refresh; 220 | } 221 | } 222 | 223 | { vec4(frag.rgb * (step(1. - luma(frag0), 1. - threshold)), frag.a); } -> 999 224 | 225 | 999 -> { @two_pass_blur(42., 1, 999); } -> 999 226 | 227 | {frag999.grba;} -> 1001 228 | 229 | 999 -> { @two_pass_blur(1., 1, 999); } -> 999 230 | 231 | {frag999.gbra;} -> 1002 232 | 233 | {frag1002;}`; 234 | const onceTest = `once { once { 'blue'4; }}`; 235 | 236 | const blueWrapped = `loop 9 { { frag0 + 'blue'4 / 9.; } }`; 237 | 238 | const blueUnwrapped = `loop 9 { frag0 + 'blue'4 / 9.; }`; 239 | 240 | const code = blueUnwrapped; 241 | 242 | console.log(code); 243 | 244 | let runner: Runner; 245 | 246 | try { 247 | runner = new Runner(gl, code, [sourceCanvas], {}); 248 | } catch (err) { 249 | console.log(err.message); 250 | throw "look at the logged error message"; 251 | } 252 | 253 | let drawingFunc = higherOrderSpiral([255, 0, 0], [0, 0, 0]); 254 | 255 | let frame = 0; 256 | 257 | /* 258 | const animate = (time: number) => { 259 | runner.draw(); 260 | drawingFunc(time / 1000, frame, source, sourceCanvas); 261 | requestAnimationFrame(animate); 262 | frame++; 263 | }; 264 | 265 | animate(0); 266 | */ 267 | 268 | drawingFunc(0, frame, source, sourceCanvas); 269 | runner.draw(); 270 | -------------------------------------------------------------------------------- /testtsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "lib": ["ES2020", "DOM"], 6 | "declaration": true, 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "typeRoots": ["node_modules/@types"], 11 | "moduleResolution": "node" 12 | }, 13 | "exclude": ["./dist/**"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.notests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["./dist/**", "**/*.test.ts", "**/test.*.ts"] 4 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | //const webpack = require("webpack"); 3 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 4 | 5 | module.exports = (env, argv) => { 6 | const name = env.playground ? "playground" : "testsuite"; 7 | 8 | // only include all the loaders and plugin when building the playground 9 | // because monaco needs that, not the test suite 10 | const rules = 11 | name === "playground" 12 | ? [ 13 | { 14 | test: /\.css$/, 15 | use: ["style-loader", "css-loader"], 16 | }, 17 | { 18 | test: /\.ttf$/, 19 | use: ["file-loader"], 20 | }, 21 | ] 22 | : []; 23 | 24 | const plugins = 25 | name === "playground" 26 | ? [ 27 | new MonacoWebpackPlugin({ 28 | features: [ 29 | "!accessibilityHelp", // TODO see what this does 30 | "!anchorSelect", 31 | "!bracketMatching", 32 | "!caretOperations", 33 | "!clipboard", 34 | "!codeAction", 35 | "!codelens", 36 | "!colorPicker", 37 | "!comment", 38 | "!contextmenu", 39 | "!coreCommands", 40 | "!cursorUndo", 41 | "!dnd", 42 | "!documentSymbols", 43 | "!find", 44 | "!folding", 45 | "!fontZoom", 46 | "!format", 47 | "!gotoError", 48 | "!gotoLine", 49 | "!gotoSymbol", 50 | "!hover", 51 | "!iPadShowKeyboard", 52 | "!inPlaceReplace", 53 | "!inlineHints", 54 | "!inspectTokens", 55 | "!linesOperations", 56 | "!linkedEditing", 57 | "!links", 58 | "!multicursor", 59 | "!parameterHints", 60 | "!quickCommand", 61 | "!quickHelp", 62 | "!quickOutline", 63 | "!referenceSearch", 64 | "!rename", 65 | "!smartSelect", 66 | "!snippets", 67 | "!suggest", 68 | "!toggleHighContrast", 69 | "!toggleTabFocusMode", 70 | "!transpose", 71 | "!viewportSemanticTokens", 72 | "!wordHighlighter", 73 | "!wordOperations", 74 | "!wordPartOperations", 75 | ], 76 | }), 77 | ] 78 | : []; 79 | 80 | return { 81 | mode: env.production ? "production" : "development", 82 | entry: env.playground ? "./dist/playground.js" : "./dist/testsuite.js", 83 | output: { 84 | filename: `bundle.js`, 85 | path: path.resolve(__dirname, `${name}-bundle`), 86 | }, 87 | module: { 88 | rules: rules, 89 | }, 90 | plugins: plugins, 91 | experiments: { 92 | topLevelAwait: true, 93 | }, 94 | }; 95 | }; 96 | --------------------------------------------------------------------------------