Initialize() / Finalize() standard procedures (managed-type intrinsics)
- Type: feature (Pascal frontend — Track P parse; lowering touches the managed-type init/final machinery = Track A gate)
- Status: resolved 2026-08-21 — superseded by [[feature-a-implement-initialize-and-finalize-over-the-arc-helpers]]; see the note at the bottom.
- Opened: 2026-07-18, out of the FPC-compiler gap analysis
- Blocks: [[goal-compile-fpc-compiler]] — used by FPC's
cclasses.pas:2394(TCmdStrListItem.GetCopy) and common in low-level FPC/Delphi library code that raw-moves objects containing managed fields.
Problem
FPC/Delphi define Initialize(v) / Finalize(v[, count]) standard procs:
treat v (a variable of a managed type, or a record/array containing
managed fields) as raw memory and set it to the initialized-empty state
(Initialize), or release its managed contents (Finalize). Canonical use:
{ TLinkedListItem.GetCopy raw-Moves all instance data -> the ansistring
field now aliases the original without a refcount bump. Reinit the field
as if it were fresh memory, then assign properly: }
Initialize(TCmdStrListItem(Result).FPStr);
TCmdStrListItem(Result).FPStr := FPstr;
pxx has the underlying machinery (managed locals get init/final generated by
the compiler) but does not expose it as callable intrinsics — grep for an
Initialize builtin comes up empty.
PREMISE RE-MEASURED — half of it is wrong, and the wrong half is worse
(frank2, 2026-08-17, at
2611fb398. Re-measured before starting, per the standing rule that a ticket's cause section ages faster than its symptom section. Did not start the implementation — see the note at the end.)
Initializeis indeed absent, and fails honestly:error: undefined variable (Initialize).
Finalizeis NOT absent. It is a silent no-op.parser.inc:23126matches the name, skips to the matching)with a paren counter, and emitsGenMakeSeq(-1, -1)— an empty sequence. The call is accepted, the argument is never even parsed as an expression, and nothing happens.Measured against FPC 3.2.2:
s := 'hello'; t := s; Finalize(s); WriteLn('s = [', s, ']'); { FPC: s = [] pxx: s = [hello] } WriteLn('t = [', t, ']'); { FPC: t = [hello] pxx: t = [hello] }So this is not only a missing feature. Under Rene's dialect rule — a refusal beats a wrong value — the
Finalizehalf is a bug, and the compat escape rule in CLAUDE.md says a parity finding that means silent wrong behaviour gets promoted out of the compat/feature bucket. The cclasses idiom this ticket exists to serve (rawMove, then reinit the managed field) is exactly where a no-op costs a leak or a double free rather than a diagnostic, and neither of those points back at this line.Whoever takes this should decide the refusal question first, because it is separable from and cheaper than the feature: making
Finalizean error until it is implemented is a few lines and removes a silent wrong value today. Against it: existing code may already callFinalizeand rely on it compiling (that is what a no-op buys), so turning it into an error could break a build that currently "works". That trade is a Track U call, not mine — filedecide-finalize-noop-vs-refusalif it is not obvious to you.Not started: implementing the pair means touching managed init/final generation and refcount semantics, which is the silent-corruption class of change, and this was measured at the end of a long session. Banking the correction rather than half-landing the feature.
Fix shape
Two intrinsics resolving against the argument's static type:
Initialize(v)→ zero/nil-fill the managed slots ofvWITHOUT finalizing previous contents (the point: memory is assumed garbage).Finalize(v)→ run the finalization the compiler would run at scope exit, then leave the slot nil/empty. Optionalcountarg for arrays-via-pointer can be a later slice. Reuse the existing per-type init/final generation; the intrinsic is a call into it with an explicit address.
Gate
make test + self-host byte-identical. Regressions: Move-then-Initialize
refcount pattern (mirror the cclasses idiom) leaks nothing and double-frees
nothing under the ansistring refcount checks; Finalize on a record with
mixed managed/unmanaged fields.
2026-08-21 — RESOLVED by [[feature-a-implement-initialize-and-finalize-over-the-arc-helpers]]
Closed rather than worked, as that ticket instructed. Its premise — that the
intrinsics were missing — was half wrong: Finalize was parsed and its
arguments discarded (an empty sequence emitted), which is worse than missing,
because FPC-shaped code compiled and silently did nothing. Initialize genuinely
did not exist.
This ticket's own canonical example now matches FPC exactly. The
TLinkedListItem.GetCopy shape — raw-Move a record containing an AnsiString,
so the copy aliases the original with no refcount bump, then Initialize the
alias before assigning through it — run against FPC 3.x -Mobjfpc -O-:
a=[orig] b=[copy] keep=[orig]
b after fin len=0 a still=[orig]
Identical on both. The original survives the copy's Finalize, which is the
property the whole idiom depends on.
Scope note for [[goal-compile-fpc-compiler]], which this listed as a blocker:
records and AnsiStrings are done; a bare dynamic-array or variant lvalue is
still refused with a compile error (deliberately, not a no-op) and is
[[feature-a-finalize-for-bare-dynarray-and-variant]]. cclasses.pas:2394 is a
record-with-managed-field case, so it is covered.
The track line here said "Track P parse; lowering touches Track A" — in the end
it was all Track A: the parse desugars a string Finalize into an assignment and
everything else is IR plus two builtinheap helpers.