Coverage Report

Created: 2026-02-23 20:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/Users/alexjokela/projects/lattice/src/phase_check.c
Line
Count
Source
1
#include "phase_check.h"
2
#include "ds/hashmap.h"
3
#include <stdlib.h>
4
#include <string.h>
5
#include <stdio.h>
6
#include <stdarg.h>
7
8
typedef struct {
9
    AstMode mode;
10
    LatVec  errors;      /* vec of char* */
11
    LatMap *scope_stack;  /* array of LatMap (string -> AstPhase) */
12
    size_t  scope_count;
13
    size_t  scope_cap;
14
    LatMap  struct_defs;  /* name -> StructDecl* */
15
    LatMap  fn_defs;      /* name -> FnDecl* */
16
} PhaseChecker;
17
18
9
static void pc_init(PhaseChecker *pc, AstMode mode) {
19
9
    pc->mode = mode;
20
9
    pc->errors = lat_vec_new(sizeof(char *));
21
9
    pc->scope_cap = 8;
22
9
    pc->scope_count = 1;
23
9
    pc->scope_stack = malloc(pc->scope_cap * sizeof(LatMap));
24
9
    pc->scope_stack[0] = lat_map_new(sizeof(AstPhase));
25
9
    pc->struct_defs = lat_map_new(sizeof(StructDecl *));
26
9
    pc->fn_defs = lat_map_new(sizeof(FnDecl *));
27
9
}
28
29
9
static void pc_free(PhaseChecker *pc) {
30
18
    for (size_t i = 0; i < pc->scope_count; i++)
31
9
        lat_map_free(&pc->scope_stack[i]);
32
9
    free(pc->scope_stack);
33
9
    lat_map_free(&pc->struct_defs);
34
9
    lat_map_free(&pc->fn_defs);
35
9
}
36
37
15
static void pc_push_scope(PhaseChecker *pc) {
38
15
    if (pc->scope_count >= pc->scope_cap) {
39
0
        pc->scope_cap *= 2;
40
0
        pc->scope_stack = realloc(pc->scope_stack, pc->scope_cap * sizeof(LatMap));
41
0
    }
42
15
    pc->scope_stack[pc->scope_count++] = lat_map_new(sizeof(AstPhase));
43
15
}
44
45
15
static void pc_pop_scope(PhaseChecker *pc) {
46
15
    if (pc->scope_count > 1) {
47
15
        pc->scope_count--;
48
15
        lat_map_free(&pc->scope_stack[pc->scope_count]);
49
15
    }
50
15
}
51
52
33
static void pc_define(PhaseChecker *pc, const char *name, AstPhase phase) {
53
33
    lat_map_set(&pc->scope_stack[pc->scope_count - 1], name, &phase);
54
33
}
55
56
87
static AstPhase pc_lookup(const PhaseChecker *pc, const char *name) {
57
105
    for (size_t i = pc->scope_count; i > 0; i--) {
58
102
        AstPhase *p = lat_map_get(&pc->scope_stack[i - 1], name);
59
102
        if (p) return *p;
60
102
    }
61
3
    return PHASE_UNSPECIFIED;
62
87
}
63
64
0
static void pc_error(PhaseChecker *pc, const char *fmt, ...) {
65
0
    char *msg = NULL;
66
0
    va_list args;
67
0
    va_start(args, fmt);
68
0
    (void)vasprintf(&msg, fmt, args);
69
0
    va_end(args);
70
0
    lat_vec_push(&pc->errors, &msg);
71
0
}
72
73
/* Forward declarations */
74
static AstPhase pc_check_expr(PhaseChecker *pc, const Expr *expr);
75
static void pc_check_stmt(PhaseChecker *pc, const Stmt *stmt);
76
static void pc_check_spawn_stmt(PhaseChecker *pc, const Stmt *stmt);
77
static void pc_check_spawn_expr(PhaseChecker *pc, const Expr *expr);
78
79
258
static AstPhase pc_check_expr(PhaseChecker *pc, const Expr *expr) {
80
258
    switch (expr->tag) {
81
42
        case EXPR_INT_LIT: case EXPR_FLOAT_LIT: case EXPR_STRING_LIT: case EXPR_BOOL_LIT:
82
42
        case EXPR_NIL_LIT:
83
42
            return PHASE_UNSPECIFIED;
84
85
84
        case EXPR_IDENT:
86
84
            return pc_lookup(pc, expr->as.str_val);
87
88
12
        case EXPR_BINOP:
89
12
            pc_check_expr(pc, expr->as.binop.left);
90
12
            pc_check_expr(pc, expr->as.binop.right);
91
12
            return PHASE_UNSPECIFIED;
92
93
0
        case EXPR_UNARYOP:
94
0
            pc_check_expr(pc, expr->as.unaryop.operand);
95
0
            return PHASE_UNSPECIFIED;
96
97
3
        case EXPR_CALL: {
98
3
            pc_check_expr(pc, expr->as.call.func);
99
3
            AstPhase arg_phases[expr->as.call.arg_count];
100
6
            for (size_t i = 0; i < expr->as.call.arg_count; i++)
101
3
                arg_phases[i] = pc_check_expr(pc, expr->as.call.args[i]);
102
            /* In strict mode, check argument phases against function parameter constraints */
103
3
            if (pc->mode == MODE_STRICT && expr->as.call.func->tag == EXPR_IDENT) {
104
3
                const char *fn_name = expr->as.call.func->as.str_val;
105
3
                FnDecl **fdp = lat_map_get(&pc->fn_defs, fn_name);
106
3
                if (fdp) {
107
3
                    FnDecl *fd = *fdp;
108
                    /* Check all overloads — error only if no overload matches */
109
3
                    bool any_match = false;
110
3
                    for (FnDecl *cand = fd; cand; cand = cand->next_overload) {
111
3
                        bool match = true;
112
3
                        size_t check = expr->as.call.arg_count < cand->param_count
113
3
                                       ? expr->as.call.arg_count : cand->param_count;
114
6
                        for (size_t i = 0; i < check; i++) {
115
3
                            if (cand->params[i].is_variadic) break;
116
3
                            AstPhase pp = cand->params[i].ty.phase;
117
3
                            AstPhase ap = arg_phases[i];
118
3
                            if (pp == PHASE_FLUID && ap == PHASE_CRYSTAL) { match = false; break; }
119
3
                            if (pp == PHASE_CRYSTAL && ap == PHASE_FLUID) { match = false; break; }
120
3
                        }
121
3
                        if (match) { any_match = true; break; }
122
3
                    }
123
3
                    if (!any_match)
124
0
                        pc_error(pc, "strict mode: no matching overload for '%s' with given argument phases", fn_name);
125
3
                }
126
3
            }
127
3
            return PHASE_UNSPECIFIED;
128
42
        }
129
130
3
        case EXPR_METHOD_CALL:
131
3
            pc_check_expr(pc, expr->as.method_call.object);
132
3
            for (size_t i = 0; i < expr->as.method_call.arg_count; i++)
133
0
                pc_check_expr(pc, expr->as.method_call.args[i]);
134
3
            return PHASE_UNSPECIFIED;
135
136
51
        case EXPR_FIELD_ACCESS: {
137
51
            AstPhase obj_phase = pc_check_expr(pc, expr->as.field_access.object);
138
51
            return (obj_phase == PHASE_CRYSTAL) ? PHASE_CRYSTAL : obj_phase;
139
42
        }
140
141
12
        case EXPR_INDEX:
142
12
            pc_check_expr(pc, expr->as.index.object);
143
12
            pc_check_expr(pc, expr->as.index.index);
144
12
            return PHASE_UNSPECIFIED;
145
146
3
        case EXPR_ARRAY:
147
6
            for (size_t i = 0; i < expr->as.array.count; i++)
148
3
                pc_check_expr(pc, expr->as.array.elems[i]);
149
3
            return PHASE_UNSPECIFIED;
150
151
0
        case EXPR_SPREAD:
152
0
            pc_check_expr(pc, expr->as.spread_expr);
153
0
            return PHASE_UNSPECIFIED;
154
155
0
        case EXPR_TUPLE:
156
0
            for (size_t i = 0; i < expr->as.tuple.count; i++)
157
0
                pc_check_expr(pc, expr->as.tuple.elems[i]);
158
0
            return PHASE_CRYSTAL;
159
160
9
        case EXPR_STRUCT_LIT:
161
30
            for (size_t i = 0; i < expr->as.struct_lit.field_count; i++)
162
21
                pc_check_expr(pc, expr->as.struct_lit.fields[i].value);
163
9
            return PHASE_UNSPECIFIED;
164
165
12
        case EXPR_FREEZE: {
166
12
            AstPhase inner = pc_check_expr(pc, expr->as.freeze.expr);
167
12
            if (expr->as.freeze.contract)
168
0
                pc_check_expr(pc, expr->as.freeze.contract);
169
12
            if (pc->mode == MODE_STRICT && inner == PHASE_CRYSTAL)
170
0
                pc_error(pc, "strict mode: cannot freeze an already crystal value");
171
12
            return PHASE_CRYSTAL;
172
42
        }
173
174
6
        case EXPR_THAW: {
175
6
            AstPhase inner = pc_check_expr(pc, expr->as.freeze_expr);
176
6
            if (pc->mode == MODE_STRICT && inner == PHASE_FLUID)
177
0
                pc_error(pc, "strict mode: cannot thaw an already fluid value");
178
6
            return PHASE_FLUID;
179
42
        }
180
181
3
        case EXPR_CLONE:
182
3
            return pc_check_expr(pc, expr->as.freeze_expr);
183
184
0
        case EXPR_ANNEAL:
185
0
            pc_check_expr(pc, expr->as.anneal.expr);
186
0
            pc_check_expr(pc, expr->as.anneal.closure);
187
0
            return PHASE_CRYSTAL;
188
189
0
        case EXPR_FORGE:
190
0
            pc_push_scope(pc);
191
0
            for (size_t i = 0; i < expr->as.block.count; i++)
192
0
                pc_check_stmt(pc, expr->as.block.stmts[i]);
193
0
            pc_pop_scope(pc);
194
0
            return PHASE_CRYSTAL;
195
196
0
        case EXPR_CRYSTALLIZE:
197
0
            pc_check_expr(pc, expr->as.crystallize.expr);
198
0
            pc_push_scope(pc);
199
0
            for (size_t i = 0; i < expr->as.crystallize.body_count; i++)
200
0
                pc_check_stmt(pc, expr->as.crystallize.body[i]);
201
0
            pc_pop_scope(pc);
202
0
            return PHASE_UNSPECIFIED;
203
204
0
        case EXPR_SUBLIMATE:
205
0
            return pc_check_expr(pc, expr->as.freeze_expr);
206
207
0
        case EXPR_IF:
208
0
            pc_check_expr(pc, expr->as.if_expr.cond);
209
0
            pc_push_scope(pc);
210
0
            for (size_t i = 0; i < expr->as.if_expr.then_count; i++)
211
0
                pc_check_stmt(pc, expr->as.if_expr.then_stmts[i]);
212
0
            pc_pop_scope(pc);
213
0
            if (expr->as.if_expr.else_stmts) {
214
0
                pc_push_scope(pc);
215
0
                for (size_t i = 0; i < expr->as.if_expr.else_count; i++)
216
0
                    pc_check_stmt(pc, expr->as.if_expr.else_stmts[i]);
217
0
                pc_pop_scope(pc);
218
0
            }
219
0
            return PHASE_UNSPECIFIED;
220
221
0
        case EXPR_BLOCK:
222
0
            pc_push_scope(pc);
223
0
            for (size_t i = 0; i < expr->as.block.count; i++)
224
0
                pc_check_stmt(pc, expr->as.block.stmts[i]);
225
0
            pc_pop_scope(pc);
226
0
            return PHASE_UNSPECIFIED;
227
228
0
        case EXPR_CLOSURE:
229
0
            if (expr->as.closure.default_values) {
230
0
                for (size_t i = 0; i < expr->as.closure.param_count; i++)
231
0
                    if (expr->as.closure.default_values[i])
232
0
                        pc_check_expr(pc, expr->as.closure.default_values[i]);
233
0
            }
234
0
            return PHASE_UNSPECIFIED;
235
236
3
        case EXPR_RANGE:
237
3
            pc_check_expr(pc, expr->as.range.start);
238
3
            pc_check_expr(pc, expr->as.range.end);
239
3
            return PHASE_UNSPECIFIED;
240
241
15
        case EXPR_PRINT:
242
30
            for (size_t i = 0; i < expr->as.print.arg_count; i++)
243
15
                pc_check_expr(pc, expr->as.print.args[i]);
244
15
            return PHASE_UNSPECIFIED;
245
246
0
        case EXPR_TRY_CATCH:
247
0
            pc_push_scope(pc);
248
0
            for (size_t i = 0; i < expr->as.try_catch.try_count; i++)
249
0
                pc_check_stmt(pc, expr->as.try_catch.try_stmts[i]);
250
0
            pc_pop_scope(pc);
251
0
            pc_push_scope(pc);
252
0
            pc_define(pc, expr->as.try_catch.catch_var, PHASE_FLUID);
253
0
            for (size_t i = 0; i < expr->as.try_catch.catch_count; i++)
254
0
                pc_check_stmt(pc, expr->as.try_catch.catch_stmts[i]);
255
0
            pc_pop_scope(pc);
256
0
            return PHASE_UNSPECIFIED;
257
258
0
        case EXPR_SCOPE:
259
0
            pc_push_scope(pc);
260
0
            for (size_t i = 0; i < expr->as.block.count; i++)
261
0
                pc_check_stmt(pc, expr->as.block.stmts[i]);
262
0
            pc_pop_scope(pc);
263
0
            return PHASE_UNSPECIFIED;
264
265
0
        case EXPR_INTERP_STRING:
266
0
            for (size_t i = 0; i < expr->as.interp.count; i++)
267
0
                pc_check_expr(pc, expr->as.interp.exprs[i]);
268
0
            return PHASE_UNSPECIFIED;
269
270
0
        case EXPR_SPAWN:
271
0
            if (pc->mode == MODE_STRICT) {
272
0
                pc_push_scope(pc);
273
0
                for (size_t i = 0; i < expr->as.block.count; i++)
274
0
                    pc_check_spawn_stmt(pc, expr->as.block.stmts[i]);
275
0
                pc_pop_scope(pc);
276
0
            } else {
277
0
                pc_push_scope(pc);
278
0
                for (size_t i = 0; i < expr->as.block.count; i++)
279
0
                    pc_check_stmt(pc, expr->as.block.stmts[i]);
280
0
                pc_pop_scope(pc);
281
0
            }
282
0
            return PHASE_UNSPECIFIED;
283
284
0
        case EXPR_MATCH:
285
0
            pc_check_expr(pc, expr->as.match_expr.scrutinee);
286
0
            for (size_t i = 0; i < expr->as.match_expr.arm_count; i++) {
287
0
                MatchArm *arm = &expr->as.match_expr.arms[i];
288
0
                if (arm->guard) pc_check_expr(pc, arm->guard);
289
0
                for (size_t j = 0; j < arm->body_count; j++)
290
0
                    pc_check_stmt(pc, arm->body[j]);
291
0
            }
292
0
            return PHASE_UNSPECIFIED;
293
294
0
        case EXPR_ENUM_VARIANT:
295
0
            for (size_t i = 0; i < expr->as.enum_variant.arg_count; i++)
296
0
                pc_check_expr(pc, expr->as.enum_variant.args[i]);
297
0
            return PHASE_UNSPECIFIED;
298
299
0
        case EXPR_TRY_PROPAGATE:
300
0
            pc_check_expr(pc, expr->as.try_propagate_expr);
301
0
            return PHASE_UNSPECIFIED;
302
303
0
        case EXPR_SELECT:
304
0
            for (size_t i = 0; i < expr->as.select_expr.arm_count; i++) {
305
0
                SelectArm *arm = &expr->as.select_expr.arms[i];
306
0
                if (arm->channel_expr) pc_check_expr(pc, arm->channel_expr);
307
0
                if (arm->timeout_expr) pc_check_expr(pc, arm->timeout_expr);
308
0
                pc_push_scope(pc);
309
0
                if (arm->binding_name)
310
0
                    pc_define(pc, arm->binding_name, PHASE_UNSPECIFIED);
311
0
                for (size_t j = 0; j < arm->body_count; j++)
312
0
                    pc_check_stmt(pc, arm->body[j]);
313
0
                pc_pop_scope(pc);
314
0
            }
315
0
            return PHASE_UNSPECIFIED;
316
258
    }
317
0
    return PHASE_UNSPECIFIED;
318
258
}
319
320
66
static void pc_check_stmt(PhaseChecker *pc, const Stmt *stmt) {
321
66
    switch (stmt->tag) {
322
27
        case STMT_BINDING: {
323
27
            AstPhase value_phase = pc_check_expr(pc, stmt->as.binding.value);
324
27
            if (pc->mode == MODE_STRICT) {
325
27
                switch (stmt->as.binding.phase) {
326
0
                    case PHASE_UNSPECIFIED:
327
0
                        pc_error(pc, "strict mode: use 'flux' or 'fix' instead of 'let' for binding '%s'",
328
0
                                 stmt->as.binding.name);
329
0
                        break;
330
15
                    case PHASE_FLUID:
331
15
                        if (value_phase == PHASE_CRYSTAL)
332
0
                            pc_error(pc, "cannot bind crystal value with flux for '%s'",
333
0
                                     stmt->as.binding.name);
334
15
                        pc_define(pc, stmt->as.binding.name, PHASE_FLUID);
335
15
                        break;
336
12
                    case PHASE_CRYSTAL:
337
12
                        pc_define(pc, stmt->as.binding.name, PHASE_CRYSTAL);
338
12
                        break;
339
27
                }
340
27
            } else {
341
0
                AstPhase eff = stmt->as.binding.phase;
342
0
                if (eff == PHASE_UNSPECIFIED) eff = value_phase;
343
0
                pc_define(pc, stmt->as.binding.name, eff);
344
0
            }
345
27
            break;
346
27
        }
347
27
        case STMT_ASSIGN:
348
18
            if (pc->mode == MODE_STRICT && stmt->as.assign.target->tag == EXPR_IDENT) {
349
3
                AstPhase p = pc_lookup(pc, stmt->as.assign.target->as.str_val);
350
3
                if (p == PHASE_CRYSTAL)
351
0
                    pc_error(pc, "strict mode: cannot assign to crystal binding '%s'",
352
0
                             stmt->as.assign.target->as.str_val);
353
3
            }
354
18
            pc_check_expr(pc, stmt->as.assign.target);
355
18
            pc_check_expr(pc, stmt->as.assign.value);
356
18
            break;
357
18
        case STMT_EXPR:
358
18
            pc_check_expr(pc, stmt->as.expr);
359
18
            break;
360
0
        case STMT_RETURN:
361
0
            if (stmt->as.return_expr) pc_check_expr(pc, stmt->as.return_expr);
362
0
            break;
363
3
        case STMT_FOR:
364
3
            pc_check_expr(pc, stmt->as.for_loop.iter);
365
3
            pc_push_scope(pc);
366
3
            pc_define(pc, stmt->as.for_loop.var, PHASE_UNSPECIFIED);
367
9
            for (size_t i = 0; i < stmt->as.for_loop.body_count; i++)
368
6
                pc_check_stmt(pc, stmt->as.for_loop.body[i]);
369
3
            pc_pop_scope(pc);
370
3
            break;
371
0
        case STMT_WHILE:
372
0
            pc_check_expr(pc, stmt->as.while_loop.cond);
373
0
            pc_push_scope(pc);
374
0
            for (size_t i = 0; i < stmt->as.while_loop.body_count; i++)
375
0
                pc_check_stmt(pc, stmt->as.while_loop.body[i]);
376
0
            pc_pop_scope(pc);
377
0
            break;
378
0
        case STMT_LOOP:
379
0
            pc_push_scope(pc);
380
0
            for (size_t i = 0; i < stmt->as.loop.body_count; i++)
381
0
                pc_check_stmt(pc, stmt->as.loop.body[i]);
382
0
            pc_pop_scope(pc);
383
0
            break;
384
0
        case STMT_BREAK:
385
0
        case STMT_CONTINUE:
386
0
            break;
387
0
        case STMT_DESTRUCTURE: {
388
0
            pc_check_expr(pc, stmt->as.destructure.value);
389
0
            AstPhase eff = stmt->as.destructure.phase;
390
0
            if (pc->mode == MODE_STRICT && eff == PHASE_UNSPECIFIED)
391
0
                pc_error(pc, "strict mode: destructure requires explicit phase (flux/fix)");
392
0
            for (size_t i = 0; i < stmt->as.destructure.name_count; i++)
393
0
                pc_define(pc, stmt->as.destructure.names[i], eff);
394
0
            if (stmt->as.destructure.rest_name)
395
0
                pc_define(pc, stmt->as.destructure.rest_name, eff);
396
0
            break;
397
0
        }
398
0
        case STMT_IMPORT:
399
            /* Import bindings are unphased */
400
0
            if (stmt->as.import.alias)
401
0
                pc_define(pc, stmt->as.import.alias, PHASE_UNSPECIFIED);
402
0
            if (stmt->as.import.selective_names) {
403
0
                for (size_t i = 0; i < stmt->as.import.selective_count; i++)
404
0
                    pc_define(pc, stmt->as.import.selective_names[i], PHASE_UNSPECIFIED);
405
0
            }
406
0
            break;
407
0
        case STMT_DEFER:
408
0
            for (size_t i = 0; i < stmt->as.defer.body_count; i++)
409
0
                pc_check_stmt(pc, stmt->as.defer.body[i]);
410
0
            break;
411
66
    }
412
66
}
413
414
12
static void pc_check_fn(PhaseChecker *pc, const FnDecl *f) {
415
12
    pc_push_scope(pc);
416
15
    for (size_t i = 0; i < f->param_count; i++)
417
3
        pc_define(pc, f->params[i].name, f->params[i].ty.phase);
418
72
    for (size_t i = 0; i < f->body_count; i++)
419
60
        pc_check_stmt(pc, f->body[i]);
420
12
    pc_pop_scope(pc);
421
12
}
422
423
/* Spawn-specific checking */
424
425
0
static void pc_check_spawn_expr(PhaseChecker *pc, const Expr *expr) {
426
0
    switch (expr->tag) {
427
0
        case EXPR_IDENT:
428
0
            if (pc_lookup(pc, expr->as.str_val) == PHASE_FLUID)
429
0
                pc_error(pc, "strict mode: cannot use fluid binding '%s' across thread boundary in spawn",
430
0
                         expr->as.str_val);
431
0
            break;
432
0
        case EXPR_BINOP:
433
0
            pc_check_spawn_expr(pc, expr->as.binop.left);
434
0
            pc_check_spawn_expr(pc, expr->as.binop.right);
435
0
            break;
436
0
        case EXPR_UNARYOP:
437
0
            pc_check_spawn_expr(pc, expr->as.unaryop.operand);
438
0
            break;
439
0
        case EXPR_CALL:
440
0
            pc_check_spawn_expr(pc, expr->as.call.func);
441
0
            for (size_t i = 0; i < expr->as.call.arg_count; i++)
442
0
                pc_check_spawn_expr(pc, expr->as.call.args[i]);
443
0
            break;
444
0
        case EXPR_METHOD_CALL:
445
0
            pc_check_spawn_expr(pc, expr->as.method_call.object);
446
0
            for (size_t i = 0; i < expr->as.method_call.arg_count; i++)
447
0
                pc_check_spawn_expr(pc, expr->as.method_call.args[i]);
448
0
            break;
449
0
        case EXPR_FIELD_ACCESS:
450
0
            pc_check_spawn_expr(pc, expr->as.field_access.object);
451
0
            break;
452
0
        case EXPR_INDEX:
453
0
            pc_check_spawn_expr(pc, expr->as.index.object);
454
0
            pc_check_spawn_expr(pc, expr->as.index.index);
455
0
            break;
456
0
        case EXPR_FREEZE:
457
0
            pc_check_spawn_expr(pc, expr->as.freeze.expr);
458
0
            if (expr->as.freeze.contract)
459
0
                pc_check_spawn_expr(pc, expr->as.freeze.contract);
460
0
            break;
461
0
        case EXPR_THAW: case EXPR_CLONE:
462
0
            pc_check_spawn_expr(pc, expr->as.freeze_expr);
463
0
            break;
464
0
        case EXPR_ANNEAL:
465
0
            pc_check_spawn_expr(pc, expr->as.anneal.expr);
466
0
            pc_check_spawn_expr(pc, expr->as.anneal.closure);
467
0
            break;
468
0
        case EXPR_PRINT:
469
0
            for (size_t i = 0; i < expr->as.print.arg_count; i++)
470
0
                pc_check_spawn_expr(pc, expr->as.print.args[i]);
471
0
            break;
472
0
        case EXPR_INTERP_STRING:
473
0
            for (size_t i = 0; i < expr->as.interp.count; i++)
474
0
                pc_check_spawn_expr(pc, expr->as.interp.exprs[i]);
475
0
            break;
476
0
        case EXPR_ARRAY:
477
0
            for (size_t i = 0; i < expr->as.array.count; i++)
478
0
                pc_check_spawn_expr(pc, expr->as.array.elems[i]);
479
0
            break;
480
0
        case EXPR_SPREAD:
481
0
            pc_check_spawn_expr(pc, expr->as.spread_expr);
482
0
            break;
483
0
        case EXPR_TUPLE:
484
0
            for (size_t i = 0; i < expr->as.tuple.count; i++)
485
0
                pc_check_spawn_expr(pc, expr->as.tuple.elems[i]);
486
0
            break;
487
0
        case EXPR_STRUCT_LIT:
488
0
            for (size_t i = 0; i < expr->as.struct_lit.field_count; i++)
489
0
                pc_check_spawn_expr(pc, expr->as.struct_lit.fields[i].value);
490
0
            break;
491
0
        default:
492
0
            pc_check_expr(pc, expr);
493
0
            break;
494
0
    }
495
0
}
496
497
0
static void pc_check_spawn_stmt(PhaseChecker *pc, const Stmt *stmt) {
498
0
    switch (stmt->tag) {
499
0
        case STMT_EXPR:
500
0
            pc_check_spawn_expr(pc, stmt->as.expr);
501
0
            break;
502
0
        case STMT_BINDING:
503
0
            pc_check_spawn_expr(pc, stmt->as.binding.value);
504
0
            if (pc->mode == MODE_STRICT && stmt->as.binding.phase == PHASE_UNSPECIFIED)
505
0
                pc_error(pc, "strict mode: use 'flux' or 'fix' instead of 'let' for binding '%s'",
506
0
                         stmt->as.binding.name);
507
0
            pc_define(pc, stmt->as.binding.name, stmt->as.binding.phase);
508
0
            break;
509
0
        case STMT_ASSIGN:
510
0
            pc_check_spawn_expr(pc, stmt->as.assign.target);
511
0
            pc_check_spawn_expr(pc, stmt->as.assign.value);
512
0
            break;
513
0
        default:
514
0
            pc_check_stmt(pc, stmt);
515
0
            break;
516
0
    }
517
0
}
518
519
/* ── Phase-dispatch registration for phase checker ── */
520
521
0
static bool pc_phase_signatures_match(const FnDecl *a, const FnDecl *b) {
522
0
    if (a->param_count != b->param_count) return false;
523
0
    for (size_t i = 0; i < a->param_count; i++) {
524
0
        if (a->params[i].ty.phase != b->params[i].ty.phase) return false;
525
0
    }
526
0
    return true;
527
0
}
528
529
12
static void pc_register_fn(LatMap *fn_defs, FnDecl *new_fn) {
530
12
    FnDecl **existing = lat_map_get(fn_defs, new_fn->name);
531
12
    if (!existing) {
532
12
        lat_map_set(fn_defs, new_fn->name, &new_fn);
533
12
        return;
534
12
    }
535
0
    FnDecl *head = *existing;
536
0
    if (pc_phase_signatures_match(head, new_fn)) {
537
0
        new_fn->next_overload = head->next_overload;
538
0
        lat_map_set(fn_defs, new_fn->name, &new_fn);
539
0
        return;
540
0
    }
541
0
    for (FnDecl *prev = head; prev->next_overload; prev = prev->next_overload) {
542
0
        if (pc_phase_signatures_match(prev->next_overload, new_fn)) {
543
0
            new_fn->next_overload = prev->next_overload->next_overload;
544
0
            prev->next_overload = new_fn;
545
0
            return;
546
0
        }
547
0
    }
548
0
    new_fn->next_overload = head;
549
0
    lat_map_set(fn_defs, new_fn->name, &new_fn);
550
0
}
551
552
/* ── Public API ── */
553
554
9
LatVec phase_check(const Program *prog) {
555
9
    PhaseChecker pc;
556
9
    pc_init(&pc, prog->mode);
557
558
    /* Register structs and functions */
559
30
    for (size_t i = 0; i < prog->item_count; i++) {
560
21
        if (prog->items[i].tag == ITEM_STRUCT) {
561
9
            StructDecl *ptr = &prog->items[i].as.struct_decl;
562
9
            lat_map_set(&pc.struct_defs, ptr->name, &ptr);
563
12
        } else if (prog->items[i].tag == ITEM_FUNCTION) {
564
12
            FnDecl *ptr = &prog->items[i].as.fn_decl;
565
12
            pc_register_fn(&pc.fn_defs, ptr);
566
12
        }
567
21
    }
568
569
    /* Check all items */
570
30
    for (size_t i = 0; i < prog->item_count; i++) {
571
21
        switch (prog->items[i].tag) {
572
12
            case ITEM_FUNCTION:
573
12
                pc_check_fn(&pc, &prog->items[i].as.fn_decl);
574
12
                break;
575
9
            case ITEM_STRUCT:
576
9
                break;
577
0
            case ITEM_STMT:
578
0
                pc_check_stmt(&pc, prog->items[i].as.stmt);
579
0
                break;
580
0
            case ITEM_TEST:
581
0
                for (size_t j = 0; j < prog->items[i].as.test_decl.body_count; j++)
582
0
                    pc_check_stmt(&pc, prog->items[i].as.test_decl.body[j]);
583
0
                break;
584
0
            case ITEM_ENUM:
585
0
                break;
586
0
            case ITEM_TRAIT:
587
0
                break;
588
0
            case ITEM_IMPL:
589
0
                for (size_t j = 0; j < prog->items[i].as.impl_block.method_count; j++)
590
0
                    pc_check_fn(&pc, &prog->items[i].as.impl_block.methods[j]);
591
0
                break;
592
21
        }
593
21
    }
594
595
9
    LatVec errors = pc.errors;
596
9
    pc.errors = lat_vec_new(sizeof(char *));  /* prevent double-free */
597
9
    pc_free(&pc);
598
9
    return errors;
599
9
}