aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-16 16:16:19 +0200
committeruser <user@clank>2026-08-16 16:16:19 +0200
commita540b74dd3bb04d33a69219487cb2db32f2022f1 (patch)
tree4e21d8c3cf739c0f9cd2e91983e23fd4e796cb63
parentsl0ppty: sin and cos (diff)
downloadslosh-a540b74dd3bb04d33a69219487cb2db32f2022f1.tar.gz
slosh-a540b74dd3bb04d33a69219487cb2db32f2022f1.tar.xz
slosh-a540b74dd3bb04d33a69219487cb2db32f2022f1.zip
sl0ppty: PI is 180
Angles here are degrees, and pi in an angle language is not a number -- it is a half turn. So `PI` is 180, `TAU` is 360, and a formula written the radian way ports across as written: sin(TAU * x / cols) one cycle over the pane's width sin(TAU * p * 3 / perim) three lobes fitted exactly to a border PI / 2 a quarter turn PI / 6 thirty degrees, exactly That last line is the argument. A numeric pi would be 3 in this language, and `3.14159 / 6` in integers is 0 -- so radians cannot express a sixth of a turn here at all, while degrees give it as a whole number. The pun is not a compromise; it is the more precise reading of the two. Which is why there is no `deg2rad` or `rad2deg` beside them, and asking for one says `unknown function`. There is one angle unit. A conversion could only lose the angle (`deg2rad(90)` is 1, and `sin(1)` is nearly nothing) or invent a second scale for the language to disagree with itself about. Spelled-out numbers rather than variables: they read no environment, carry no dependency, and fold into the program like the literals they are. The browser preview learned the same two, from the same table. Verified: 12 cases in `tests/expr_test.c`, including that `deg2rad` and `rad2deg` are refused; the cross-check agrees with the compiler on 700 evaluations, now including `sin(TAU * x / cols)` and friends. Two smaller things ride along, both from the repl. `PI`/`TAU` tab-complete, and its drift test grew a third table to check -- it already caught `sin`/`cos` missing before I did. And it now checks the script *parses* before starting a pane that runs it: I broke the indentation of one line here, and the failure arrived as `IndexError: list index out of range` from a pane that died before its prompt, which is a long way from "line 212".
-rw-r--r--config/config.kdl7
-rwxr-xr-xcontrib/shader-repl6
-rw-r--r--contrib/shadertoy.html7
-rw-r--r--docs/shaders.md9
-rw-r--r--src/expr.c27
-rw-r--r--tests/expr_test.c18
-rwxr-xr-xtests/test_shader_repl.py22
-rw-r--r--tests/test_shadertoy.py5
8 files changed, 99 insertions, 2 deletions
diff --git a/config/config.kdl b/config/config.kdl
index 6fc59d2..9ee458a 100644
--- a/config/config.kdl
+++ b/config/config.kdl
@@ -475,6 +475,13 @@ states {
// breathe with nothing to rescale. Any angle
// wraps, negative ones included, since `t / 4`
// does not stop at 360.
+// constants PI TAU 180 and 360: pi as an *angle*, which is all it can
+// be here -- a half turn and a whole one. So a
+// radian-shaped formula ports across as written,
+// `sin(TAU * x / cols)` is one cycle over the pane,
+// and the common fractions stay exact where
+// `3.14159 / 6` in integers would be 0. There is no
+// deg2rad/rad2deg: one unit, nothing to convert.
//
// There are no loops and no recursion, on purpose: a config cannot spin, and
// there is nothing to sandbox. An expression that does not compile drops that
diff --git a/contrib/shader-repl b/contrib/shader-repl
index f154bb8..4410a24 100755
--- a/contrib/shader-repl
+++ b/contrib/shader-repl
@@ -65,6 +65,8 @@ PROPS = ["amount=", "color=", "channel=", "where=", "at=", "radius=", "band=",
VALUES = ['"chrome"', '"content"', '"fg"', '"bg"', '"both"']
VARS = ["x", "y", "cols", "rows", "curx", "cury", "cursor", "focused", "t",
"since"]
+# Angles are degrees, so pi is a half turn: `PI` is 180 and `TAU` is 360.
+CONSTS = ["PI", "TAU"]
FUNCS = ["min(", "max(", "abs(", "clamp(", "dist(", "sin(", "cos("]
HISTORY = os.path.join(
@@ -77,7 +79,7 @@ def complete(text, state):
inside an expression, but a prompt that guesses which you meant and offers
nothing when it guesses wrong is worse company than one that offers
everything starting with what you typed."""
- words = COMMANDS + SHADERS + PROPS + VALUES + VARS + FUNCS
+ words = COMMANDS + SHADERS + PROPS + VALUES + VARS + CONSTS + FUNCS
hits = [w for w in words if w.startswith(text)]
return hits[state] if state < len(hits) else None
@@ -207,6 +209,8 @@ def main():
print(" shaders: " + " ".join(SHADERS))
print(" keys: " + " ".join(p.rstrip("=") for p in PROPS))
print(" vars: " + " ".join(VARS))
+ print(" consts: " + " ".join(CONSTS)
+ + " (degrees, so PI is a half turn)")
print(" funcs: " + " ".join(f.rstrip("(") for f in FUNCS))
print(" tab completes all of it; up/down and ctrl-r are history")
continue
diff --git a/contrib/shadertoy.html b/contrib/shadertoy.html
index 070f139..b1c9614 100644
--- a/contrib/shadertoy.html
+++ b/contrib/shadertoy.html
@@ -134,6 +134,12 @@
const DEP = { POS: 1, CURSOR: 2, TIME: 4, FOCUS: 8, SIZE: 16 };
+/* Pi is not a number in an angle language, it is a half turn -- and angles here
+ are degrees, so `PI` is 180 and `TAU` is 360. A radian-shaped formula ports
+ across unchanged, and the common fractions of a turn stay whole. Same table as
+ src/expr.c, spelled-out numbers rather than variables. */
+const CONSTS = { PI: 180, TAU: 360 };
+
const VARS = {
x: { dep: DEP.POS },
y: { dep: DEP.POS },
@@ -206,6 +212,7 @@ function compile(src) {
ws(); if (src[i] !== ")") fail("expected )"); i++;
return makeCall(name, args);
}
+ if (name in CONSTS) { const k = CONSTS[name]; return () => k; }
const v = VARS[name];
if (!v) { i = s; fail("unknown name"); }
deps |= v.dep;
diff --git a/docs/shaders.md b/docs/shaders.md
index 716395a..5197cb9 100644
--- a/docs/shaders.md
+++ b/docs/shaders.md
@@ -66,6 +66,15 @@ shaders {
| functions | `min(a,b)` `max(a,b)` `abs(a)` `clamp(v,lo,hi)` |
| | `dist(x1,y1,x2,y2)` — counts a row double, because a cell is about twice as tall as it is wide |
| | `sin(deg)` `cos(deg)` — **degrees** in, −255..255 out, so `128 + sin(t / 4) / 2` is a breathe |
+| constants | `PI` = 180, `TAU` = 360 — pi as an *angle*, which is what it is here: a half turn |
+
+`PI` being a half turn is what makes a radian-shaped formula port across as
+written: `sin(TAU * x / cols)` is one cycle over the pane's width, `PI / 2` is a
+quarter turn, `PI / 6` is thirty degrees. It is also *more* exact than radians
+could be in a language with no fractions — a sixth of a turn is 30 whole degrees,
+where `3.14159 / 6` in integers is 0. For the same reason there is no `deg2rad`
+or `rad2deg`: there is one angle unit, and a conversion could only lose the angle
+or invent a second scale to disagree with.
There are no loops and no recursion, on purpose: a config cannot spin and there
is nothing to sandbox. An expression that does not compile drops that one shader
diff --git a/src/expr.c b/src/expr.c
index 92a1dcf..501ac03 100644
--- a/src/expr.c
+++ b/src/expr.c
@@ -27,6 +27,28 @@ enum {
enum { V_X, V_Y, V_COLS, V_ROWS, V_CURX, V_CURY, V_CURSOR, V_FOCUSED, V_T,
V_SINCE };
+/* Angles are counted in degrees here (see `isin`), and pi is not a number in an
+ * angle language -- it is a half turn. So `PI` is 180 and `TAU` is 360, and a
+ * formula written the radian way ports across unchanged: `sin(TAU * x / cols)` is
+ * one cycle across the pane, `PI / 2` is a quarter turn, `PI / 6` is thirty
+ * degrees. It is also *more* exact than radians can be in integers, where
+ * 3.14159 / 6 is 0 and the common fractions of a turn are whole degrees.
+ *
+ * Which is why there is no `deg2rad` or `rad2deg` beside them. There is one angle
+ * unit and these are two of its landmarks; a conversion would have to either lose
+ * the angle (`deg2rad(90)` is 1, and `sin(1)` is nearly nothing) or invent a
+ * second scale for the language to disagree with itself about.
+ *
+ * Spelled-out numbers rather than variables: they read no environment, carry no
+ * dependency, and fold into the program like the literals they are. */
+static const struct {
+ const char *name;
+ int32_t value;
+} CONSTS[] = {
+ {"PI", 180}, /* a half turn */
+ {"TAU", 360}, /* ...and a whole one */
+};
+
static const struct {
const char *name;
int slot;
@@ -219,6 +241,11 @@ static void parse_primary(parser_t *ps, expr_prog_t *pr) {
return;
}
+ for (size_t i = 0; i < sizeof CONSTS / sizeof *CONSTS; i++) {
+ if (strcmp(CONSTS[i].name, name) != 0) continue;
+ emit(ps, pr, OP_PUSH, CONSTS[i].value);
+ return;
+ }
for (size_t i = 0; i < sizeof VARS / sizeof *VARS; i++) {
if (strcmp(VARS[i].name, name) != 0) continue;
pr->deps |= VARS[i].dep;
diff --git a/tests/expr_test.c b/tests/expr_test.c
index b84e355..f775065 100644
--- a/tests/expr_test.c
+++ b/tests/expr_test.c
@@ -132,6 +132,24 @@ int main(void) {
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);
diff --git a/tests/test_shader_repl.py b/tests/test_shader_repl.py
index c3c46cf..8736420 100755
--- a/tests/test_shader_repl.py
+++ b/tests/test_shader_repl.py
@@ -10,6 +10,7 @@ becomes a list of what was true once.
The rest is the loop itself, driven the way a person drives it -- typed lines,
history, a tab -- in a real session with a real pty.
"""
+import ast
import os
import re
import subprocess
@@ -34,7 +35,8 @@ def repl_lists():
body = re.search(r"^%s = (\[[^\]]*\])" % var, src, re.M).group(1)
return [w.strip().strip('"\'').rstrip("=(").strip('"')
for w in re.findall(r'["\'][^"\']+["\']', body)]
- return {v: names(v) for v in ("COMMANDS", "SHADERS", "PROPS", "VARS", "FUNCS")}
+ return {v: names(v)
+ for v in ("COMMANDS", "SHADERS", "PROPS", "VARS", "CONSTS", "FUNCS")}
def source_names(path, pattern):
@@ -70,6 +72,19 @@ def session(cfg_path, data_home):
cols=84, rows=16, config=cfg_path, env=env)
+def test_the_script_is_a_script():
+ """First, because everything below it starts a pane that runs this file: a
+ syntax error otherwise shows up as `IndexError: list index out of range` from
+ a pane that died before printing its prompt, which is a long way from saying
+ line 212 is indented wrong."""
+ try:
+ ast.parse(open(REPL).read())
+ ok, why = True, ""
+ except SyntaxError as exc:
+ ok, why = False, "%s line %s: %s" % (exc.__class__.__name__, exc.lineno, exc.msg)
+ check("contrib/shader-repl parses", ok, why)
+
+
def test_the_completion_list_matches_the_code():
lists = repl_lists()
@@ -90,6 +105,10 @@ def test_the_completion_list_matches_the_code():
variables <= set(lists["VARS"]),
"missing: %s" % sorted(variables - set(lists["VARS"])))
+ consts = source_names("expr.c", r'\{"([A-Z]+)", \d+\},?\s')
+ check("every constant can be completed", consts <= set(lists["CONSTS"]),
+ "missing: %s" % sorted(consts - set(lists["CONSTS"])))
+
funcs = source_names("expr.c", r'\{"([a-z]+)", \d, OP_[A-Z]+\}')
check("every expression function can be completed",
funcs <= set(lists["FUNCS"]),
@@ -185,6 +204,7 @@ def test_help_lists_what_there_is():
if __name__ == "__main__":
+ test_the_script_is_a_script()
test_the_completion_list_matches_the_code()
test_every_offered_shader_is_accepted_by_a_session()
test_history_survives_the_next_run()
diff --git a/tests/test_shadertoy.py b/tests/test_shadertoy.py
index 2f88225..99e724a 100644
--- a/tests/test_shadertoy.py
+++ b/tests/test_shadertoy.py
@@ -123,6 +123,11 @@ EXPRS = [
"abs(sin(x * 9 + t / 6))",
"clamp(sin(t / 5) + 128, 0, 255)",
"sin(sin(t / 3))",
+ # pi as an angle: a half turn, in the degrees this language counts in
+ "PI", "TAU", "PI * 2 == TAU",
+ "sin(PI)", "sin(PI / 2)", "sin(PI / 6)", "cos(TAU)",
+ "sin(TAU * x / cols)", "sin(TAU * y / rows + t / 5)",
+ "128 + sin(TAU * t / 4000) / 2",
]