feat(types): add packed record qualifier

Introduces the `packed record ... end` syntax.  Field layout in a
packed record skips natural-alignment padding between fields and
skips the record's tail padding, so SizeOf equals the cumulative
byte size of the fields.  ARC-managed field types (string, class,
interface, dynamic array) keep their natural 8-byte alignment so
that _StringRelease / _ClassRelease etc. can keep using aligned
64-bit loads through the field pointer.

`packed` is only legal directly before `record`.  `packed class`
and `packed array` are parse errors — neither has a meaningful
implementation in Blaise's heap-allocated class model nor in its
existing tightly-strided array layout.

Implementation:
  - Lexer: tkPacked token, PACKED keyword
  - AST: TRecordTypeDef.IsPacked, propagated through CloneTypeDef
  - Parser: optional PACKED prefix before RECORD; rejects other
    forms with a clear error message
  - uSymbolTable: TRecordTypeDesc.IsPacked + new FieldAlign helper;
    AddField / PackedSize / TotalSize / MaxAlign honour it
  - uSemantic: propagates IsPacked from def to desc in pass 1
  - OPDF: no change — emits whatever TotalSize reports

Tests: 11 IR-level + 2 e2e in cp.test.packedrecord.pas, plus the
TTokenKind audit bumped from 82 to 83.

Grammar and language-rationale updated.
This commit is contained in:
Graeme Geldenhuys 2026-05-22 11:31:42 +01:00
parent 7e613c6853
commit e732c71cd1
10 changed files with 600 additions and 12 deletions

View file

@ -506,8 +506,12 @@ type
TRecordTypeDef = class(TASTTypeDef)
public
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
IsPacked: Boolean; { True iff declared `packed record` — disables
field alignment padding and tail padding;
ARC-managed fields (string/class/intf) still
retain 8-byte storage alignment }
constructor Create;
destructor Destroy; override;
end;
@ -2012,6 +2016,7 @@ begin
else if ASrc is TRecordTypeDef then
begin
RT := TRecordTypeDef.Create;
RT.IsPacked := TRecordTypeDef(ASrc).IsPacked;
for I := 0 to TRecordTypeDef(ASrc).Fields.Count - 1 do
RT.Fields.Add(CloneFieldDecl(TFieldDecl(TRecordTypeDef(ASrc).Fields.Items[I])));
for I := 0 to TRecordTypeDef(ASrc).Methods.Count - 1 do

View file

@ -31,6 +31,8 @@ type
tkUses,
tkType,
tkRecord,
tkPacked, { 'packed' — record-layout qualifier (only legal before
'record'; affects field offsets + tail padding) }
tkClass,
tkProcedure,
tkFunction,
@ -170,6 +172,7 @@ begin
else if AUpper = 'END' then Result := tkEnd
else if AUpper = 'TYPE' then Result := tkType
else if AUpper = 'RECORD' then Result := tkRecord
else if AUpper = 'PACKED' then Result := tkPacked
else if AUpper = 'CLASS' then Result := tkClass
else if AUpper = 'PROCEDURE' then Result := tkProcedure
else if AUpper = 'FUNCTION' then Result := tkFunction
@ -587,6 +590,7 @@ begin
tkUses: Result := 'uses';
tkType: Result := 'type';
tkRecord: Result := 'record';
tkPacked: Result := 'packed';
tkClass: Result := 'class';
tkProcedure: Result := 'procedure';
tkFunction: Result := 'function';

View file

