about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-05-06 18:49:26 +0200
committerMel <mel@rnrd.eu>2026-05-06 18:49:26 +0200
commit952250b6d79063aa1066b0e945d58360bd70d09f (patch)
tree8eff74504b558d5a83485cebd4d8c5808e0064a9
parentac696b22b3f03ae9670a9d1d154ce7ff848b5e55 (diff)
downloadcatskill-952250b6d79063aa1066b0e945d58360bd70d09f.tar.zst
catskill-952250b6d79063aa1066b0e945d58360bd70d09f.zip
Lowering pass test harness, and initial pass test suite
Signed-off-by: Mel <mel@rnrd.eu>
-rw-r--r--boot/catboot.c5
-rw-r--r--boot/tests/lower/c_for_loop.cskt35
-rw-r--r--boot/tests/lower/compound_assigns.cskt31
-rw-r--r--boot/tests/lower/empty_main.cskt23
-rw-r--r--boot/tests/lower/if_else_chain.cskt38
-rw-r--r--boot/tests/lower/range_for_loop.cskt33
-rw-r--r--boot/tests/lower/shadowing_error.cskt29
-rw-r--r--boot/tests/lower/simple_var.cskt26
-rw-r--r--boot/tests/lower/structural_dedup.cskt29
-rw-r--r--boot/tests/lower/type_cycle_error.cskt29
-rw-r--r--boot/tests/lower/type_simple_struct.cskt23
-rw-r--r--boot/tests/lower/type_with_pointer_self.cskt21
-rw-r--r--boot/tests/lower/while_loop.cskt34
-rw-r--r--boot/tests/test.c23
14 files changed, 373 insertions, 6 deletions
diff --git a/boot/catboot.c b/boot/catboot.c
index c75efed..3919a2a 100644
--- a/boot/catboot.c
+++ b/boot/catboot.c
@@ -133,7 +133,10 @@ debug_lower_pass(struct Source_File source_file)
     lower_tree(&tree, source_file, &unit);
     printer(&unit);
 
-    return unit.had_error ? 1 : 0;
+    // diagnostics are part of the printed unit, so the caller can read
+    // them from the dump. always returning success keeps snapshot tests
+    // viable for both the green and the diagnostic paths.
+    return 0;
 }
 
 integer
