diff options
| author | user <user@clank> | 2026-08-16 15:49:07 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-16 15:49:07 +0200 |
| commit | 7cd9de2c15f5b00b99ea8d974a4c73f0c2d4f972 (patch) | |
| tree | 476b659e7f5b54b0511b1b5e4dd9a559fa7230bf /src/expr.c | |
| parent | sl0ppty: zero strength is not a pass (diff) | |
| download | slosh-7cd9de2c15f5b00b99ea8d974a4c73f0c2d4f972.tar.gz slosh-7cd9de2c15f5b00b99ea8d974a4c73f0c2d4f972.tar.xz slosh-7cd9de2c15f5b00b99ea8d974a4c73f0c2d4f972.zip | |
sl0ppty: bits
`&` `|` `^` `~` `<<` `>>`, on the 32-bit value the VM already used. There was
nothing: `x & 7` answered `unexpected trailing input at offset 2` and
`(x ^ y) % 16 < 2` answered `expected ) at offset 3`, which is a fair
description of a parser that had never heard of either.
`^` is the reason to bother. `(x ^ y) & 1` is the cheapest interesting texture
there is, and the language could express a sweep, a ramp and a comparison but
not one bit of pattern without `%` gymnastics.
**Bits bind tighter than comparison, which is not what C does.** `x & 7 == 0`
means `x & (7 == 0)` in C -- a precedence Ritchie put on record as a mistake,
kept for a decade of compatibility that a shader expression in a config file
does not have. Here it asks what it looks like it asks. Among themselves the
usual `&` then `^` then `|`; shifts stay where C has them, just under `+ -`.
`&&` and `||` still parse as themselves: the bitwise rungs decline the first
character when a second one follows it.
Semantics are JavaScript's, deliberately and to the letter -- the shift count
masked to five bits (`1 << 33` is 2), `>>` keeping the sign (`-8 >> 1` is -4).
Not taste: `contrib/shadertoy.html` reimplements this language in JS so the
preview can run in a browser, and `tests/test_shadertoy.py` compares the two on
every expression it can think of. Anything C leaves implementation-defined had
to be pinned to what JS would show, so the signed right shift is written out in
the VM rather than left to the compiler.
Verified: 22 new cases in `tests/expr_test.c` covering the operators, both
precedence questions, the sign, and the masked count; the preview learned the
same six operators and the same ladder, and the cross-check now agrees with the
compiler on 564 evaluations, including generated ones that reach for bits. Then
seven bitwise chains in a real pane -- an XOR texture, a drifting one, a stepped
pulse that walks eight shades of `t >> 7 & 7` and needs no `abs`.
Diffstat (limited to 'src/expr.c')
| -rw-r--r-- | src/expr.c | 96 |
1 files changed, 94 insertions, 2 deletions
@@ -14,6 +14,11 @@ enum { OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, OP_NEG, OP_LT, OP_GT, OP_LE, OP_GE, OP_EQ, OP_NE, OP_AND, OP_OR, OP_NOT, + /* On the 32-bit value, with JavaScript's semantics to the letter: the shift + * count is masked to five bits and `>>` keeps the sign. Not a free choice -- + * contrib/shadertoy.html reimplements this language in JS and a test compares + * the two, so `x << 33` has to mean the same thing in both. */ + OP_BAND, OP_BXOR, OP_BOR, OP_BNOT, OP_SHL, OP_SHR, OP_MIN, OP_MAX, OP_ABS, OP_CLAMP, OP_DIST, OP_SELECT, /* cond a b -> a or b, both already evaluated */ }; @@ -140,6 +145,12 @@ static void parse_primary(parser_t *ps, expr_prog_t *pr) { emit(ps, pr, OP_NEG, 0); return; } + if (*ps->p == '~') { + ps->p++; + parse_primary(ps, pr); + emit(ps, pr, OP_BNOT, 0); + return; + } if (*ps->p == '!') { ps->p++; parse_primary(ps, pr); @@ -251,12 +262,70 @@ static void parse_add(parser_t *ps, expr_prog_t *pr) { } } -static void parse_cmp(parser_t *ps, expr_prog_t *pr) { +/* Bitwise, between arithmetic and comparison. + * + * C puts `&` `^` `|` *below* comparison, so `x & 7 == 0` there means + * `x & (7 == 0)`. Ritchie called that a mistake and it is one; a small language + * copying it would be inheriting a wart for the sake of familiarity, so here the + * bits bind tighter and the line asks what it looks like it asks. Shifts sit + * where C puts them, just under `+ -`. */ +static void parse_shift(parser_t *ps, expr_prog_t *pr) { parse_add(ps, pr); for (;;) { if (ps->failed) return; skip_ws(ps); uint8_t op; + if (eat(ps, "<<")) op = OP_SHL; + else if (eat(ps, ">>")) op = OP_SHR; + else return; + parse_add(ps, pr); + emit(ps, pr, op, 0); + } +} + +static void parse_band(parser_t *ps, expr_prog_t *pr) { + parse_shift(ps, pr); + for (;;) { + if (ps->failed) return; + skip_ws(ps); + /* Not the `&` of `&&`: that one belongs to the logical rung above. */ + if (*ps->p != '&' || ps->p[1] == '&') return; + ps->p++; + parse_shift(ps, pr); + emit(ps, pr, OP_BAND, 0); + } +} + +static void parse_bxor(parser_t *ps, expr_prog_t *pr) { + parse_band(ps, pr); + for (;;) { + if (ps->failed) return; + skip_ws(ps); + if (*ps->p != '^') return; + ps->p++; + parse_band(ps, pr); + emit(ps, pr, OP_BXOR, 0); + } +} + +static void parse_bor(parser_t *ps, expr_prog_t *pr) { + parse_bxor(ps, pr); + for (;;) { + if (ps->failed) return; + skip_ws(ps); + if (*ps->p != '|' || ps->p[1] == '|') return; + ps->p++; + parse_bxor(ps, pr); + emit(ps, pr, OP_BOR, 0); + } +} + +static void parse_cmp(parser_t *ps, expr_prog_t *pr) { + parse_bor(ps, pr); + for (;;) { + if (ps->failed) return; + skip_ws(ps); + uint8_t op; /* Longest first: `<` would otherwise eat the `<` of `<=` and leave an `=` * that parses as nothing. */ if (eat(ps, "<=")) op = OP_LE; @@ -266,7 +335,7 @@ static void parse_cmp(parser_t *ps, expr_prog_t *pr) { else if (*ps->p == '<') { ps->p++; op = OP_LT; } else if (*ps->p == '>') { ps->p++; op = OP_GT; } else return; - parse_add(ps, pr); + parse_bor(ps, pr); emit(ps, pr, op, 0); } } @@ -337,6 +406,7 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) { case OP_VAR: st[sp++] = vars[in->imm]; break; case OP_NEG: if (sp) st[sp - 1] = -st[sp - 1]; break; case OP_NOT: if (sp) st[sp - 1] = !st[sp - 1]; break; + case OP_BNOT: if (sp) st[sp - 1] = (int32_t)~(uint32_t)st[sp - 1]; break; case OP_ABS: if (sp) st[sp - 1] = st[sp - 1] < 0 ? -st[sp - 1] : st[sp - 1]; break; default: { int need = in->op == OP_CLAMP ? 3 : in->op == OP_DIST ? 4 @@ -358,6 +428,27 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) { case OP_EQ: st[sp - 2] = a == b; sp--; break; case OP_NE: st[sp - 2] = a != b; sp--; break; case OP_AND: st[sp - 2] = a && b; sp--; break; + case OP_BAND: st[sp - 2] = (int32_t)((uint32_t)a & (uint32_t)b); sp--; break; + case OP_BXOR: st[sp - 2] = (int32_t)((uint32_t)a ^ (uint32_t)b); sp--; break; + case OP_BOR: st[sp - 2] = (int32_t)((uint32_t)a | (uint32_t)b); sp--; break; + /* Count masked to five bits, like JS, so a silly shift is defined + * rather than undefined. Shifted unsigned because moving a bit into + * the sign of a signed value is undefined in C and simply wraps in JS, + * which is what the preview will show. */ + case OP_SHL: + st[sp - 2] = (int32_t)((uint32_t)a << (b & 31)); + sp--; + break; + /* Arithmetic: the sign is kept, so `-8 >> 1` is -4. Written out rather + * than left to the compiler, because a signed right shift is + * implementation-defined in C and this one has to match JS. */ + case OP_SHR: { + unsigned k = (unsigned)(b & 31); + uint32_t u = (uint32_t)a; + st[sp - 2] = (int32_t)(a < 0 ? ~((~u) >> k) : u >> k); + sp--; + break; + } case OP_OR: st[sp - 2] = a || b; sp--; break; case OP_MIN: st[sp - 2] = a < b ? a : b; sp--; break; case OP_MAX: st[sp - 2] = a > b ? a : b; sp--; break; @@ -413,6 +504,7 @@ static bool stack_fits(const expr_prog_t *pr) { case OP_VAR: sp++; break; case OP_NEG: case OP_NOT: + case OP_BNOT: case OP_ABS: break; case OP_CLAMP: case OP_SELECT: sp -= 2; break; |
