/**
* `harn precompile` ported to .harn — see harn#2313 (W13).
*
* Walks a file or directory tree, dispatches each `.harn` source to a
* `harn precompile <single-file>` child that runs the Rust compiler
* entrypoint. The .harn script owns:
*
* * argv parsing (target, --out, --keep-going, --quiet)
* * directory walking (single source vs recursive .harn discovery)
* * per-file --out path mirroring under the source root
* * the per-file render and the trailing "N compiled, N reused, N
* failed" summary
*
* The actual parse+typecheck+compile work stays in Rust on the spawned
* child (toggled via HARN_PRECOMPILE_INNER=1 so the child doesn't recurse
* back into this script). That keeps the bytecode-cache wire-format,
* compiler diagnostics, and module artifact emission untouched while
* letting the orchestration / file-walk layer live in Harn.
*
* Inputs (from the dispatch shim in crates/harn-cli/src/commands/precompile.rs):
* HARN_CLI_SELF_EXE — absolute path to the running `harn` binary,
* captured via `std::env::current_exe()` so the
* child invocation is robust to $PATH ordering.
* argv[0] — target file or directory (required)
* HARN_PRECOMPILE_OUT — output directory (optional; mirrors source tree)
* HARN_PRECOMPILE_KEEP_GOING — "1" to continue after a per-file error
* HARN_PRECOMPILE_QUIET — "1" to suppress per-file progress
*/
fn __walk_harn_files(fs: HarnessFs, root: string) -> dict {
// walk_dir returns a flat list of {path, is_dir, is_file, depth}. We
// only emit `.harn` files. Walk order is stable per the builtin
// contract; we sort + dedupe afterwards to match the Rust impl which
// calls `files.sorted(); files.dedup();`.
//
// The Rust collect_harn_files() (crates/harn-cli/src/commands/mod.rs)
// also drops files with a sibling `<name>.conformance-skip` marker;
// mirror that here so a directory with skip markers walks identically.
//
// The walk's own depth-0 entry is returned alongside the files, and it is
// the root every destination path is computed against. The caller's root is
// whatever the user typed and may be relative, while every walked path is
// absolute; deriving a relative path by mixing the two silently yields
// nothing, which is how the output mirror came to collapse. Reading the root
// back out of the same walk that produced the files keeps one spelling.
const entries = fs.walk(root)
let walked_root = ""
let files = []
for entry in entries {
const path = entry["path"]
const is_file = entry["is_file"] ?? false
if !is_file {
if (entry["depth"] ?? -1) == 0 {
walked_root = path
}
continue
}
if path_extension(path) != ".harn" {
continue
}
const skip_marker = path_with_extension(path, "conformance-skip")
if fs.exists(skip_marker) {
continue
}
files = files.appending(path)
}
const sorted = files.sorted()
// dedupe stable-sorted list: keep entry when it differs from previous.
let out = []
let prev = ""
for path in sorted {
if path != prev || len(out) == 0 {
out = out.appending(path)
}
prev = path
}
return {root: walked_root, files: out}
}
fn __destination_path(
source_path: string,
source_root: string,
out_root: string,
extension: string,
) -> string? {
// Mirror the Rust output_path() exactly: when out_root is empty, write
// adjacent to source; otherwise compute the relative path from
// source_root and join under out_root, then swap the extension.
//
// Returns nil rather than a basename when a source under a directory walk
// will not relativize. A basename there is not a fallback, it is a different
// destination: every equally named module in the tree lands on it and all
// but the last are overwritten, with nothing written to the paths the caller
// was promised and a zero exit. An empty source_root is the single-file
// case, where the basename IS the mirrored path.
if out_root == "" {
return path_with_extension(source_path, extension)
}
if source_root == "" {
return path_with_extension(path_join(out_root, path_basename(source_path)), extension)
}
const relative = path_relative_to(source_path, source_root)
if relative == nil {
return nil
}
return path_with_extension(path_join(out_root, relative), extension)
}
fn __ensure_parent(fs: HarnessFs, path: string) {
const parent = path_parent(path)
if parent != "" && !fs.exists(parent) {
fs.mkdir(parent)
}
}
fn __spawn_precompile(
process: HarnessProcess,
bin: string,
source_path: string,
per_file_out: string,
source_root: string,
) -> dict {
// Always force the child into the Rust compiler entrypoint: this script IS
// the .harn dispatch target, so leaving HARN_PRECOMPILE_INNER unset would
// re-enter us and spin forever.
//
// --quiet on the child suppresses its own per-file println; the
// parent script owns the user-visible output so the byte-for-byte
// shape is preserved when this port becomes the default.
let args = ["precompile", "--quiet", source_path]
if source_root != "" {
args = args.appending("--relocatable")
}
if per_file_out != "" {
args = args.appending("--out")
args = args.appending(per_file_out)
}
return process.run({program: bin, args: args, env: {HARN_PRECOMPILE_INNER: "1"}})
}
fn __child_reused(child: dict) -> bool {
// The child reports per-source outcome on stdout, which this driver captures
// and never forwards. Absence means compiled, not unknown: a child that
// succeeded without printing the marker is an older binary than this script,
// and counting that as a reuse would report work as skipped that was done.
const stdout = child["stdout"] ?? ""
for line in split(stdout, "\n") {
if trim(line) == "precompile-outcome: reused" {
return true
}
}
return false
}
fn __render_failure(source: string, child: dict) -> string {
// Match the Rust eprintln format `{source}: {err}` where `err` is
// either the child's first stderr line or a compact exit summary.
const stderr = child["stderr"] ?? ""
const trimmed = trim(stderr)
if trimmed != "" {
const lines = split(trimmed, "\n")
return source + ": " + lines[0]
}
const exit_code = child["exit_code"] ?? -1
return source + ": precompile exited with code " + to_string(exit_code)
}
fn main(harness: Harness) {
if len(argv) < 1 {
harness.stdio.eprintln("precompile: target path is required")
harness.runtime.exit(2)
}
const bin = harness.env.get_or("HARN_CLI_SELF_EXE", "")
if bin == "" {
harness.stdio.eprintln("internal error: HARN_CLI_SELF_EXE not set by dispatch shim")
harness.runtime.exit(70)
}
const target = argv[0]
const out_root = harness.env.get_or("HARN_PRECOMPILE_OUT", "")
const keep_going = harness.env.get_or("HARN_PRECOMPILE_KEEP_GOING", "0") == "1"
const quiet = harness.env.get_or("HARN_PRECOMPILE_QUIET", "0") == "1"
if !harness.fs.exists(target) {
harness.stdio.eprintln("error: target does not exist: " + target)
harness.runtime.exit(1)
}
const info = harness.fs.stat(target)
const is_dir = info["is_dir"] ?? false
const walked = if is_dir {
__walk_harn_files(harness.fs, target)
} else {
{root: "", files: [target]}
}
const sources = walked["files"] ?? []
// The walk's own spelling of the root, not the caller's. See
// __walk_harn_files: mixing a relative root with absolute walked paths is
// what collapsed the mirror.
const source_root = walked["root"] ?? ""
if is_dir && source_root == "" {
harness.stdio.eprintln(
"internal error: directory walk of " + target + " reported no root entry",
)
harness.runtime.exit(70)
}
if len(sources) == 0 {
harness.stdio.eprintln("error: no .harn files found under " + target)
harness.runtime.exit(1)
}
let compiled = 0
let reused = 0
let failed = 0
for source in sources {
// For each source, pre-compute the destination dir so the child --out
// points at the exact directory the .harnbc should land in. This
// preserves the source-tree mirroring the Rust output_path() does;
// without this, the child (which sees a single-file target) would
// collapse everything into out_root with no subdirectories.
const placed = __destination_path(source, source_root, out_root, "harnbc")
if placed == nil {
harness.stdio.eprintln(source + ": cannot place under --out: not relative to " + source_root)
harness.runtime.exit(1)
}
// Unreachable default: the exit above is terminal, but the checker does
// not narrow through it.
const mirrored = placed ?? ""
const per_file_out = if out_root == "" {
""
} else {
__ensure_parent(harness.fs, mirrored)
path_parent(mirrored)
}
const child = __spawn_precompile(harness.process, bin, source, per_file_out, source_root)
const exit_code = child["exit_code"] ?? -1
if exit_code == 0 {
if __child_reused(child) {
reused = reused + 1
} else {
compiled = compiled + 1
}
if !quiet {
harness.stdio.println(source + " -> " + mirrored)
}
} else {
failed = failed + 1
harness.stdio.eprintln(__render_failure(source, child))
if !keep_going {
break
}
}
}
if !quiet {
harness.stdio.eprintln(
"precompile: " + to_string(compiled) + " compiled, "
+ to_string(reused)
+ " reused, "
+ to_string(failed)
+ " failed",
)
}
if failed > 0 {
harness.runtime.exit(1)
}
}