diff --git a/boot/tests/lower/c_for_loop.cskt b/boot/tests/lower/c_for_loop.cskt
new file mode 100644
index 0000000..98d78f0
--- /dev/null
+++ b/boot/tests/lower/c_for_loop.cskt
@@ -0,0 +1,35 @@
+c-style for loops are turned into the single loop kind,
+with the initializer and the iteration step statements synthesized into
+statements within the block.
+
+<<<
+
+main = fun () {
+    for i int = 0, i < 10, i++ {
+        var x int = i
+    }
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block
+			(block
+				(declaration i (ref int) (initializer (expr 0)))
+				(loop (condition (expr (binary < (expr (name i)) (expr 10)))) (block
+					(declaration x (ref int) (initializer (expr (name i))))
+					(assign (expr (name i)) (expr (binary + (expr (name i)) (expr 1))))
+				))
+			)
+		)))
+	(emission_order 0 1 2 3 4 5 6 7))
diff --git a/boot/tests/lower/compound_assigns.cskt b/boot/tests/lower/compound_assigns.cskt
new file mode 100644
index 0000000..214fe59
--- /dev/null
+++ b/boot/tests/lower/compound_assigns.cskt
@@ -0,0 +1,31 @@
+compound assignment operators are split during lowering.
+
+<<<
+
+main = fun () {
+    var x int = 0
+    x += 1
+    x *= 2
+    x -= 3
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block
+			(declaration x (ref int) (initializer (expr 0)))
+			(assign (expr (name x)) (expr (binary + (expr (name x)) (expr 1))))
+			(assign (expr (name x)) (expr (binary * (expr (name x)) (expr 2))))
+			(assign (expr (name x)) (expr (binary - (expr (name x)) (expr 3))))
+		)))
+	(emission_order 0 1 2 3 4 5 6 7))
diff --git a/boot/tests/lower/empty_main.cskt b/boot/tests/lower/empty_main.cskt
new file mode 100644
index 0000000..8e4635f
--- /dev/null
+++ b/boot/tests/lower/empty_main.cskt
@@ -0,0 +1,23 @@
+the simplest possible translation unit. seeds the eight primitive
+types and registers a single, empty `main` function.
+
+<<<
+
+main = fun () {
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block)))
+	(emission_order 0 1 2 3 4 5 6 7))
\ No newline at end of file
diff --git a/boot/tests/lower/if_else_chain.cskt b/boot/tests/lower/if_else_chain.cskt
new file mode 100644
index 0000000..a8bf358
--- /dev/null
+++ b/boot/tests/lower/if_else_chain.cskt
@@ -0,0 +1,38 @@
+a three-branch if/else-if/else chain lowers to a single flat
+conditional.
+
+<<<
+
+classify = fun (n int) int {
+    if n < 0 {
+        return -1
+    } else if n == 0 {
+        return 0
+    } else {
+        return 1
+    }
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 classify (returns (ref int)) (param n (ref int)) (block
+			(conditional (when (expr (binary < (expr (name n)) (expr 0)))) (block
+				(return (expr (unary - (expr 1))))
+			) (when (expr (binary == (expr (name n)) (expr 0)))) (block
+				(return (expr 0))
+			) (else) (block
+				(return (expr 1))
+			))
+		)))
+	(emission_order 0 1 2 3 4 5 6 7))
diff --git a/boot/tests/lower/range_for_loop.cskt b/boot/tests/lower/range_for_loop.cskt
new file mode 100644
index 0000000..64ffe0b
--- /dev/null
+++ b/boot/tests/lower/range_for_loop.cskt
@@ -0,0 +1,33 @@
+a range for-each is rewritten into a c-style loop.
+
+<<<
+
+main = fun () {
+    for i int = 0..10 {
+        var x int = i
+    }
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block
+			(block
+				(declaration i (ref int) (initializer (expr 0)))
+				(loop (condition (expr (binary < (expr (name i)) (expr 10)))) (block
+					(declaration x (ref int) (initializer (expr (name i))))
+					(assign (expr (name i)) (expr (binary + (expr (name i)) (expr 1))))
+				))
+			)
+		)))
+	(emission_order 0 1 2 3 4 5 6 7))
diff --git a/boot/tests/lower/shadowing_error.cskt b/boot/tests/lower/shadowing_error.cskt
new file mode 100644
index 0000000..b22dddf
--- /dev/null
+++ b/boot/tests/lower/shadowing_error.cskt
@@ -0,0 +1,29 @@
+declaring two locals with the same name in overlapping scopes is
+a hard error.
+
+<<<
+
+main = fun () {
+    var x int = 1
+    var x int = 2
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block
+			(declaration x (ref int) (initializer (expr 1)))
+		)))
+	(emission_order 0 1 2 3 4 5 6 7)
+	(diagnostics
+		(error "name 'x' shadows an existing binding")))
diff --git a/boot/tests/lower/simple_var.cskt b/boot/tests/lower/simple_var.cskt
new file mode 100644
index 0000000..b1b0c92
--- /dev/null
+++ b/boot/tests/lower/simple_var.cskt
@@ -0,0 +1,26 @@
+a single typed variable declaration with an integer initializer
+inside the body of `main`.
+
+<<<
+
+main = fun () {
+    var x int = 5
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block
+			(declaration x (ref int) (initializer (expr 5)))
+		)))
+	(emission_order 0 1 2 3 4 5 6 7))
\ No newline at end of file
diff --git a/boot/tests/lower/structural_dedup.cskt b/boot/tests/lower/structural_dedup.cskt
new file mode 100644
index 0000000..ee4bcd3
--- /dev/null
+++ b/boot/tests/lower/structural_dedup.cskt
@@ -0,0 +1,29 @@
+two functions taking the same anonymous tuple type produce just
+one synthesized type entry.
+
+<<<
+
+f = fun (a (int, int)) int { return 0 }
+g = fun (b (int, int)) int { return 0 }
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive)
+		(type 8 __cat_type_0 synthetic structure (field _0 (ref int)) (field _1 (ref int)) (depends_on 0 0)))
+	(functions
+		(function 0 f (returns (ref int)) (param a (ref __cat_type_0)) (block
+			(return (expr 0))
+		))
+		(function 1 g (returns (ref int)) (param b (ref __cat_type_0)) (block
+			(return (expr 0))
+		)))
+	(emission_order 0 1 2 3 4 5 6 7 8))
diff --git a/boot/tests/lower/type_cycle_error.cskt b/boot/tests/lower/type_cycle_error.cskt
new file mode 100644
index 0000000..29b791e
--- /dev/null
+++ b/boot/tests/lower/type_cycle_error.cskt
@@ -0,0 +1,29 @@
+two types referencing each other by value form a cycle that
+we cannot resolve.
+the diagnostic should point out the correct cycle.
+
+<<<
+
+A = type { b B }
+B = type { a A }
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive)
+		(type 8 A structure (field b (ref B)) (depends_on 9))
+		(type 9 B structure (field a (ref A)) (depends_on 8)))
+	(emission_order 0 1 2 3 4 5 6 7)
+	(diagnostics
+		(error "type cycle detected:
+  A (line 1) depends on B
+  B (line 2) depends on A
+hint: break the cycle with a reference (use `&T` instead of `T`). :)")))
diff --git a/boot/tests/lower/type_simple_struct.cskt b/boot/tests/lower/type_simple_struct.cskt
new file mode 100644
index 0000000..085d9db
--- /dev/null
+++ b/boot/tests/lower/type_simple_struct.cskt
@@ -0,0 +1,23 @@
+a top-level struct declaration produces a structure type entry
+with each field's type interned.
+the by-value dependencies are correctly added as dependencies
+and contribute to the ordering.
+
+<<<
+
+Point = type { x int, y int }
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive)
+		(type 8 Point structure (field x (ref int)) (field y (ref int)) (depends_on 0 0)))
+	(emission_order 0 1 2 3 4 5 6 7 8))
diff --git a/boot/tests/lower/type_with_pointer_self.cskt b/boot/tests/lower/type_with_pointer_self.cskt
new file mode 100644
index 0000000..6a0b689
--- /dev/null
+++ b/boot/tests/lower/type_with_pointer_self.cskt
@@ -0,0 +1,21 @@
+a self-referential structure relying on indirection via a reference.
+a dependency should not be added as the reference is not by-value.
+
+<<<
+
+Node = type { value int, next &Node }
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive)
+		(type 8 Node structure (field value (ref int)) (field next (ref & Node)) (depends_on 0)))
+	(emission_order 0 1 2 3 4 5 6 7 8))
diff --git a/boot/tests/lower/while_loop.cskt b/boot/tests/lower/while_loop.cskt
new file mode 100644
index 0000000..b191df5
--- /dev/null
+++ b/boot/tests/lower/while_loop.cskt
@@ -0,0 +1,34 @@
+while loops are 1:1 with the intermediate representation's
+single loop kind.
+compound assignments desugar into a simple pair of a simple
+assignment and binary operation.
+
+<<<
+
+main = fun () {
+    var i int = 0
+    while i < 10 {
+        i++
+    }
+}
+
+>>>
+
+(unit
+	(types
+		(type 0 int primitive)
+		(type 1 uint primitive)
+		(type 2 bool primitive)
+		(type 3 string primitive)
+		(type 4 float primitive)
+		(type 5 byte primitive)
+		(type 6 ascii primitive)
+		(type 7 void primitive))
+	(functions
+		(function 0 main main (returns (ref void)) (block
+			(declaration i (ref int) (initializer (expr 0)))
+			(loop (condition (expr (binary < (expr (name i)) (expr 10)))) (block
+				(assign (expr (name i)) (expr (binary + (expr (name i)) (expr 1))))
+			))
+		)))
+	(emission_order 0 1 2 3 4 5 6 7))
diff --git a/boot/tests/test.c b/boot/tests/test.c
index 841df09..051dbc4 100644
--- a/boot/tests/test.c
+++ b/boot/tests/test.c
@@ -63,9 +63,10 @@ enum Test_Target
     TARGET_NONE = 0,
     TARGET_LEX = 1 << 0,
     TARGET_PARSE = 1 << 1,
