6l: port ET_DYN dynamic linking to the ww side

Ports cmd/6l/{dyn,dynout}.c into selfhost/cmd/6l/{dyn,dynout}.ww:
ET_DYN .so loading + PT_INTERP/PT_DYNAMIC ELF emission with .rela.plt,
.gnu.version_r, BIND_NOW. lsym grows dyn fields; pass.ww promotes
undefs to dyn; out.ww dispatches; main.ww takes -L/-l. The ww driver
forwards -L/-l to 6l_ww so 'ww_ww build snake.ww -L /usr/lib -l ncurses
-l c' runs without cc.

Test 996 pins byte-identical output to C-6l on snake.

'make bootstrap' gains a fourth stage with cmp ww3 == ww4, proving
ww3 is byte-stable when used as a compiler — not just a coincidental
two-stage equilibrium.

Four wwstage 6c cgen quirks surfaced and are documented in dynout.ww's
header (two-level field-write through a pointer field, (scalar, str)
tuple returns, def : str, ≤6 arg calling convention).
This commit is contained in:
2026-05-11 12:47:36 +09:00
parent edfc4273f6
commit e217cd32d1
13 changed files with 3361 additions and 111 deletions

View File

@@ -695,18 +695,28 @@ fn append_lit(stem: *u8, suffix: str) *u8 = {
return buf;
};
// linker flags bundled as a struct so build_one stays at the wwstage
// 6c's 6-argument calling-convention limit.
type lflags = struct {
libdirs: **u8,
n_libdirs: i32,
libs: **u8,
n_libs: i32,
};
// build_one — compile `src` into the executable named `out`.
// self_dir: NUL-terminated dir containing this driver and the
// wwstage tools (6c_ww/6a_ww/6l_ww)
// src: NUL-terminated path to the .ww file
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// self_dir: NUL-terminated dir containing this driver and the
// wwstage tools (6c_ww/6a_ww/6l_ww)
// src: NUL-terminated path to the .ww file
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
//
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
// still drives the C-built 6c/6a/6l. Test 993 pins the two
// pipelines to byte-identical output on a corpus.
fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = {
fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
let a: *arena = newarena();
let c6: *u8 = join_path_lit(self_dir, "6c_ww");
@@ -792,15 +802,45 @@ fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = {
};
};
// Step 4: 6l -o <out> <stem>.o libwwrt.a
// Step 4: 6l -o <out> <stem>.o libwwrt.a [-L<dir>...] [-l<name>...]
{
let argv: **u8 = os.alloc(48u64): **u8;
let nl_dirs: i32 = 0;
let nl_libs: i32 = 0;
let l_dirs: **u8 = nil;
let l_libs: **u8 = nil;
if (lf != nil) {
nl_dirs = lf.n_libdirs;
nl_libs = lf.n_libs;
l_dirs = lf.libdirs;
l_libs = lf.libs;
};
// argv slots: 5 fixed (6l, -o, out, objf, libwwrt)
// + 2 * n_libdirs (-L, dir)
// + 2 * n_libs (-l, name)
// + 1 nil terminator.
let total: i32 = 5 + 2 * nl_dirs + 2 * nl_libs + 1;
let argv: **u8 = os.alloc((total: u64) * 8u64): **u8;
argv[0] = "6l\0".ptr;
argv[1] = "-o\0".ptr;
argv[2] = out;
argv[3] = objf;
argv[4] = libwwrt;
argv[5] = nil;
let pos: i32 = 5;
let k: i32 = 0;
for (k < nl_dirs) {
argv[pos] = "-L\0".ptr;
argv[pos + 1] = l_dirs[k];
pos += 2;
k += 1;
};
k = 0;
for (k < nl_libs) {
argv[pos] = "-l\0".ptr;
argv[pos + 1] = l_libs[k];
pos += 2;
k += 1;
};
argv[pos] = nil;
if (proc_run(l6, argv) != 0) {
os.write(2, "ww: 6l failed\n".ptr, 14u64);
return 1;
@@ -859,11 +899,16 @@ fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let inc_off: u64 = 0u64;
cstr_seal(incs, 0u64);
let max_lflags: i32 = 32;
let libdirs: **u8 = os.alloc((max_lflags: u64) * 8u64): **u8;
let n_libdirs: i32 = 0;
let libs: **u8 = os.alloc((max_lflags: u64) * 8u64): **u8;
let n_libs: i32 = 0;
let i: i32 = start;
for (i < argc) {
let p: *u8 = argv[i];
// -I <dir>
if (p[0u64] == 45u8) {
if (p[0u64] == 45u8) { // '-'
if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
@@ -882,14 +927,46 @@ fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
};
inc_off = cstr_into(incs, inc_off, dir);
cstr_seal(incs, inc_off);
} else {
// -lLIB silently ignored for now (driver doesn't yet
// pass extra archives to 6l).
if (p[1u64] != 108u8) {
os.write(2, "ww build: unknown flag\n".ptr, 23u64);
} else { if (p[1u64] == 76u8) { // '-L'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
} else {
if (i + 1 >= argc) {
os.write(2, "ww build: -L needs an argument\n".ptr, 31u64);
return 2;
};
i += 1;
dir = argv[i];
};
if (n_libdirs >= max_lflags) {
os.write(2, "ww build: too many -L\n".ptr, 22u64);
return 2;
};
};
libdirs[n_libdirs] = dir;
n_libdirs += 1;
} else { if (p[1u64] == 108u8) { // '-l'
let nm: *u8 = nil;
if (p[2u64] != 0u8) {
nm = p + 2u64;
} else {
if (i + 1 >= argc) {
os.write(2, "ww build: -l needs an argument\n".ptr, 31u64);
return 2;
};
i += 1;
nm = argv[i];
};
if (n_libs >= max_lflags) {
os.write(2, "ww build: too many -l\n".ptr, 22u64);
return 2;
};
libs[n_libs] = nm;
n_libs += 1;
} else {
os.write(2, "ww build: unknown flag\n".ptr, 23u64);
return 2;
}; }; };
} else {
if (src == nil) { src = p; };
};
@@ -901,7 +978,12 @@ fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
return 2;
};
let out: *u8 = default_out_path(src);
return build_one(self_dir, src, out, incs);
let lf: lflags;
lf.libdirs = libdirs;
lf.n_libdirs = n_libdirs;
lf.libs = libs;
lf.n_libs = n_libs;
return build_one(self_dir, src, out, incs, &lf);
};
// Format the scratch path /tmp/ww_run_<pid> into buf. Returns NUL-
@@ -941,7 +1023,7 @@ fn do_run(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
};
let tmp: *u8 = os.alloc(PATH_MAX): *u8;
make_run_tmp(tmp);
if (build_one(self_dir, argv[start], tmp, "\0".ptr) != 0) { return 1; };
if (build_one(self_dir, argv[start], tmp, "\0".ptr, nil) != 0) { return 1; };
let exec_argv: **u8 = os.alloc(16u64): **u8;
exec_argv[0] = tmp;
exec_argv[1] = nil;

