#!/usr/bin/env bash
# ltoLink.sh - ThinLTO-style link driver for the W65816 backend.
#
# Phase 5.2 of docs/GAP_CLOSURE_PLAN.md.  Takes a mix of LLVM bitcode
# (.bc) and native asm objects (.o) plus a final output object name and
# does:
#
#   1. llvm-link: merge all bitcode inputs into a single module.
#   2. opt -passes='w65816-layer2-gate': hard-fail if any two TUs in
#      the merged module disagree on `-mllvm -w65816-dbr-safe-ptrs`
#      (Phase 1.12 silent-miscompile gate).  Refuses on mismatch --
#      that's the entire point of having the gate at all.
#   3. opt -O2 + -inline-threshold=50: IR-level optimization with the
#      same inline threshold as per-TU codegen, to keep code size sane.
#      We pass --mtriple=w65816 explicitly because `opt` does NOT
#      invoke TargetPassConfig, so the TM-init hook that sets
#      inline-threshold in W65816TargetMachine.cpp does not fire here.
#   4. llc -filetype=obj: produce the final native .o.
#   5. (caller hands the .o + the native asm objects to link816)
#
# Usage:
#   bash scripts/ltoLink.sh -o <out.o> <input1.bc|.ll> [<input2.bc|...> ...]
#
# Flags:
#   -o <out>          output object path (required)
#   --keep-temps      do not delete the merged.bc / opt.bc intermediates
#   --layer2          stamp the merged module with Layer 2 = true (use
#                     when ALL input TUs were built with -mllvm
#                     -w65816-dbr-safe-ptrs).  The gate also enforces
#                     this via per-TU stamps; --layer2 just lets the
#                     driver document caller intent in the log.
#   --inline-threshold N
#                     override the default IR-optimization inline
#                     threshold (default 50, mirrors the target's
#                     per-TU default).
#   --emit-ll         additionally emit a human-readable .ll of the
#                     post-opt module for debugging.
#
# Native asm objects (handed to link816 by buildGno.sh / link816
# directly) are NOT part of the bitcode merge -- they're passed through
# unchanged.  Caller must pass `.o` files to link816 separately.  This
# script only consumes `.bc` / `.ll` and produces ONE `.o`.

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

LLVM_BIN="$ROOT/tools/llvm-mos-build/bin"
LLVM_LINK="$LLVM_BIN/llvm-link"
LLVM_DIS="$LLVM_BIN/llvm-dis"
LLVM_AS="$LLVM_BIN/llvm-as"
OPT="$LLVM_BIN/opt"
LLC="$LLVM_BIN/llc"

for tool in "$LLVM_LINK" "$OPT" "$LLC" "$LLVM_AS" "$LLVM_DIS"; do
    if [ ! -x "$tool" ]; then
        echo "ltoLink: missing tool: $tool" >&2
        echo "  Run scripts/installLlvmMos.sh to build the LTO chain." >&2
        exit 1
    fi
done

OUT=""
KEEP_TEMPS=0
LAYER2=0
INLINE_THRESHOLD=50
EMIT_LL=0
INPUTS=()

while [ $# -gt 0 ]; do
    case "$1" in
        -o)
            OUT="$2"
            shift 2
            ;;
        --keep-temps)
            KEEP_TEMPS=1
            shift
            ;;
        --layer2)
            LAYER2=1
            shift
            ;;
        --inline-threshold)
            INLINE_THRESHOLD="$2"
            shift 2
            ;;
        --emit-ll)
            EMIT_LL=1
            shift
            ;;
        --)
            shift
            while [ $# -gt 0 ]; do
                INPUTS+=("$1")
                shift
            done
            ;;
        -*)
            echo "ltoLink: unknown flag: $1" >&2
            exit 2
            ;;
        *)
            INPUTS+=("$1")
            shift
            ;;
    esac
done

if [ -z "$OUT" ]; then
    echo "ltoLink: -o <out> is required" >&2
    exit 2
fi

if [ "${#INPUTS[@]}" -eq 0 ]; then
    echo "ltoLink: no input bitcode files" >&2
    exit 2
fi

OUT_DIR="$(dirname "$OUT")"
OUT_BASE="$(basename "$OUT" .o)"
MERGED="$OUT_DIR/$OUT_BASE.merged.bc"
OPTD="$OUT_DIR/$OUT_BASE.opt.bc"
LL="$OUT_DIR/$OUT_BASE.opt.ll"

mkdir -p "$OUT_DIR"

cleanup() {
    if [ "$KEEP_TEMPS" -eq 0 ]; then
        rm -f "$MERGED" "$OPTD"
        if [ "$EMIT_LL" -eq 0 ]; then
            rm -f "$LL"
        fi
    fi
}
trap cleanup EXIT

# Pre-flight: convert any .ll inputs to .bc so llvm-link gets a uniform
# input set.  llvm-link does accept .ll directly but mixing the two in
# one invocation has bitten us with module-flag mismatches.
NORMALIZED=()
TMP_BCS=()
for f in "${INPUTS[@]}"; do
    case "$f" in
        *.ll)
            tmpbc="$OUT_DIR/$(basename "${f%.ll}").tmp.bc"
            "$LLVM_AS" "$f" -o "$tmpbc"
            NORMALIZED+=("$tmpbc")
            TMP_BCS+=("$tmpbc")
            ;;
        *.bc)
            NORMALIZED+=("$f")
            ;;
        *)
            echo "ltoLink: input must be .bc or .ll: $f" >&2
            exit 2
            ;;
    esac
done

echo "ltoLink: merging ${#NORMALIZED[@]} bitcode module(s) -> $MERGED"
"$LLVM_LINK" "${NORMALIZED[@]}" -o "$MERGED"

# Drop any .ll->.bc temporaries; the merged bitcode is the source of truth from here.
for t in "${TMP_BCS[@]}"; do
    rm -f "$t"
done

# Phase 1.12 Layer 2 gate: hard-fail if TUs disagree.  Refuse-on-mismatch
# is the gate's contract -- mixing Layer 2 + non-Layer 2 in one module
# produces silent wrong code in struct-field deref hot paths.
echo "ltoLink: running Layer 2 LTO consistency gate"
"$OPT" -passes='w65816-layer2-gate' "$MERGED" -o /dev/null

# Run -O2 with the W65816-appropriate inline threshold.  -O2 fires the
# inliner, GVN, SROA, etc.  -inline-threshold is explicitly set here
# because opt does NOT invoke TargetPassConfig and therefore does NOT
# pick up W65816TargetMachine.cpp's default-50 override; without -inline-
# threshold here opt would default to the LLVM stock 225 and bloat the
# binary.
#
# Stamp pass NOT re-run here -- the per-TU stamps are already present
# in the bitcode (they were written by the new-PM stamp pass at the
# start of each TU's opt pipeline during clang -c).  Running stamp
# again post-link could only ever reset attributes to whatever
# DbrSafePtrs is in opt's CommandLine context, which would defeat the
# gate.
echo "ltoLink: opt -O2 (inline-threshold=$INLINE_THRESHOLD) -> $OPTD"
"$OPT" --mtriple=w65816 \
    -passes='default<O2>' \
    -inline-threshold="$INLINE_THRESHOLD" \
    "$MERGED" -o "$OPTD"

if [ "$EMIT_LL" -eq 1 ]; then
    echo "ltoLink: emitting human-readable IR -> $LL"
    "$LLVM_DIS" "$OPTD" -o "$LL"
fi

echo "ltoLink: llc -filetype=obj -> $OUT"
"$LLC" --mtriple=w65816 -filetype=obj "$OPTD" -o "$OUT"

# Document Layer 2 status in the log.  The actual enforcement happened
# in step 2 (the gate); this is just for human readers.
if [ "$LAYER2" -eq 1 ]; then
    echo "ltoLink: caller asserts Layer 2 (--layer2); gate confirmed all TUs match"
else
    echo "ltoLink: Layer 2 OFF (gate confirmed all TUs match)"
fi

echo "ltoLink: done -> $OUT"