-    TARGET_TRANSPILE = 1 << 2,
+    TARGET_LOWER = 1 << 2,
+    TARGET_TRANSPILE = 1 << 3,
 
-    TARGET_ALL = TARGET_LEX | TARGET_PARSE | TARGET_TRANSPILE,
+    TARGET_ALL = TARGET_LEX | TARGET_PARSE | TARGET_LOWER | TARGET_TRANSPILE,
 };
 
 struct Known_Target
@@ -73,9 +74,8 @@ struct Known_Target
     const ascii* name;
     enum Test_Target target;
 } known_targets[] = {
-    { "lex", TARGET_LEX },
-    { "parse", TARGET_PARSE },
-    { "transpile", TARGET_TRANSPILE },
+    { "lex", TARGET_LEX },     { "parse", TARGET_PARSE },
+    { "lower", TARGET_LOWER }, { "transpile", TARGET_TRANSPILE },
 
     { "all", TARGET_ALL },
 };
@@ -532,6 +532,15 @@ parse_tests(bool adjust)
 }
 
 struct Result_Summary
+lower_tests(bool adjust)
+{
+    const ascii* base_path = "./boot/tests/lower/";
+    const ascii* base_command = "./build/catboot --test-lower";
+
+    return tests(base_path, base_command, adjust);
+}
+
+struct Result_Summary
 transpile_tests(bool adjust)
 {
     const ascii* base_path = "./boot/tests/transpile/";
@@ -582,6 +591,10 @@ main(int argc, const ascii** argv)
             summary = parse_tests(adjust);
             break;
 
+        case TARGET_LOWER:
+            summary = lower_tests(adjust);
+            break;
+
         case TARGET_TRANSPILE:
             summary = transpile_tests(adjust);
             break;