← board

Finalize an out Variant and an out managed record

What already works, and why these two do not

[[bug-a-an-out-parameter-of-a-managed-type-is-not-cleared]] made out finalize its parameter on entry the cheap way: a managed ASSIGNMENT already releases the old value, and an assignment through a by-ref param already writes the caller's variable, so s := '' / d := nil / f := nil at the head of the body IS the finalize, with no new emitter and nothing per backend.

That works only for kinds with an empty-value literal. The remaining two:

FPC 3.2.2 pxx
out v: Variant (caller had 'vv') [] [vv]
out r: TRecM (record with an AnsiString field, caller had 'q') [] [q]

Worth doing as one primitive, not two arms

Both want the same thing — finalize the managed thing at this ADDRESS — which is also what an out param of a future managed kind will want, and close to what scope-exit cleanup does for locals by frame offset. A single address-taking finalize op would also give Initialize/Finalize on a bare dynarray or Variant, which ir.inc:7526 currently refuses outright with "not implemented yet; assign nil instead, or wrap it in a record (feature-a-finalize-for-bare-dynarray-and-variant)". Check whether that ticket and this one are the same work before starting either.

Side question this turned up

An empty Variant printing None is a NilPy spelling reaching Pascal output; FPC prints the empty string. Worth its own look — it is not specific to out.

Gate

Track A's, plus the two rows above matching fpc 3.2.2 added to test/test_out_parameter_of_a_managed_type_is_cleared.pas, and its ARC round trip extended to a Variant and a managed record (a second owner of the record's string field must survive the clear).

Landed 2026-08-24

Both rows now match fpc 3.2.2 byte for byte, on x86-64, aarch64, arm32 and i386.

The two tickets in the "worth doing as one primitive" note above turned out to be the same work, and the answer was that the primitive already existed: AN_MANAGED_INIT, the node Finalize(x) builds. It knew records from the day it landed, and it learned bare Variants in this same change ([[feature-a-finalize-for-bare-dynarray-and-variant]]'s separable half). So ClearManagedOutParam did not need a new emitter after all — it builds that node over the parameter's ident and compiles it, which is literally "emit the intrinsic the user could have written by hand, at the head of the body".

Finalize and not Initialize, for the reason the two differ by: an out param's incoming bytes ARE the caller's live references, so dropping them without releasing is a leak, which is the second half of what the ticket reported.

Two negative rows guard the other direction, and both are in the gated test: unmgdrec 1 2 (a record with NO managed fields is not cleared — FPC does not finalize it either, so the caller's value must survive) and rec [] 3 (the managed field is released, the unmanaged n is untouched — the line between Finalize and FillChar).

The side question — an empty Variant rendering as None — was fixed first, as [[bug-a-a-null-variant-renders-as-none-in-pascal]]; without it the variant [] row could not have been written at all.

Not done, and not this ticket: VarClear is still missing from the Pascal surface (uses variants; VarClear(a) is "undefined variable"), which is [[bug-b-vartostr-is-missing-from-variants]]' shape in Track B's lane.

Log