Repository navigation
Expand file tree
/
Copy pathcodegen_func.c
More file actions
3040 lines (2868 loc) · 143 KB
/
Copy pathcodegen_func.c
File metadata and controls
3040 lines (2868 loc) · 143 KB
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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "codegen_internal.h"
#include "../aether_error.h"
// True when this function definition is annotated `@c_callback`.
// The annotation (#235) marks a function as having a stable, externally-
// visible C symbol so it can be passed across module boundaries to C
// externs that take function pointers (HTTP route handlers, signal
// handlers, qsort comparators, libcurl callbacks).
int is_c_callback(ASTNode* func) {
return func && func->annotation &&
strncmp(func->annotation, "c_callback:", 11) == 0;
}
/* Imported functions are private to each translation unit so linkers that
reject duplicate symbols (macOS ld64) do not see one copy per importer.
Trailing-underscore names are the file-local convention (#279).
@c_callback overrides both: that symbol must stay externally addressable.
Top-level functions keep external linkage. Making them static would also
have solved the libaether collisions in #1366, but it breaks the #703
guarantee that an Aether-owned definition matches a force-included C
header, so collisions are resolved by renaming instead (see
rename_extern_colliding_functions). */
int fn_has_internal_linkage(ASTNode* func) {
if (!func || is_c_callback(func)) return 0;
if (func->is_imported) return 1;
if (func->value) {
size_t n = strlen(func->value);
if (n > 0 && func->value[n - 1] == '_') return 1; /* #279 */
}
return 0;
}
/* #2123: a small, loop-free, non-recursive function with internal
* linkage is emitted `static inline`. Every Aether function reached C as
* plain `static`, and at -O2 gcc's budget for a function not declared
* inline (max-inline-insns-auto, 15 insns) left the small value-returning
* maths of a physics engine — a 3x3 product, a quaternion rotation, a
* 3x3 solve — out of line: 1.6x on a joint-grid solve against the same
* functions marked inline. `inline` on a static function is a hint that
* raises the budget to max-inline-insns-single (200); the function keeps
* its address and its semantics. Only internal-linkage functions
* qualify: `inline` on an external C99 function changes what the
* translation unit must provide. */
static int fn_body_is_small_leaf(ASTNode* n, const char* self, int* budget) {
if (!n) return 1;
if (n->type == AST_WHILE_LOOP || n->type == AST_FOR_LOOP || n->type == AST_CLOSURE ||
n->type == AST_MATCH_STATEMENT) return 0;
if (n->type == AST_FUNCTION_CALL && n->value && self && strcmp(n->value, self) == 0) return 0;
if (--*budget < 0) return 0;
for (int i = 0; i < n->child_count; i++) {
if (!fn_body_is_small_leaf(n->children[i], self, budget)) return 0;
}
return 1;
}
static int fn_body_inlinable(ASTNode* func) {
if (!func || func->child_count == 0) return 0;
ASTNode* body = func->children[func->child_count - 1];
if (!body || body->type != AST_BLOCK) return 0;
int budget = 160; /* AST nodes — a 3x3 matrix product is ~120 */
return fn_body_is_small_leaf(body, func->value, &budget);
}
int fn_is_inline_candidate(ASTNode* func) {
if (!func || !fn_has_internal_linkage(func) || is_c_callback(func)) return 0;
return fn_body_inlinable(func);
}
// Returns the C symbol bound by a `@c_callback` annotation.
// `@c_callback("name") foo(...)` — uses "name" verbatim.
// `@c_callback foo(...)` — falls back to func->value, which
// after the module merger is the
// namespace-prefixed form (e.g.
// `vcr_dispatch`). That's the safe
// default — no symbol collisions
// between modules — and callers who
// want an unmangled symbol opt in
// explicitly with the parenthesised
// form.
// Returns NULL when the function is not @c_callback-annotated.
const char* c_callback_symbol(ASTNode* func) {
if (!is_c_callback(func)) return NULL;
const char* tag = func->annotation + 11;
return (tag && tag[0]) ? tag : (func->value ? func->value : NULL);
}
/* #2664: the @c_callback definition behind the function `fn`: `fn` itself
* when annotated, or, for a function written as several clauses, the first
* annotated clause, which binds the symbol of the whole set (its
* dispatcher's); NULL when none is. */
ASTNode* fn_c_callback_def(CodeGenerator* gen, ASTNode* fn) {
const DefClauses* dc = gen ? fn_def_clause_set(gen->program, fn) : NULL;
if (!dc) return is_c_callback(fn) ? fn : NULL;
/* The index found it once (#2897): a scan per clause was quadratic. */
return dc->c_callback;
}
// Look up a top-level @c_callback function by its current AST value
// (post-merge: the prefixed `<ns>_<name>` form for imported callbacks;
// the bare name for in-file ones) and return the C symbol it's bound
// to. Returns NULL when no such callback exists, so the caller can
// fall through to the default identifier-emission path.
//
// #2007: through the program index. This is asked for every identifier
// emitted, and a scan of the top level each time made codegen quadratic
// in the number of functions.
const char* lookup_c_callback_symbol(CodeGenerator* gen, const char* name) {
if (!gen || !gen->program || !name) return NULL;
ProgramIndex* ix = program_index(gen->program);
return ix ? strmap_get(&ix->c_callbacks, name) : NULL;
}
// The C symbol bound by `@extern("c_symbol")`, interned (whole however
// long, #2539), or NULL when `ext` was not declared via `@extern`
// (annotation begins with "c_symbol:"). The stored annotation is
// `c_symbol:NAME` for a plain `@extern` and `c_symbol:NAME;varargs`
// when the `@extern` declaration also carries a trailing `...`; the
// `;` delimiter never occurs inside a C identifier, so only NAME is
// taken regardless.
const char* extern_c_symbol(const ASTNode* ext) {
if (!ext || !ext->annotation) return NULL;
if (strncmp(ext->annotation, "c_symbol:", 9) != 0) return NULL;
const char* s = ext->annotation + 9;
const char* semi = strchr(s, ';');
size_t n = semi ? (size_t)(semi - s) : strlen(s);
return cg_intern_n(s, n);
}
// Is `ext` a variadic extern? True for the bare
// `extern foo(fmt: string, ...)` form and for the
// `@extern("sym") foo(fmt: string, ...)` form (annotation
// "c_symbol:NAME;varargs"). Delimiter-aware, so a C symbol that merely
// contains the substring "varargs" can never be misread as variadic.
static int extern_is_varargs(const ASTNode* ext) {
if (!ext) return 0;
return annotation_has_marker(ext->annotation, "varargs");
}
// `@c_import` (#1239): the prototype belongs to a C header, so emit none.
static int extern_is_c_import(const ASTNode* ext) {
if (!ext) return 0;
return annotation_has_marker(ext->annotation, "c_import");
}
// Register an extern function's parameter types for call-site cast emission.
// Called whenever generate_extern_declaration() processes a function.
void register_extern_func(CodeGenerator* gen, ASTNode* ext) {
if (!ext || !ext->value) return;
// Grow registry if needed
if (gen->extern_registry_count >= gen->extern_registry_capacity) {
int new_cap = gen->extern_registry_capacity * 2 + 8;
void* new_reg = realloc(gen->extern_registry,
(size_t)new_cap * sizeof(*gen->extern_registry));
if (!new_reg) return; // OOM: skip registration
gen->extern_registry = new_reg;
gen->extern_registry_capacity = new_cap;
}
int idx = gen->extern_registry_count++;
gen->extern_registry[idx].name = strdup(ext->value);
gen->extern_registry[idx].c_name = NULL;
// @extern("c_symbol") rebinds the call-site emission to a chosen
// C symbol while keeping the Aether-side name in the namespace.
{
const char* c_sym = extern_c_symbol(ext);
if (c_sym) {
gen->extern_registry[idx].c_name = strdup(c_sym);
}
}
gen->extern_registry[idx].param_count = ext->child_count;
gen->extern_registry[idx].is_variadic = extern_is_varargs(ext);
gen->extern_registry[idx].ret_type = ext->node_type;
gen->extern_registry[idx].params = NULL;
gen->extern_registry[idx].param_full = NULL;
gen->extern_registry[idx].params_aether = NULL;
gen->extern_registry[idx].params_retain = NULL;
gen->extern_registry[idx].params_noescape = NULL;
gen->extern_registry[idx].params_holds = NULL;
if (ext->child_count > 0) {
gen->extern_registry[idx].params = malloc(ext->child_count * sizeof(TypeKind));
/* Param annotations are stored as a comma-separated set on
* `param->annotation` so multiple stack on one slot
* (`@aether @retain string`). Test for an individual tag
* via substring match. The lazy-allocation pattern below
* keeps the per-extern parallel arrays NULL in the common
* (no-annotation) case. */
int any_aether = 0, any_retain = 0, any_noescape = 0, any_holds = 0;
for (int i = 0; i < ext->child_count; i++) {
ASTNode* param = ext->children[i];
if (!param || !param->annotation) continue;
if (strstr(param->annotation, "aether_param")) any_aether = 1;
if (strstr(param->annotation, "retain_param")) any_retain = 1;
if (strstr(param->annotation, "noescape_param")) any_noescape = 1;
if (strstr(param->annotation, "into_param") ||
strstr(param->annotation, "keep_param")) any_holds = 1;
}
if (any_aether) {
gen->extern_registry[idx].params_aether =
calloc(ext->child_count, sizeof(int));
}
if (any_retain) {
gen->extern_registry[idx].params_retain =
calloc(ext->child_count, sizeof(int));
}
if (any_noescape) {
gen->extern_registry[idx].params_noescape =
calloc(ext->child_count, sizeof(int));
}
if (any_holds) {
gen->extern_registry[idx].params_holds =
calloc(ext->child_count, sizeof(int));
}
gen->extern_registry[idx].param_full =
calloc(ext->child_count, sizeof(Type*));
for (int i = 0; i < ext->child_count; i++) {
ASTNode* param = ext->children[i];
if (param && param->node_type) {
gen->extern_registry[idx].params[i] = param->node_type->kind;
if (gen->extern_registry[idx].param_full) {
gen->extern_registry[idx].param_full[i] = param->node_type;
}
} else {
gen->extern_registry[idx].params[i] = TYPE_UNKNOWN;
}
if (param && param->annotation) {
if (gen->extern_registry[idx].params_aether &&
strstr(param->annotation, "aether_param")) {
gen->extern_registry[idx].params_aether[i] = 1;
}
if (gen->extern_registry[idx].params_retain &&
strstr(param->annotation, "retain_param")) {
gen->extern_registry[idx].params_retain[i] = 1;
}
if (gen->extern_registry[idx].params_noescape &&
strstr(param->annotation, "noescape_param")) {
gen->extern_registry[idx].params_noescape[i] = 1;
}
if (gen->extern_registry[idx].params_holds &&
(strstr(param->annotation, "into_param") ||
strstr(param->annotation, "keep_param"))) {
gen->extern_registry[idx].params_holds[i] = 1;
}
}
}
}
}
// Check if a function name is registered as a builder function. The
// registry holds module-qualified names with dots as underscores.
int is_builder_func_reg(CodeGenerator* gen, const char* func_name) {
if (!gen || !func_name) return 0;
const char* normalized = codegen_normalise_callee(func_name);
for (int i = 0; i < gen->builder_func_reg_count; i++) {
if (gen->builder_funcs_reg[i].name && strcmp(gen->builder_funcs_reg[i].name, normalized) == 0) {
return 1;
}
}
return 0;
}
/* Find a user-fn definition by name in the program AST. Helper for
* the bare-fn-adapter discovery pre-pass. */
static ASTNode* find_user_function_by_name(CodeGenerator* gen, const char* name) {
if (!gen || !gen->program || !name) return NULL;
ASTNode* direct = find_function_definition_by_name(gen->program, name);
if (direct) return direct;
/* #940: a qualified call `mod.fn(...)` keeps its dotted name on the AST,
* but the merged definition is `<namespace>_fn` — where the namespace is
* the LAST segment of the module path (module_get_namespace's rule:
* `std.foo.bar.fn` → namespace `bar` → `bar_fn`). Reconstruct that
* merged form and retry, so cross-module callees resolve here. Without
* it the bare-fn-arg discovery below never finds the callee, the
* fn-typed param is never inspected, and the env-ignoring adapter for a
* bare function passed across the import boundary is never emitted
* (`_aether_bare_adapter_<name>' undeclared` in the caller's TU). */
const char* last_dot = strrchr(name, '.');
if (last_dot && last_dot != name) {
const char* fn = last_dot + 1;
/* Namespace = last path segment before the function name. */
const char* ns_start = name;
for (const char* p = name; p < last_dot; p++) {
if (*p == '.') ns_start = p + 1;
}
size_t ns_len = (size_t)(last_dot - ns_start);
if (ns_len > 0) {
const char* merged = cg_internf("%.*s_%s", (int)ns_len, ns_start, fn);
for (int i = 0; i < gen->program->child_count; i++) {
ASTNode* c = gen->program->children[i];
if (c && (c->type == AST_FUNCTION_DEFINITION ||
c->type == AST_BUILDER_FUNCTION) &&
c->value && strcmp(c->value, merged) == 0) {
return c;
}
}
}
}
return NULL;
}
/* Pre-walk the AST registering bare-fn adapters for every coercion site
* where a bare named function is wrapped as `(_AeClosure){.fn=name,
* .env=NULL}`. This mirrors the registration that happens lazily at
* the wrap-site codegen — doing it eagerly lets emit_bare_fn_adapters
* run before main is emitted, so adapter forward declarations are
* visible to user code that calls through them.
*
* The mirror is conservative: we register any bare-fn identifier
* appearing as a call argument where the callee has a `fn`-typed
* param, or in an `_aether_box_closure(...)` wrap position
* (which the call-site arg coercion also handles). False positives
* are harmless — they just emit an unused adapter the C compiler
* inlines / discards. */
static void discover_bare_fn_adapters_walk(CodeGenerator* gen, ASTNode* node) {
if (!gen || !node) return;
/* AST_FUNCTION_CALL: register any bare-fn arg whose callee param
* is `fn`-typed. */
if (node->type == AST_FUNCTION_CALL && node->value && gen->program) {
/* box_closure is a builtin, so it never resolves as a user
* function below; register its bare-fn argument here or the
* adapter the wrap site emits is never declared. */
if (strcmp(node->value, "box_closure") == 0 && node->child_count == 1) {
ASTNode* arg = node->children[0];
if (arg && arg->type == AST_IDENTIFIER && arg->value &&
find_user_function_by_name(gen, arg->value)) {
register_bare_fn_adapter(gen, arg->value);
}
}
ASTNode* callee = find_user_function_by_name(gen, node->value);
if (callee) {
int pi = 0;
for (int j = 0; j < callee->child_count; j++) {
ASTNode* p = callee->children[j];
if (!p || p->type == AST_GUARD_CLAUSE || p->type == AST_BLOCK) continue;
/* Map param index to call-arg index. Skip _ctx
* auto-injection — the user's child index 0 maps to
* the callee's first non-_ctx param. */
if (p->node_type && p->node_type->kind == TYPE_FUNCTION &&
!p->node_type->is_fnptr &&
pi < node->child_count) {
ASTNode* arg = node->children[pi];
if (arg && arg->type == AST_IDENTIFIER && arg->value) {
if (find_user_function_by_name(gen, arg->value)) {
register_bare_fn_adapter(gen, arg->value);
}
}
}
pi++;
}
} else {
/* #3038: an extern's `fn` parameter, a binary import's stub
* among them, takes a named function through its adapter as an
* Aether function's does. Registered only at the wrap site, the
* adapter came after the code that called it: `undeclared`. */
const char* ext = extern_name_for_call(gen, node->value);
for (int ai = 0; ext && ai < node->child_count; ai++) {
Type* pt = lookup_extern_param_type(gen, ext, ai);
ASTNode* arg = node->children[ai];
if (pt && pt->kind == TYPE_FUNCTION && !pt->is_fnptr && arg &&
arg->type == AST_IDENTIFIER && arg->value &&
find_user_function_by_name(gen, arg->value))
register_bare_fn_adapter(gen, arg->value);
}
}
}
/* #2055: an identifier the typechecker marked as a function value. */
if (node->type == AST_IDENTIFIER && node->value && node->annotation &&
strcmp(node->annotation, "fn_value") == 0 &&
find_user_function_by_name(gen, node->value)) {
register_bare_fn_adapter(gen, node->value);
}
/* A bare function stored into a `ptr` slot (a field store, a struct
* literal's or a message's field, a global) is its code address, not a
* box, so it needs no adapter there (#2890, emit_closure_into_ptr). */
for (int i = 0; i < node->child_count; i++) {
discover_bare_fn_adapters_walk(gen, node->children[i]);
}
}
void discover_bare_fn_adapters(CodeGenerator* gen) {
if (!gen || !gen->program) return;
discover_bare_fn_adapters_walk(gen, gen->program);
}
/* Register `bare_fn_name` as needing an env-ignoring adapter emitted at
* file finalisation. Returns 1 if newly added, 0 if it was already
* registered. Idempotent / deduping — multiple wrap sites for the same
* bare fn share one adapter. */
int register_bare_fn_adapter(CodeGenerator* gen, const char* bare_fn_name) {
if (!gen || !bare_fn_name) return 0;
for (int i = 0; i < gen->bare_fn_adapter_count; i++) {
if (strcmp(gen->bare_fn_adapter_names[i], bare_fn_name) == 0) return 0;
}
if (gen->bare_fn_adapter_count >= gen->bare_fn_adapter_capacity) {
int new_cap = gen->bare_fn_adapter_capacity ? gen->bare_fn_adapter_capacity * 2 : 8;
char** nn = realloc(gen->bare_fn_adapter_names, sizeof(char*) * new_cap);
if (!nn) return 0;
gen->bare_fn_adapter_names = nn;
gen->bare_fn_adapter_capacity = new_cap;
}
gen->bare_fn_adapter_names[gen->bare_fn_adapter_count++] = strdup(bare_fn_name);
return 1;
}
/* Resolve a registered bare-fn name to its definition and emit the adapter
* signature `static R _aether_bare_adapter_<name>(void* _env, P0 _a0, ...)`.
* Returns the resolved fdef (and fills params/param_count/ret_c) so the
* caller can either terminate with `;` (forward decl) or `{ ... }` (body).
* Returns NULL when the bare fn can't be resolved (caller skips it). */
static ASTNode* emit_bare_fn_adapter_signature(CodeGenerator* gen,
const char* fname,
ASTNode** params,
int* param_count,
const char** ret_c_out) {
ASTNode* fdef = NULL;
for (int j = 0; j < gen->program->child_count; j++) {
ASTNode* c = gen->program->children[j];
if (c && (c->type == AST_FUNCTION_DEFINITION ||
c->type == AST_BUILDER_FUNCTION) &&
c->value && strcmp(c->value, fname) == 0) {
fdef = c;
break;
}
}
if (!fdef) return NULL;
/* The parameters and result as the function's prototype spells them,
* a clause set's decided over its clauses (#2644, #2645). */
int pc = 0;
for (int k = 0; k < fn_param_count(fdef) && pc < 16; k++) params[pc++] = fn_param_at(fdef, k);
*param_count = pc;
Type* rt = fn_result_type(gen, fdef);
const char* ret_c = rt ? get_c_type(rt) : "void";
*ret_c_out = ret_c;
fprintf(gen->output, "static AETHER_MAYBE_UNUSED %s _aether_bare_adapter_%s(void* _env",
ret_c, fname);
for (int k = 0; k < pc; k++) {
Type* pt = fn_param_type_at(gen, fdef, k);
fprintf(gen->output, ", %s _a%d", pt ? get_c_type(pt) : "int", k);
}
fprintf(gen->output, ")");
return fdef;
}
/* #2586: named functions used as values (a bare name that is not a call),
* found by a walk over the whole program before anything is classified or
* emitted.
*
* A function pointer typed `fn(...) -> string` may point at an Aether
* function or at C, and a call through it cannot tell which, nor which
* return of an Aether function ran. Since 0.792 a `string` function
* returning a field returns a copy of its own, so taking every result as
* borrowed leaked one string per call (ae3d's component getters, called as
* `setter(dst, getter(src))`), while taking every result as owned frees a
* string C keeps (strerror's static buffer). So the call decides at run
* time: a function used as a value that hands over owned strings marks the
* one it returns (aether_fnptr_give, at its uniform-heap returns), and the
* call takes its result as owned when it is the marked pointer, copying
* anything else (aether_fnptr_take). A C function never marks, whatever
* way its pointer arrived (cast from a raw `ptr`, an extern's result, a
* field of C memory, an extern named as a value), and a function handed to
* C is the function itself.
*
* The arguments follow the closure convention, slot by slot: a call through
* such a pointer frees an owned string argument after the call, and a
* parameter passed on through one is not kept by passing it, when no
* function used as a value keeps what it receives at that slot (a named
* function keeps a `string` parameter only through a reference of its own,
* unless it stores it where nothing releases it; a `ptr` one, which a
* string can reach, is kept by any escape). A parameter returned as it came
* is no keep there: the call copies an unmarked result before it frees its
* arguments. A C function behind the pointer takes its `string` arguments
* as an extern's are taken, borrowed. */
static char** g_fnval_names = NULL;
static int g_fnval_count = 0;
static int g_fnval_cap = 0;
static void fnval_add(const char* name) {
for (int i = 0; i < g_fnval_count; i++)
if (strcmp(g_fnval_names[i], name) == 0) return;
if (g_fnval_count == g_fnval_cap) {
g_fnval_cap = g_fnval_cap ? g_fnval_cap * 2 : 16;
g_fnval_names = (char**)aether_xrealloc(g_fnval_names, sizeof(char*) * (size_t)g_fnval_cap);
}
g_fnval_names[g_fnval_count++] = strdup(name);
}
/* The parameters in scope during the walk: a function's or a closure's
* parameter shadows a function of its name in that body (std.mem's
* `steady_growth(round: fn, ...)` names no user function `round`). */
#define FNVAL_SHADOW_MAX 256
static const char* g_fnval_shadow[FNVAL_SHADOW_MAX];
static int g_fnval_shadow_count = 0;
static int fnval_shadowed(const char* name) {
for (int i = g_fnval_shadow_count - 1; i >= 0; i--)
if (strcmp(g_fnval_shadow[i], name) == 0) return 1;
return 0;
}
/* A name in value position that names a function, and no parameter in
* scope. A local spelt like a function is still taken: whether it is in
* scope at the use is the checker's to say, and taking a function that is
* not a value only costs it a mark and the argument slots it keeps. */
static void discover_fn_values_walk(CodeGenerator* gen, ASTNode* n) {
if (!n) return;
if (n->type == AST_IDENTIFIER && n->value && !fnval_shadowed(n->value)) {
ASTNode* fd = find_function_definition_by_name(gen->program, n->value);
if (fd && fd->type == AST_FUNCTION_DEFINITION) fnval_add(n->value);
}
int pushed = 0;
if (n->type == AST_FUNCTION_DEFINITION || n->type == AST_BUILDER_FUNCTION ||
n->type == AST_CLOSURE) {
for (int i = 0; i < n->child_count; i++) {
ASTNode* p = n->children[i];
if (p && p->value && g_fnval_shadow_count < FNVAL_SHADOW_MAX &&
(p->type == AST_PATTERN_VARIABLE || p->type == AST_CLOSURE_PARAM ||
(p->type == AST_VARIABLE_DECLARATION && n->type != AST_CLOSURE))) {
g_fnval_shadow[g_fnval_shadow_count++] = p->value;
pushed++;
}
}
}
for (int i = 0; i < n->child_count; i++) discover_fn_values_walk(gen, n->children[i]);
g_fnval_shadow_count -= pushed;
}
void discover_fn_values(CodeGenerator* gen) {
for (int i = 0; i < g_fnval_count; i++) free(g_fnval_names[i]);
g_fnval_count = 0;
g_fnval_shadow_count = 0;
if (!gen || !gen->program) return;
discover_fn_values_walk(gen, gen->program);
}
/* Is `name` a function used as a typed fn-pointer value (discover_fn_values)? */
int fn_value_name(const char* name) {
if (!name) return 0;
for (int i = 0; i < g_fnval_count; i++)
if (strcmp(g_fnval_names[i], name) == 0) return 1;
return 0;
}
/* #943: emit FORWARD DECLARATIONS for every registered bare-fn adapter.
* Closure bodies (emitted before emit_bare_fn_adapters) may reference an
* adapter, so its prototype must be in scope by then — otherwise the closure
* function sees `_aether_bare_adapter_<name>' undeclared'. Emitted right
* before the closure definitions; the full bodies still come later. */
void emit_bare_fn_adapter_decls(CodeGenerator* gen) {
if (!gen || gen->bare_fn_adapter_count == 0 || !gen->program) return;
print_line(gen, "// Bare-fn adapter forward declarations (closures may call them)");
for (int i = 0; i < gen->bare_fn_adapter_count; i++) {
ASTNode* params[16];
int param_count = 0;
const char* ret_c = "void";
if (emit_bare_fn_adapter_signature(gen, gen->bare_fn_adapter_names[i],
params, ¶m_count, &ret_c)) {
fprintf(gen->output, ";\n");
}
}
gen->bare_fn_adapter_declared = gen->bare_fn_adapter_count;
}
/* Emit `_aether_bare_adapter_<name>(void* env, args) -> R` for every
* registered bare-fn name. The adapter ignores env and forwards to the
* bare fn with its real C signature. Looks up each bare fn in the
* program AST to derive its param + return type list; for fns that
* can't be resolved (shouldn't happen — registration only fires from
* the bare-fn coercion path which already confirms the def exists),
* a no-op stub is emitted so the codegen isn't blocked. Must be called
* AFTER all user fn forward decls and bodies — the adapter calls into
* the bare fn by its real C name. */
void emit_bare_fn_adapters(CodeGenerator* gen) {
if (!gen || gen->bare_fn_adapter_count == 0) return;
if (!gen->program) return;
print_line(gen, "// Bare-fn → fn-typed-slot env-ignoring adapters (ASK 3)");
for (int i = 0; i < gen->bare_fn_adapter_count; i++) {
const char* fname = gen->bare_fn_adapter_names[i];
ASTNode* params[16];
int param_count = 0;
const char* ret_c = "void";
ASTNode* fdef = emit_bare_fn_adapter_signature(gen, fname, params,
¶m_count, &ret_c);
if (!fdef) continue; /* Shouldn't happen; registration gate
* already confirmed existence. */
Type* rt = fn_result_type(gen, fdef);
/* A string result is handed over owned, as a closure's is (#2054):
* the caller of a fn value cannot tell which return sites of the
* function behind it are heap, so it frees every string result.
* Wrapped the way the function's own callers see it -- passed
* through when the function returns owned strings, copied when it
* returns a literal or a borrow. */
int owned_string = rt && rt->kind == TYPE_STRING;
fprintf(gen->output, " {\n (void)_env;\n ");
if (rt) fprintf(gen->output, "return ");
if (owned_string) fprintf(gen->output, "aether_uniform_heap_str((const char*)(");
/* A @c_callback function is its bound symbol (#2664). */
ASTNode* cb_def = fn_c_callback_def(gen, fdef);
fprintf(gen->output, "%s(", cb_def ? c_callback_symbol(cb_def) : safe_c_name(fname));
/* #2499: a closure call borrows its arguments. A `string` the
* function keeps is its own reference, taken by its body (each
* clause's, #2644) on entry (fn_def_string_param_captures), so the
* adapter passes the caller's argument as it is. */
for (int k = 0; k < param_count; k++) {
if (k > 0) fprintf(gen->output, ", ");
fprintf(gen->output, "_a%d", k);
}
fprintf(gen->output, ")");
if (owned_string) {
fprintf(gen->output, "), %d)", function_def_returns_heap_string(gen, fdef) ? 1 : 0);
}
fprintf(gen->output, ";\n}\n");
}
}
// Get the factory function for a builder function (default: "map_new").
const char* get_builder_factory(CodeGenerator* gen, const char* func_name) {
if (!gen || !func_name) return "map_new";
const char* normalized = codegen_normalise_callee(func_name);
for (int i = 0; i < gen->builder_func_reg_count; i++) {
if (gen->builder_funcs_reg[i].name && strcmp(gen->builder_funcs_reg[i].name, normalized) == 0) {
return gen->builder_funcs_reg[i].factory ? gen->builder_funcs_reg[i].factory : "map_new";
}
}
return "map_new";
}
// Look up a registry entry by name, trying both the exact form and
// the dot-normalised form. Module-qualified call sites store the
// function name with a dot (e.g. "http.response_set_body_n"), but
// externs are registered under the raw extern name (e.g.
// "http_response_set_body_n"). Without the second pass, a call
// reaching an extern via `import std.http` is not recognised as an
// extern call, and the codegen skips behaviours gated on
// is_extern_func — most importantly the #297 auto-unwrap of
// `string`-typed args. Inline `extern` declarations in the same
// .ae file matched fine because the call site's name and the
// registered name are identical.
static int find_extern_registry_index(CodeGenerator* gen, const char* func_name) {
if (!gen || !func_name) return -1;
for (int i = 0; i < gen->extern_registry_count; i++) {
if (gen->extern_registry[i].name && strcmp(gen->extern_registry[i].name, func_name) == 0) {
return i;
}
}
/* Try the dot-normalised form: "ns.fn" → "ns_fn", whole however
* long the name (#2539). */
if (strchr(func_name, '.')) {
const char* norm = codegen_normalise_callee(func_name);
for (int i = 0; i < gen->extern_registry_count; i++) {
if (gen->extern_registry[i].name &&
strcmp(gen->extern_registry[i].name, norm) == 0) {
return i;
}
}
}
return -1;
}
// Check if a function name is registered as an extern function.
int is_extern_func(CodeGenerator* gen, const char* func_name) {
return find_extern_registry_index(gen, func_name) >= 0;
}
// If `func_name` was declared via @extern("c_symbol"), return the C
// symbol bound to it. Otherwise returns `func_name` unchanged. Used
// at call sites: codegen translates the Aether-side name (which lives
// in the module namespace) into the actual C symbol the linker wants.
// See #234.
const char* lookup_extern_c_name(CodeGenerator* gen, const char* func_name) {
if (!gen || !func_name) return func_name;
int idx = find_extern_registry_index(gen, func_name);
if (idx >= 0 && gen->extern_registry[idx].c_name) {
return gen->extern_registry[idx].c_name;
}
return func_name;
}
/* The extern table's name for the extern a call to `func_name` reaches, or
* NULL: the name itself (whole or dot-normalised), else, for `mod.f` where
* `mod_f` is no Aether function of the program, the `f` after the dot, as
* call_c_name resolves the call's C symbol. A call's parameter lookups key
* on this, never on the C symbol: an `@extern("sym")` binding is filed under
* its Aether name, and several bindings may share one symbol with different
* parameters (#2915). */
const char* extern_name_for_call(CodeGenerator* gen, const char* func_name) {
if (!gen || !func_name) return NULL;
int idx = find_extern_registry_index(gen, func_name);
const char* qdot = strchr(func_name, '.');
if (idx < 0 && qdot && qdot[1] &&
!(gen->program && find_function_definition_by_name(
gen->program, codegen_normalise_callee(safe_c_name(func_name))))) {
idx = find_extern_registry_index(gen, qdot + 1);
}
return idx >= 0 ? gen->extern_registry[idx].name : NULL;
}
// Look up the expected TypeKind for the nth parameter of an extern function.
// Returns TYPE_UNKNOWN if the function or parameter is not found.
TypeKind lookup_extern_param_kind(CodeGenerator* gen, const char* func_name, int param_idx) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return TYPE_UNKNOWN;
if (param_idx < gen->extern_registry[idx].param_count && gen->extern_registry[idx].params) {
return gen->extern_registry[idx].params[param_idx];
}
return TYPE_UNKNOWN;
}
// Full Type* sibling of the above (#1033) — NULL when unregistered or
// the param carried no node_type. Borrowed from the extern's AST.
Type* lookup_extern_param_type(CodeGenerator* gen, const char* func_name, int param_idx) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return NULL;
if (param_idx >= 0 && param_idx < gen->extern_registry[idx].param_count &&
gen->extern_registry[idx].param_full) {
return gen->extern_registry[idx].param_full[param_idx];
}
return NULL;
}
Type* lookup_extern_return_type(CodeGenerator* gen, const char* func_name) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return NULL;
return gen->extern_registry[idx].ret_type;
}
// Returns 1 if the nth parameter of `func_name` was declared with the
// `@aether` annotation (`name: @aether string`), 0 otherwise. Used by
// call-site codegen to suppress the aether_string_data() unwrap on
// that arg slot — receiver is Aether-emitted C and dispatches on the
// AetherString magic via str_len; passing the unwrapped data pointer
// would strlen-truncate binary content at embedded NULs. See #351.
int is_aether_extern_param(CodeGenerator* gen, const char* func_name, int param_idx) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return 0;
if (!gen->extern_registry[idx].params_aether) return 0;
if (param_idx < 0 || param_idx >= gen->extern_registry[idx].param_count) return 0;
return gen->extern_registry[idx].params_aether[param_idx];
}
/* Is argument `arg_idx` of a call to extern `func_name` one of its C
* varargs: past the declared parameters of an extern declared with a
* trailing `...` (#2914)? */
int is_extern_vararg_position(CodeGenerator* gen, const char* func_name, int arg_idx) {
int idx = find_extern_registry_index(gen, func_name);
return idx >= 0 && gen->extern_registry[idx].is_variadic &&
arg_idx >= gen->extern_registry[idx].param_count;
}
/* Is argument `arg_idx` of a call to `func_name` in a C-variadic `...`:
* an extern's past its declared parameters (#2914), or an Aether
* function's declared `f(fixed..., ...)` past its named ones? The type is
* erased there, so whatever reads it, `va_arg` or a forwarded `va_list`
* (#1244), reads it as C does (R4). */
int is_vararg_call_position(CodeGenerator* gen, const char* func_name, int arg_idx) {
const char* ext = extern_name_for_call(gen, func_name);
if (ext) return is_extern_vararg_position(gen, ext, arg_idx);
if (!gen->program || !func_name) return 0;
ASTNode* fdef = find_function_definition_by_name(gen->program, func_name);
if (!fdef) fdef = find_function_definition_by_name(gen->program, codegen_normalise_callee(func_name));
return fdef && annotation_has_marker(fdef->annotation, "varargs") &&
arg_idx >= fn_param_count(fdef);
}
/* Returns 1 if extern `func_name`'s parameter at `param_idx` was
* declared with `@retain`: the function keeps the pointer beyond
* the call, so the call hands it a reference of its own (R4).
* See codegen_func.c::register_extern_func and
* the parser's @retain handling. Returns 0 if the function
* isn't a registered extern, has no annotation, or the index
* is out of range. */
int is_retain_extern_param(CodeGenerator* gen, const char* func_name, int param_idx) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return 0;
if (!gen->extern_registry[idx].params_retain) return 0;
if (param_idx < 0 || param_idx >= gen->extern_registry[idx].param_count) return 0;
return gen->extern_registry[idx].params_retain[param_idx];
}
/* #2523: was extern `func_name`'s parameter at `param_idx` declared
* `@noescape`, so the function uses the argument only during the call and
* neither stores nor frees it? A closure passed there is the caller's to
* release once the call returns, and a `ptr` slot takes a box on the
* caller's stack. 0 for an unregistered extern, no annotation or an index
* out of range: the conservative answer, under which the callee keeps it. */
int is_noescape_extern_param(CodeGenerator* gen, const char* func_name, int param_idx) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return 0;
if (!gen->extern_registry[idx].params_noescape) return 0;
if (param_idx < 0 || param_idx >= gen->extern_registry[idx].param_count) return 0;
return gen->extern_registry[idx].params_noescape[param_idx];
}
/* Was extern `func_name`'s parameter at `param_idx` declared `@into` (the
* call's result holds the argument past the call, #2990) or `@keep` (a
* binary import's stub for an Aether function that keeps it, #3013)? A
* closure passed there is given a heap box with a reference of its own, as
* for `@retain`. */
int is_holding_extern_param(CodeGenerator* gen, const char* func_name, int param_idx) {
int idx = find_extern_registry_index(gen, func_name);
if (idx < 0) return 0;
if (!gen->extern_registry[idx].params_holds) return 0;
if (param_idx < 0 || param_idx >= gen->extern_registry[idx].param_count) return 0;
return gen->extern_registry[idx].params_holds[param_idx];
}
// Check if an AST subtree contains a return statement with a value.
// Stops at AST_CLOSURE boundaries: a nested lambda's `return` belongs
// to that lambda, not to the enclosing function/closure. Without this
// stop, `|| { inner = |x| { return x*2 }; call(inner, 3) }` would have
// its outer closure mis-typed as returning-int (picking up inner's
// `return`) when the outer actually has no return statement of its own.
// A call's trailing block is no such boundary: it runs inline, and its
// `return` is the enclosing function's (#2766).
int has_return_value(ASTNode* node) {
if (!node) return 0;
if (ast_is_own_function(node)) return 0;
if (node->type == AST_RETURN_STATEMENT && node->child_count > 0 && node->children[0]) {
// Print statements don't count as "return values" - they're void
if (node->children[0]->type == AST_PRINT_STATEMENT) {
return 0;
}
return 1;
}
for (int i = 0; i < node->child_count; i++) {
if (has_return_value(node->children[i])) return 1;
}
return 0;
}
// Names that are unconditionally declared by the C standard library
// (or POSIX headers we already include via the prelude) and whose
// signatures we don't want to redeclare from the user's `extern`. The
// generated C already pulls in <stdlib.h>/<stdio.h>/<string.h>/<unistd.h>,
// so a redeclaration of `sleep` / `exit` / `printf` / etc. with the
// Aether-translated prototype would conflict whenever the libc and
// Aether parameter shapes differ (e.g. unsigned vs int sleep, varargs
// vs typed printf). The user's `extern foo(...)` is still useful — it
// registers the function for type-aware call-site emission — but we
// skip writing the C forward declaration. Issue #233.
/* The C library functions whose declaration an `extern` of the same name
* does not emit: the system header's prototype is the canonical one, and
* Aether's spelling of it (`void sleep(int)` for libc's `unsigned int
* sleep(unsigned int)`) would conflict. Skipping is sound only when that
* header is in the translation unit, so each name maps to the header that
* declares it, and emit_libc_extern_headers (codegen.c) includes it for
* every skipped name a program declares; NULL is a header the prelude
* always includes (stdio.h, stdlib.h, string.h, time.h). `extern
* signal(...)` was skipped with no <signal.h> anywhere, and gcc 14 rejects
* the implicit declaration (#2676). */
typedef struct { const char* name; const char* header; } LibcExtern;
static const LibcExtern libc_externs[] = {
/* Process / signal */
{ "sleep", "unistd.h" }, { "usleep", "unistd.h" },
{ "exit", NULL }, { "_exit", "unistd.h" }, { "abort", NULL }, { "atexit", NULL },
{ "fork", "unistd.h" }, { "wait", "sys/wait.h" }, { "waitpid", "sys/wait.h" },
{ "execv", "unistd.h" }, { "execvp", "unistd.h" }, { "execve", "unistd.h" },
{ "getpid", "unistd.h" }, { "getppid", "unistd.h" }, { "getuid", "unistd.h" },
{ "geteuid", "unistd.h" }, { "getgid", "unistd.h" }, { "getegid", "unistd.h" },
{ "signal", "signal.h" }, { "kill", "signal.h" }, { "raise", "signal.h" },
/* Standard I/O */
{ "printf", NULL }, { "fprintf", NULL }, { "sprintf", NULL }, { "snprintf", NULL },
{ "vprintf", NULL }, { "vfprintf", NULL }, { "vsprintf", NULL }, { "vsnprintf", NULL },
{ "puts", NULL }, { "fputs", NULL }, { "gets", NULL }, { "fgets", NULL },
{ "getc", NULL }, { "putc", NULL }, { "getchar", NULL }, { "putchar", NULL },
{ "ungetc", NULL }, { "fopen", NULL }, { "fclose", NULL }, { "fflush", NULL },
{ "freopen", NULL }, { "fread", NULL }, { "fwrite", NULL }, { "fseek", NULL },
{ "ftell", NULL }, { "rewind", NULL }, { "feof", NULL }, { "ferror", NULL },
{ "fileno", NULL }, { "perror", NULL }, { "clearerr", NULL }, { "rename", NULL },
/* Memory */
{ "malloc", NULL }, { "calloc", NULL }, { "realloc", NULL }, { "free", NULL },
/* String: strdup/strndup and the size_t-argument functions like
* strstr/strchr clash on prototype shape too. */
{ "strlen", NULL }, { "strcmp", NULL }, { "strncmp", NULL }, { "strcpy", NULL },
{ "strncpy", NULL }, { "strcat", NULL }, { "strncat", NULL }, { "strdup", NULL },
{ "strndup", NULL }, { "strstr", NULL }, { "strchr", NULL }, { "strrchr", NULL },
{ "strpbrk", NULL }, { "strspn", NULL }, { "strcspn", NULL }, { "strtok", NULL },
{ "strtok_r", NULL }, { "strcasecmp", "strings.h" }, { "strncasecmp", "strings.h" },
{ "memcpy", NULL }, { "memset", NULL }, { "memmove", NULL }, { "memcmp", NULL },
{ "memchr", NULL },
/* Number conversion */
{ "atoi", NULL }, { "atol", NULL }, { "atoll", NULL }, { "atof", NULL },
{ "strtol", NULL }, { "strtoul", NULL }, { "strtoll", NULL }, { "strtoull", NULL },
{ "strtod", NULL }, { "strtof", NULL }, { "abs", NULL }, { "labs", NULL },
{ "llabs", NULL },
/* Sort / search: fn-pointer parameters clash with Aether's `ptr`
* lowering to `void*`. */
{ "qsort", NULL }, { "qsort_r", NULL }, { "bsearch", NULL },
/* Environment */
{ "getenv", NULL }, { "setenv", NULL }, { "putenv", NULL }, { "unsetenv", NULL },
/* POSIX file operations */
{ "open", "fcntl.h" }, { "close", "unistd.h" }, { "read", "unistd.h" },
{ "write", "unistd.h" }, { "lseek", "unistd.h" }, { "dup", "unistd.h" },
{ "dup2", "unistd.h" }, { "pipe", "unistd.h" }, { "access", "unistd.h" },
{ "isatty", "unistd.h" }, { "unlink", "unistd.h" }, { "rmdir", "unistd.h" },
{ "mkdir", "sys/stat.h" }, { "stat", "sys/stat.h" }, { "fstat", "sys/stat.h" },
{ "lstat", "sys/stat.h" },
/* Time */
{ "time", NULL }, { "clock", NULL }, { "clock_gettime", NULL },
{ "gettimeofday", "sys/time.h" }, { "localtime", NULL }, { "gmtime", NULL },
{ "mktime", NULL }, { "strftime", NULL },
{ NULL, NULL }
};
static const LibcExtern* libc_extern_find(const char* name) {
if (!name) return NULL;
for (int i = 0; libc_externs[i].name; i++) {
if (strcmp(name, libc_externs[i].name) == 0) return &libc_externs[i];
}
return NULL;
}
static int extern_name_is_libc_conflict(const char* name) {
return libc_extern_find(name) != NULL;
}
const char* libc_extern_header(const char* name) {
const LibcExtern* e = libc_extern_find(name);
return e ? e->header : NULL;
}
/* #2799: the header of cl.exe's C library that declares, under its POSIX
* name, a function the table above finds in <unistd.h>, which MSVC does not
* have: getpid and isatty were undeclared (C4013). NULL for a name MSVC does
* not provide, or whose header the table already names. */
const char* libc_extern_msvc_header(const char* name) {
static const LibcExtern msvc[] = {
{ "getpid", "process.h" }, { "_exit", "process.h" }, { "execv", "process.h" },
{ "execvp", "process.h" }, { "execve", "process.h" },
{ "close", "io.h" }, { "read", "io.h" }, { "write", "io.h" }, { "lseek", "io.h" },
{ "dup", "io.h" }, { "dup2", "io.h" }, { "access", "io.h" }, { "isatty", "io.h" },
{ "unlink", "io.h" }, { "rmdir", "direct.h" },
{ NULL, NULL }
};
for (int i = 0; name && msvc[i].name; i++)
if (strcmp(name, msvc[i].name) == 0) return msvc[i].header;
return NULL;
}
// Generate extern C function declaration
// extern printf(format: string) -> int => extern int printf(const char*);
void generate_extern_declaration(CodeGenerator* gen, ASTNode* ext) {
if (!ext || ext->type != AST_EXTERN_FUNCTION) return;
// Register parameter types for call-site type-aware casting
register_extern_func(gen, ext);
// Skip the C forward declaration for libc-conflicting names — the
// libc headers we include in the prelude already declare them with
// the canonical prototype. Without this skip, e.g. `extern sleep(ms:
// int)` produces `void sleep(int);` which conflicts with libc's
// `unsigned int sleep(unsigned int)` and breaks compilation.
// The function's name is still registered above, so call-site code
// generation uses the correct cast-to-libc-type emission.
// @extern("c_symbol") aether_name(...) — the Aether-side name
// (ext->value) lives in the module namespace, but the C forward
// declaration and every call site use the annotated C symbol.
// Closes #234.
const char* c_name = ext->value;
const char* c_sym = extern_c_symbol(ext);
if (c_sym) {
c_name = c_sym;
}
if (extern_name_is_libc_conflict(c_name)) {
fprintf(gen->output,
"// Extern C function: %s (libc-provided, declaration skipped)\n",
c_name);
return;
}
/* `@c_import` (#1239): a C header owns this prototype. Emitting our own
* would put a second declaration in the TU whose spelling is only
* ABI-compatible, not identical (int vs uint8_t, long vs size_t, void*
* vs a typed pointer), the source of the LTO type-mismatch warnings.
* Emitting nothing makes disagreement impossible, and the C compiler
* still checks every call site against the header. Also the only shape
* that is safe for a `static inline` helper (#1241), which has no
* linkable symbol for a non-static prototype to refer to. The name is
* registered above, so call-site casts are unaffected. */
if (extern_is_c_import(ext)) {
fprintf(gen->output,
"// Extern C function: %s (@c_import, header owns the prototype)\n",
c_name);
return;
}
fprintf(gen->output, "// Extern C function: %s\n", c_name);
// Generate return type (map Aether types to C types)
if (ext->node_type && ext->node_type->kind != TYPE_VOID) {
/* A C ABI alias / qualified type (`c_alias` set: `size_t`,
* `const void*`, `char*`, ...) emits its exact C spelling so
* the prototype matches the system header byte-for-byte. */
if (ext->node_type->c_alias) {
fprintf(gen->output, "%s", ext->node_type->c_alias);
} else
switch (ext->node_type->kind) {