/**
* `harn precompile` ported to .harn — see harn#2313 (W13).
*
* Walks a file or directory tree and hands every `.harn` source to one
* batch `harn precompile` child that runs the Rust compiler entrypoint on a
* worker pool. 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=batch, which also makes the
* child read its target as the source manifest, so it doesn't recurse back
* into this script). One child shares process startup, stdlib
* preparation, and every shared import's parse across all sources; a child
* per source repeated them for each file.
*
* 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
* HARN_PRECOMPILE_JOBS — maximum concurrent compiles (>= 1); the
* shim always sets it, defaulting to the
* machine's available parallelism
* HARN_PRECOMPILE_BATCH_DIR — shim-owned directory for the source manifest
*
* The child reports one record per source, in source order, holding what that
* source's own single-file compile prints and exits with. Every line is
* rendered from those records afterwards, so stdout, stderr, the summary, and
* the exit status do not depend on the bound or on completion order.
*/
type PrecompileRecord = {source: string, exit_code: int, stdout: string, stderr: string}
const RECORD_PREFIX = "precompile-record:"
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-package/src/precompile.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.push(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.push(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 __batch_records(
harness: Harness,
bin: string,
plans: list,
source_root: string,
jobs: int,
keep_going: bool,
) -> Result<list<PrecompileRecord>, string> {
// 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. The manifest carries each source's exact
// single-file arguments: --relocatable for a directory walk, and the
// directory its artifacts land in.
const batch_dir = harness.env.get_or("HARN_PRECOMPILE_BATCH_DIR", "")
if batch_dir == "" {
return Err("HARN_PRECOMPILE_BATCH_DIR not set by dispatch shim")
}
let sources = []
for plan in plans {
sources.push({source: plan.source, out: plan.per_file_out})
}
const manifest = path_join(batch_dir, "sources.json")
harness.fs.write_text(
manifest,
json_stringify({relocatable: source_root != "", sources: sources}),
)
let args = ["precompile", "--quiet", "--jobs", to_string(jobs)]
if keep_going {
args.push("--keep-going")
}
args.push(manifest)
const child = harness.process.run(
{program: bin, args: args, env: {HARN_PRECOMPILE_INNER: "batch"}},
)
if (child["exit_code"] ?? -1) != 0 {
return Err(
__render_failure("precompile batch", child["stderr"] ?? "", child["exit_code"] ?? -1),
)
}
let records = []
for line in split(child["stdout"] ?? "", "\n") {
if !starts_with(line, RECORD_PREFIX) {
continue
}
const decoded = json_decode(
substring(line, len(RECORD_PREFIX), len(line)),
schema_of(PrecompileRecord),
)
match decoded {
Ok(record) -> { records.push(record) }
Err(error) -> { return Err("unreadable precompile record: " + error.message) }
}
}
return Ok(records)
}
fn __reused(stdout: string) -> bool {
// Absence means compiled, not unknown: counting a missing marker as a reuse
// would report work as skipped that was done.
for line in split(stdout, "\n") {
if trim(line) == "precompile-outcome: reused" {
return true
}
}
return false
}
fn __render_failure(source: string, stderr: string, exit_code: int) -> string {
// Match the Rust eprintln format `{source}: {err}` where `err` is
// either the compile's first stderr line or a compact exit summary.
const trimmed = trim(stderr)
if trimmed != "" {
const lines = split(trimmed, "\n")
return source + ": " + lines[0]
}
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)
}
const jobs_raw = harness.env.get_or("HARN_PRECOMPILE_JOBS", "1")
const jobs = to_int(jobs_raw) ?? 0
if jobs < 1 {
harness.stdio.eprintln(
"internal error: HARN_PRECOMPILE_JOBS must be a positive integer, got " + jobs_raw,
)
harness.runtime.exit(70)
}
// Place every source before launching any child. Destinations are pure path
// arithmetic, so an unplaceable source is refused before any work starts,
// and the parent directories exist before children race to write into them.
let plans = []
for source in sources {
// The child sees a single-file target, so --out must name the exact
// directory the .harnbc lands in; otherwise every artifact would collapse
// 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)
}
plans.push({source: source, mirrored: mirrored, per_file_out: per_file_out})
}
// Without --keep-going the child starts no source after the first failure
// and reports none past it, exactly as the serial walk never ran them.
const batch = __batch_records(harness, bin, plans, source_root, jobs, keep_going)
if is_err(batch) {
harness.stdio.eprintln("internal error: " + unwrap_err(batch))
harness.runtime.exit(70)
}
const records = unwrap(batch)
let compiled = 0
let reused = 0
let failed = 0
for (index, plan) in iter(plans).enumerate() {
if index >= len(records) {
// Records stop only after a failure without --keep-going, and the break
// below has already left the loop by then.
harness.stdio.eprintln("internal error: " + plan.source + " was skipped before any failure")
harness.runtime.exit(70)
}
const record = records[index]
if record.source != plan.source {
harness.stdio.eprintln(
"internal error: precompile record for " + record.source + " where " + plan.source
+ " was expected",
)
harness.runtime.exit(70)
}
if record.exit_code == 0 {
if __reused(record.stdout) {
reused = reused + 1
} else {
compiled = compiled + 1
}
if !quiet {
harness.stdio.println(plan.source + " -> " + plan.mirrored)
}
continue
}
failed = failed + 1
harness.stdio.eprintln(__render_failure(plan.source, record.stderr, record.exit_code))
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)
}
}