aboutsummaryrefslogtreecommitdiffstats
path: root/tests/expr_test.c
blob: 4745ca33e3582c3ede5069af5ae6043aa5135d1a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/* The shader expression compiler and its VM. Pure text in -> numbers out, so
 * pure tests: no screen, no terminal, no timing. */
#include "expr.h"

#include <stdio.h>
#include <string.h>

static int fails = 0;
static char detail[256];

static void ok(const char *name, bool cond, const char *what) {
  if (!cond) fails++;
  printf("%s %-56s %s\n", cond ? "ok  " : "FAIL", name, cond ? "" : what);
}

/* Evaluate at a fixed point, so a test says what it means about the maths. */
static int at(const char *src, int x, int y) {
  char err[128] = {0};
  expr_prog_t *p = expr_compile(src, err, sizeof err);
  if (!p) {
    snprintf(detail, sizeof detail, "%s: %s", src, err);
    return -99999;
  }
  expr_env_t env = {.x = x,
                    .y = y,
                    .cols = 80,
                    .rows = 24,
                    .curx = 10,
                    .cury = 5,
                    .cursor = 1,
                    .focused = 1,
                    .t = 1000,
                    .since = 250};
  int v = expr_eval(p, &env);
  expr_free(p);
  return v;
}

static void eq(const char *src, int want) {
  int got = at(src, 3, 4);
  if (got != want)
    snprintf(detail, sizeof detail, "%s -> %d, wanted %d", src, got, want);
  ok(src, got == want, detail);
}

static void refused(const char *label, const char *src) {
  char err[128] = {0};
  expr_prog_t *p = expr_compile(src, err, sizeof err);
  if (p) expr_free(p);
  snprintf(detail, sizeof detail, "%s compiled", src);
  ok(label, p == NULL && err[0] != 0, detail);
}

