{
  Blaise - An Object Pascal Compiler
  Copyright (c) 2026 Graeme Geldenhuys
  SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
  Licensed under the Apache License v2.0 with Runtime Library Exception.
  See LICENSE file in the project root for full license terms.
}

unit blaise.codegen.driver;

{ Cross-backend driver abstraction.

  Each backend registers a TBackendDriver subclass singleton.  Blaise.pas
  and TCompileWorker drive the shared pipeline through the base class and
  stop branching on the backend kind for per-backend tool invocation,
  file extensions, and codegen construction.

  Responsibilities a driver OWNS:

    * Codegen construction: which ICodeGen to create and how to apply its
      backend-specific knobs (e.g. the native backend's SetTarget).

    * IR-text artefact shape: the file extension used for the emitted IR,
      so the shared pipeline can pick the right file name without a
      backend switch.

    * Lowering and linking: turning the emitted IR file into a
      relocatable object (per-unit incremental path) or the final linked
      binary (program path).  The QBE driver runs qbe + cc; the native
      driver runs cc on its assembly directly, or the in-process
      assembler + linker driver when --assembler internal is selected.

  Responsibilities a driver does NOT own:

    * Warm-cache discovery, source-hash validation, prebuilt-object
      probing.  Those stay in uUnitLoader and are backend-agnostic.
      SupportsWarmCache exists only as a hint for future "this backend's
      .o isn't trusted for cache reuse yet" gating.

    * .bif embedding into unit objects — an object-format concern shared
      across backends; it stays with the caller (Blaise.pas).

  The architecture follows Andrew Haines' unify_backend_interface
  proposal, with a class-based dispatch surface (virtual methods on an
  abstract base) instead of an interface so shared behaviour — toolchain
  resolution, the common link line — lives in the base class.

  Lifetime: driver singletons are registered once at unit initialization
  into a fixed array of class references.  They are ARC-managed globals,
  released by the program-exit global release pass (the codebase norm for
  unit-level singletons — no explicit finalization needed). }

interface

uses
  Classes,
  blaise.codegen,
  blaise.codegen.target,
  uToolchain;

type
  TBackendKind = (bkQBE, bkNative);

  { Result of offering an unrecognised flag to a driver (Chain of
    Responsibility). }
  TOptionAccept = (
    oaUnknown,        { not my flag — keep trying / report unknown }
    oaConsumedFlag,   { mine, took no value argument }
    oaConsumedValue   { mine, consumed the following arg as its value }
  );

  { Cross-cutting flags that affect codegen, lowering, and linking.
    Built once by the Blaise.pas flag parser and shared (read-only) with
    the compile workers.  Adding a new backend knob is a field here plus
    the driver that reads it; Blaise.pas does not branch on backend to
    apply it. }
  TBackendOpts = class
  public
    Target: TTargetDesc;
    DebugMode: Boolean;       { codegen debug / leak tracking }
    OPDFEnabled: Boolean;     { OPDF code shaping }
    OPDFAsmFile: string;      { OPDF sidecar path, if any }
    UseInternalAsm: Boolean;  { --assembler internal (native backend) }
    AssemblerExplicit: Boolean;  { True when user passed --assembler }
    AssemblerChoiceBad: Boolean;  { a --assembler value was given that is
                                neither 'internal' nor 'external'; the
                                native driver's ValidateOptions rejects it }
    UseInternalLinker: Boolean;  { --linker internal (native backend) }
    LinkerExplicit: Boolean;     { True when user passed --linker }
    LinkerChoiceBad: Boolean;    { --linker value not 'internal' or 'external' }
    RTLSrcDir: string;           { --rtl-src DIR: explicit RTL source directory.
                                   Overrides the binary/CWD-relative lookup, for
                                   a relocated/release binary whose RTL source
                                   lives elsewhere (empty = use default lookup) }
    Static: Boolean;             { --static: link a freestanding, libc-free ELF
                                   (native internal linker only).  The kernel
                                   leaf (runtime.syscall.<os> + runtime.cstub +
                                   runtime.start.static.<os>) replaces libc; the
                                   linker emits a non-PIE ET_EXEC with no
                                   PT_INTERP.  docs/linux-syscall-migration.adoc }
    LinkLibs: TStringList;       { extra libraries to link (-l<name>), unioned by
                                   the frontend from the program's and its used
                                   units' 'external ''lib''' declarations
                                   (TUnitInterface.LinkLibs).  nil = none.  Owned;
                                   ARC-released with the opts object. }
  end;

  TBackendDriver = class
  public
    { Static description of the backend. }
    function Kind: TBackendKind; virtual; abstract;
    function Name: string; virtual; abstract;       { '--backend' identifier }
    function IRFileExt: string; virtual; abstract;  { '.ssa' | '.s' }

    { True when the driver can emit a per-unit linkable artefact for the
      --incremental worker pool.  A driver returning True here MUST
      return a non-nil codegen from CreateUnitCodeGen — the worker fails
      loudly otherwise. }
    function SupportsIncremental: Boolean; virtual;

    { True when uUnitLoader may reuse this backend's .o + .bif on a
      content-hash match.  QBE = true today; native = false until it
      learns to write .bif sidecars and the loader trusts them. }
    function SupportsWarmCache: Boolean; virtual;

    { The external tools this backend needs for ATarget, as resolvable
      specs.  uToolchain.ResolveSpec turns each into a path (env override,
      $BLAISE_TOOLCHAIN_PREFIX, cross-triple prefix, $PATH, host ext).  The
      base lists the shared linker (cc, or the mingw cross-linker for a
      Windows target); a backend overrides to add its own (e.g. the LLVM
      driver appends llc) — call inherited and extend. }
    function DescribeTools(const ATarget: TTargetDesc): TToolSpecArray; virtual;

    { Resolve one named tool from DescribeTools for ATarget; '' if the
      backend declares no such tool. }
    function ToolPath(const AName: string; const ATarget: TTargetDesc): string;

    { Verify any tools the backend needs are reachable.  Returns '' on
      success, an error message otherwise.  Called once before the
      front-end runs (skipped for stdout-only modes, which need no
      toolchain). }
    function CheckToolchain(AOpts: TBackendOpts): string; virtual;

    { Construct the backend's code generator and apply opts.  Returns an
      ARC-managed ICodeGen — do not Free. }
    function CreateCodeGen(AOpts: TBackendOpts): ICodeGen; virtual; abstract;

    { Per-unit codegen for the parallel incremental worker, configured to
      emit a single unit's IR with exports visible to sibling units.
      Default nil: the driver does not support separate-unit emission and
      the dispatcher falls back to the QBE driver. }
    function CreateUnitCodeGen(AOpts: TBackendOpts): ICodeGen; virtual;

    { Lower one unit's IR file to a relocatable object (--incremental
      worker path and unit-as-top-level mode).  Returns '' on success,
      an error message otherwise.  Default fails loudly: a driver that
      claims SupportsIncremental must override this. }
    function LowerToObject(const AIRFile, AObjFile: string;
      AOpts: TBackendOpts): string; virtual;

    { Lower the top program's IR file and link the final binary —
      including the OPDF sidecar, prebuilt dep objects, the RTL archive,
      and -lm/-lpthread.  Returns '' on success, an error message
      otherwise.  AExtraObjects may be nil. }
    function LinkProgram(const AIRFile, AOutputFile: string;
      AOpts: TBackendOpts; AExtraObjects: TStringList): string; virtual; abstract;

    { --- Option contract (Chain of Responsibility + Template Method) --- }

    { The common parser, on a flag it does not own, offers it here.  The
      driver writes any parsed state into the shared AOpts (the same
      TBackendOpts the drivers later read).  Returns oaConsumedValue when
      it took ANextArg as the flag's value, oaConsumedFlag when the flag
      stood alone, oaUnknown when the flag is not this driver's.  Default
      oaUnknown. }
    function AcceptOption(const AFlag, ANextArg: string;
      AOpts: TBackendOpts): TOptionAccept; virtual;

    { Contribute this backend's flag lines to --help.  One (flag,
      description) pair per AddOptionLine call; FormatFlagLine owns the
      column layout so driver lines never drift from the shared block.
      Default: nothing. }
    procedure DescribeOptions(ALines: TStringList); virtual;

    { Post-parse validation with the resolved TBackendOpts visible.
      Returns '' when valid, else a user-facing message.  THIS is where
      cross-cutting rules live ("OPDF debug info is not supported by the
      LLVM backend").  Default ''. }
    function ValidateOptions(AOpts: TBackendOpts): string; virtual;

    { Selection policy: does this backend produce the IR text that
      --emit-ir prints?  QBE = True (fixpoint / RTL-Makefile contract on
      byte-identical QBE IR); native = False (its IR IS assembly, surfaced
      via --emit-asm).  PickTopDriver asks this instead of hard-coding
      bkQBE.  Default False. }
    function ClaimsEmitIR: Boolean; virtual;

  protected
    { The shared linker tool spec for ATarget (cc/clang, or the mingw
      cross-linker for a Windows target).  A subclass's DescribeTools builds
      its array from this plus its own tools — the parser has no
      expression-position `inherited`, so this is the reuse seam. }
    function LinkerToolSpec(const ATarget: TTargetDesc): TToolSpec;

    { Shared link line: cc-driver resolved via uToolchain (env overrides
      and target awareness apply), input file, OPDF sidecar, extra
      objects, the RTL objects, -lm, -lpthread.  Used by every driver's
      LinkProgram. }
    function LinkViaToolchain(const AInputFile, AOutputFile: string;
      AOpts: TBackendOpts; AExtraObjects: TStringList): string;

    { Compile the implicit RTL units (runtime.* + rtl.platform.*) from source
      into a per-compiler object cache and return their .o paths in AObjPaths
      (link order).  A unit is (re)compiled only when its cached .o is missing
      or older than the source.  Replaces the pre-built blaise_rtl.a — the RTL
      is built from source by the compiler itself (docs/rtl-unification-plan.adoc).
      Both the cc link line and the native internal linker call this.

      RTL units carry inline asm, so they are always built with the native
      backend + internal assembler.  Building goes through a --unit-cache so a
      unit that uses another RTL unit references its globals externally instead
      of re-defining them — without that, the per-unit objects clash on
      duplicate global definitions when linked directly (the archive's member
      selection used to hide this).  The cache also auto-builds intermediate
      deps (e.g. rtl.platform), which are collected too.

      AIncludeStartup controls whether runtime.start.o (which defines the bare
      `_start` entry) is included.  The native internal linker needs it (Blaise
      owns the entry point); the cc/QBE link line must omit it (libc's startup
      provides `_start` and calls `main`), or the two `_start`s collide.

      AAlreadyProvided lists object paths the caller already puts on the link
      line (the program's own prebuilt/incremental deps).  A program that uses
      an RTL unit (e.g. `uses classes` pulls runtime.arc) compiles that unit
      into its own object cache, so supplying our copy too would double-define
      it.  Any RTL unit whose <name>.o basename matches an entry here is skipped.
      Pass nil when the caller adds no objects of its own.

      Returns '' on success, else an error message. }
    function EnsureRTLObjects(AOpts: TBackendOpts; AIncludeStartup: Boolean;
      AAlreadyProvided, AObjPaths: TStringList): string;
  end;

{ Run an external tool, capturing combined output.  Shared by the
  drivers; exposed because the lowering steps run from worker threads
  as well as the main compile path. }
function RunProcess(const AExe: string; AArgs: TStringList;
  out AOutput: string): Integer;

{ Format one --help flag line with the shared 2-space indent and flag
  column.  Single source of truth for the column width so the common
  usage block (Blaise.pas PrintUsage) and each driver's DescribeOptions
  cannot drift.  Example:
    FormatFlagLine('--assembler <id>', 'internal | external (default: external)')
  produces '  --assembler <id>    internal | external (default: external)'. }
function FormatFlagLine(const AFlag, ADesc: string): string;

{ Registry.  Each backend unit registers its singleton in its
  initialization block; consumers fetch by kind.  Looking up an
  unregistered kind raises an exception (programmer error — the backend
  unit wasn't pulled into the uses clause). }

procedure RegisterDriver(ADriver: TBackendDriver);
function GetDriver(AKind: TBackendKind): TBackendDriver;

{ Enumerate registered backends in TBackendKind ordinal order.  Returns a
  TStringList of Name values; caller owns and frees.  Drives --backend
  validation and the usage printer so neither hard-codes the list. }
function RegisteredBackendNames: TStringList;

{ Parse a --backend identifier against the registered drivers.  Returns
  False on an unknown name; caller writes the user-facing error. }
function ParseBackendName(const AName: string; out AKind: TBackendKind): Boolean;

{ The single backend-selection policy decision.  --emit-ir always forces
  QBE (the fixpoint check + RTL Makefile depend on byte-identical QBE
  IR); --emit-asm implies native (its IR IS the .s text the consumer
  expects); otherwise --backend selects directly. }
function PickTopDriver(ABackend: TBackendKind;
  AEmitIR, AEmitAsm: Boolean): TBackendDriver;

implementation

uses
  SysUtils,
  Process,
  blaise.elfreader,
  uToolchain;

{ Process id + directory removal, for the per-process private RTL build
  directory.  Declared directly rather than via GRtlPlatform to avoid a
  unit-initialisation-order dependency (this unit may run before the platform
  layer assigns GRtlPlatform). }
function _driver_getpid: Integer; external name 'getpid';
function _driver_rmdir(APath: PChar): Integer; external name 'rmdir';

{ Collect the names of all symbols an ELF object DEFINES with global or weak
  binding (STB_GLOBAL/STB_WEAK and Shndx not SHN_UNDEF) into ADest.
  Used by EnsureRTLObjects to decide, by symbol rather than by filename,
  whether an RTL unit is already supplied by the program's own objects.  Any
  read/parse failure (non-ELF target, truncated file) leaves ADest unchanged —
  the caller then falls back to supplying its copy, which is always safe. }
procedure CollectDefinedSymbols(const AObjPath: string; ADest: TStringList);
var
  Obj: TElfObjectFile;
  I:   Integer;
  Sym: TRdSymbol;
begin
  Obj := nil;
  try
    try
      Obj := ReadElfObjectFile(AObjPath);
    except
      Exit;  { not a readable ELF object — be conservative }
    end;
    for I := 0 to Obj.Symbols.Count - 1 do
    begin
      Sym := Obj.Symbols.Get(I);
      if (Sym.Name <> '') and (Sym.Shndx <> SHN_UNDEF) and
         ((Sym.Bind = STB_GLOBAL) or (Sym.Bind = STB_WEAK)) then
        if ADest.IndexOf(Sym.Name) < 0 then
          ADest.Add(Sym.Name);
    end;
  finally
    Obj.Free();
  end;
end;

{ Indexed by Ord(TBackendKind).  The bound is a literal because the
  parser only accepts integer literals on array decls; keep the upper
  bound in sync with the enum's highest ordinal (bkNative = 1). }
var
  GDrivers: array[0..1] of TBackendDriver;

function TBackendDriver.SupportsIncremental: Boolean;
begin
  Result := False;
end;

function TBackendDriver.SupportsWarmCache: Boolean;
begin
  Result := False;
end;

function TBackendDriver.LinkerToolSpec(const ATarget: TTargetDesc): TToolSpec;
begin
  { Shared linker slot.  A target tool (HostTool=False): native build uses
    cc/clang; a Windows target cross-links via the mingw clang driver
    (x86_64-w64-mingw32-clang) and never falls back to the host cc. }
  Result.Name     := 'linker';
  Result.EnvVar   := 'BLAISE_LINKER';
  SetLength(Result.Cands, 2);
  Result.Cands[0] := 'cc';
  Result.Cands[1] := 'clang';
  Result.HostTool := False;
  if ATarget.OS = osWindows then
    Result.CrossPrefix := 'x86_64-w64-mingw32-'
  else
    Result.CrossPrefix := '';
end;

function TBackendDriver.DescribeTools(const ATarget: TTargetDesc): TToolSpecArray;
begin
  SetLength(Result, 1);
  Result[0] := Self.LinkerToolSpec(ATarget);
end;

function TBackendDriver.ToolPath(const AName: string;
  const ATarget: TTargetDesc): string;
var
  Specs: TToolSpecArray;
  I:     Integer;
begin
  Result := '';
  Specs := Self.DescribeTools(ATarget);
  for I := 0 to High(Specs) do
    if SameText(Specs[I].Name, AName) then
      Exit(ResolveSpec(Specs[I], ATarget));
end;

function TBackendDriver.CheckToolchain(AOpts: TBackendOpts): string;
begin
  { No tools to probe by default.  AOpts is part of the signature so a
    backend probe can read e.g. AOpts.Target. }
  Result := '';
end;

function TBackendDriver.AcceptOption(const AFlag, ANextArg: string;
  AOpts: TBackendOpts): TOptionAccept;
begin
  Result := oaUnknown;
end;

procedure TBackendDriver.DescribeOptions(ALines: TStringList);
begin
  { No backend-private flags by default. }
end;

function TBackendDriver.ValidateOptions(AOpts: TBackendOpts): string;
begin
  Result := '';
end;

function TBackendDriver.ClaimsEmitIR: Boolean;
begin
  Result := False;
end;

function TBackendDriver.CreateUnitCodeGen(AOpts: TBackendOpts): ICodeGen;
begin
  Result := nil;
end;

function TBackendDriver.LowerToObject(const AIRFile, AObjFile: string;
  AOpts: TBackendOpts): string;
begin
  Result := Self.Name() +
    ' backend does not support per-unit object lowering';
end;

{ The implicit RTL units, in dependency/link order (leaf-first).  The compiler
  emits calls to their symbols (_SetArgs, _BlaiseGetMem, _start, ARC helpers, …)
  in every program, so every program links them.  Names are the dotted-flat
  unit names; the source files are <name>.pas in the RTL source directory.
  rtl.platform is the base abstraction layout.linux + posix both use; it owns
  the shared globals (GPlatformLayout, GRtlPlatform) so it must be built first
  and linked once. }
const
  RTL_UNITS: array[0..14] of string = (
    'rtl.platform',
    'runtime.start', 'runtime.atomic', 'runtime.setjmp', 'runtime.utf8',
    'runtime.mem', 'runtime.str', 'runtime.set', 'runtime.arc',
    'runtime.weak', 'runtime.float', 'runtime.thread', 'runtime.exc',
    'rtl.platform.layout.linux', 'rtl.platform.posix');

function TBackendDriver.EnsureRTLObjects(AOpts: TBackendOpts;
  AIncludeStartup: Boolean; AAlreadyProvided, AObjPaths: TStringList): string;
var
  SrcDir, CacheDir, BuildDir, BlaiseBin: string;
  SrcFile, ObjFile, TmpObj: string;
  I, J, ExitCode: Integer;
  Args: TStringList;
  Msg: string;
  ProvidedSyms, MySyms, Units: TStringList;
  Redundant: Boolean;
begin
  Result := '';
  { Collect every symbol the caller's own objects already DEFINE (the program's
    prebuilt/incremental deps).  An RTL unit the program compiled itself (e.g.
    `uses classes` pulls runtime.arc) is already on the link line; supplying our
    copy too would double-define it.  Match by symbol rather than by filename so
    a same-named-but-different object (e.g. a stale mangled cache entry) does NOT
    suppress the real provider — this mirrors the old archive's member
    selection. }
  ProvidedSyms := TStringList.Create();
  ProvidedSyms.CaseSensitive := True;
  ProvidedSyms.Sorted := True;
  ProvidedSyms.Duplicates := dupIgnore;
  MySyms := TStringList.Create();
  if AAlreadyProvided <> nil then
    for J := 0 to AAlreadyProvided.Count - 1 do
      CollectDefinedSymbols(AAlreadyProvided.Strings[J], ProvidedSyms);
  try
  { RTL source lives in the compiler's own source tree.  Resolution order:
      1. --rtl-src DIR (AOpts.RTLSrcDir) — explicit, for a relocated binary;
      2. $BLAISE_RTL_SRC;
      3. binary/CWD-relative default (RTLSourceDir). }
  if AOpts.RTLSrcDir <> '' then
    SrcDir := ExpandFileName(AOpts.RTLSrcDir)
  else
  begin
    SrcDir := GetEnvironmentVariable('BLAISE_RTL_SRC');
    if SrcDir = '' then
      SrcDir := ExpandFileName(RTLSourceDir());
  end;
  if not DirectoryExists(SrcDir) then
    Exit('RTL source directory not found (' + SrcDir +
      '); pass --rtl-src DIR or set $BLAISE_RTL_SRC to compiler/src/main/pascal');

  { Object cache lives beside the compiler binary so repeated program builds
    reuse it (compiler/target/rtl/).  The shared cache is read for reuse and is
    the link source, but the actual build happens in a per-process private
    sub-directory (rtl/build-<pid>/) and finished objects are atomically renamed
    into the shared cache.  This keeps cross-build reuse while making concurrent
    builders (e.g. parallel e2e suites all warming a cold cache) race-free: a
    reader never sees a half-written object, and two builders just publish
    identical results, last-writer-wins. }
  CacheDir := IncludeTrailingPathDelimiter(CompilerBinDir()) + 'rtl';
  ForceDirectories(CacheDir);
  BuildDir := IncludeTrailingPathDelimiter(CacheDir) + 'build-' +
              IntToStr(_driver_getpid());
  ForceDirectories(BuildDir);
  BlaiseBin := ParamStr(0);

  { The unit list to build/link, in leaf-first order.  For a --static link the
    kernel leaf replaces libc: drop runtime.start (libc/__libc_start_main entry)
    in favour of the freestanding runtime.start.static.<os>, and add the raw
    syscall stubs (runtime.syscall.<os>) + the freestanding C functions
    (runtime.cstub).  A dynamic (libc) link uses the plain RTL_UNITS. }
  Units := TStringList.Create();
  for I := 0 to High(RTL_UNITS) do
    if AOpts.Static and SameText(RTL_UNITS[I], 'runtime.start') then
      Units.Add('runtime.start.static.linux')
    else
      Units.Add(RTL_UNITS[I]);
  if AOpts.Static then
  begin
    Units.Add('runtime.syscall.linux');
    Units.Add('runtime.cstub');
    Units.Add('runtime.libc.linux');
    Units.Add('runtime.libc2.linux');
    Units.Add('runtime.thread.static.linux');
  end;

  for I := 0 to Units.Count - 1 do
  begin
    SrcFile := IncludeTrailingPathDelimiter(SrcDir) + Units.Strings[I] + '.pas';
    ObjFile := IncludeTrailingPathDelimiter(CacheDir) + Units.Strings[I] + '.o';
    if not FileExists(SrcFile) then
      Exit('RTL unit source missing: ' + SrcFile);

    { Recompile only when the shared cached object is missing or stale.  Build
      into the private BuildDir (its own --unit-cache so each unit's RTL deps
      are referenced externally and intermediate deps are built leaf-first),
      then atomically rename the result into the shared CacheDir.  RTL units
      carry inline asm, so the native backend + internal assembler is mandatory. }
    if (not FileExists(ObjFile)) or (FileAge(ObjFile) < FileAge(SrcFile)) then
    begin
      TmpObj := IncludeTrailingPathDelimiter(BuildDir) + Units.Strings[I] + '.o';
      Args := TStringList.Create();
      try
        Args.Add('--backend');     Args.Add('native');
        Args.Add('--assembler');   Args.Add('internal');
        Args.Add('--source');      Args.Add(SrcFile);
        Args.Add('--unit-path');   Args.Add(SrcDir);
        Args.Add('--unit-cache');  Args.Add(BuildDir);
        Args.Add('--output');      Args.Add(TmpObj);
        ExitCode := RunProcess(BlaiseBin, Args, Msg);
      finally
        Args.Free();
      end;
      if ExitCode <> 0 then
        Exit('failed to build RTL unit ' + Units.Strings[I] +
          ' (exit ' + IntToStr(ExitCode) + '): ' + Msg);
      { Publish atomically.  RenameFile within the same directory tree is an
        atomic rename(2); a concurrent builder doing the same is harmless. }
      if not RenameFile(TmpObj, ObjFile) then
        Exit('failed to publish RTL object ' + ObjFile);
    end;

    { runtime.start defines the bare _start entry — include it only for the
      native internal linker (Blaise owns the entry); the cc link line gets
      _start from libc and must omit it to avoid a multiple-definition. }
    if SameText(Units.Strings[I], 'runtime.start') and (not AIncludeStartup) then
      Continue;
    { Skip this RTL object if EVERY symbol it defines is already defined by the
      caller's objects — i.e. the program compiled this RTL unit itself.  A
      partially-overlapping object is still supplied (its unique symbols are
      needed); the linker resolves the shared ones to the first definition.
      The unit is always built above so later RTL units that depend on it
      resolve against our cache's iface. }
    if ProvidedSyms.Count > 0 then
    begin
      MySyms.Clear();
      CollectDefinedSymbols(ObjFile, MySyms);
      Redundant := MySyms.Count > 0;
      for J := 0 to MySyms.Count - 1 do
        if ProvidedSyms.IndexOf(MySyms.Strings[J]) < 0 then
        begin
          Redundant := False;
          Break;
        end;
      if Redundant then
        Continue;
    end;
    AObjPaths.Add(ObjFile);
  end;

  { Tidy the private build directory.  The published .o files live in the shared
    cache now; the build dir holds only intermediate .bif/.o stragglers.  Remove
    the known per-unit artefacts and the (now-empty) directory.  Best-effort —
    a leftover dir is harmless. }
  for I := 0 to Units.Count - 1 do
  begin
    DeleteFile(IncludeTrailingPathDelimiter(BuildDir) + Units.Strings[I] + '.o');
    DeleteFile(IncludeTrailingPathDelimiter(BuildDir) + Units.Strings[I] + '.bif');
  end;
  _driver_rmdir(PChar(ExcludeTrailingPathDelimiter(BuildDir)));
  Units.Free();
  finally
    ProvidedSyms.Free();
    MySyms.Free();
  end;
end;

function TBackendDriver.LinkViaToolchain(const AInputFile, AOutputFile: string;
  AOpts: TBackendOpts; AExtraObjects: TStringList): string;
var
  Args: TStringList;
  RTLObjs: TStringList;
  Msg, RTLErr: string;
  ExitCode: Integer;
  I: Integer;
begin
  Result := '';
  { Every Blaise program links the implicit RTL units (runtime.* +
    rtl.platform.*).  They are no longer shipped as a pre-built blaise_rtl.a;
    the compiler builds them from source into a per-compiler object cache
    (EnsureRTLObjects) and links the resulting .o files directly.  This is the
    same RTL the native internal linker uses — one source of truth. }
  Args := TStringList.Create();
  RTLObjs := TStringList.Create();
  try
    { cc link line: libc supplies _start, so omit runtime.start.  Pass the
      program's prebuilt deps so an RTL unit it compiled itself (e.g. via
      `uses classes`) is not supplied twice. }
    RTLErr := Self.EnsureRTLObjects(AOpts, False, AExtraObjects, RTLObjs);
    if RTLErr <> '' then
      Exit(RTLErr);
    Args.Add('-o');
    Args.Add(AOutputFile);
    Args.Add(AInputFile);
    { Linker resolved via DescribeTools so a Windows target picks the mingw
      cross-linker and a Windows host adds the .exe extension. }
    { OPDF sidecar (QBE backend only — the native backend appends its
      exact-facts OPDF section to the main assembly instead). }
    if (AOpts.OPDFAsmFile <> '') and FileExists(AOpts.OPDFAsmFile) then
      Args.Add(AOpts.OPDFAsmFile);
    { Pre-built dep object files (auto-discovered by the loader or
      produced by the --incremental workers). }
    if AExtraObjects <> nil then
      for I := 0 to AExtraObjects.Count - 1 do
        Args.Add(AExtraObjects.Strings[I]);
    { RTL objects, in link order. }
    for I := 0 to RTLObjs.Count - 1 do
      Args.Add(RTLObjs.Strings[I]);
    Args.Add('-lm');       { math functions (sqrt, sin, cos, etc.) }
    Args.Add('-lpthread'); { POSIX threads (blaise_thread unit) }
    { Libraries declared via 'external ''lib''' in the program or any used unit
      (unioned by the frontend into AOpts.LinkLibs).  Emitted as -l<name> — ld
      expands to lib<name>.so/.a.  Additive to the always-needed -lm/-lpthread. }
    if AOpts.LinkLibs <> nil then
      for I := 0 to AOpts.LinkLibs.Count - 1 do
        Args.Add('-l' + AOpts.LinkLibs.Strings[I]);
    ExitCode := RunProcess(Self.ToolPath('linker', AOpts.Target), Args, Msg);
  finally
    RTLObjs.Free();
    Args.Free();
  end;
  if ExitCode <> 0 then
    Result := 'link error (exit ' + IntToStr(ExitCode) + '): ' + Msg;
end;

function FormatFlagLine(const AFlag, ADesc: string): string;
const
  { Matches the shared usage block in Blaise.pas PrintUsage: 2-space
    indent, flag field padded so the description starts at column 20. }
  Indent = '  ';
  FlagFieldWidth = 18;
var
  Pad: Integer;
begin
  Result := Indent + AFlag;
  Pad := FlagFieldWidth - Length(AFlag);
  if Pad < 1 then
    Pad := 1;   { always at least one space before the description }
  while Pad > 0 do
  begin
    Result := Result + ' ';
    Pad := Pad - 1;
  end;
  Result := Result + ADesc;
end;

function ReadProcessChunk(AProc: TProcess): string;
begin
  Result := AProc.ReadOutput()
end;

function RunProcess(const AExe: string; AArgs: TStringList;
  out AOutput: string): Integer;
var
  Proc: TProcess;
  Chunk: string;
  I: Integer;
begin
  Proc := TProcess.Create(nil);
  try
    Proc.Executable := AExe;
    for I := 0 to AArgs.Count - 1 do
      Proc.Parameters.Add(AArgs.Strings[I]);
    Proc.Execute();
    AOutput := '';
    repeat
      Chunk := ReadProcessChunk(Proc);
      AOutput := AOutput + Chunk;
    until (Chunk = '') and not Proc.Running;
    Proc.WaitOnExit();
    Result := Proc.ExitCode;
  finally
    Proc.Free();
  end;
end;

procedure RegisterDriver(ADriver: TBackendDriver);
begin
  GDrivers[Ord(ADriver.Kind())] := ADriver;
end;

function GetDriver(AKind: TBackendKind): TBackendDriver;
begin
  Result := GDrivers[Ord(AKind)];
  if Result = nil then
    raise Exception.Create(
      'blaise.codegen.driver: no driver registered for backend kind ' +
      IntToStr(Ord(AKind)) + ' (unit not pulled into uses clause?)');
end;

function RegisteredBackendNames: TStringList;
var
  I: Integer;
begin
  Result := TStringList.Create();
  for I := 0 to 1 do
    if GDrivers[I] <> nil then
      Result.Add(GDrivers[I].Name());
end;

function ParseBackendName(const AName: string; out AKind: TBackendKind): Boolean;
var
  I: Integer;
begin
  Result := False;
  for I := 0 to 1 do
    if (GDrivers[I] <> nil) and SameText(AName, GDrivers[I].Name()) then
    begin
      AKind := GDrivers[I].Kind();
      Result := True;
      Exit;
    end;
end;

function PickTopDriver(ABackend: TBackendKind;
  AEmitIR, AEmitAsm: Boolean): TBackendDriver;
var
  I: Integer;
begin
  if AEmitIR then
  begin
    { --emit-ir prints the IR text of whichever backend claims it.  Ask
      the drivers instead of hard-coding bkQBE, so a future IR-producing
      backend (LLVM) needs no carve-out here. }
    for I := 0 to 1 do
      if (GDrivers[I] <> nil) and GDrivers[I].ClaimsEmitIR() then
        Exit(GDrivers[I]);
    { No registered backend claims --emit-ir: fall back to QBE, the
      historical owner of the byte-identical IR contract. }
    Result := GetDriver(bkQBE);
  end
  else if AEmitAsm then
    Result := GetDriver(bkNative)
  else
    Result := GetDriver(ABackend);
end;

end.
