-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathstdrot.c
More file actions
2614 lines (2475 loc) · 114 KB
/
Copy pathstdrot.c
File metadata and controls
2614 lines (2475 loc) · 114 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
/* stdrot.c - Standard Brainrot library loader and AST bridge
*
* This file is the glue between:
* • the AST/interpreter (understands ASTNode, ArgumentList, Variable, etc.)
* • the stdrot implementations (pure I/O functions, zero interpreter
* dependency), reached one of two ways depending on STDROT_STATIC:
*
* STDROT_STATIC undefined (default, native build):
* the stdrot sources are compiled into libstdrot.so and dlopen'd at
* runtime.
* 1. Dynamic loader (stdrot_load/unload) that opens libstdrot.so and
* discovers all functions via stdrot_get_api_v3()
* 2. Thin varargs stubs (yapping/yappin/baka) and per-type slorp/
* ragequit/chill stubs that dlsym their real implementation by name
* on first use
*
* STDROT_STATIC defined (wasm build, see `make wasm`):
* the stdrot sources are compiled directly into the same binary —
* there is no .so and no dlopen surface at all (wasm has no dynamic
* loader worth using for a single-artifact build). stdrot_load() calls
* stdrot_get_api_v3() directly, and ragequit/chill/slorp_* are provided
* solely by their stdrot definitions — this file only keeps the
* yapping/yappin/baka varargs stubs, redirecting them straight to
* v_yapping/v_yappin/v_baka instead of looking them up by name.
*
* 3. AST bridge functions (execute_*_call) that evaluate arguments and
* call the raw implementations — identical in both modes.
*/
#include "stdrot.h"
#include "ast.h"
#include "lib/mem.h"
#include "lib/module_path.h" /* MODULE_NATIVE_LOADER: can this build load a
* #cooked <name> native module? (Shared with
* module_path.c and lang.l -- one definition.) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#ifndef STDROT_STATIC
#include <dlfcn.h> /* core library loader (dlopen/dlsym) -- POSIX native build */
#elif defined(_WIN32)
#include <stdint.h> /* uintptr_t, for the GetProcAddress round-trip below */
#include <windows.h> /* Win32 native-module loader (LoadLibraryA/GetProcAddress) */
#endif
/* ── Native-module loader shim ────────────────────────────────────────────
* One trio -- open/sym/close -- over dlopen and the Win32 loader, so
* stdrot_load_module() and stdrot_unload() read identically on both. The
* existing void* handle fields hold either a dlopen handle or an HMODULE
* (a pointer). Only compiled where a loader exists. */
#ifdef MODULE_NATIVE_LOADER
#if defined(_WIN32)
static void *br_module_open(const char *path)
{
return (void *)LoadLibraryA(path);
}
static void *br_module_sym(void *handle, const char *name)
{
/* GetProcAddress returns FARPROC; round-trip through uintptr_t to a data
* pointer, the same shape dlsym() hands back. */
return (void *)(uintptr_t)GetProcAddress((HMODULE)handle, name);
}
static void br_module_close(void *handle)
{
FreeLibrary((HMODULE)handle);
}
/* Formats the last loader error into a static buffer (single-threaded loader
* path, same as dlerror()'s own not-thread-safe contract). */
static const char *br_module_error(void)
{
static char buf[256];
DWORD err = GetLastError();
DWORD n = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, err, 0, buf, (DWORD)sizeof(buf), NULL);
if (n == 0)
{
snprintf(buf, sizeof(buf), "Win32 error %lu", (unsigned long)err);
}
else
{
/* Trim the trailing CR/LF FormatMessage appends. */
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r'))
{
buf[--n] = '\0';
}
}
return buf;
}
#else
/* RTLD_LOCAL, unlike the core library's own dlopen(): a cooked module is
* looked up entirely by explicit handle (br_module_sym below searches that
* object only), so its exports -- including its own brainrot_module_init_v3,
* shared by every module -- never enter the process-wide symbol scope, and
* two modules exporting that name never collide. */
static void *br_module_open(const char *path)
{
return dlopen(path, RTLD_LAZY | RTLD_LOCAL);
}
static void *br_module_sym(void *handle, const char *name)
{
return dlsym(handle, name);
}
static void br_module_close(void *handle)
{
dlclose(handle);
}
static const char *br_module_error(void)
{
return dlerror();
}
#endif
#endif /* MODULE_NATIVE_LOADER */
/* ── Global execution context ────────────────────────────────────────────── */
ExecutionContext g_exec_context = {0, {NULL, 0}, {NULL, 0}};
/* ── External interpreter functions ──────────────────────────────────────── */
extern void yyerror(const char *s);
extern String evaluate_expression_string(ASTNode *node);
extern void *evaluate_multi_array_access(ASTNode *node);
extern bool set_bool_variable(const String name, bool value,
TypeModifiers mods);
extern bool set_char_variable(const String name, int value, TypeModifiers mods);
/* ── Dynamic library state (native build only) ───────────────────────────── */
#ifndef STDROT_STATIC
static void *lib_handle = NULL;
#endif
static const StdrotEntry *const *functions = NULL;
static int function_count = 0;
#ifndef STDROT_STATIC
/* Symbol cache to avoid repeated dlsym calls */
#define STDROT_CACHE_SIZE 64
typedef struct
{
String name;
void *ptr;
} SymbolCache;
static SymbolCache symbol_cache[STDROT_CACHE_SIZE];
static int cache_count = 0;
#endif /* !STDROT_STATIC (core-library dynamic state) */
#ifdef MODULE_NATIVE_LOADER
/* ── Cooked native modules (#cooked <name> resolving to a .so/.dll) ────────
* A SEPARATE list from the core library's own functions/function_count
* above, rather than unifying the two: the core lib is always loaded
* unconditionally, once, before any Brainrot program has even been
* parsed, and is exercised by nearly every existing test in this repo --
* keeping it untouched keeps this purely additive, opt-in mechanism from
* putting that already thoroughly-tested path at risk. is_builtin_
* function()/get_native_function() below check the core lib first, then
* this list, in #cooked order.
*
* Fixed-size, not realloc'd: STDROT_MAX_COOKED_MODULES mirrors lang.l's
* own MAX_COOKED_FILES (the actual enforcement point -- lang.l refuses to
* even resolve a name once its shared visited-file/module budget is
* exhausted, so this array can never be asked to hold more than that many
* entries in practice). The bounds check in stdrot_load_module() below is
* defense in depth, the same relationship validate_native_registry() has
* to the semantic analyzer's own already-enforced checks. */
#define STDROT_MAX_COOKED_MODULES 128
typedef struct
{
void *handle;
char *name; /* the #cooked <name> spelling, for diagnostics */
const StdrotEntry *const *functions;
int function_count;
} LoadedNativeModule;
static LoadedNativeModule cooked_modules[STDROT_MAX_COOKED_MODULES];
static int cooked_module_count = 0;
/* An in-flight module handle stdrot_load_module() has dlopen'd but not yet
* either closed itself or fully committed into cooked_modules[] -- see
* that function's own comment on why this exists. NULL whenever no
* stdrot_load_module() call is in progress. */
static void *pending_module_handle = NULL;
#endif /* MODULE_NATIVE_LOADER */
#ifdef STDROT_STATIC
/* Statically linked in from stdrot/yapping.c and stdrot/baka.c — called
* directly below instead of going through dlsym-by-name.
* stdrot_get_api_v3() (statically linked from stdrot/registry.c) is
* already declared by stdrot_api.h, included transitively via stdrot.h
* above. */
extern void v_yapping(const char *fmt, va_list ap);
extern void v_yappin(const char *fmt, va_list ap);
extern void v_baka(const char *fmt, va_list ap);
#else
/* ── Dynamic symbol lookup with caching ──────────────────────────────────── */
static void *stdrot_lookup_symbol(const String symbol_name)
{
if (!lib_handle || !symbol_name.data)
return NULL;
/* Check cache first */
for (int i = 0; i < cache_count; i++)
{
if (strcmp(symbol_cache[i].name.data, symbol_name.data) == 0)
{
return symbol_cache[i].ptr;
}
}
/* Not in cache, lookup via dlsym */
void *ptr = dlsym(lib_handle, symbol_name.data);
if (ptr && cache_count < STDROT_CACHE_SIZE)
{
symbol_cache[cache_count].name.data = symbol_name.data;
symbol_cache[cache_count].name.len = symbol_name.len;
symbol_cache[cache_count].ptr = ptr;
cache_count++;
}
return ptr;
}
#endif /* STDROT_STATIC */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
/* ── Registry validation ───────────────────────────────────────────────────
* A malformed StdrotEntry is a bug in the native binding itself (a hand-
* written STDROT_EXPORT_SIG (or STDROT_EXPORT_SIG_IDENTITY) invocation with
* inconsistent arguments), never something a Brainrot program can trigger
* -- so, like every other native-binding bug this ABI catches (enforce_
* return_type()/enforce_arg_type()), it should fail loudly and immediately
* rather than surfacing piecemeal depending on which call path happens to
* touch the broken field first. Run once, right after the registry loads,
* instead of at each individual call site.
*
* What "validated" means here: every field is internally coherent (in
* range, self-consistent with the other fields on the same descriptor --
* pointer_level only meaningful alongside STDROT_PTR, return_like_arg only
* meaningful naming a STDROT_ANY mandatory argument, and so on). It does
* NOT mean every *capability* a structurally-valid descriptor could
* describe is actually implemented end to end. STDROT_CSTRING as a
* *return* type is a real, well-formed StdrotType value -- reserved
* groundwork for a capability this ABI hasn't finished (see
* stdrot_api.h's own STDROT_CSTRING comment) -- not malformed metadata.
* STDROT_HANDLE was in that set until #213 and no longer is: handles are
* now implemented in both directions. A descriptor using either loads
* successfully; semantic_check_native_call() (semantic_analyzer.c)
* rejects any *call* to it before that call could ever reach a marshaller
* with no code path to honor it. That is a deliberate two-tier design,
* not a gap in this function: collapsing "structurally coherent" and
* "fully implemented" into one load-time check would mean a single .so
* exporting one reserved-but-unimplemented capability refuses to load at
* all, taking down every unrelated, fully-supported native alongside it,
* for a call that might never even happen. */
/* An intentionally generous upper bound on how many natives a single
* libstdrot.so could plausibly export -- this codebase ships roughly a
* dozen production natives plus another dozen or so test-only ones; four
* orders of magnitude more than that is not "a large but legitimate
* library," it's a corrupt or hostile StdrotAPI.count that would
* otherwise send validate_native_registry()'s loop below walking through
* arbitrary address space one StdrotEntry pointer at a time. */
#define STDROT_MAX_PLAUSIBLE_FUNCTION_COUNT 4096
static void validate_native_registry(const StdrotEntry *const *functions,
int function_count)
{
/* Validate the table itself before ever indexing into it --
stdrot_get_api_v3() (registry.c) is trusted to return a StdrotAPI
shaped the way this header currently declares, but "the ABI is
versioned" only means the STRUCT LAYOUT is trustworthy, not that
every possible bit pattern inside it is coherent. A negative
count would make the loop below execute zero times, silently
accepting a nonsensical table as if it legitimately had no
exports; a positive count paired with a NULL functions pointer,
or an implausibly large count (a corrupt or hostile library,
intentionally or not), would walk this loop into a NULL
dereference or arbitrary out-of-bounds memory well before any
individual entry's own fields are ever examined. */
if (function_count < 0 ||
function_count > STDROT_MAX_PLAUSIBLE_FUNCTION_COUNT)
{
fprintf(stderr,
"stdrot: registry function_count (%d) is not a plausible "
"value (expected 0 <= count <= %d)\n",
function_count, STDROT_MAX_PLAUSIBLE_FUNCTION_COUNT);
exit(1);
}
if (function_count > 0 && !functions)
{
fprintf(stderr,
"stdrot: registry function_count (%d) is > 0 but "
"functions is NULL\n",
function_count);
exit(1);
}
for (int i = 0; i < function_count; i++)
{
const StdrotEntry *entry = functions[i];
if (!entry || !entry->name)
{
fprintf(stderr, "stdrot: registry entry %d has no name\n", i);
exit(1);
}
if (entry->return_type.type < STDROT_ANY ||
entry->return_type.type > STDROT_NONE)
{
fprintf(stderr,
"stdrot: native '%s': return_type.type (%d) is not a "
"valid StdrotType\n",
entry->name, (int)entry->return_type.type);
exit(1);
}
if (entry->min_args < 0 || entry->param_count < 0)
{
fprintf(stderr,
"stdrot: native '%s': min_args (%d) and param_count "
"(%d) must both be >= 0\n",
entry->name, entry->min_args, entry->param_count);
exit(1);
}
if (entry->min_args > entry->param_count)
{
fprintf(stderr,
"stdrot: native '%s': min_args (%d) cannot exceed "
"param_count (%d)\n",
entry->name, entry->min_args, entry->param_count);
exit(1);
}
if (entry->param_count > 0 && !entry->params)
{
fprintf(stderr,
"stdrot: native '%s': param_count (%d) > 0 but params "
"is NULL\n",
entry->name, entry->param_count);
exit(1);
}
/* Safe to dereference entry->params[p] from here on: the NULL
check just above guarantees it's non-NULL whenever param_count
> 0 (the only way this loop actually iterates). */
for (int p = 0; p < entry->param_count; p++)
{
if (entry->params[p].type < STDROT_ANY ||
entry->params[p].type > STDROT_NONE)
{
fprintf(stderr,
"stdrot: native '%s': params[%d].type (%d) is not "
"a valid StdrotType\n",
entry->name, p, (int)entry->params[p].type);
exit(1);
}
}
if (!entry->fn)
{
fprintf(stderr, "stdrot: native '%s': fn is NULL\n", entry->name);
exit(1);
}
if (entry->promote_variadic_tail && !entry->is_variadic)
{
/* promote_variadic_tail (stdrot_api.h) only means something
for arguments beyond param_count -- there's no "tail" to
promote at all when is_variadic is false. */
fprintf(stderr,
"stdrot: native '%s': promote_variadic_tail is true "
"but is_variadic is false\n",
entry->name);
exit(1);
}
if (entry->return_type.pointer_level < 0)
{
fprintf(stderr,
"stdrot: native '%s': return_type.pointer_level (%d) "
"must be >= 0\n",
entry->name, entry->return_type.pointer_level);
exit(1);
}
/* pointer_level is only meaningful stacked on top of STDROT_PTR
(StdrotParam's own comment, stdrot_api.h: "{STDROT_PTR, NULL,
N} describes N levels of indirection on top of whatever
pointer_level counts" -- {STDROT_PTR, NULL, 0} is one pointer
level, {STDROT_PTR, NULL, 1} is two, and so on). A non-PTR base
type with a nonzero pointer_level is an internally
contradictory descriptor: static checking (semantic_analyzer.c)
reads type + pointer_level directly and would approve it as an
ordinary N-level pointer to that base type, but runtime
marshalling (ast_expr_to_stdrot_value()) tags ANY expression
with pointer_level > 0 as STDROT_PTR regardless of declared
base type -- so the argument would always arrive tagged
STDROT_PTR while the descriptor insists it's something else,
and enforce_arg_type()/enforce_return_type() would reject
every call this "valid" descriptor was supposed to describe.
Reject the contradiction at the source instead of certifying a
descriptor nothing downstream can actually honor. */
if (entry->return_type.type != STDROT_PTR &&
entry->return_type.pointer_level != 0)
{
fprintf(stderr,
"stdrot: native '%s': return_type.pointer_level (%d) "
"must be 0 when return_type.type isn't STDROT_PTR\n",
entry->name, entry->return_type.pointer_level);
exit(1);
}
for (int p = 0; p < entry->param_count; p++)
{
if (entry->params[p].pointer_level < 0)
{
fprintf(stderr,
"stdrot: native '%s': params[%d].pointer_level "
"(%d) must be >= 0\n",
entry->name, p, entry->params[p].pointer_level);
exit(1);
}
if (entry->params[p].type != STDROT_PTR &&
entry->params[p].pointer_level != 0)
{
fprintf(stderr,
"stdrot: native '%s': params[%d].pointer_level "
"(%d) must be 0 when params[%d].type isn't "
"STDROT_PTR\n",
entry->name, p, entry->params[p].pointer_level, p);
exit(1);
}
/* A parameter's type describes what representation it
consumes; STDROT_NONE ("void return") only makes sense as
a RETURN type -- a parameter that "consumes void" cannot
coherently accept an actual argument. Zero-argument
natives are already expressed via param_count == 0; a
STDROT_NONE-typed parameter slot is never necessary and
would leave both static checking and enforce_arg_type()
with no coherent rule for what value could ever satisfy
it. */
if (entry->params[p].type == STDROT_NONE)
{
fprintf(stderr,
"stdrot: native '%s': params[%d].type must not be "
"STDROT_NONE -- a void-typed parameter can't "
"consume an argument; use param_count to express "
"zero arguments instead\n",
entry->name, p);
exit(1);
}
/* A by-value aggregate is only checkable against its tag:
`gang Vector2 {chad x, y;}` and `gang Size {chad w, h;}`
have identical size and alignment, so a STDROT_STRUCT
descriptor with no type_name leaves both static checking
(semantic_check_native_call()) and enforce_arg_type() with
nothing to compare but a byte count that cannot tell them
apart -- the native would silently receive whichever
8-byte struct the caller happened to pass. Reject the
descriptor at load time rather than certify a parameter
nothing downstream can honestly type-check. Empty is
rejected alongside NULL: "" matches no `gang` tag, so it
is a descriptor that can never accept any argument at
all, which is a bug in the binding, not a usable
signature. */
if (entry->params[p].type == STDROT_STRUCT &&
(!entry->params[p].type_name ||
entry->params[p].type_name[0] == '\0'))
{
fprintf(stderr,
"stdrot: native '%s': params[%d].type is "
"STDROT_STRUCT but type_name is missing -- a "
"by-value struct parameter must name the "
"gang/chungus tag it accepts (size alone cannot "
"distinguish two same-sized structs)\n",
entry->name, p);
exit(1);
}
}
/* return_like_arg (StdrotEntry's own comment, stdrot_api.h): -1
means "not identity-polymorphic," otherwise it must name a
MANDATORY argument -- an identity relationship ("same type as
argument N") is meaningless if argument N might not be
supplied. Catches e.g. STDROT_EXPORT_SIG_IDENTITY(name, fn,
params, 1, 0) -- one optional param, zero mandatory ones, so
.return_like_arg = 0 (hardcoded by that macro) could point at
an argument that was never actually passed. */
if (entry->return_like_arg != -1 &&
(entry->return_like_arg < 0 ||
entry->return_like_arg >= entry->min_args))
{
fprintf(stderr,
"stdrot: native '%s': return_like_arg (%d) must be -1 "
"or a mandatory argument index (0 <= return_like_arg "
"< min_args = %d)\n",
entry->name, entry->return_like_arg, entry->min_args);
exit(1);
}
/* return_like_arg must name a STDROT_ANY parameter, full stop.
"identity-polymorphic" only means something coherent if the
parameter itself carries no fixed representation to coerce
into -- T -> T, with T decided entirely by the caller. If the
parameter is a fixed type (STDROT_CSTRING, STDROT_DOUBLE,
STDROT_PTR, ...), static analysis infers the result type from
the *source* expression (before coercion) while the runtime
marshaller/enforce_return_type() see the argument *after*
parameter coercion -- two different types for the same call
whenever the source type and the fixed parameter type differ
(STRING literal coerced to CSTRING, INT coerced to DOUBLE, a
bare pointer with no STDROT_PTR-shaped static inference,
etc). Forcing STDROT_ANY here removes the coercion step
entirely, so there is only one type to agree on. A native that
genuinely wants "same value, fixed type" should declare that
fixed type as an ordinary (non-identity) return_type instead
of borrowing this mechanism. */
if (entry->return_like_arg != -1 &&
entry->params[entry->return_like_arg].type != STDROT_ANY)
{
fprintf(stderr,
"stdrot: native '%s': return_like_arg (%d) names a "
"parameter that isn't STDROT_ANY -- identity-"
"polymorphic natives may only alias a STDROT_ANY "
"parameter, since any fixed parameter type undergoes "
"coercion that static inference and runtime "
"enforcement would then disagree about\n",
entry->name, entry->return_like_arg);
exit(1);
}
for (int j = 0; j < i; j++)
{
if (functions[j] && functions[j]->name &&
strcmp(functions[j]->name, entry->name) == 0)
{
fprintf(stderr,
"stdrot: duplicate native export '%s' -- "
"get_native_function() would silently resolve "
"every call to whichever entry happens to come "
"first in the linker section\n",
entry->name);
exit(1);
}
}
}
}
/* ── Loader ──────────────────────────────────────────────────────────────── */
#ifdef STDROT_STATIC
void stdrot_load(void)
{
/* stdrot/registry.c is linked directly into this binary, so the
* function table is just a direct call away — no loader needed, and
* no dlsym-based version check either: this is a single statically
* linked binary, compiled from one copy of stdrot_api.h, so the ABI
* mismatch stdrot_get_api_v3()'s naming exists to catch (an old
* libstdrot.so loaded by a new host, see STDROT_ABI_VERSION's own
* comment) is structurally impossible here. */
StdrotAPI api = stdrot_get_api_v3();
functions = api.functions;
function_count = api.count;
validate_native_registry(functions, function_count);
}
#else
void stdrot_load(void)
{
/* First, make main binary symbols available to subsequently loaded
* libraries by loading the main program's symbols with RTLD_GLOBAL
*/
dlopen(NULL, RTLD_LAZY | RTLD_GLOBAL);
/* STDROT_LIB_PATH, when set, names an exact library to load instead --
* used exclusively by `make test`/`make valgrind` and CI's test job to
* point at tests/libstdrot.so (production natives plus test-only ones
* from tests/stdrot/, see that directory's own comment) without ever
* touching the plain "./libstdrot.so" lookup below, which is what
* `make install` and every ordinary invocation of this binary still
* resolve to. Unset in normal use, so this changes nothing for anyone
* not explicitly opting into a different library. */
const char *lib_path_override = getenv("STDROT_LIB_PATH");
if (lib_path_override)
{
lib_handle = dlopen(lib_path_override, RTLD_LAZY | RTLD_GLOBAL);
if (!lib_handle)
{
fprintf(stderr, "Failed to load STDROT_LIB_PATH=%s: %s\n",
lib_path_override, dlerror());
exit(EXIT_FAILURE);
}
}
/* Try cwd-relative ./libstdrot.so first, then the dynamic linker's
* search path. Release builds add rpath so the leaf-name lookup can find
* libstdrot.so next to the binary after the cwd lookup misses. Use
* RTLD_GLOBAL so the library can access symbols from the main binary
* (e.g., g_exec_context). */
if (!lib_handle)
{
lib_handle = dlopen("./libstdrot.so", RTLD_LAZY | RTLD_GLOBAL);
}
if (!lib_handle)
{
lib_handle = dlopen("libstdrot.so", RTLD_LAZY | RTLD_GLOBAL);
}
if (!lib_handle)
{
fprintf(stderr, "Failed to load libstdrot.so: %s\n", dlerror());
exit(EXIT_FAILURE);
}
/* Get the API entrypoint -- by its versioned name (STDROT_ABI_VERSION,
stdrot_api.h), never the pre-v2 "stdrot_get_api". A libstdrot.so
built before this ABI existed (StdrotEntry == {name, fn}, no
STDROT_ANY at StdrotType index 0, registry section holding
StdrotEntry structs directly rather than pointers to them) simply
doesn't export this symbol -- dlsym() fails the lookup cleanly,
instead of finding an old stdrot_get_api() under the old name and
calling it as if its StdrotAPI were shaped like this version's.
That would silently reinterpret the old .so's actual memory (e.g.
an entry's own `name` field bytes) as this version's `functions`
array of StdrotEntry POINTERS -- exactly the class of ABI-version
confusion this rename exists to make structurally impossible to
reach, not just unlikely. */
StdrotAPI (*get_api)(void);
*(void **)(&get_api) = dlsym(lib_handle, "stdrot_get_api_v3");
if (!get_api)
{
fprintf(stderr,
"libstdrot.so is missing stdrot_get_api_v3() -- it was "
"built against an incompatible stdrot ABI (expected "
"STDROT_ABI_VERSION %d). Rebuild libstdrot.so from this "
"checkout (`make lib`) before running this binary.\n",
STDROT_ABI_VERSION);
dlclose(lib_handle);
/* exit() below runs every registered atexit handler, including
stdrot_unload() (atexit(stdrot_unload), lang.y) -- which would
otherwise dlclose() this same, already-closed handle again
(its own guard is `if (lib_handle)`, which does nothing to
protect against a stale pointer this function itself already
passed to dlclose()). A pre-existing bug in this exact error
path, uncovered by tests/old_abi_sim's fixture -- confirmed
via valgrind (invalid reads inside glibc's own _dl_close,
deep in freed loader bookkeeping) before this fix, clean
after it. */
lib_handle = NULL;
exit(EXIT_FAILURE);
}
/* Discover all functions */
StdrotAPI api = get_api();
functions = api.functions;
function_count = api.count;
validate_native_registry(functions, function_count);
}
#endif /* STDROT_STATIC -- core-library load path */
/* Unloads the core library (dynamic-core builds only -- Windows and wasm
* compile the core in, so there is no lib_handle to close there) and every
* #cooked native module (wherever a module loader exists). Two independent
* guards, not one: on Windows the core is static yet modules still load via
* br_module_open() and must be freed here. */
void stdrot_unload(void)
{
#ifndef STDROT_STATIC
if (lib_handle)
{
dlclose(lib_handle);
lib_handle = NULL;
functions = NULL;
function_count = 0;
cache_count = 0;
}
#else
functions = NULL;
function_count = 0;
#endif
#ifdef MODULE_NATIVE_LOADER
for (int i = 0; i < cooked_module_count; i++)
{
br_module_close(cooked_modules[i].handle);
free(cooked_modules[i].name);
}
cooked_module_count = 0;
if (pending_module_handle)
{
/* stdrot_load_module() exited (e.g. via validate_native_registry())
before either closing this handle itself or committing it into
cooked_modules[] above -- see that function's own comment. */
br_module_close(pending_module_handle);
pending_module_handle = NULL;
}
#endif
}
#ifdef MODULE_NATIVE_LOADER
/* Describes whichever already-registered source (the core library, or an
* earlier #cooked module) provides `func_name` -- used only to name that
* source in stdrot_load_module()'s duplicate-export diagnostic below.
* Caller must already know func_name IS registered somewhere (e.g. via
* is_builtin_function()); returns a generic fallback description otherwise,
* which should be unreachable in practice. */
static const char *describe_native_source(const char *func_name)
{
for (int i = 0; i < function_count; i++)
{
if (strcmp(func_name, functions[i]->name) == 0)
{
return "the core standard library";
}
}
for (int m = 0; m < cooked_module_count; m++)
{
for (int i = 0; i < cooked_modules[m].function_count; i++)
{
if (strcmp(func_name, cooked_modules[m].functions[i]->name) == 0)
{
return cooked_modules[m].name;
}
}
}
return "another already-loaded source";
}
/* Loads a native module (a .so resolved from #cooked <name>, module_path.c)
* and registers its functions alongside the core library's. `name` is the
* #cooked <name> the user wrote; `so_path` is the already-resolved absolute
* path. Exits with a diagnostic on any failure -- dlopen, a missing/
* incompatible brainrot_module_init_v3, a malformed registry, or a name
* already provided by the core library or an earlier #cooked module -- the
* same fail-loud posture stdrot_load() already has for the core library:
* none of these are something a Brainrot program can trigger or recover
* from, and every existing ABI-enforcement function in this file already
* treats that class of failure as exit(1), not a value to propagate. */
void stdrot_load_module(const char *name, const char *so_path)
{
if (cooked_module_count >= STDROT_MAX_COOKED_MODULES)
{
fprintf(stderr,
"stdrot: too many distinct #cooked files/modules (max %d)\n",
STDROT_MAX_COOKED_MODULES);
exit(1);
}
/* br_module_open() loads the module in isolation -- RTLD_LOCAL on POSIX,
and the Win32 loader's per-module handle scope -- so its exports never
enter the process-wide symbol namespace. That matters most for the
module's OWN brainrot_module_init_v3: every cooked module (built with
-DSTDROT_REGISTRY_ENTRYPOINT=brainrot_module_init_v3) exports one under
that exact name, and isolation is why two of them are never a collision,
regardless of load order -- not because the name happens to be unique
(it isn't). br_module_sym() below looks the entrypoint up on this
handle specifically, never globally. See the shim near the top. */
void *handle = br_module_open(so_path);
if (!handle)
{
fprintf(stderr, "Error: cannot load module '%s' (%s): %s\n", name,
so_path, br_module_error());
exit(1);
}
/* Recorded before this handle is fully validated, for the same reason
PendingNativeCallArgs (above) tracks an in-flight native call's own
scratch before it's done with it: validate_native_registry() below
can itself exit(1) on a malformed table, and that exit() doesn't
unwind this function's stack -- without this, `handle` would still
be open (mmap'd, not merely a heap pointer, so nothing else in this
file's cleanup would ever see it) with nothing tracking it for
stdrot_unload() (atexit(stdrot_unload), lang.y) to dlclose. Cleared
on every path out of this function, success or failure, so it never
describes a handle this function itself already closed or handed
off to cooked_modules[]. */
pending_module_handle = handle;
/* Same versioned-entrypoint discipline as stdrot_get_api_v3() above,
for the same reason: a module built against a stdrot_api.h whose
layout has since changed must fail this dlsym() cleanly, not have
its actual memory misread as the current shape.
This symbol carried NO version suffix through ABI v2, on the
reasoning that there was no prior, differently-shaped
"brainrot_module_init" to disambiguate from -- with the standing
caveat that a future incompatible change would have to rename it
the same way stdrot_get_api itself was renamed. ABI v3
(STDROT_STRUCT, #208) is that change, and it is worth being
precise about why, because the usual tell was absent: StdrotAPI,
StdrotEntry and StdrotParam all kept their exact v2 layouts, so a
stale module's function TABLE would have been read back
correctly. What changed is the calling convention on the other
side of that table -- StdrotValue gained val.blob and grew from 24
to 32 bytes, and StdrotType renumbered STDROT_NONE out from under
every v2-compiled switch. A v2 module would therefore have loaded
silently and then had every argument and return value passed at
the wrong width: memory corruption on the very first call, with no
diagnostic anywhere. Renaming to _v3 turns that into the loud
dlsym() failure below.
The lesson for the next bump: this symbol needs renaming whenever
ANYTHING crossing it changes shape -- StdrotValue and StdrotType
included -- not only when StdrotAPI/StdrotEntry do. */
StdrotAPI (*module_init)(void);
*(void **)(&module_init) = br_module_sym(handle, "brainrot_module_init_v3");
if (!module_init)
{
fprintf(stderr,
"Error: module '%s' (%s) does not export "
"brainrot_module_init_v3() -- it was built against an "
"incompatible or missing module ABI (expected "
"STDROT_ABI_VERSION %d). Rebuild this module against the "
"current stdrot_api.h.\n",
name, so_path, STDROT_ABI_VERSION);
br_module_close(handle);
pending_module_handle = NULL;
exit(1);
}
StdrotAPI api = module_init();
validate_native_registry(api.functions, api.count);
/* validate_native_registry() only proved this module's OWN table is
internally coherent -- it has no way to know about the core library
or any module cooked earlier in this same compilation. Without this,
a name colliding with an existing export would silently resolve to
whichever source happened to register it first, making a call's
target depend on #cooked order instead of on the program's own
text -- the exact class of ambiguity validate_native_registry()'s
own within-one-table duplicate check exists to reject, just across
tables instead of within one. */
for (int i = 0; i < api.count; i++)
{
const char *entry_name = api.functions[i]->name;
const String probe = {.data = (char *)entry_name,
.len = strlen(entry_name)};
if (is_builtin_function(probe))
{
fprintf(stderr,
"Error: module '%s' (%s): native export '%s' is "
"already provided by %s\n",
name, so_path, entry_name,
describe_native_source(entry_name));
br_module_close(handle);
pending_module_handle = NULL;
exit(1);
}
}
char *name_copy = strdup(name);
if (!name_copy)
{
fprintf(stderr, "out of memory\n");
br_module_close(handle);
pending_module_handle = NULL;
exit(1);
}
cooked_modules[cooked_module_count].handle = handle;
cooked_modules[cooked_module_count].name = name_copy;
cooked_modules[cooked_module_count].functions = api.functions;
cooked_modules[cooked_module_count].function_count = api.count;
cooked_module_count++;
pending_module_handle =
NULL; /* ownership transferred to cooked_modules[] */
}
#else /* !MODULE_NATIVE_LOADER */
/* wasm has no dynamic loader worth using (see this file's own top comment)
* -- module_path_resolve() (module_path.c) never resolves a #cooked <name>
* to a native module in this build, so this should never actually be
* called here. Fails loudly instead of silently doing nothing, on the same
* principle as every other ABI-enforcement function in this file: a path
* that's "supposed to be unreachable" still needs to fail safely if it's
* ever reached anyway (a module_path.c bug, or a future caller that
* doesn't route through the resolver), not corrupt state or crash. */
void stdrot_load_module(const char *name, const char *so_path)
{
(void)so_path;
fprintf(stderr,
"Error: cannot load native module '%s' -- native modules are "
"not supported in this build (no dynamic loader)\n",
name);
exit(1);
}
#endif /* MODULE_NATIVE_LOADER */
/* ── Runtime query ──────────────────────────────────────────────────────────
*/
bool is_builtin_function(const String func_name)
{
if (!func_name.data)
return false;
for (int i = 0; i < function_count; i++)
{
if (strcmp(func_name.data, functions[i]->name) == 0)
{
return true;
}
}
#ifdef MODULE_NATIVE_LOADER
for (int m = 0; m < cooked_module_count; m++)
{
for (int i = 0; i < cooked_modules[m].function_count; i++)
{
if (strcmp(func_name.data, cooked_modules[m].functions[i]->name) ==
0)
{
return true;
}
}
}
#endif
return false;
}
const StdrotEntry *get_native_function(const String func_name)
{
if (!func_name.data)
return NULL;
for (int i = 0; i < function_count; i++)
{
if (strcmp(func_name.data, functions[i]->name) == 0)
{
return functions[i];
}
}
#ifdef MODULE_NATIVE_LOADER
for (int m = 0; m < cooked_module_count; m++)
{
for (int i = 0; i < cooked_modules[m].function_count; i++)
{
if (strcmp(func_name.data, cooked_modules[m].functions[i]->name) ==
0)
{
return cooked_modules[m].functions[i];
}
}
}
#endif
return NULL;
}
VarType stdrot_type_to_vartype(StdrotType type)
{
switch (type)
{
case STDROT_INT:
/* giga/thicc (STDROT_LONG): a 64-bit integer is still VAR_INT at the type
level; its width lives in the is_long/is_long_long modifiers (#282). It
shares this branch with STDROT_INT rather than a clone of it. */
case STDROT_LONG:
return VAR_INT;
case STDROT_FLOAT:
return VAR_FLOAT;
case STDROT_DOUBLE:
return VAR_DOUBLE;
case STDROT_SHORT:
return VAR_SHORT;
case STDROT_BOOL:
return VAR_BOOL;
case STDROT_CHAR:
return VAR_CHAR;
case STDROT_STRING:
case STDROT_CSTRING:
return VAR_STRING;
case STDROT_PTR:
/* A real, known category -- "opaque pointer, base type
intentionally erased" -- not NONE ("unknown, skip checking").
See VAR_PTR's own comment in ast.h for why conflating the two
would silently defeat every "type == NONE, don't validate"
shortcut this analyzer already relies on. */
return VAR_PTR;
case STDROT_NONE:
/* Same reasoning as STDROT_PTR/VAR_PTR just above, for the exact
same class of bug: STDROT_NONE means a native's descriptor
return type genuinely is void -- known with total certainty to
produce no value -- not "unknown, don't validate." Mapping it
to plain NONE meant `rizz x = a_void_native();` type-checked,
because every "type == NONE, fail open" shortcut in this
analyzer treated a certainly-void expression as an unknowable
one. See VAR_VOID's own comment (ast.h) for the full
reasoning. */
return VAR_VOID;
case STDROT_STRUCT:
/* A real, known category, like STDROT_PTR above and unlike
STDROT_HANDLE below: an aggregate passed by value has a
genuine Brainrot type (VAR_STRUCT) that the analyzer can and
must check the argument against. The base VarType alone is
not the whole check, though -- two different `gang`s are both
VAR_STRUCT -- so semantic_check_native_call() compares the
tag (StdrotParam.type_name) separately rather than relying on
this mapping to distinguish them. */
return VAR_STRUCT;
case STDROT_HANDLE:
/* An opaque native resource (#213). VAR_PTR, for the same reason
STDROT_PTR is: from the type system's point of view a handle IS
an address whose base type is deliberately erased, and VAR_PTR
is exactly that category. What distinguishes a handle from a
plain pointer is not its Brainrot type but its `kind` tag and
the owning library's live-handle registry -- see
STDROT_HANDLE's own comment in stdrot_api.h. enforce_arg_type()