590 lines
17 KiB
Plaintext
590 lines
17 KiB
Plaintext
|
|
= The Blaise Interface File (`.bif`) Format
|
|||
|
|
:doctype: article
|
|||
|
|
:toc: left
|
|||
|
|
:toclevels: 3
|
|||
|
|
:sectnums:
|
|||
|
|
:icons: font
|
|||
|
|
:source-highlighter: rouge
|
|||
|
|
|
|||
|
|
== Purpose
|
|||
|
|
|
|||
|
|
A `.bif` ("Blaise Interface File") is the serialised form of a unit's
|
|||
|
|
**interface section** — every type, const, var, routine, generic body,
|
|||
|
|
and inlinable body that the compiler needs in order to consume the unit
|
|||
|
|
*without re-parsing the source*.
|
|||
|
|
|
|||
|
|
It is the Blaise equivalent of FPC's `.ppu` or C/C++'s precompiled
|
|||
|
|
header: a stable wire format the compiler can read at unit-resolve time
|
|||
|
|
to populate a `TUnitInterface` (see `uUnitInterface.pas`) and feed
|
|||
|
|
import into the symbol table.
|
|||
|
|
|
|||
|
|
There are two ways a `.bif` is consumed:
|
|||
|
|
|
|||
|
|
. **Standalone file** next to the source (or in a cache dir), read via
|
|||
|
|
`ReadUnitInterfaceFromFile`. Used by tooling and tests.
|
|||
|
|
. **Embedded section** inside a companion `.o`, read via
|
|||
|
|
`LoadEmbeddedBifString`. This is the production path used by the
|
|||
|
|
unit loader's auto-discovery (`uUnitLoader.LoadTransitive`). See
|
|||
|
|
<<embedding>>.
|
|||
|
|
|
|||
|
|
Both paths feed the same parser (`ReadUnitInterface`) and produce the
|
|||
|
|
same in-memory `TUnitInterface`.
|
|||
|
|
|
|||
|
|
== Design Constraints
|
|||
|
|
|
|||
|
|
The format is **text**, not binary. The rationale (from the unit
|
|||
|
|
header):
|
|||
|
|
|
|||
|
|
> Why text and not binary: easier to diff and inspect during
|
|||
|
|
> development, no endianness concerns, no bit-twiddling. Once the
|
|||
|
|
> layout stabilises we can re-encode as a compact binary form without
|
|||
|
|
> changing the surface API.
|
|||
|
|
|
|||
|
|
Three structural rules underpin everything else:
|
|||
|
|
|
|||
|
|
. **Length-prefixed strings everywhere.** Every variable-length payload
|
|||
|
|
is encoded as `<decimal-len>:` followed by exactly `<len>` raw bytes.
|
|||
|
|
No escaping is needed — the reader consumes by byte count, never by
|
|||
|
|
delimiter scan. Embedded `:`, newlines, quotes, and `END` markers
|
|||
|
|
all flow through unmolested.
|
|||
|
|
|
|||
|
|
. **A single primitive in the reader.** Everything — counts, booleans,
|
|||
|
|
Int64 values, qualified type refs — is encoded as an lpstr. The
|
|||
|
|
reader has exactly one byte-consumption primitive (`ReadLpstrAt`),
|
|||
|
|
plus thin wrappers (`ReadInt64At`, `ReadFlagsAt`, `ReadTag`). Adding
|
|||
|
|
a new field type is mechanical.
|
|||
|
|
|
|||
|
|
. **Section-tagged framing.** Each top-level section opens with a tag
|
|||
|
|
line (`META`, `TYPE 5`, `CONST 2`, …) and closes with `END`.
|
|||
|
|
Out-of-order sections are tolerated; unknown sections are not.
|
|||
|
|
|
|||
|
|
== Top-Level Layout
|
|||
|
|
|
|||
|
|
A complete `.bif` has the following structure:
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
BLAISE-IFACE <version>\n <-- magic + format version
|
|||
|
|
<unit-name lpstr>\n <-- unit identifier
|
|||
|
|
META
|
|||
|
|
<meta-line>
|
|||
|
|
END
|
|||
|
|
TYPE <count>
|
|||
|
|
<type-line> × count
|
|||
|
|
END
|
|||
|
|
CONST <count>
|
|||
|
|
<const-line> × count
|
|||
|
|
END
|
|||
|
|
VAR <count>
|
|||
|
|
<var-line> × count
|
|||
|
|
END
|
|||
|
|
ROUT <count>
|
|||
|
|
<routine-line> × count
|
|||
|
|
END
|
|||
|
|
GENROUT <count>
|
|||
|
|
<generic-routine-line> × count
|
|||
|
|
END
|
|||
|
|
INLINE <count>
|
|||
|
|
<inline-body-line> × count
|
|||
|
|
END
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Section order is fixed in the writer (`WriteUnitInterface` at
|
|||
|
|
`uUnitInterfaceIO.pas:895`). The reader uses `ReadTag` to dispatch and
|
|||
|
|
will accept sections in any order, but does not skip unknown tags —
|
|||
|
|
they raise `EIfaceFormatError`.
|
|||
|
|
|
|||
|
|
=== Magic + Version
|
|||
|
|
|
|||
|
|
`BLAISE-IFACE 2` is the current header. The version field is the
|
|||
|
|
constant `IFACE_VERSION` in `uUnitInterfaceIO.pas`. The version was
|
|||
|
|
bumped from `1` → `2` when the `META` block grew `CompilerId` and
|
|||
|
|
`SourceModTime` fields (see <<versioning>>).
|
|||
|
|
|
|||
|
|
A wrong magic or version raises `EIfaceFormatError` immediately from
|
|||
|
|
`ReadHeader`.
|
|||
|
|
|
|||
|
|
=== Unit Name
|
|||
|
|
|
|||
|
|
The single line after the header is `<lpstr>` carrying the unit's
|
|||
|
|
canonical name as written in the `unit ... ;` declaration. Case is
|
|||
|
|
preserved.
|
|||
|
|
|
|||
|
|
== Encoding Primitives
|
|||
|
|
|
|||
|
|
[cols="1,2,3",options="header"]
|
|||
|
|
|===
|
|||
|
|
| Primitive | Wire form | Notes
|
|||
|
|
|
|||
|
|
| `lpstr`
|
|||
|
|
| `<n>:<n bytes>`
|
|||
|
|
| Length-prefixed byte sequence. The only variable-length primitive.
|
|||
|
|
|
|||
|
|
| `Int64`
|
|||
|
|
| lpstr containing decimal digits
|
|||
|
|
| Signed. No size prefix on the digits themselves.
|
|||
|
|
|
|||
|
|
| `Count` / `Bool`
|
|||
|
|
| lpstr containing `'0'`, `'1'`, …
|
|||
|
|
| Same shape as Int64; called out separately for documentation only.
|
|||
|
|
|
|||
|
|
| `QualRef`
|
|||
|
|
| lpstr containing `"UnitName.TypeName"`
|
|||
|
|
| For cross-unit type references. `$builtin.Integer` denotes a
|
|||
|
|
primitive; `.<empty>` denotes "untyped" (consts of an inferred
|
|||
|
|
literal type).
|
|||
|
|
|
|||
|
|
| `Flags`
|
|||
|
|
| lpstr containing a decimal byte
|
|||
|
|
| Bit-packed booleans. Bit assignments are call-site specific
|
|||
|
|
(see `EncodeFlags`, `EncodeParamFlags`).
|
|||
|
|
|
|||
|
|
| Enum members
|
|||
|
|
| lpstr containing `Name=Ord,Name=Ord,...`
|
|||
|
|
| Inner format is comma-separated `name=ordinal` pairs.
|
|||
|
|
|===
|
|||
|
|
|
|||
|
|
A subtle but load-bearing detail: lpstrs are **concatenated without
|
|||
|
|
separators**. The reader doesn't skip whitespace between fields on a
|
|||
|
|
record line — it advances exactly `<len>` bytes for each field and
|
|||
|
|
the next field starts at the next byte. Newlines exist only between
|
|||
|
|
*lines* (added by the writer via `TStringList.Add`), not between
|
|||
|
|
fields within a line.
|
|||
|
|
|
|||
|
|
== Section: META
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
META\n
|
|||
|
|
<source-file lpstr><source-hash lpstr><compiler-id lpstr>
|
|||
|
|
<source-mod-time Int64><used-units stringlist>\n
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Fields:
|
|||
|
|
|
|||
|
|
`SourceFile`::
|
|||
|
|
Absolute or repo-relative path the iface was generated from. Used at
|
|||
|
|
load time for staleness checks — if the file exists on the search
|
|||
|
|
path, the loader hashes it and compares to `SourceHash`.
|
|||
|
|
|
|||
|
|
`SourceHash`::
|
|||
|
|
FNV-1a 64-bit hash, lowercase hex (16 chars), of the source file's
|
|||
|
|
contents at write time. Empty string means "format predates
|
|||
|
|
hashing" — the loader treats that as a forced miss and falls back to
|
|||
|
|
recompiling from source. See `ContentHashFnv1a64`.
|
|||
|
|
|
|||
|
|
`CompilerId`::
|
|||
|
|
The string `COMPILER_ID` from `uCompilerId.pas` baked in at the
|
|||
|
|
moment of write. Format: `blaise-<version>-<channel>+<phase>`.
|
|||
|
|
Used as the freshness signal when source isn't available on the
|
|||
|
|
search path.
|
|||
|
|
|
|||
|
|
`SourceModTime`::
|
|||
|
|
`Int64` modification timestamp captured at write. Currently
|
|||
|
|
informational — the loader trusts the hash, not the mtime.
|
|||
|
|
|
|||
|
|
`UsedUnits`::
|
|||
|
|
List of unit names this unit `uses`. Stored so the loader can
|
|||
|
|
recurse without re-parsing the source.
|
|||
|
|
|
|||
|
|
A `stringlist` is encoded as `<count lpstr>` followed by `<count>`
|
|||
|
|
lpstrs back-to-back.
|
|||
|
|
|
|||
|
|
== Section: TYPE
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
TYPE <count>\n
|
|||
|
|
<kind lpstr><name lpstr><payload...> (× count)
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Each type record opens with two lpstrs: the discriminator (`enum`,
|
|||
|
|
`set`, `alias`, `record`, `class`, `interface`, `proc`,
|
|||
|
|
`generic-class`, `generic-interface`) and the type name. The payload
|
|||
|
|
shape depends on the kind:
|
|||
|
|
|
|||
|
|
[cols="1,3",options="header"]
|
|||
|
|
|===
|
|||
|
|
| Kind | Payload
|
|||
|
|
|
|||
|
|
| `enum`
|
|||
|
|
| lpstr containing `Name=Ord,Name=Ord,...`
|
|||
|
|
|
|||
|
|
| `set`
|
|||
|
|
| `<base-type-name lpstr>`
|
|||
|
|
|
|||
|
|
| `alias`
|
|||
|
|
| `<aliased-type-name lpstr>`
|
|||
|
|
|
|||
|
|
| `record`
|
|||
|
|
| `<is-packed bool><field-list>`
|
|||
|
|
|
|||
|
|
| `class`
|
|||
|
|
| `<parent qualref><instance-size lpstr><attributes stringlist>` +
|
|||
|
|
`<implements stringlist><field-list><method-list>`
|
|||
|
|
|
|||
|
|
| `interface`
|
|||
|
|
| `<parent-name lpstr><method-decl-list>`
|
|||
|
|
|
|||
|
|
| `proc`
|
|||
|
|
| `<is-function bool><is-method-ptr bool><return-type-name lpstr>` +
|
|||
|
|
`<param-count lpstr>(<param-name><param-type><param-flags>) × count`
|
|||
|
|
|
|||
|
|
| `generic-class`
|
|||
|
|
| `<type-param-list>(<as for class>)`
|
|||
|
|
|
|||
|
|
| `generic-interface`
|
|||
|
|
| `<type-param-list><parent-name lpstr><method-decl-list>`
|
|||
|
|
|===
|
|||
|
|
|
|||
|
|
A `field-list` is a count lpstr followed by `<name><type-name><is-weak>`
|
|||
|
|
triples — flattened so multi-name declarations like `X, Y: Integer`
|
|||
|
|
produce two records.
|
|||
|
|
|
|||
|
|
A `method-list` and `method-decl-list` are structurally similar but
|
|||
|
|
not identical:
|
|||
|
|
|
|||
|
|
* `method-list` (from `EncodeMethodSig`) carries class-method extras
|
|||
|
|
the **codegen** needs: `IsVirtual`, `IsOverride`, `ResolvedQbeName`,
|
|||
|
|
`VTableSlot`. Used by classes.
|
|||
|
|
* `method-decl-list` (from `EncodeMethodDecl`) lacks the resolved
|
|||
|
|
names — interfaces don't carry codegen state. Used by interfaces.
|
|||
|
|
|
|||
|
|
A `type-param-list` is a count lpstr followed by
|
|||
|
|
`<param-name><constraint>` pairs.
|
|||
|
|
|
|||
|
|
Three TTypeDef kinds are **not** serialised: anything for which
|
|||
|
|
`TypeEntryKind` returns `''`. These are filtered out of the count and
|
|||
|
|
skipped in the write loop. The reader will never see them.
|
|||
|
|
|
|||
|
|
== Section: CONST
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
CONST <count>\n
|
|||
|
|
<name lpstr><type qualref><int-value Int64><str-value lpstr><flags> (× count)
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
A single record carries both the integer and string representations;
|
|||
|
|
the flags byte (low 2 bits) tells the reader which is live:
|
|||
|
|
|
|||
|
|
* bit 0 = `IsString` — `<str-value>` is the value
|
|||
|
|
* bit 1 = `IsFloat` — `<str-value>` is a decimal float string
|
|||
|
|
* otherwise — `<int-value>` is the value
|
|||
|
|
|
|||
|
|
`<type qualref>` is `<UnitName>.<TypeName>` or `$builtin.Integer` /
|
|||
|
|
`.<empty>` for inferred-typed literals.
|
|||
|
|
|
|||
|
|
== Section: VAR
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
VAR <count>\n
|
|||
|
|
<name lpstr><type qualref> (× count)
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Unit-level variable declarations. No initial value — initialisation
|
|||
|
|
runs in the unit's initialisation section (which is in the
|
|||
|
|
*implementation*, not the interface, and therefore not in the `.bif`).
|
|||
|
|
|
|||
|
|
== Section: ROUT
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
ROUT <count>\n
|
|||
|
|
<name lpstr><is-function lpstr><return-type qualref>
|
|||
|
|
<param-count lpstr>
|
|||
|
|
(<param-name><param-type><param-flags>) × param-count
|
|||
|
|
(× count)
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Free routines (not methods). `<is-function>` is `'0'` or `'1'`
|
|||
|
|
encoded *as a decimal-digit lpstr*, not as an `EncodeBool` lpstr — a
|
|||
|
|
historical inconsistency, not a bug. The reader uses
|
|||
|
|
`ReadDecimalAt`-equivalent parsing.
|
|||
|
|
|
|||
|
|
Param flags (`EncodeParamFlags`):
|
|||
|
|
|
|||
|
|
* bit 0 = `IsVarParam`
|
|||
|
|
* bit 1 = `IsConstParam`
|
|||
|
|
* bit 2 = `IsOpenArray`
|
|||
|
|
|
|||
|
|
== Section: GENROUT
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
GENROUT <count>\n
|
|||
|
|
<name lpstr><type-param-list><method-decl><block> (× count)
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Generic *free* routines. The interface needs the full body because
|
|||
|
|
instantiation happens at the call site — the consumer has to be able
|
|||
|
|
to clone-substitute the body locally. `<block>` is a serialised
|
|||
|
|
`TBlock` (see <<ast-encoding>>).
|
|||
|
|
|
|||
|
|
Generic *type* bodies (e.g. `TList<T>`) are skipped here — they ride
|
|||
|
|
along inside their owning class's `generic-class` TYPE record.
|
|||
|
|
|
|||
|
|
== Section: INLINE
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
INLINE <count>\n
|
|||
|
|
<routine-name lpstr><block> (× count)
|
|||
|
|
END\n
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Inline-eligible routine bodies. The body is a serialised `TBlock`
|
|||
|
|
chunk encoded with the same expression/statement protocol as GENROUT.
|
|||
|
|
A consumer that wants to inline a call recovers the AST from this
|
|||
|
|
section instead of re-parsing the source.
|
|||
|
|
|
|||
|
|
[#ast-encoding]
|
|||
|
|
== AST Encoding (Generic & Inline Bodies)
|
|||
|
|
|
|||
|
|
`TASTExpr` / `TASTStmt` / `TBlock` are serialised inline within the
|
|||
|
|
surrounding lpstr stream. Each node is `<kind-tag lpstr>` followed by
|
|||
|
|
a kind-specific tail of further lpstrs. `nil` children are encoded as
|
|||
|
|
`<kind 'nil'>`.
|
|||
|
|
|
|||
|
|
Kind tags (current set in `EncodeExpr` / `EncodeStmt`):
|
|||
|
|
|
|||
|
|
[cols="1,2,3",options="header"]
|
|||
|
|
|===
|
|||
|
|
| Tag | Node class | Tail
|
|||
|
|
|
|||
|
|
| `int`
|
|||
|
|
| `TIntLiteral`
|
|||
|
|
| `<value lpstr>`
|
|||
|
|
|
|||
|
|
| `float`
|
|||
|
|
| `TFloatLiteral`
|
|||
|
|
| `<value lpstr>` (decimal string)
|
|||
|
|
|
|||
|
|
| `str`
|
|||
|
|
| `TStringLiteral`
|
|||
|
|
| `<value lpstr>`
|
|||
|
|
|
|||
|
|
| `nillit`
|
|||
|
|
| `TNilLiteral`
|
|||
|
|
| _none_
|
|||
|
|
|
|||
|
|
| `id`
|
|||
|
|
| `TIdentExpr`
|
|||
|
|
| `<name lpstr>`
|
|||
|
|
|
|||
|
|
| `bin`
|
|||
|
|
| `TBinaryExpr`
|
|||
|
|
| `<op-ord lpstr><lhs expr><rhs expr>`
|
|||
|
|
|
|||
|
|
| `not`
|
|||
|
|
| `TNotExpr`
|
|||
|
|
| `<expr>`
|
|||
|
|
|
|||
|
|
| `call`
|
|||
|
|
| `TFuncCallExpr`
|
|||
|
|
| `<name lpstr><arg expr-list>`
|
|||
|
|
|
|||
|
|
| `mcall`
|
|||
|
|
| `TMethodCallExpr`
|
|||
|
|
| `<obj-name lpstr><method-name lpstr><obj-expr><arg expr-list>`
|
|||
|
|
|
|||
|
|
| `field`
|
|||
|
|
| `TFieldAccessExpr`
|
|||
|
|
| `<record-name lpstr><field-name lpstr><base expr><prop-index expr>`
|
|||
|
|
|
|||
|
|
| `deref`
|
|||
|
|
| `TDerefExpr`
|
|||
|
|
| `<expr>`
|
|||
|
|
|
|||
|
|
| `addr`
|
|||
|
|
| `TAddrOfExpr`
|
|||
|
|
| `<expr>`
|
|||
|
|
|
|||
|
|
| `ssub`
|
|||
|
|
| `TStringSubscriptExpr`
|
|||
|
|
| `<str expr><index expr>`
|
|||
|
|
|
|||
|
|
| `alit`
|
|||
|
|
| `TArrayLiteralExpr`
|
|||
|
|
| `<elements expr-list>`
|
|||
|
|
|
|||
|
|
| `isop` / `asop` / `supp`
|
|||
|
|
| `TIsExpr` / `TAsExpr` / `TSupportsExpr`
|
|||
|
|
| `<obj expr><type-name lpstr>(+<out-var-name> for supp)`
|
|||
|
|
|===
|
|||
|
|
|
|||
|
|
(Statement kinds — `assign`, `if`, `while`, `repeat`, `for`, `case`,
|
|||
|
|
`break`, `continue`, `exit`, `try`, `raise`, `with`-`stub`, `block`,
|
|||
|
|
etc. — follow the same pattern. See `EncodeStmt` for the full table.)
|
|||
|
|
|
|||
|
|
**Versioning implication**: any new node class added to `uAST.pas`
|
|||
|
|
that is reachable from a generic or inline body must get a new kind
|
|||
|
|
tag here, with reader-side dispatch in `ReadExpr` / `ReadStmt`. The
|
|||
|
|
reader's `else` branch raises `EIfaceFormatError`, so the failure
|
|||
|
|
mode is loud and well-localised. The version field bumps at the same
|
|||
|
|
time.
|
|||
|
|
|
|||
|
|
[#embedding]
|
|||
|
|
== Embedding `.bif` Inside `.o`
|
|||
|
|
|
|||
|
|
The production path doesn't ship `.bif` as a sidecar file. Instead,
|
|||
|
|
the `.bif` bytes are embedded inside the companion object file under
|
|||
|
|
a non-loaded section, via GNU `objcopy`:
|
|||
|
|
|
|||
|
|
[source,shell]
|
|||
|
|
----
|
|||
|
|
# Embed
|
|||
|
|
objcopy --add-section .blaise.iface=foo.bif \
|
|||
|
|
--set-section-flags .blaise.iface=noload,readonly \
|
|||
|
|
foo.o foo.o
|
|||
|
|
|
|||
|
|
# Extract
|
|||
|
|
objcopy --dump-section .blaise.iface=out.bif foo.o /dev/null
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
Section name varies by object format (`uIfaceObject.pas`):
|
|||
|
|
|
|||
|
|
[cols="1,1",options="header"]
|
|||
|
|
|===
|
|||
|
|
| Format | Section name
|
|||
|
|
| ELF (Linux) | `.blaise.iface`
|
|||
|
|
| PE/COFF (Windows) | `.bliface` (8-char limit)
|
|||
|
|
|===
|
|||
|
|
|
|||
|
|
PE/COFF support is stubbed out — only the constant is wired. The
|
|||
|
|
runtime currently only emits ELF.
|
|||
|
|
|
|||
|
|
**Why embed at all?** From `uIfaceObject.pas`:
|
|||
|
|
|
|||
|
|
> Keep .bif and .o in lock-step. When the iface lives as its own
|
|||
|
|
> file next to the .o, a build-system glitch or stale copy can
|
|||
|
|
> produce a mismatch where the compiler trusts an interface for
|
|||
|
|
> symbols that the object file no longer exports. Embedding the
|
|||
|
|
> bytes inside the .o makes the artefacts inseparable — copying the
|
|||
|
|
> .o brings the iface along, and a regenerated .o can't carry an
|
|||
|
|
> older iface by accident.
|
|||
|
|
|
|||
|
|
The section is marked `noload` so it never reaches the final
|
|||
|
|
executable. GNU `ld` drops it at link time. `clang -fuse-ld=lld`
|
|||
|
|
preserves the same behaviour.
|
|||
|
|
|
|||
|
|
[#versioning]
|
|||
|
|
== Versioning & Compatibility
|
|||
|
|
|
|||
|
|
The `IFACE_VERSION` constant is the only version surface. The format
|
|||
|
|
gates compatibility on three checks, in order:
|
|||
|
|
|
|||
|
|
. **Magic mismatch.** Anything other than `BLAISE-IFACE` at byte zero
|
|||
|
|
is rejected outright.
|
|||
|
|
. **Version mismatch.** A `.bif` with a version other than
|
|||
|
|
`IFACE_VERSION` is rejected. There is no down-conversion; producers
|
|||
|
|
and consumers must use the same compiler revision (or the
|
|||
|
|
`CompilerId`-match path, below, must apply).
|
|||
|
|
. **Content drift.** Even with a matching version, a `.bif` may be
|
|||
|
|
stale relative to its source. The loader (`uUnitLoader.ValidateIface`)
|
|||
|
|
decides:
|
|||
|
|
|
|||
|
|
** **Source available on path** → recompute FNV-1a hash of the
|
|||
|
|
source bytes; if it matches `SourceHash`, accept; otherwise
|
|||
|
|
recompile from source.
|
|||
|
|
** **Source not available** → compare the embedded `CompilerId` to
|
|||
|
|
this compiler's `COMPILER_ID`. Match → accept (the binary `.o`
|
|||
|
|
and its `.bif` were produced together by an identical compiler
|
|||
|
|
and there's no way to do better). Mismatch → fatal: we have no
|
|||
|
|
way to regenerate.
|
|||
|
|
|
|||
|
|
**When to bump `IFACE_VERSION`:**
|
|||
|
|
|
|||
|
|
* Adding a new top-level section (e.g. `MACRO`, `RESOURCE`).
|
|||
|
|
* Changing the field set of an existing record kind.
|
|||
|
|
* Adding a new TYPE-kind discriminator.
|
|||
|
|
* Adding a new AST node kind referenced from generic/inline bodies.
|
|||
|
|
|
|||
|
|
**When not to bump:**
|
|||
|
|
|
|||
|
|
* Adding a new entry to a `stringlist` (the count drives parsing).
|
|||
|
|
* Adding a new flag bit to an existing flags byte (the bits we don't
|
|||
|
|
use are presently zero; widening is forward-compatible as long as
|
|||
|
|
readers mask the bits they care about).
|
|||
|
|
* Cosmetic changes to non-content (whitespace between sections is
|
|||
|
|
consumed and ignored).
|
|||
|
|
|
|||
|
|
== Failure Modes & Diagnostics
|
|||
|
|
|
|||
|
|
`EIfaceFormatError` is the single error type. It's raised by
|
|||
|
|
`ReadLpstrAt` (bad length syntax, length runs off the end of input),
|
|||
|
|
`ReadTag` / `ReadHeader` (wrong magic / version / section tag),
|
|||
|
|
`ReadExpr` / `ReadStmt` (unknown kind tag), and the per-section
|
|||
|
|
readers (unexpected `END` or unexpected non-`END`).
|
|||
|
|
|
|||
|
|
Higher up, `uUnitLoader.LoadIfaceFromObject` catches the exception
|
|||
|
|
and falls back to compiling from source — a malformed embedded iface
|
|||
|
|
is recoverable as long as the `.pas` is on the search path. The
|
|||
|
|
warning surfaces to stderr:
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
warning: unreadable iface in foo.o: <message>
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
A stale-but-readable iface (source hash mismatch) produces:
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
note: foo.o iface stale vs source on path; recompiling from source
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
A binary-only dep with a wrong compiler id produces an error and
|
|||
|
|
fails the build:
|
|||
|
|
|
|||
|
|
----
|
|||
|
|
error: foo.o iface compiled by 'blaise-0.9.0-dev+6c-M' (this compiler
|
|||
|
|
is 'blaise-0.9.0-dev+6c-N'); source unavailable to rebuild
|
|||
|
|
----
|
|||
|
|
|
|||
|
|
== Round-Trip Guarantee
|
|||
|
|
|
|||
|
|
`ReadUnitInterface(WriteUnitInterface(I))` is structurally equal to
|
|||
|
|
`I` for every field the format encodes. **Fields the format does not
|
|||
|
|
encode** (mostly implementation-section state — initialisation blocks,
|
|||
|
|
private types, finalization order) are not part of the iface contract
|
|||
|
|
and are not carried.
|
|||
|
|
|
|||
|
|
The "fields encoded" list is exactly:
|
|||
|
|
|
|||
|
|
* Unit `Name`, `UsedUnits`
|
|||
|
|
* `Consts`, `Vars`
|
|||
|
|
* `Types` where `TypeEntryKind <> ''` (i.e. except untaggable kinds)
|
|||
|
|
* `Routines` (free routine signatures)
|
|||
|
|
* `GenericBodies` where `not G.IsType` (generic free routines with
|
|||
|
|
bodies)
|
|||
|
|
* `InlineBodies` (inline-eligible routine bodies)
|
|||
|
|
* META: `SourceFile`, `SourceHash`, `CompilerId`, `SourceModTime`
|
|||
|
|
|
|||
|
|
If a producer populates a field not in this list and expects the
|
|||
|
|
consumer to see it, that's a bug in the producer's expectations, not
|
|||
|
|
in the format.
|
|||
|
|
|
|||
|
|
== References
|
|||
|
|
|
|||
|
|
[cols="1,2",options="header"]
|
|||
|
|
|===
|
|||
|
|
| File | Role
|
|||
|
|
|
|||
|
|
| `compiler/src/main/pascal/uUnitInterface.pas`
|
|||
|
|
| In-memory data model — `TUnitInterface` and its sub-records
|
|||
|
|
|
|||
|
|
| `compiler/src/main/pascal/uUnitInterfaceIO.pas`
|
|||
|
|
| Serialiser + parser; the canonical source of truth for the format
|
|||
|
|
|
|||
|
|
| `compiler/src/main/pascal/uIfaceObject.pas`
|
|||
|
|
| `.bif` ↔ `.o` embedding via `objcopy`
|
|||
|
|
|
|||
|
|
| `compiler/src/main/pascal/uUnitLoader.pas`
|
|||
|
|
| Auto-discovery + freshness validation at unit-resolve time
|
|||
|
|
|
|||
|
|
| `compiler/src/main/pascal/uCompilerId.pas`
|
|||
|
|
| The `COMPILER_ID` constant baked into the META block
|
|||
|
|
|
|||
|
|
| `docs/extending-ast.adoc`
|
|||
|
|
| Companion doc — what to do when adding a new AST node that needs
|
|||
|
|
serialisation
|
|||
|
|
|===
|