Sprint 19: CLI Surface + Diagnostics (-map, -why_live)
Prerequisites
Sprints 18–18.5 — executable and dylib milestones reached.
Goals
Full ld-compatible CLI surface for the flags armfortas already uses and those fortsh is likely to invoke. Includes the two diagnostics surfaces we declared launch-blocking: -map (text link map) and -why_live (dead-strip reason chain). No polish-tier deferral.
Deliverables
1. Full flag list
Recognized:
Inputs/outputs:
-o <path>- positional
<input> -l<name>/-l <name>-L <dir>-framework <name>-weak_framework <name>-force_load <archive>-all_load-ObjC(skippable no-op unless inputs have ObjC — they won't from armfortas today)
Target & platform:
-arch arm64-syslibroot <path>-platform_version macos <min> <sdk>
Output kind:
- (default) executable
-dylib-r(relocatable — deferred; errors for now)-bundle(deferred; errors for now)
Entry & startup:
-e <symbol>(default_mainfor executables)
Runtime search paths:
-rpath <path>-install_name <path>(dylib only)-compatibility_version <v>(dylib only)-current_version <v>(dylib only)
Symbol handling:
-undefined <error|warning|suppress|dynamic_lookup>(default: error)-exported_symbols_list <file>-unexported_symbols_list <file>-exported_symbol <sym>-unexported_symbol <sym>-x(strip locals)-S(strip debug)
Layout & output metadata:
-no_uuid-dead_strip(gates Sprint 23 pass)-icf=safe/-icf=none(gates Sprint 24 pass)-fixup_chains/-no_fixup_chains
Diagnostics:
-map <path>: emit text link map-why_live <symbol>: print dead-strip reason chain-t/-trace: print input file paths as they are loaded-v/--version-h/--help
Passthrough / compat:
-Wl,<comma-separated>: normalize into separate flags.- Unknown flags: error with suggestion (Levenshtein-3 over the list above).
2. -map <path> output format
Text file mirroring ld's link map:
# Path: <output path>
# Arch: arm64
# Object files:
[ 0] linker synthesized
[ 1] hello.o
[ 2] libarmfortas_rt.a(runtime.o)
...
# Sections:
# Address Size Segment Section
0x100003f9c 0x00000018 __TEXT __text
0x100003fb4 0x00000024 __TEXT __stubs
...
# Symbols:
# Address Size File Name
0x100003f9c 0x00000014 [ 1] _main
0x100003fb0 0x00000004 [ 1] .alt_entry_of_main
0x100003fb4 0x0000000c linker _printf (stub)
...
# Dead stripped:
<file> <symbol>
[ 2] _unused_helper
3. -why_live <symbol> output
Walks the live-edge graph from Sprint 23 backward from the named symbol to a root:
_main is live because:
_main is in -e _main (GC root)
_afs_write_char is live because:
_afs_write_char is reachable from _afs_print
_afs_print is reachable from _main
_main is in -e _main (GC root)
When used before -dead_strip has been applied, the diagnostic explains that -dead_strip was not requested. Multiple -why_live names allowed.
4. Exported / unexported symbols files
Each line of the file is a symbol name. Wildcards: * matches any chars, ? matches one. Used to adjust the final export trie and to mark symbols N_PEXT when -unexported_symbol is set. Consumed by Sprint 14's symbol-table construction (which this sprint amends).
5. CLI parser
afs-ld/src/args.rs:
- Hand-rolled, no clap.
- Streaming argv scan.
- Error messages cite the flag, the invalid value, and the expected format.
-Wl,-map,foo.txtnormalized to-map foo.txtbefore dispatch.
6. -t trace output
As each input file is loaded:
afs-ld: loading hello.o
afs-ld: loading libarmfortas_rt.a
afs-ld: loading libarmfortas_rt.a(io.o)
afs-ld: loading /usr/lib/libSystem.tbd
Testing Strategy
- One test per flag: parse the flag, assert
LinkOptionsfield set correctly. - Error-message snapshot tests for every invalid-flag case.
-mapdifferential: produce a map, compare shape (not exact byte) told's map on hello-world.-why_live _mainproduces a root-only explanation.-why_live <transitively-reachable-sym>produces a chain.-Wl,-map,foo.txtparsed identically to-map foo.txt.
Definition of Done
- Every flag listed above parses and wires correctly.
-mapproduces human-readable output covering object files, sections, symbols, dead-stripped entries.-why_liveproduces a coherent chain on fixtures with dead-strip enabled.- Unknown-flag errors include a did-you-mean suggestion.
- CLI surface passes a snapshot test against the
--helpoutput.
View source
| 1 | # Sprint 19: CLI Surface + Diagnostics (`-map`, `-why_live`) |
| 2 | |
| 3 | ## Prerequisites |
| 4 | Sprints 18–18.5 — executable and dylib milestones reached. |
| 5 | |
| 6 | ## Goals |
| 7 | Full `ld`-compatible CLI surface for the flags armfortas already uses and those fortsh is likely to invoke. Includes the two diagnostics surfaces we declared launch-blocking: `-map` (text link map) and `-why_live` (dead-strip reason chain). No polish-tier deferral. |
| 8 | |
| 9 | ## Deliverables |
| 10 | |
| 11 | ### 1. Full flag list |
| 12 | Recognized: |
| 13 | |
| 14 | **Inputs/outputs**: |
| 15 | - `-o <path>` |
| 16 | - positional `<input>` |
| 17 | - `-l<name>` / `-l <name>` |
| 18 | - `-L <dir>` |
| 19 | - `-framework <name>` |
| 20 | - `-weak_framework <name>` |
| 21 | - `-force_load <archive>` |
| 22 | - `-all_load` |
| 23 | - `-ObjC` (skippable no-op unless inputs have ObjC — they won't from armfortas today) |
| 24 | |
| 25 | **Target & platform**: |
| 26 | - `-arch arm64` |
| 27 | - `-syslibroot <path>` |
| 28 | - `-platform_version macos <min> <sdk>` |
| 29 | |
| 30 | **Output kind**: |
| 31 | - (default) executable |
| 32 | - `-dylib` |
| 33 | - `-r` (relocatable — deferred; errors for now) |
| 34 | - `-bundle` (deferred; errors for now) |
| 35 | |
| 36 | **Entry & startup**: |
| 37 | - `-e <symbol>` (default `_main` for executables) |
| 38 | |
| 39 | **Runtime search paths**: |
| 40 | - `-rpath <path>` |
| 41 | - `-install_name <path>` (dylib only) |
| 42 | - `-compatibility_version <v>` (dylib only) |
| 43 | - `-current_version <v>` (dylib only) |
| 44 | |
| 45 | **Symbol handling**: |
| 46 | - `-undefined <error|warning|suppress|dynamic_lookup>` (default: error) |
| 47 | - `-exported_symbols_list <file>` |
| 48 | - `-unexported_symbols_list <file>` |
| 49 | - `-exported_symbol <sym>` |
| 50 | - `-unexported_symbol <sym>` |
| 51 | - `-x` (strip locals) |
| 52 | - `-S` (strip debug) |
| 53 | |
| 54 | **Layout & output metadata**: |
| 55 | - `-no_uuid` |
| 56 | - `-dead_strip` (gates Sprint 23 pass) |
| 57 | - `-icf=safe` / `-icf=none` (gates Sprint 24 pass) |
| 58 | - `-fixup_chains` / `-no_fixup_chains` |
| 59 | |
| 60 | **Diagnostics**: |
| 61 | - `-map <path>`: emit text link map |
| 62 | - `-why_live <symbol>`: print dead-strip reason chain |
| 63 | - `-t` / `-trace`: print input file paths as they are loaded |
| 64 | - `-v` / `--version` |
| 65 | - `-h` / `--help` |
| 66 | |
| 67 | **Passthrough / compat**: |
| 68 | - `-Wl,<comma-separated>`: normalize into separate flags. |
| 69 | - Unknown flags: error with suggestion (Levenshtein-3 over the list above). |
| 70 | |
| 71 | ### 2. `-map <path>` output format |
| 72 | Text file mirroring ld's link map: |
| 73 | ``` |
| 74 | # Path: <output path> |
| 75 | # Arch: arm64 |
| 76 | # Object files: |
| 77 | [ 0] linker synthesized |
| 78 | [ 1] hello.o |
| 79 | [ 2] libarmfortas_rt.a(runtime.o) |
| 80 | ... |
| 81 | |
| 82 | # Sections: |
| 83 | # Address Size Segment Section |
| 84 | 0x100003f9c 0x00000018 __TEXT __text |
| 85 | 0x100003fb4 0x00000024 __TEXT __stubs |
| 86 | ... |
| 87 | |
| 88 | # Symbols: |
| 89 | # Address Size File Name |
| 90 | 0x100003f9c 0x00000014 [ 1] _main |
| 91 | 0x100003fb0 0x00000004 [ 1] .alt_entry_of_main |
| 92 | 0x100003fb4 0x0000000c linker _printf (stub) |
| 93 | ... |
| 94 | |
| 95 | # Dead stripped: |
| 96 | <file> <symbol> |
| 97 | [ 2] _unused_helper |
| 98 | ``` |
| 99 | |
| 100 | ### 3. `-why_live <symbol>` output |
| 101 | Walks the live-edge graph from Sprint 23 backward from the named symbol to a root: |
| 102 | ``` |
| 103 | _main is live because: |
| 104 | _main is in -e _main (GC root) |
| 105 | |
| 106 | _afs_write_char is live because: |
| 107 | _afs_write_char is reachable from _afs_print |
| 108 | _afs_print is reachable from _main |
| 109 | _main is in -e _main (GC root) |
| 110 | ``` |
| 111 | |
| 112 | When used before `-dead_strip` has been applied, the diagnostic explains that `-dead_strip` was not requested. Multiple `-why_live` names allowed. |
| 113 | |
| 114 | ### 4. Exported / unexported symbols files |
| 115 | Each line of the file is a symbol name. Wildcards: `*` matches any chars, `?` matches one. Used to adjust the final export trie and to mark symbols `N_PEXT` when `-unexported_symbol` is set. Consumed by Sprint 14's symbol-table construction (which this sprint amends). |
| 116 | |
| 117 | ### 5. CLI parser |
| 118 | `afs-ld/src/args.rs`: |
| 119 | - Hand-rolled, no clap. |
| 120 | - Streaming argv scan. |
| 121 | - Error messages cite the flag, the invalid value, and the expected format. |
| 122 | - `-Wl,-map,foo.txt` normalized to `-map foo.txt` before dispatch. |
| 123 | |
| 124 | ### 6. `-t` trace output |
| 125 | As each input file is loaded: |
| 126 | ``` |
| 127 | afs-ld: loading hello.o |
| 128 | afs-ld: loading libarmfortas_rt.a |
| 129 | afs-ld: loading libarmfortas_rt.a(io.o) |
| 130 | afs-ld: loading /usr/lib/libSystem.tbd |
| 131 | ``` |
| 132 | |
| 133 | ## Testing Strategy |
| 134 | - One test per flag: parse the flag, assert `LinkOptions` field set correctly. |
| 135 | - Error-message snapshot tests for every invalid-flag case. |
| 136 | - `-map` differential: produce a map, compare shape (not exact byte) to `ld`'s map on hello-world. |
| 137 | - `-why_live _main` produces a root-only explanation. |
| 138 | - `-why_live <transitively-reachable-sym>` produces a chain. |
| 139 | - `-Wl,-map,foo.txt` parsed identically to `-map foo.txt`. |
| 140 | |
| 141 | ## Definition of Done |
| 142 | - Every flag listed above parses and wires correctly. |
| 143 | - `-map` produces human-readable output covering object files, sections, symbols, dead-stripped entries. |
| 144 | - `-why_live` produces a coherent chain on fixtures with dead-strip enabled. |
| 145 | - Unknown-flag errors include a did-you-mean suggestion. |
| 146 | - CLI surface passes a snapshot test against the `--help` output. |