@ -503,7 +503,20 @@ begin
else
begin
Expect(tkEquals);
if Check(tkRecord) then
if Check(tkPacked) then
begin
{ Only `packed record` is supported. `packed class` and `packed
array` are legal Delphi/FPC syntax but Blaise rejects them
explicitly so users get a clear error rather than silent no-op. }
Advance;
if not Check(tkRecord) then
raise EParseError.Create(Format(
'''packed'' may only precede ''record'' at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
TD.Def := ParseRecordDef;
TRecordTypeDef(TD.Def).IsPacked := True;
end
else if Check(tkRecord) then
TD.Def := ParseRecordDef
else if Check(tkClass) and (PeekKind = tkOf) then
begin

View file

@ -2206,7 +2206,10 @@ begin
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
if TD.Def is TRecordTypeDef then
RT := FTable.NewRecordType(TD.Name)
begin
RT := FTable.NewRecordType(TD.Name);
RT.IsPacked := TRecordTypeDef(TD.Def).IsPacked;
end
else if TD.Def is TClassTypeDef then
RT := FTable.NewClassType(TD.Name)
else if TD.Def is TGenericTypeDef then

View file

@ -234,7 +234,10 @@ type
FHasDestroyMethod: Boolean; { True when the class declares a 'Destroy' method }
FHasAbstractMethods: Boolean; { True when any vtable slot is abstract (no impl) }
FClassAttributes: TStringList; { resolved attribute type names e.g. 'ThreadedAttribute' }
FIsPacked: Boolean; { True for `packed record` — skip per-field
alignment and tail padding (records only) }
public
property IsPacked: Boolean read FIsPacked write FIsPacked;
constructor Create(const AName: string; AKind: TTypeKind);
destructor Destroy; override;
procedure AddField(const AName: string; AType: TTypeDesc);
@ -242,6 +245,7 @@ type
function PackedSize: Integer;
function TotalSize: Integer;
function MaxAlign: Integer;
function FieldAlign(AType: TTypeDesc): Integer;
{ Vtable management }
function HasVTable: Boolean;
@ -565,6 +569,23 @@ begin
inherited Destroy;
end;
{ Returns the alignment required for AType when laid out inside this record.
Normally that is AType.AllocAlign. For a `packed record` the alignment
drops to 1 — except for ARC-managed fields (string / class / interface /
dynamic array), which must remain at their natural alignment because the
runtime helpers (`_StringRelease`, `_ClassRelease`, etc.) perform 64-bit
loads through the field pointer. }
function TRecordTypeDesc.FieldAlign(AType: TTypeDesc): Integer;
begin
Result := AType.AllocAlign;
if not FIsPacked then Exit;
case AType.Kind of
tyString, tyClass, tyInterface, tyDynArray: { keep natural alignment } ;
else
Result := 1;
end;
end;
{ Cumulative byte position immediately after the last field (or after the
vptr if no fields have been added yet). Excludes the record's tail
padding — that is only applied in TotalSize. }
@ -580,7 +601,7 @@ begin
for I := 0 to FFields.Count - 1 do
begin
FT := TFieldInfo(FFields.Items[I]).TypeDesc;
A := FT.AllocAlign;
A := FieldAlign(FT);
if (A > 1) and (Result mod A <> 0) then
Inc(Result, A - Result mod A);
Inc(Result, FT.ByteSize);
@ -594,7 +615,7 @@ var
A: Integer;
begin
Offset := PackedSize;
A := AType.AllocAlign;
A := FieldAlign(AType);
if (A > 1) and (Offset mod A <> 0) then
Offset := Offset + (A - Offset mod A);
Info := TFieldInfo.Create;
@ -624,8 +645,10 @@ begin
and back-to-back fields stay naturally aligned. Class instances are
heap-allocated singletons (never directly in arrays — only references
to them are), so we leave their size unpadded to match FPC/Delphi
InstanceSize semantics. }
InstanceSize semantics. `packed record` similarly suppresses tail
padding. }
if Kind = tyClass then Exit;
if FIsPacked then Exit;
Algn := MaxAlign;
if (Algn > 1) and (Result mod Algn <> 0) then
Inc(Result, Algn - Result mod Algn);
@ -634,13 +657,18 @@ end;
function TRecordTypeDesc.MaxAlign: Integer;
var
I, A: Integer;
FT: TTypeDesc;
begin
Result := 1;
if HasVTable then
Result := 8; { vptr requires 8-byte alignment }
for I := 0 to FFields.Count - 1 do
begin
A := TFieldInfo(FFields.Items[I]).TypeDesc.AllocAlign;
FT := TFieldInfo(FFields.Items[I]).TypeDesc;
if FIsPacked then
A := FieldAlign(FT) { ARC fields keep natural align; PODs become 1 }
else
A := FT.AllocAlign;
if A > Result then
Result := A;
end;

View file

@ -20,6 +20,7 @@ uses
cp.test.symtable,
cp.test.semantic,
cp.test.records,
cp.test.packedrecord,
cp.test.classes,
cp.test.arc,
cp.test.methods,

View file

@ -0,0 +1,436 @@
{
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 cp.test.packedrecord;
{ Tests for the `packed record` qualifier.
Coverage:
- `packed` reserved word lexes to tkPacked.
- `packed record ... end` parses into a TRecordTypeDef with IsPacked=True.
- In a packed record, fields are laid out at the next byte (no alignment
padding) and the record itself has no tail padding.
- The non-packed form is unaffected.
- ARC-managed fields (string / class / interface) still require 8-byte
storage alignment; the field offset is forced to the next 8-byte
boundary regardless of `packed`.
- Record-level alignment (MaxAlign) drops to 1 for packed records, so
arrays of packed records pack tight too.
- `packed` is only legal in front of `record`; `packed class` / `packed
array` produce a parse error. }
interface
uses
blaise.testing, cp.test.e2e.base,
uLexer, uParser, uAST, uSymbolTable, uSemantic, uCodeGenQBE;
type
TPackedRecordTests = class(TTestCase)
private
function AnalyseSrc(const ASrc: string): TProgram;
function GenIR(const ASrc: string): string;
published
procedure TestLexer_Packed_Keyword;
procedure TestParse_PackedRecord_SetsIsPacked;
procedure TestParse_PlainRecord_IsPackedFalse;
procedure TestSemantic_PackedRecord_TypeDescIsPacked;
procedure TestSemantic_PackedRecord_ByteThenInt_NoAlignPadding;
procedure TestSemantic_PackedRecord_TotalSize_NoTailPad;
procedure TestSemantic_PackedRecord_MaxAlign_Is1;
procedure TestSemantic_PackedRecord_StringField_StaysAlignedTo8;
procedure TestParse_PackedClass_RaisesError;
procedure TestParse_PackedArray_RaisesError;
procedure TestCodegen_PackedRecord_TypeSizeMatchesPacked;
end;
[Threaded]
TPackedRecordE2ETests = class(TE2ETestCase)
protected
procedure SetUp; override;
published
procedure TestRun_PackedRecord_ByteInt_Offsets;
procedure TestRun_PackedRecord_SizeOfMatchesPacked;
end;
implementation
function TPackedRecordTests.AnalyseSrc(const ASrc: string): TProgram;
var
L: TLexer;
P: TParser;
A: TSemanticAnalyser;
begin
L := TLexer.Create(ASrc);
P := TParser.Create(L);
try
Result := P.Parse;
finally
P.Free;
L.Free;
end;
A := TSemanticAnalyser.Create;
try
A.Analyse(Result);
finally
A.Free;
end;
end;
function TPackedRecordTests.GenIR(const ASrc: string): string;
var
Prog: TProgram;
CG: TCodeGenQBE;
begin
Prog := AnalyseSrc(ASrc);
try
CG := TCodeGenQBE.Create;
try
CG.Generate(Prog);
Result := CG.GetOutput;
finally
CG.Free;
end;
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestLexer_Packed_Keyword;
var
L: TLexer;
T: TToken;
begin
L := TLexer.Create('packed');
try
T := L.Next;
AssertEquals('packed maps to tkPacked', Ord(tkPacked), Ord(T.Kind));
finally
L.Free;
end;
end;
procedure TPackedRecordTests.TestParse_PackedRecord_SetsIsPacked;
const
Src = '''
program P;
type
TFoo = packed record
A: Byte;
B: Integer;
end;
begin end.
''';
var
Prog: TProgram;
TD: TTypeDecl;
RD: TRecordTypeDef;
begin
Prog := AnalyseSrc(Src);
try
TD := TTypeDecl(Prog.Block.TypeDecls.Items[0]);
AssertTrue('is TRecordTypeDef', TD.Def is TRecordTypeDef);
RD := TRecordTypeDef(TD.Def);
AssertTrue('IsPacked = True', RD.IsPacked);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestParse_PlainRecord_IsPackedFalse;
const
Src = '''
program P;
type TFoo = record A: Integer; end;
begin end.
''';
var
Prog: TProgram;
RD: TRecordTypeDef;
begin
Prog := AnalyseSrc(Src);
try
RD := TRecordTypeDef(TTypeDecl(Prog.Block.TypeDecls.Items[0]).Def);
AssertFalse('IsPacked = False on plain record', RD.IsPacked);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestSemantic_PackedRecord_TypeDescIsPacked;
const
Src = '''
program P;
type TFoo = packed record A: Integer; end;
var R: TFoo;
begin end.
''';
var
Prog: TProgram;
RT: TRecordTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
AssertTrue('IsPacked propagated to TRecordTypeDesc', RT.IsPacked);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestSemantic_PackedRecord_ByteThenInt_NoAlignPadding;
const
Src = '''
program P;
type TFoo = packed record
A: Byte;
B: Integer;
end;
var R: TFoo;
begin end.
''';
var
Prog: TProgram;
RT: TRecordTypeDesc;
FldA, FldB: TFieldInfo;
begin
Prog := AnalyseSrc(Src);
try
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
FldA := RT.FindField('A');
FldB := RT.FindField('B');
AssertEquals('A offset', 0, FldA.Offset);
AssertEquals('B offset (no align padding)', 1, FldB.Offset);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestSemantic_PackedRecord_TotalSize_NoTailPad;
const
Src = '''
program P;
type TFoo = packed record
A: Byte;
B: Integer;
end;
var R: TFoo;
begin end.
''';
var
Prog: TProgram;
RT: TRecordTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
AssertEquals('TotalSize = 1 + 4 = 5 (no tail pad)', 5, RT.TotalSize);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestSemantic_PackedRecord_MaxAlign_Is1;
const
Src = '''
program P;
type TFoo = packed record
A: Byte;
B: Int64;
end;
var R: TFoo;
begin end.
''';
var
Prog: TProgram;
RT: TRecordTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
AssertEquals('MaxAlign = 1 for packed', 1, RT.MaxAlign);
AssertEquals('B offset = 1 (no 8-byte align)', 1,
RT.FindField('B').Offset);
AssertEquals('TotalSize = 9 (no tail pad)', 9, RT.TotalSize);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestSemantic_PackedRecord_StringField_StaysAlignedTo8;
const
{ String fields are ARC-managed pointers — _StringRelease does a 64-bit load
so they must remain 8-byte aligned even inside a packed record. }
Src = '''
program P;
type TFoo = packed record
A: Byte;
S: string;
end;
var R: TFoo;
begin end.
''';
var
Prog: TProgram;
RT: TRecordTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
AssertEquals('A at 0', 0, RT.FindField('A').Offset);
AssertEquals('S aligned to 8', 8, RT.FindField('S').Offset);
finally
Prog.Free;
end;
end;
procedure TPackedRecordTests.TestParse_PackedClass_RaisesError;
const
Src = '''
program P;
type TFoo = packed class A: Integer; end;
begin end.
''';
var
L: TLexer;
P: TParser;
Prog: TProgram;
Raised: Boolean;
begin
Raised := False;
L := TLexer.Create(Src);
P := TParser.Create(L);
try
try
Prog := P.Parse;
Prog.Free;
except
on EParseError do Raised := True;
end;
finally
P.Free;
L.Free;
end;
AssertTrue('packed class rejected', Raised);
end;
procedure TPackedRecordTests.TestParse_PackedArray_RaisesError;
const
Src = '''
program P;
type TArr = packed array[0..3] of Byte;
begin end.
''';
var
L: TLexer;
P: TParser;
Prog: TProgram;
Raised: Boolean;
begin
Raised := False;
L := TLexer.Create(Src);
P := TParser.Create(L);
try
try
Prog := P.Parse;
Prog.Free;
except
on EParseError do Raised := True;
end;
finally
P.Free;
L.Free;
end;
AssertTrue('packed array rejected', Raised);
end;
procedure TPackedRecordTests.TestCodegen_PackedRecord_TypeSizeMatchesPacked;
const
Src = '''
program P;
type TFoo = packed record
A: Byte;
B: Integer;
end;
var R: TFoo;
begin
WriteLn(SizeOf(TFoo))
end.
''';
var
IR: string;
begin
IR := GenIR(Src);
AssertTrue('IR not empty', IR <> '');
{ SizeOf folds at codegen — packed Byte+Integer = 5 bytes }
AssertTrue('SizeOf folds to 5', Pos('copy 5', IR) > 0);
end;
procedure TPackedRecordE2ETests.SetUp;
begin
inherited SetUp;
SetUpScratch('compiler/target/test-e2e-packed-record');
end;
const
LE = #10;
SrcByteIntOffsets = '''
program P;
type TFoo = packed record
A: Byte;
B: Integer;
end;
var R: TFoo;
begin
R.A := 9;
R.B := 12345;
WriteLn(R.A);
WriteLn(R.B);
WriteLn(SizeOf(TFoo))
end.
''';
SrcPackedVsUnpacked = '''
program P;
type
TPlain = record A: Byte; B: Int64; end;
TPacked = packed record A: Byte; B: Int64; end;
begin
WriteLn(SizeOf(TPlain));
WriteLn(SizeOf(TPacked))
end.
''';
procedure TPackedRecordE2ETests.TestRun_PackedRecord_ByteInt_Offsets;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcByteIntOffsets, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('A=9, B=12345, SizeOf=5',
'9' + LE + '12345' + LE + '5' + LE, Output);
end;
procedure TPackedRecordE2ETests.TestRun_PackedRecord_SizeOfMatchesPacked;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcPackedVsUnpacked, Output, RCode));
AssertEquals('exit 0', 0, RCode);
{ Plain: A at 0, B at 8 (8-byte aligned), tail pad to MaxAlign=8 → 16.
Packed: A at 0, B at 1, no tail pad → 9. }
AssertEquals('plain=16, packed=9',
'16' + LE + '9' + LE, Output);
end;
initialization
RegisterTest(TPackedRecordTests);
RegisterTest(TPackedRecordE2ETests);
end.

View file

@ -118,11 +118,11 @@ implementation
token kind is appended to the enum — the test below will fail and remind
you to also update TokenKindName. }
const
ExpectedMaxTokenKindOrd = 82; { tkAt }
ExpectedMaxTokenKindOrd = 83; { tkAt }
procedure TTokenKindNameTests.TestMaxOrdinal_IstkAt;
begin
AssertEquals('tkAt must be the last TTokenKind (ord 82); update this test and TokenKindName when adding new kinds',
AssertEquals('tkAt must be the last TTokenKind (ord 83); update this test and TokenKindName when adding new kinds',
ExpectedMaxTokenKindOrd, Ord(tkAt));
end;

View file

@ -86,6 +86,7 @@ IMPLEMENTATION = "implementation" ;
USES = "uses" ;
TYPE = "type" ;
RECORD = "record" ;
PACKED = "packed" ;
CLASS = "class" ;
VAR = "var" ;
BEGIN = "begin" ;
@ -235,14 +236,23 @@ TypeDef
;
RecordDef
= RECORD { FieldDecl | MethodDecl } END
= [ PACKED ] RECORD { FieldDecl | MethodDecl } END
;
(* Records may declare methods alongside fields. Method bodies are written
* out-of-line as TRecordName.MethodName, identical to classes. The
* receiver is passed by pointer (`Self` is a `var` parameter under the hood);
* the language gives normal value semantics — assignments copy the record.
* No constructors, destructors, or virtual methods are allowed in records. *)
* No constructors, destructors, or virtual methods are allowed in records.
*
* `packed` is permitted only in front of `record`. It disables natural
* alignment padding between fields and disables tail padding on the record
* as a whole, so the record's size equals the cumulative byte size of its
* fields. ARC-managed field types (`string`, class, interface, dynamic
* array) keep their natural 8-byte alignment even inside a packed record
* because the runtime helpers (`_StringRelease`, `_ClassRelease`, etc.)
* perform 64-bit loads through the field pointer. `packed class` and
* `packed array` are rejected as parse errors. *)
ClassDef
= CLASS [ LPAREN GenericName { COMMA GenericName } RPAREN ]

View file

@ -596,6 +596,94 @@ for any realistic string length.
---
=== Packed Records
==== Decision
Blaise supports the `packed record` qualifier with strict semantics:
* `packed record ... end` lays out fields at successive byte offsets
with no alignment padding between them, and applies no tail padding
to the record as a whole.
* `packed` is permitted only directly in front of `record`. The forms
`packed class` and `packed array`, accepted (often as no-ops) by FPC
and Delphi, are parse errors in Blaise.
* ARC-managed field types — `string`, class references, interfaces,
and dynamic arrays — retain their natural 8-byte alignment inside a
packed record. Other field types (`Byte`, `Word`/`SmallInt`,
`Integer`/`UInt32`, `Int64`/`UInt64`, `Double`, enums, sets,
static arrays of these, and nested packed/non-packed records)
pack tight.
* `MaxAlign` of a packed record collapses to `1` unless it contains
an ARC-managed field (in which case it stays at `8`). Arrays of
fully POD packed records therefore stride at the record's exact
field-byte sum.
==== Rationale
The intended use of `packed` is interop with binary formats whose
field layout is byte-addressed: file headers, network protocol
records, and ABI structures defined by external libraries. In every
such case the user wants byte-exact layout that matches an external
specification, and any silent padding the compiler inserts is a bug
from the caller's point of view.
The decision to *forbid* `packed class` and `packed array` rather
than silently accept them is deliberate: a `class` in Blaise carries
a vptr and lives on the heap behind an `_ClassAlloc` header, so
"packing" it would either lie (the qualifier is ignored) or break
runtime invariants. Arrays in Blaise are already either dynamic
(heap-managed, no inter-element padding) or static (element-strided
by the element's own size). Pretending `packed` applies to them
gives users false confidence in code that won't behave the way the
keyword suggests.
The exception for ARC-managed field types preserves a hard runtime
invariant: `_StringRelease`, `_ClassRelease`, `_IntfRelease`, and
`_DynArrayRelease` all perform 64-bit aligned loads through the
field pointer. Honouring a 1-byte-aligned ARC field would silently
slow x86_64 code and outright fault on stricter ARM targets. A
1-byte-stride layout for these types would also break the existing
ARC invariant that every reference-holding slot is loadable as a
single 8-byte word. Users who genuinely need a byte-exact layout
that includes a string field should use `Byte` arrays or a typed
pointer instead.
==== Examples
[source,pascal]
----
type
TPlain = record
A: Byte;
B: Int64;
end; { SizeOf = 16 (A at 0, pad to 8, B at 8, no tail) }
TPacked = packed record
A: Byte;
B: Int64;
end; { SizeOf = 9 (A at 0, B at 1, no tail) }
THybrid = packed record
A: Byte;
S: string;
end; { SizeOf = 16 (A at 0, pad to 8, S at 8) — S stays aligned }
----
==== Alternatives rejected
* *Always pack, even ARC-managed fields* — rejected. Unaligned 64-bit
loads through ARC pointers would either slow down or trap at runtime.
* *Per-field `packed` annotation* — rejected. The Delphi/FPC syntax
already covers the common case; finer-grained control is rare and
can be expressed with manual `Byte` arrays.
* *`{$ALIGN n}` / `{$PACKRECORDS n}` directives* — deferred. Blaise
does not yet have a compiler directive subsystem; if a use case for
intermediate alignments (2, 4) emerges, this is the obvious place
to add it later.
---
=== Indexed Properties
==== Decision