Found in a modularity review of main @ 8e8970a. Tracking issue, ordered cheapest-fix-first. The pipeline (lexer → parser → compiler → chunk → vm → builtins → ext) is a real and defensible design; nothing enforces it.
1. eigenscript.c includes two extension private headers and uses zero symbols from them — trivially removable
eigenscript.c:13 includes ext_db_internal.h and :16 includes model_internal.h. Verified usage in that TU:
| Symbol |
Uses in eigenscript.c |
TransformerModel |
0 |
PGconn |
0 |
g_db_conn |
0 |
ne_matmul_buf |
0 |
quantize_ternary |
0 |
json_obj_get |
0 |
ObserverBuffer |
0 |
ext_db_internal.h:4 states "Only included by ext_db.c" and pulls <libpq-fe.h>. So in the full variant the core TU cannot compile without PostgreSQL headers on the include path, for a dead include. Deleting both lines is safe and removes the only core→ext include edge in the tree.
2. vm.c:242-278 carries 32 stale extern re-declarations of already-public symbols
All are declared in eigenscript.h. One is commented "declared in eval.c" (vm.c:266-268) — a TU deleted when the bytecode VM replaced the tree-walker. val_incref there (vm.c:242) is inert, because eigenscript.h:903 defines it static inline. Harmless today, a silent divergence hazard the moment any of those signatures changes.
3. No per-layer headers; eigenscript.h is a 1253-line umbrella every TU includes
There is no lexer.h, parser.h, compiler.h, chunk.h, or builtins.h — only vm.h, jit.h, trace.h, state.h. eigenscript.h spans tokenizer (:114), AST (:159), values (:217), arena (:418), EigsThread (:442), env (:927), parser (:1054), registration (:1082), MODEL tensor kernels (:1194), handle table (:1204), store (:1214), step (:1217), fmt+lint (:1222) — 26 structs with every field visible, 167 declarations, included by 29 of ~30 TUs.
Measured consequences:
The header graph itself is clean and acyclic (eigenscript.h → value_slot.h, vm.h → value_slot.h, everything else → eigenscript.h), so this is a hub problem, not a tangle. The external API discipline is notably better: eigs_embed.h plus six *_internal.h headers.
4. Filesystem utilities live in the builtins leaf and are consumed upward
read_file_util (builtins.c:2806) and resolve_eigenscript_file_from (:2880/2886) are used by vm.c:5034-5036 (the import opcode re-declares both extern at block scope inside the dispatch loop), main.c:298, lint.c:2157,2622,2691, and fmt.c:383.
So nothing can link file reading without the whole 7407-line builtins TU — visibly why LSP_SOURCES (Makefile:39) and FUZZ_SOURCES (:296) link the entire runtime after hand-picked subsets bitrotted. The VM's import handler additionally performs path resolution, realpath, and module-cache canonicalization inline (vm.c:5028-5083), so the dispatch loop owns filesystem policy.
5. Two TUs carry several responsibilities each
vm.c (6224): dispatch (vm_run_ex, :2365-5496 — a single 3131-line function covering 85 opcodes), the JIT helper runtime ABI (jit_helper_*, :759-2255, ~1500 lines), the cooperative task scheduler (:5502-6190, self-contained TaskScheduler), and the SIGUSR1 observer dump (:33-240).
builtins.c (7407): at least ten groups — bitwise (:396), dict (:700), regex (:1636), string (:1785), argv/path/fs (:2070), corpus/JSON (:2285), file+module loading (:2800), stdio/exec/proc (:3099), eval + bytecode assembly (:4068), channels/tasks (:5032), buffers/DSP (:5938), DEFLATE (:6135).
Worth noting what the size metric doesn't say: builtins.c has 248 functions averaging 29 lines and jit.c 180 averaging 22 — both are well-decomposed files that are merely long. vm.c (101 functions, 61-line average, one at 3131) is the genuine cohesion outlier.
Mechanical splits available now: vm.c's task scheduler → task.c (own struct, own statics); builtins.c's buffer/DSP kernels and DEFLATE → builtins_buf.c (no shared statics). Entangled: jit_helper_* must stay with vm.c because it consumes static inline vm_push/vm_pop/vm_slot_lift (vm.c:582-645); moving it needs those promoted to a private header first.
Found in a modularity review of
main@8e8970a. Tracking issue, ordered cheapest-fix-first. The pipeline (lexer → parser → compiler → chunk → vm → builtins → ext) is a real and defensible design; nothing enforces it.1.
eigenscript.cincludes two extension private headers and uses zero symbols from them — trivially removableeigenscript.c:13includesext_db_internal.hand:16includesmodel_internal.h. Verified usage in that TU:eigenscript.cTransformerModelPGconng_db_connne_matmul_bufquantize_ternaryjson_obj_getObserverBufferext_db_internal.h:4states "Only included by ext_db.c" and pulls<libpq-fe.h>. So in thefullvariant the core TU cannot compile without PostgreSQL headers on the include path, for a dead include. Deleting both lines is safe and removes the only core→ext include edge in the tree.2.
vm.c:242-278carries 32 staleexternre-declarations of already-public symbolsAll are declared in
eigenscript.h. One is commented "declared in eval.c" (vm.c:266-268) — a TU deleted when the bytecode VM replaced the tree-walker.val_increfthere (vm.c:242) is inert, becauseeigenscript.h:903defines itstatic inline. Harmless today, a silent divergence hazard the moment any of those signatures changes.3. No per-layer headers;
eigenscript.his a 1253-line umbrella every TU includesThere is no
lexer.h,parser.h,compiler.h,chunk.h, orbuiltins.h— onlyvm.h,jit.h,trace.h,state.h.eigenscript.hspans tokenizer (:114), AST (:159), values (:217), arena (:418),EigsThread(:442), env (:927), parser (:1054), registration (:1082), MODEL tensor kernels (:1194), handle table (:1204), store (:1214), step (:1217), fmt+lint (:1222) — 26 structs with every field visible, 167 declarations, included by 29 of ~30 TUs.Measured consequences:
compiler.c:1692-1712increments the parser'sg_parse_depthEigsThreadfield as its own recursion guard, and lexer/parser/compiler all writeg_parse_errors(lexer.c:228,266,442;parser.c:102,626;compiler.c:427,2108) — the front end mutating runtime thread state.The header graph itself is clean and acyclic (
eigenscript.h → value_slot.h,vm.h → value_slot.h, everything else →eigenscript.h), so this is a hub problem, not a tangle. The external API discipline is notably better:eigs_embed.hplus six*_internal.hheaders.4. Filesystem utilities live in the builtins leaf and are consumed upward
read_file_util(builtins.c:2806) andresolve_eigenscript_file_from(:2880/2886) are used byvm.c:5034-5036(theimportopcode re-declares bothexternat block scope inside the dispatch loop),main.c:298,lint.c:2157,2622,2691, andfmt.c:383.So nothing can link file reading without the whole 7407-line builtins TU — visibly why
LSP_SOURCES(Makefile:39) andFUZZ_SOURCES(:296) link the entire runtime after hand-picked subsets bitrotted. The VM'simporthandler additionally performs path resolution,realpath, and module-cache canonicalization inline (vm.c:5028-5083), so the dispatch loop owns filesystem policy.5. Two TUs carry several responsibilities each
vm.c(6224): dispatch (vm_run_ex,:2365-5496— a single 3131-line function covering 85 opcodes), the JIT helper runtime ABI (jit_helper_*,:759-2255, ~1500 lines), the cooperative task scheduler (:5502-6190, self-containedTaskScheduler), and the SIGUSR1 observer dump (:33-240).builtins.c(7407): at least ten groups — bitwise (:396), dict (:700), regex (:1636), string (:1785), argv/path/fs (:2070), corpus/JSON (:2285), file+module loading (:2800), stdio/exec/proc (:3099), eval + bytecode assembly (:4068), channels/tasks (:5032), buffers/DSP (:5938), DEFLATE (:6135).Worth noting what the size metric doesn't say:
builtins.chas 248 functions averaging 29 lines andjit.c180 averaging 22 — both are well-decomposed files that are merely long.vm.c(101 functions, 61-line average, one at 3131) is the genuine cohesion outlier.Mechanical splits available now: vm.c's task scheduler →
task.c(own struct, own statics); builtins.c's buffer/DSP kernels and DEFLATE →builtins_buf.c(no shared statics). Entangled:jit_helper_*must stay with vm.c because it consumesstatic inline vm_push/vm_pop/vm_slot_lift(vm.c:582-645); moving it needs those promoted to a private header first.