int main(void) {
  printf("-- arithmetic and precedence\n");
  eq("1 + 2 * 3", 7);
  eq("(1 + 2) * 3", 9);
  eq("10 - 3 - 2", 5); /* left-associative, not 9 */
  eq("7 % 3", 1);
  eq("-5 + 1", -4);
  eq("100 / 7", 14); /* integer division, truncating */

  printf("\n-- division by zero is defined, not a trap\n");
  eq("5 / 0", 0);
  eq("5 % 0", 0);
  eq("1 + 5 / 0", 1);

  printf("\n-- comparisons and logic produce 0 or 1\n");
  eq("3 < 4", 1);
  eq("4 < 3", 0);
  eq("3 <= 3", 1);
  eq("3 >= 4", 0);
  eq("3 == 3", 1);
  eq("3 != 3", 0);
  eq("1 && 0", 0);
  eq("1 || 0", 1);
  eq("!0", 1);
  eq("!5", 0);
  /* The reason comparisons return numbers: masking is how you write a rule. */
  eq("(3 < 4) * 200", 200);

  printf("\n-- bitwise, on the 32-bit value\n");
  eq("12 & 10", 8);
  eq("12 | 10", 14);
  eq("12 ^ 10", 6);
  eq("~0", -1);
  eq("~12", -13);
  eq("1 << 4", 16);
  eq("48 >> 2", 12);
  /* `>>` keeps the sign, so it halves a negative rather than turning it into a
   * large positive. Written out in the VM because C leaves it
   * implementation-defined and JavaScript does not -- and the browser preview is
   * JavaScript. */
  eq("0 - 8 >> 1", -4);
  eq("0 - 1 >> 28", -1);
  /* The count is masked to five bits, which is what JS does with it: a shift
   * nobody meant is defined rather than undefined. */
  eq("1 << 33", 2);
  eq("1 << 32", 1);
  eq("8 >> 33", 4);
  /* Bits bind tighter than comparison, unlike C, where `12 & 8 == 8` means
   * `12 & (8 == 8)` and answers 0. Ritchie's own regret; not worth inheriting. */
  eq("12 & 8 == 8", 1);
  eq("5 ^ 3 == 6", 1);
  /* And among themselves: & then ^ then |, as everywhere else. */
  eq("1 | 6 & 4", 5);
  eq("1 ^ 6 & 4", 5);
  eq("3 & 1 | 4 & 4", 5);
  /* `&&` and `||` still parse as themselves, one character longer. */
  eq("1 & 3 && 2 & 1", 0);
  eq("(1 & 3) && (2 & 2)", 1);
  eq("0 | 0 || 1 & 1", 1);
  /* The XOR texture, which is the reason to want any of this. */
  eq("x ^ y", 7);
  eq("(x ^ y) & 1", 1);

  printf("\n-- sin and cos: degrees in, -255..255 out\n");
  eq("sin(0)", 0);
  eq("sin(30)", 127); /* round(255 * 0.5) is 127, not 128: 127.5 rounds down */
  eq("sin(90)", 255);
  eq("sin(180)", 0);
  eq("sin(270)", -255);
  eq("cos(0)", 255);
  eq("cos(90)", 0);
  eq("cos(180)", -255);
  /* Symmetry, so the four quadrants are one curve and not four. */
  eq("sin(45) - sin(135)", 0);
  eq("sin(30) + sin(210)", 0);
  eq("cos(60) - sin(30)", 0);
  /* Any angle at all: wrapped, including negative ones, because `t / 4` does not
   * stop at 360 and an expression that dies at one turn is no use for animation. */
  eq("sin(360)", 0);
  eq("sin(450)", 255);
  eq("sin(0 - 90)", -255);
  eq("sin(0 - 450)", -255);
  eq("sin(36000 + 90)", 255);
  /* The shape a strength actually wants: a breathe centred half way up, which is
   * why the range is the same 255 the amounts use. */
  eq("128 + sin(90) / 2", 255);
  eq("128 + sin(270) / 2", 1); /* -255/2 truncates toward zero, so 128 - 127 */
  eq("abs(sin(270))", 255);

  printf("\n-- PI and TAU are angles, not numbers\n");
  eq("PI", 180);
  eq("TAU", 360);
  eq("PI * 2 == TAU", 1);
  /* The point of the pun: a formula written the radian way works as written. */
  eq("sin(PI)", 0);
  eq("sin(PI / 2)", 255);
  eq("cos(TAU)", 255);
  eq("sin(TAU * 3 / 4)", -255);
  /* ...and exactly, which real radians could not manage in integers: a sixth of
   * a turn is 30 whole degrees, where 3.14159 / 6 would be 0. */
  eq("sin(PI / 6)", 127);
  eq("sin(PI / 3)", 221);
  eq("sin(PI / 4)", 180);
  /* No conversions, because there is one unit and nothing to convert. */
  refused("deg2rad is not a thing", "deg2rad(90)");
  refused("nor rad2deg", "rad2deg(1)");

  printf("\n-- variables\n");
  eq("x", 3);
  eq("y", 4);
  eq("cols", 80);
  eq("rows", 24);
  eq("curx", 10);
  eq("cury", 5);
  eq("cursor", 1);
  eq("focused", 1);
  eq("t", 1000);
  eq("since", 250);
  /* What a flash is: full strength for a moment after the state began, then
   * nothing. `t` cannot express this — it says what time it is, not how long
   * ago something happened. */
  eq("(since < 300) * 255", 255);
  eq("(since < 200) * 255", 0);

  printf("\n-- functions\n");
  eq("min(3, 9)", 3);
  eq("max(3, 9)", 9);
  eq("abs(0 - 7)", 7);
  eq("clamp(300, 0, 255)", 255);
  eq("clamp(-5, 0, 255)", 0);
  eq("clamp(100, 0, 255)", 100);
  /* dist counts a row double, because a cell is about twice as tall as it is
   * wide. Four columns apart is 4; two rows apart is also 4. */
  eq("dist(0, 0, 4, 0)", 4);
  eq("dist(0, 0, 0, 2)", 4);

  printf("\n-- the ternary evaluates both sides and selects\n");
  eq("1 ? 10 : 20", 10);
  eq("0 ? 10 : 20", 20);
  eq("0 ? 1 / 0 : 7", 7); /* the untaken side is safe because nothing traps */

  printf("\n-- an expression that reads nothing is a constant\n");
  {
    char err[128] = {0};
    expr_prog_t *p = expr_compile("2 * 3 + 1", err, sizeof err);
    ok("it compiles", p != NULL, err);
    ok("with no dependencies", p && expr_deps(p) == 0, "deps");
    ok("folded at compile time", p && expr_constant(p) == 7, "value");
    expr_free(p);
  }

  printf("\n-- dependencies are derived from the source, not declared\n");
  {
    struct {
      const char *src;
      unsigned want;
    } cases[] = {
        {"128", 0},
        {"x * 2", EXPR_DEP_POS},
        {"y", EXPR_DEP_POS},
        {"cols / 2",
         EXPR_DEP_SIZE}, /* not constant: the rect is not known yet */
        {"dist(x, y, curx, cury)", EXPR_DEP_POS | EXPR_DEP_CURSOR},
        {"focused * 90", EXPR_DEP_FOCUS},
        {"t % 1000", EXPR_DEP_TIME},
        {"since", EXPR_DEP_TIME}, /* a clock, so never cached */
    };
    for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
      char err[128] = {0};
      expr_prog_t *p = expr_compile(cases[i].src, err, sizeof err);
      unsigned got = p ? expr_deps(p) : 0xffff;
      snprintf(detail, sizeof detail, "%s -> %u, wanted %u", cases[i].src, got,
               cases[i].want);
      ok(cases[i].src, p && got == cases[i].want, detail);
      expr_free(p);
    }
  }

  printf("\n-- the map is the whole point: computed once, reused\n");
  {
    char err[128] = {0};
    expr_prog_t *p = expr_compile("x + y", err, sizeof err);
    expr_env_t env = {.cols = 8, .rows = 4};
    uint8_t *m1 = NULL, *m2 = NULL;
    ok("a positional program has a map", expr_amount_map(p, &env, &m1), err);
    ok("with the right values", m1 && m1[0] == 0 && m1[8 * 1 + 2] == 3,
       "values");
    ok("asking again returns the same buffer",
       expr_amount_map(p, &env, &m2) && m2 == m1, "buffer");

    /* Resizing must rebuild it: the size is part of what it was computed from,
     * and a map kept across a resize is the stale-state bug this design is
     * supposed to make unrepresentable. */
    env.cols = 4;
    env.rows = 2;
    ok("a different rect rebuilds it",
       expr_amount_map(p, &env, &m2) && m2[4 * 1 + 1] == 2, "rebuild");
    expr_free(p);
  }

  printf("\n-- a clock-reading program has nothing to reuse\n");
  {
    char err[128] = {0};
    expr_prog_t *p = expr_compile("t % 255", err, sizeof err);
    expr_env_t env = {.cols = 4, .rows = 2};
    uint8_t *m = NULL;
    ok("no map is offered", !expr_amount_map(p, &env, &m), "map");
    expr_free(p);
  }

  printf("\n-- the cursor is part of the key\n");
  {
    char err[128] = {0};
    expr_prog_t *p = expr_compile("dist(x, y, curx, cury)", err, sizeof err);
    expr_env_t env = {.cols = 4, .rows = 2, .cursor = 1, .curx = 0, .cury = 0};
    uint8_t *m = NULL;
    expr_amount_map(p, &env, &m);
    int before = m[3];
    env.curx = 3;
    expr_amount_map(p, &env, &m);
    snprintf(detail, sizeof detail, "%d then %d", before, m[3]);
    ok("moving the cursor rebuilds the map", m[3] != before, detail);
    expr_free(p);
  }

  printf("\n-- what is refused, and says why\n");
  refused("an unknown name", "nosuchvar + 1");
  refused("an unknown function", "wobble(1, 2)");
  refused("an unbalanced paren", "(1 + 2");
  refused("a missing operand", "1 +");
  refused("trailing junk", "1 + 2 )");
  refused("an empty expression", "");
  refused("a ternary without its colon", "1 ? 2");
  refused("something that is not an expression at all", "@@@");

  printf("\n-- the source survives compilation\n");
  {
    /* The dump has to write back the line somebody wrote: bytecode cannot be
     * un-compiled, so the program keeps its text. Byte for byte, including a
     * constant expression -- folding must not eat the spelling. */
    const char *src = "(y % 2) * 40";
    char err[128] = {0};
    expr_prog_t *p = expr_compile(src, err, sizeof err);
    ok("a program remembers its source",
       p && expr_source(p) && strcmp(expr_source(p), src) == 0,
       p ? expr_source(p) : err);
    if (p) expr_free(p);
    p = expr_compile("1 + 2", err, sizeof err);
    ok("even one that folded to a constant",
       p && expr_source(p) && strcmp(expr_source(p), "1 + 2") == 0,
       p ? expr_source(p) : err);
    if (p) expr_free(p);
  }

  printf("\n-- totality: there is no loop to write\n");
  {
    /* The language has no way to express unbounded work, so this is a
     * statement about the grammar rather than a timing test: a deeply nested
     * expression is bounded by the program length limit and is refused rather
     * than accepted and run forever. */
    char big[4096];
    size_t n = 0;
    for (int i = 0; i < 200; i++)
      n += (size_t)snprintf(big + n, sizeof big - n, "1+");
    snprintf(big + n, sizeof big - n, "1");
    char err[128] = {0};
    expr_prog_t *p = expr_compile(big, err, sizeof err);
    if (p) expr_free(p);
    ok("an over-long program is refused, not truncated", p == NULL, err);
  }

  printf("\n%s (%d failures)\n", fails ? "FAILED" : "all green", fails);
  return fails ? 1 : 0;
}