Skip to content

change compiler linker API to pass all exports every update #23616

@mlugg

Description

@mlugg

Right now, the frontend communicates exports to the linker using 2 functions:

    /// This is called for every exported thing. `exports` is almost always
    /// a list of size 1, meaning that `exported` is exported once. However, it is possible
    /// to export the same thing with multiple different symbol names (aliases).
    /// May be called before or after updateDecl for any given Decl.
    pub fn updateExports(
        base: *File,
        pt: Zcu.PerThread,
        exported: Zcu.Exported,
        export_indices: []const Zcu.Export.Index,
    ) UpdateExportsError!void;

    pub fn deleteExport(
        base: *File,
        exported: Zcu.Exported,
        name: InternPool.NullTerminatedString,
    ) void;

However, notice the doc comment on updateExports; it tells the linker about every export of exported. In fact, the frontend calls this function (for every exported thing) at the end of every incremental update, after doing a traversal over every export and gathering them up. This traversal is necessary to report export name conflicts, so the concept of updateExports is reasonable.

The problem then is... what even is the point of deleteExport? Well, if all exports for a given Zcu.Exported are removed, then updateExports won't be called, so we need to use deleteExport instead. The current frontend behavior is to call deleteExport on all exports performed by a given AnalUnit before it is re-analyzed. Note that this re-analysis may discover the same export, leading to pointless churn in the executable.

This system also doesn't play nicely with lazy analysis. If, on an incremental update, an exporter (that is, an AnalUnit which performs an export) becomes unreferenced, then we need to delete its exports; but we can't discover this until the end of an update anyway!

So, here's an alternative API which the frontend could conform to, with no extra overhead compared to what it needs to compute and store anyway, and which gives the linker full freedom in how to handle exports:

/// Called exactly once per successful update, after semantic analysis concludes.
/// Updates the full list of exported symbols in this `File`. If an export which was
/// in a previous update is not passed here, that export has been removed.
pub fn updateExports(
    base: *File,
    pt: Zcu.PerThread,
    export_names: []const []const u8,
    /// `export_names.len == export_indices.len`.
    /// `export_names[i]` corresponds to `export_indices[i]`.
    export_indices: []const Zcu.Export.Index,
) UpdateExportsError!void;

Or, if linkers are likely to want to know about aliases (i.e. one thing exported under multiple names), we could have the frontend build up something else as it iterates. The exact data structure isn't a huge concern, since there are never enough exports for it to be a problem to build up said data structure. What's important is that all of this data is passed in one go, so the linker can use it as efficiently as possible rather than having to treat exports as changing more incrementally than they really are..

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementSolving this issue will likely involve adding new logic or components to the codebase.linking

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions