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
|
/*
* lowering pass to create an ir from a catskill source tree.
*
* the idea is to fully de-sugar a catskill source file
* into an ir that can be very easily re-expressed into
* a low-level language, like our transpilation target, c.
*
* the lowering pass is itself split into two passes, one
* initial pass, which collects all top-level type & function
* declarations to build up a table of all available language objects,
* and then a second pass going over each function body.
* this de-couples usage of the functions and types from their ordering
* allowing for more free-flowing files than c would allow.
*
* additionally, we handle type dependencies by collecting every
* direct reference a type has to another, and topologically
* sort the types to create the correct ordering of them,
* pointing out any unbreakable cycles to the user as they come up.
*
* Copyright (c) 2026, Mel G. <mel@rnrd.eu>
*
* SPDX-License-Identifier: MPL-2.0
*/
#pragma once
#include "catboot.h"
void
lower_emit_error(struct Unit* unit, struct Span span, struct String message)
{
struct Diagnostic d = {
.severity = DIAGNOSTIC_ERROR,
.span = span,
.message = message,
};
array_push(&unit->diagnostics, &d);
unit->had_error = true;
}
void
lower_emit_error_c(struct Unit* unit, struct Span span, const ascii* message)
{
lower_emit_error(unit, span, string_from_c_string(message));
}
bool
lower_type_lookup_by_name(struct Unit* unit, struct String name, Type_Id* out_id)
{
FOR_EACH_ARRAY(struct Type_Name_To_Id, mapping, &unit->types.by_name, {
if (string_equals(mapping.name, name)) {
*out_id = mapping.id;
return true;
}
})
return false;
}
bool
lower_function_lookup_by_name(struct Unit* unit, struct String name, Function_Id* out_id)
{
FOR_EACH_ARRAY(struct Function_Name_To_Id, mapping, &unit->functions.by_name, {
if (string_equals(mapping.name, name)) {
*out_id = mapping.id;
return true;
}
})
return false;
}
Type_Id
lower_seed_primitive(struct Unit* unit, const ascii* name)
{
Type_Id id = array_length(&unit->types.entries);
struct String name_str = string_from_static_c_string(name);
struct Type* type = type_new(id, TYPE_PRIMITIVE, name_str, span_empty());
array_push(&unit->types.entries, &type);
struct Type_Name_To_Id mapping = { .name = name_str, .id = id };
array_push(&unit->types.by_name, &mapping);
return id;
}
// turns a tree-side type expression into an ir Type_Ref. for now this only
// resolves simple named types (primitives + user-named types already in the
// table); compound and structural shapes get deferred to later commits.
struct Type_Ref
lower_intern_type_ref(struct Unit* unit, struct Tree_Type* tree_type)
{
struct Type_Ref ref = {
.type_id = unit->types.primitive_void_id,
.mods = array_new(enum Type_Modifier, 4),
};
if (!tree_type || tree_type->type == TREE_TYPE_NONE) return ref;
switch (tree_type->type) {
case TREE_TYPE_NAME: {
Type_Id id;
if (lower_type_lookup_by_name(unit, tree_type->value.name.name, &id)) {
ref.type_id = id;
} else {
lower_emit_error(
unit, tree_type->span,
string_concatenate(
ARG_ASCII, "undefined type '", ARG_STRING, tree_type->value.name.name,
ARG_ASCII, "'", ARG_END));
}
return ref;
}
default:
// compound types (T?, [T], (T1, T2), &T, etc.) arrive in a later commit.
lower_emit_error_c(
unit, tree_type->span, "unimplemented: only simple named types are supported for now");
return ref;
}
}
void
lower_declare_function(
struct Unit* unit, struct String name, struct Tree_Expression* fn_expr, struct Span span)
{
Function_Id existing;
if (lower_function_lookup_by_name(unit, name, &existing)) {
lower_emit_error(
unit, span,
string_concatenate(
ARG_ASCII, "duplicate function '", ARG_STRING, name, ARG_ASCII, "'", ARG_END));
return;
}
Function_Id id = array_length(&unit->functions.entries);
struct Function* fn = function_new(id, name);
fn->is_main = string_equals_c_str(name, "main");
fn->return_type = lower_intern_type_ref(unit, fn_expr->value.function.header.return_type);
fn->params = array_new(struct Param, 16);
bool variadic = false;
FOR_EACH (
struct Tree_Type*, param_type, fn_expr->value.function.header.parameters_type_and_name) {
struct Param p = {
.name = param_type->value_name,
.type = lower_intern_type_ref(unit, param_type),
};
array_push(&fn->params, &p);
if (param_type->variadic) variadic = true;
}
fn->variadic = variadic;
fn->main_takes_args = fn->is_main && array_length(&fn->params) > 0;
fn->ast_body = &fn_expr->value.function.body;
fn->body = nil;
array_push(&unit->functions.entries, &fn);
struct Function_Name_To_Id mapping = { .name = name, .id = id };
array_push(&unit->functions.by_name, &mapping);
}
// extract `name = fun (...) ret { ... }` from a top-level statement,
// or return false if it's not that shape.
bool
lower_match_function_decl(
struct Tree_Statement* stmt, struct String* out_name, struct Tree_Expression** out_fn_expr)
{
if (stmt->kind != TREE_STATEMENT_EXPRESSION) return false;
struct Tree_Expression* expr = stmt->value.expression.inner;
if (!expr || expr->kind != TREE_EXPRESSION_BINARY_OPERATION) return false;
if (expr->value.binary_operator.operation != BINARY_ASSIGN) return false;
struct Tree_Expression* lhs = expr->value.binary_operator.left_operand;
struct Tree_Expression* rhs = expr->value.binary_operator.right_operand;
if (!lhs || lhs->kind != TREE_EXPRESSION_NAME) return false;
if (!rhs || rhs->kind != TREE_EXPRESSION_FUNCTION) return false;
*out_name = lhs->value.name.name;
*out_fn_expr = rhs;
return true;
}
bool
lower_match_type_decl(struct Tree_Statement* stmt)
{
if (stmt->kind != TREE_STATEMENT_EXPRESSION) return false;
struct Tree_Expression* expr = stmt->value.expression.inner;
if (!expr || expr->kind != TREE_EXPRESSION_BINARY_OPERATION) return false;
if (expr->value.binary_operator.operation != BINARY_ASSIGN) return false;
struct Tree_Expression* lhs = expr->value.binary_operator.left_operand;
struct Tree_Expression* rhs = expr->value.binary_operator.right_operand;
if (!lhs || lhs->kind != TREE_EXPRESSION_NAME) return false;
if (!rhs || rhs->kind != TREE_EXPRESSION_TYPE) return false;
return true;
}
// pass 1 walks every top-level statement and collects declarations into
// the unit's tables. function bodies stay as raw ast for pass 2 to lower.
void
lower_pass_1(struct Unit* unit, struct Tree* tree)
{
FOR_EACH (struct Tree_Statement*, stmt, tree->top_level_statements) {
struct String name;
struct Tree_Expression* fn_expr;
if (lower_match_function_decl(stmt, &name, &fn_expr)) {
lower_declare_function(unit, name, fn_expr, stmt->span);
continue;
}
if (lower_match_type_decl(stmt)) {
lower_emit_error_c(unit, stmt->span, "unimplemented: type declarations");
continue;
}
if (stmt->kind == TREE_STATEMENT_PRAGMA) {
lower_emit_error_c(unit, stmt->span, "unimplemented: top-level pragmas");
continue;
}
lower_emit_error_c(unit, stmt->span, "unsupported top-level statement");
}
}
void
lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit)
{
(void)source;
unit->types.entries = array_new(struct Type*, 256);
unit->types.by_hash = array_new(struct Type_Hash_To_Id, 256);
unit->types.by_name = array_new(struct Type_Name_To_Id, 256);
unit->functions.entries = array_new(struct Function*, 256);
unit->functions.by_name = array_new(struct Function_Name_To_Id, 256);
unit->imports = array_new(struct Import, 32);
unit->type_emission_order = array_new(Type_Id, 256);
unit->had_error = false;
unit->diagnostics = array_new(struct Diagnostic, 64);
unit->types.primitive_int_id = lower_seed_primitive(unit, "int");
unit->types.primitive_uint_id = lower_seed_primitive(unit, "uint");
unit->types.primitive_bool_id = lower_seed_primitive(unit, "bool");
unit->types.primitive_string_id = lower_seed_primitive(unit, "string");
unit->types.primitive_float_id = lower_seed_primitive(unit, "float");
unit->types.primitive_byte_id = lower_seed_primitive(unit, "byte");
unit->types.primitive_ascii_id = lower_seed_primitive(unit, "ascii");
unit->types.primitive_void_id = lower_seed_primitive(unit, "void");
if (tree) lower_pass_1(unit, tree);
}
|