View File

@@ -407,18 +407,28 @@ fn append_lit(stem: *u8, suffix: str) *u8 = {
return buf;
};
// linker flags bundled as a struct so build_one stays at the wwstage
// 6c's 6-argument calling-convention limit.
type lflags = struct {
libdirs: **u8,
n_libdirs: i32,
libs: **u8,
n_libs: i32,
};
// build_one — compile `src` into the executable named `out`.
// self_dir: NUL-terminated dir containing this driver and the
// wwstage tools (6c_ww/6a_ww/6l_ww)
// src: NUL-terminated path to the .ww file
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// self_dir: NUL-terminated dir containing this driver and the
// wwstage tools (6c_ww/6a_ww/6l_ww)
// src: NUL-terminated path to the .ww file
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
//
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
// still drives the C-built 6c/6a/6l. Test 993 pins the two
// pipelines to byte-identical output on a corpus.
fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = {
fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
let a: *arena = newarena();
let c6: *u8 = join_path_lit(self_dir, "6c_ww");
@@ -504,15 +514,45 @@ fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = {
};
};
// Step 4: 6l -o <out> <stem>.o libwwrt.a
// Step 4: 6l -o <out> <stem>.o libwwrt.a [-L<dir>...] [-l<name>...]
{
let argv: **u8 = os.alloc(48u64): **u8;
let nl_dirs: i32 = 0;
let nl_libs: i32 = 0;
let l_dirs: **u8 = nil;
let l_libs: **u8 = nil;
if (lf != nil) {
nl_dirs = lf.n_libdirs;
nl_libs = lf.n_libs;
l_dirs = lf.libdirs;
l_libs = lf.libs;
};
// argv slots: 5 fixed (6l, -o, out, objf, libwwrt)
// + 2 * n_libdirs (-L, dir)
// + 2 * n_libs (-l, name)
// + 1 nil terminator.
let total: i32 = 5 + 2 * nl_dirs + 2 * nl_libs + 1;
let argv: **u8 = os.alloc((total: u64) * 8u64): **u8;
argv[0] = "6l\0".ptr;
argv[1] = "-o\0".ptr;
argv[2] = out;
argv[3] = objf;
argv[4] = libwwrt;
argv[5] = nil;
let pos: i32 = 5;
let k: i32 = 0;
for (k < nl_dirs) {
argv[pos] = "-L\0".ptr;
argv[pos + 1] = l_dirs[k];
pos += 2;
k += 1;
};
k = 0;
for (k < nl_libs) {
argv[pos] = "-l\0".ptr;
argv[pos + 1] = l_libs[k];
pos += 2;
k += 1;
};
argv[pos] = nil;
if (proc_run(l6, argv) != 0) {
os.write(2, "ww: 6l failed\n".ptr, 14u64);
return 1;
@@ -571,11 +611,16 @@ fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let inc_off: u64 = 0u64;
cstr_seal(incs, 0u64);
let max_lflags: i32 = 32;
let libdirs: **u8 = os.alloc((max_lflags: u64) * 8u64): **u8;
let n_libdirs: i32 = 0;
let libs: **u8 = os.alloc((max_lflags: u64) * 8u64): **u8;
let n_libs: i32 = 0;
let i: i32 = start;
for (i < argc) {
let p: *u8 = argv[i];
// -I <dir>
if (p[0u64] == 45u8) {
if (p[0u64] == 45u8) { // '-'
if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
@@ -594,14 +639,46 @@ fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
};
inc_off = cstr_into(incs, inc_off, dir);
cstr_seal(incs, inc_off);
} else {
// -lLIB silently ignored for now (driver doesn't yet
// pass extra archives to 6l).
if (p[1u64] != 108u8) {
os.write(2, "ww build: unknown flag\n".ptr, 23u64);
} else { if (p[1u64] == 76u8) { // '-L'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
} else {
if (i + 1 >= argc) {
os.write(2, "ww build: -L needs an argument\n".ptr, 31u64);
return 2;
};
i += 1;
dir = argv[i];
};
if (n_libdirs >= max_lflags) {
os.write(2, "ww build: too many -L\n".ptr, 22u64);
return 2;
};
};
libdirs[n_libdirs] = dir;
n_libdirs += 1;
} else { if (p[1u64] == 108u8) { // '-l'
let nm: *u8 = nil;
if (p[2u64] != 0u8) {
nm = p + 2u64;
} else {
if (i + 1 >= argc) {
os.write(2, "ww build: -l needs an argument\n".ptr, 31u64);
return 2;
};
i += 1;
nm = argv[i];
};
if (n_libs >= max_lflags) {
os.write(2, "ww build: too many -l\n".ptr, 22u64);
return 2;
};
libs[n_libs] = nm;
n_libs += 1;
} else {
os.write(2, "ww build: unknown flag\n".ptr, 23u64);
return 2;
}; }; };
} else {
if (src == nil) { src = p; };
};
@@ -613,7 +690,12 @@ fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
return 2;
};
let out: *u8 = default_out_path(src);
return build_one(self_dir, src, out, incs);
let lf: lflags;
lf.libdirs = libdirs;
lf.n_libdirs = n_libdirs;
lf.libs = libs;
lf.n_libs = n_libs;
return build_one(self_dir, src, out, incs, &lf);
};
// Format the scratch path /tmp/ww_run_<pid> into buf. Returns NUL-
@@ -653,7 +735,7 @@ fn do_run(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
};
let tmp: *u8 = os.alloc(PATH_MAX): *u8;
make_run_tmp(tmp);
if (build_one(self_dir, argv[start], tmp, "\0".ptr) != 0) { return 1; };
if (build_one(self_dir, argv[start], tmp, "\0".ptr, nil) != 0) { return 1; };
let exec_argv: **u8 = os.alloc(16u64): **u8;
exec_argv[0] = tmp;
exec_argv[1] = nil;