Completes generic methods: the out-of-line implementation form now parses
and links, alongside the inline-body form added previously.
type TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T; // declaration
end;
function TUtil.Pick<T>(cond: Boolean; a, b: T): T; // out-of-line body
begin if cond then Result := a else Result := b end;
- Parser: a method-level <T> appearing AFTER the qualified name
(Owner.Method<T>) is now parsed into TypeParams. (The <T> before the dot
remains the owner's type params, as for TList<T>.Add.)
- Semantic: LinkClassMethodImpls detects a generic-method impl
(TypeParams <> nil) and transfers its body onto the in-class
generic-method template, instead of trying to resolve its T-typed params.
Also fixes a real hole found via the out-of-line probe: generic-method
call arguments were not type-checked against the parameters (the path
bypasses ResolveMethodOverload), so e.g. passing a string for a Boolean
parameter compiled silently. The call site now validates argument count
and types against the monomorphised signature.
Verified out-of-line impl (string + Integer instantiations) on both
backends, and that a mismatched argument is now rejected. Adds
TE2EGenericsTests.TestRun_GenericMethod_OutOfLineImpl. Grammar and
language-rationale updated to drop the inline-only caveat.
A class or record method may now declare its own type parameters,
independent of the enclosing type, and is monomorphised per call site:
type TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T;
begin if cond then Result := a else Result := b end;
end;
...
u.Pick<Integer>(True, 7, 9); // -> 7
u.Pick<string>(False, 'a', 'b'); // -> 'b'
Each distinct set of explicit type arguments produces one concrete body
named <Owner>_<Method>_<Args> (e.g. TUtil_Pick_Integer); the implicit
Self is preserved, so a generic method can read the receiver's fields and
call its other methods. This is distinct from (and composes with) methods
of a generic CLASS.
Implementation mirrors the existing generic free-function machinery:
- Parser: a method-call folds an explicit <...> type-arg list into the
method name (Pick<Integer>), using the same two-token '<' lookahead as
generic free-function calls.
- Semantic: generic-method templates (TypeParams <> nil) are registered
by Owner.Method and skipped from signature/vtable/body analysis;
InstantiateGenericMethod clones the template, substitutes the type
params, keeps OwnerTypeName + Self, analyses via AnalyseMethodDecl, and
records a TGenericMethodInstance (deduplicated per owner+args). The
call site resolves obj.M<T>(...) to the instance.
- Codegen (both backends): GenericMethodInstances are emitted as ordinary
methods (Self param + mangled ResolvedQbeName).
Verified pick/echo, two distinct instantiations (Integer + string), use
of a Self field, and two type parameters on both backends. Adds IR tests
(TGenericFuncTests.TestCodegen_GenericMethod_{Body,Call}Emitted) and
dual-backend e2e tests (TE2EGenericsTests.TestRun_GenericMethod_*).
Grammar and language-rationale updated.
Limitation (logged in bugs.txt): only the inline-body declaration form is
supported; the out-of-line function TOwner.Method<T>(...) form is not
yet parsed.
Adds the `default` directive on an indexed property, enabling subscript
sugar on the object itself:
property Items[I: Integer]: T read Get write Put; default;
...
V[0] := 10; // lowers to V.Put(0, 10)
WriteLn(V[0]); // lowers to V.Get(0)
This is the mechanism behind the familiar List[i] syntax and was a real
foundational gap (the directive did not even parse — "Expected ':'").
Implementation:
- Parser: accept the trailing `default;` directive after a property
declaration; set TPropertyDecl.IsDefault.
- AST / symbol table: IsDefault on TPropertyDecl and TPropertyInfo;
TRecordTypeDesc.FindDefaultProperty walks the inheritance chain.
- Semantic: Obj[I] read (AnalyseStringSubscriptExpr) synthesises the
default property's field access and reuses the indexed-property read
path; Obj[I] := V write (AnalyseStaticSubscriptAssign) records the
setter on the TStaticSubscriptAssign node.
- Codegen (both backends): the write path emits the setter call via the
existing PropAccessorTarget / EmitPropAccessorCallNative helpers, so it
honours virtual/override on the accessor; the read path delegates the
TStringSubscriptExpr to its folded property-read field access.
- Cross-unit: IsDefault is serialised in the .bif interface so a default
property declared in one unit keeps its subscript sugar elsewhere.
Verified read, write, string-element, inherited, and cross-unit cases on
both backends. Adds IR tests (TPropertyTests.TestCodegen_DefaultProperty_
{Read,Write}) and dual-backend e2e tests (TE2EPropertyTests.TestRun_
DefaultProperty_{ReadWrite,StringElement,Inherited}). Grammar and
language-rationale updated; the two new TStaticSubscriptAssign fields are
marked safe in bif-coverage.status.
A non-generic class could not inherit from a generic-class instance:
TBox<T> = class ... end;
TIntBox = class(TBox<Integer>) ... end; // rejected, then bad IR
Two bugs:
1. Semantic: AnalyseTypeDecls assumed any generic name in the first
heritage slot was an interface — it cast FindTypeOrInstantiate's
result to TInterfaceTypeDesc and moved it to the implements list, so a
generic CLASS parent was rejected as "Unknown interface 'TBox<Integer>'
in implements list". It now classifies the instantiated descriptor: a
tyInterface result is an implements entry, anything else stays as the
parent class for normal class-parent resolution.
2. QBE codegen: the inheriting class's typeinfo referenced its parent as
$typeinfo_TBox<Integer> via ClassSymName (which does not mangle <>),
but the generic instance's typeinfo is defined under the QBEMangle'd
symbol $typeinfo_TBox_Integer — producing invalid QBE IR ("invalid
character <"). The parent reference now uses QBEMangle for a generic
instance, matching its definition. The native backend already mangled
correctly and only needed the semantic fix.
Verified inherited method + field access and a virtual method declared on
the generic base and overridden in the derived class, on both backends.
Adds TE2EGenericsTests.TestRun_InheritFromGenericInstance_{MethodAndField,
VirtualOverride} and a note in docs/language-rationale.adoc.
A derived class that declared an `overload` method with the same name as
an overload inherited from its base SHADOWED the inherited variants
instead of merging with them:
TBase = class
function F(x: Integer): string; overload;
end;
TDerived = class(TBase)
function F(s: string): string; overload;
end;
d.F('a'); // ok
d.F(5); // was rejected: "expected string but got Integer"
Two causes, both fixed:
1. ResolveMethodOverload stopped walking the inheritance chain as soon
as the first class declaring the name yielded any arity match, so the
base overloads were never considered. It now unions the overload
groups up the parent chain, stopping only when a level declares the
name WITHOUT `overload` — a non-overload method hides inherited ones
(the conventional redeclaration-hides rule), an `overload` method
extends the set (Delphi semantics).
2. The variable-receiver method-call path used FindMethodDecl (first
match in the chain) and never invoked overload resolution at all. It
now analyses the arguments and calls ResolveMethodOverload, falling
back to FindMethodDecl for the no-overload case. (Arguments must be
analysed before resolution so candidates can be scored against their
resolved types.)
Adds TE2EInheritTests.TestRun_OverloadMergeAcrossInheritance (dual
backend) and an "Overload sets merge across inheritance" subsection to
docs/language-rationale.adoc.
A chained subscript WRITE on a dynamic array of dynamic arrays —
m[i][j] := v — was rejected at semantic time with "Multi-dimensional
subscript base must be a static array". Only a static-array base was
accepted in the chained-assign path; reading via an intermediate row
variable (row := m[i]; row[j] := v) worked, but the direct write did
not, making jagged 2-D+ dynamic arrays awkward to fill.
Semantic: AnalyseStaticSubscriptAssign now accepts a tyDynArray base in
the BaseExpr (chained) path and derives the element type from
TDynArrayTypeDesc.
Codegen (both backends): when the chained-write base resolves to a
dynamic array, evaluate BaseExpr (m[i]) to the inner array's data
pointer and index into it — heap indirection per level — instead of the
inline-offset static-array path. QBE adds a BaseExpr branch in the
dynarray block of EmitStaticSubscriptAssign. Native stashes the base
pointer below the spilled value on the stack and reloads it at the
base-resolve step, with matching cleanup on every Exit path (float,
record, string/class, and scalar).
Verified 2-D int, 2-D string (with overwrite), and 3-D int nesting on
both backends. Adds three dual-backend e2e tests in
cp.test.e2e.dynarray.pas and a "Dynamic-array nesting (jagged arrays)"
subsection to docs/language-rationale.adoc.
TList<TList<Integer>> and TBox<TPair<Integer, string>> failed to parse:
each type argument was read as a single bare identifier, with no recursion
into a nested <...>, so a nested '<' produced "Expected '>' but got '<'"
(type position) or "Expected '.' or '(' after generic type arguments"
(constructor/expression position).
Both type-argument parse sites now recurse through ParseTypeName, so a type
argument may itself be a generic specialisation, to arbitrary depth
(TList<TList<TList<Integer>>> works). The expression-position heuristic also
accepts tkLessThan as the lookahead-2 token (the first arg being itself
generic). The comparison-operator disambiguation (a < b, (a < b) and (b < c))
is unaffected.
Found by the e2e generics hardening sweep. Regression:
TE2EMiscTests.TestRun_NestedGenericTypeArgs (TBox<TBox<Integer>>, both
backends). docs/grammar.ebnf TypeArgList rule updated to recurse via
GenericName.
All three fixpoints + full suite (3224 tests) pass.
A statement beginning with '(' was rejected ("Expected statement"), so a
parenthesised cast could not be an assignment TARGET:
(a as TB).FX := 42; -> Parse error
Reading the same expression worked, and the hard-cast target TB(a).FX := 42
worked, so only this statement-parser entry point was missing.
ParseStmt now handles a leading '(' by parsing the parenthesised expression
and requiring a '.Field := Expr' suffix, building a TFieldAssignment whose
receiver is that expression (ObjExpr) — the same AST + semantic + codegen path
already used for element-field writes (a[i].F := v). No AST/semantic/codegen
change was needed; the gap was purely the parser.
Found by the e2e test-hardening sweep of the inheritance cluster. Regression:
TE2EMiscTests.TestRun_ParenCastAsAssignmentTarget, run on BOTH backends.
docs/grammar.ebnf FieldAssignment rule updated.
All three fixpoints + full suite (3223 tests) pass.
`inherited` previously parsed only as a statement, so calling an inherited
FUNCTION and using its result failed to parse:
Result := inherited Value() + 100; -> "Parse error: Expected expression"
This blocked the normal OOP pattern of an overriding function extending its
parent's result.
Adds the expression form alongside the existing statement form:
* uAST: new TInheritedCallExpr (sibling of TInheritedCallStmt) + CloneExpr case.
* uParser: tkInherited handled in ParseFactor (primary expression).
* uSemantic: AnalyseInheritedCallExpr resolves to the parent method (must be a
non-void function) and sets ResolvedType to the return type.
* Codegen: static (non-virtual) call to the parent slot, result returned as a
value. QBE EmitInheritedCallExpr; native EmitInheritedCallSeq (shared by the
statement and expression forms) leaves the result in %rax/%xmm0.
* uUnitInterfaceIO: 'inhc' encode/decode so inherited-expr in an inline method
body round-trips through the .bif unit-interface cache; bif-coverage.status
marks the two fields serialise (bif-coverage OK).
* docs/grammar.ebnf + docs/language-rationale.adoc updated (same commit).
Found by the e2e test-hardening sweep of the inheritance feature cluster.
Regression: TE2EMiscTests.TestRun_InheritedFunctionCall_InExpression covers a
value-returning inherited call and an inherited call with an argument, run on
BOTH backends via AssertRunsOnAll.
All three fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK) and
the full suite (3222 tests) pass.
A set literal element may now be a constant range:
e := [Red..Blue]; { {Red, Green, Blue} }
e := [m0, m2..m4, m7]; { ranges mix with single members }
The parser builds a transient TSetRangeExpr for each lo..hi element; the
semantic pass (AnalyseSetLiteralExpr) expands it into the individual member
idents before any other consumer runs, so overload resolution, the bitmask
folder, the jumbo-set path, and both code generators see an ordinary member
list and need no range-specific logic.
Both bounds must be compile-time constants of the set's base type. A
reversed constant range [Blue..Red] is a compile-time error rather than a
silently empty set — matching Blaise's preference for rejecting
confusing-but-legal constructs over FPC's empty-set-with-warning behaviour.
Variable bounds [lo..hi] are rejected ("set range bound must be a
constant"); runtime-variable ranges are deferred.
TSetRangeExpr is wired into the .bif serialiser and the AST cloner so a
generic template body containing an unexpanded range round-trips through
separate compilation (verified by hand: a generic returning [m1..m3]
compiled to .o, then instantiated from the .bif with source hidden).
Set base types remain enumerations; the issue's set-of-byte example needs
ordinal-base set types, tracked separately in docs/future-improvements.adoc.
Range expansion is base-type-agnostic, so ranges will work for those
automatically once they land.
Docs: grammar.ebnf SetElement rule; language-rationale.adoc decision +
alternatives. bif-coverage.status regenerated for the new node.
FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3201 tests, 11 new).
A variable declaration sharing its identifier with any visible type name
compiled without complaint — e.g. an Iface interface and an iface variable
in the same program (Pascal is case-insensitive, so these are one
identifier). The variable silently shadows the type, which is confusing
and almost always a mistake.
Blaise already rejected var-vs-var, var-vs-const and type-vs-type clashes;
var-vs-type slipped through because type decls are registered in the
block's enclosing scope (so they outlive the block scope) while var decls
are registered one scope deeper, hiding the type from FTable.Define's
duplicate check. AnalyseVarDecls now looks up the name in the type
namespace (FTable.FindType, which honours uses-chain visibility) and
rejects any match.
The rule is stricter than FPC mode objfpc, which permits shadowing a
built-in or outer-scope type (var Integer: Int64 compiles in FPC). Blaise
rejects the whole class — same-block, outer-scope, imported, or built-in —
to eliminate the confusion rather than carry FPC's footgun. Recorded in
docs/language-rationale.adoc with the alternatives considered.
FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3190 tests, 5 new).
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.
Representation (uSymbolTable): TSetTypeDesc.IsJumbo (BitCount > 64) and
RawByteSize; RawSize/ByteSize/AllocAlign sized accordingly. TSymbol gains
ConstSetBytes/ConstSetQbe for jumbo constants (can't fit an Int64 mask).
RTL: new runtime/src/main/pascal/blaise_set.pas — _SetIn/_SetInclude/
_SetExclude/_SetUnion/_SetInter/_SetDiff/_SetEqual/_SetCopy over byte-array
bitmaps (overlap-safe). Wired into runtime/Makefile.
Semantic: the four >64 caps become >256. AnalyseSetConstDecl folds a jumbo
const to a byte array. The anonymous set for 'X in [a,b,c]' is sized to the
largest listed ordinal (when constant), not the full enum — keeping the
common low-ordinal membership test (incl. the compiler's own TokenKind
tests) on the fast register path. This also fixes a latent miscompile: the
old fixed-l representation silently dropped any listed ordinal >= 64.
Codegen (both backends): jumbo branches for literal, in, +/-/*, =/<>,
Include/Exclude, for-in, assignment, params (pmJumboSetValue ABI), and sret
returns. The register 'in' gained a range guard for literal-sized sets.
Native reserves two 32-byte scratch slots per frame (and .bss in main) for
set-op/literal result buffers. QBE adds IsAggregateAddrType so jumbo sets
ride the record/static-array address paths and are never promoted.
OPDF: no format change — recSet SizeInBytes (1 byte) already covers <=32.
Verified: FIXPOINT_OK and NATIVE_FIXPOINT_OK; full suite (3172 tests) green
built by the stage-2 binary, the QBE fixpoint binary, and the native
fixpoint binary; bif-coverage OK. Tests: cp.test.jumboset (12 IR) and
cp.test.e2e.jumboset (6 e2e, both backends via AssertRunsOnAll); the
>256-member rejection test in cp.test.sets updated. Docs: grammar.ebnf and
language-rationale set-type sections updated for the 256 cap and literal
sizing.
Closes the design discussion behind #81.
A const declaration may now take a compile-time integer expression on the
right-hand side, not just a single literal:
const A = 2 * 3; // 6
B = 2 + 3 * 4; // 14 (precedence)
C = (2 + 3) * 4; // 20 (parentheses)
D = 100 div 7; // 14
E = Base * 2 + 1; // references a prior const
Previously only single literals and a flat, single-precedence bit-op chain
(a or b or c) were accepted; 2 * 3 failed with "Expected ';' but got '*'"
and (2 * 3) with "Expected numeric or string constant".
Parser: when the const RHS starts an integer expression (a leading '(', an
integer literal or leading-minus literal followed by a binary operator, or an
ident followed by a numeric operator), parse it with the existing
full-precedence ParseExpr into a normal expression AST stored on
TConstDecl.IntValueExpr. A bare literal keeps the fast IntVal path; '+' on an
ident is left to the string-concat path for backward compatibility.
Semantic: EvalConstIntExpr recursively folds the AST to an Int64, resolving
named-constant references against the symbol table. Folding in the semantic
pass (after all consts are registered) is what makes forward references work
regardless of declaration order. Wired into both the const-block and the
initialised-variable (var G = expr) fold sites. Operator precedence and
grouping come from the AST shape, superseding the precedence-unaware bit-op
token chain.
Scope: folds to integer. Float constant arithmetic and using a const as a
static-array bound remain separate, pre-existing limitations (noted in the
rationale).
Tests: eight IR/fold cases in cp.test.constants (multiply, parens, precedence,
div/mod, named-ref, unary minus, mixed arith+bitwise); one e2e case on both
backends in cp.test.e2e.misc. Docs: grammar.ebnf gains the ConstIntExpr rules;
language-rationale gains a Constant Expressions section.
Range-indexed array constants now support multiple dimensions in both the
comma form (array[0..1, 0..2] of Integer) and the equivalent nested form
(array[0..1] of array[0..1] of Integer), with nested initialiser groups
((1,2,3),(4,5,6)). Previously the const-declaration parser had its own
single-dimension array-type and value-list parsers, so any multi-dim const
failed at the type (Expected ']' but got ',') or the nested value group.
Parser: ParseConstArrayType walks one-or-more 'array[...] of' headers,
reading comma-separated ranges per header and recursing on a nested 'array';
each dimension's bounds go to CD.ArrayDimLows/ArrayDimHighs. The value
parser is now recursive (ParseConstArrayGroup/ParseConstArrayScalar),
flattening nested groups into ArrayElements in row-major order. Dim-0 bounds
mirror onto the legacy ArrayLowBound/ArrayHighBound so the single-dim path
is unchanged.
Semantic: BuildConstArrayType builds the nested static-array type
innermost-first and validates the flat element count equals the product of
dimension extents. Shared by the program/unit and class-const sites.
Codegen: the QBE emitter already lays a flat row-major blob, so it needs no
change for integer/string elements. The native emitter now drills through
nested static-array types to the innermost scalar to pick the element
directive.
Tests: parse/semantic/IR in cp.test.constants; e2e (comma, nested, 3-D) on
both backends via AssertRunsOnAll in cp.test.e2e.staticarray. Docs:
grammar.ebnf gains ConstArrayType/ConstArrayValue rules; language-rationale
updated (the stale 'enum index only' constraint corrected to document
range and multi-dimensional const arrays).
A parameter declared 'array of const' accepts a single call-site bracket list
of mixed-type values, boxed into an array of the intrinsic record TVarRec and
passed via the existing open-array ABI:
procedure Log(args: array of const);
...
Log([42, 'hi', 3.5, True]);
This is the one loosely-typed-passing mechanism Blaise adopts (see the
rationale section); untyped params, varargs, and Variant remain omitted.
- RTL/builtins: TVarRec is registered as a compiler-intrinsic record
{ VType: Byte; VValue: Pointer } (16-byte layout, mirroring TMethod), with
vt* discriminant constants - all available with no uses clause, matching
Delphi's auto-available System.TVarRec. Blaise has no record variant parts,
so the callee reads each element by reinterpret-casting the single VValue
slot (Integer(v.VValue), string(PChar(v.VValue)), PDouble(v.VValue)^, ...).
- Parser: 'array of const' parses as an open array whose element is TVarRec.
- Semantic: a heterogeneous bracket literal is typed 'array of TVarRec' rather
than rejected; overload resolution binds it (and homogeneous / empty
literals) to an array-of-const formal; retyping runs for proc, func, and
method calls.
- Codegen (both backends): EmitConstArrayLiteral builds one 16-byte TVarRec
per element, tagging by inferred type. Borrow semantics (FPC) - strings and
objects are stored without AddRef. Doubles are heap-boxed via _BlaiseGetMem
(vtExtended holds a PDouble) since a double does not fit the pointer slot.
- Native: also fixes a pre-existing gap - reading a float through a pointer
deref (PDouble^) in EmitExprToXmm0 - needed for vtExtended read-back.
Tests: cp.test.arrayofconst (parser/semantic/IR) and cp.test.e2e.arrayofconst
(compile + run on both backends, including value read-back, empty/homogeneous
lists, and string-variable borrow). Grammar and language rationale documented.
Record Blaise's stance on the four conflated "pass a loosely-typed value"
features: untyped const/var parameters, array of const, varargs, and Variant.
Blaise adopts only array of const - the one mechanism overloads and generics
cannot replace (arbitrary-length, mixed-type argument lists, as used by
Format-style APIs); it is a compiler-bounded, per-element-tagged call construct
rather than a free-floating dynamic value.
Untyped parameters are omitted on footgun grounds: they implicitly take an
address and erase the type, whereas a typed pointer (PByte / ^T) does the same
job explicitly and keeps type information for indexing/field access. varargs is
omitted as C-interop-only and untagged; Variant is omitted in favour of
overloads, generics, and explicit tagged unions that keep checking at compile
time.
A set type may now be written inline anywhere a type is expected (var,
parameter, record field, ...), not only in a named type declaration. The
element type may be a named enumeration or an anonymous enumeration written
in place:
var
Days: set of TWeekday; // named enum element
Flags: set of (fA, fB, fC); // anonymous enum element
Implementation resolves an inline set entirely from its canonical type-name
string, with no parser-side state:
- Parser: ParseTypeName gains a 'set of' branch. A named element yields
'set of <Name>'; an anonymous element is encoded verbatim as
'set of (a,b,c)' (ParseAnonEnumName), self-contained in the type string.
- Semantic: FindTypeOrInstantiate recognises the 'set of ' prefix and builds
the TSetTypeDesc on demand (as it already does for 'array of'/'^'/'class
of'). For the anonymous form, SynthAnonEnum synthesises an enum type from
the encoded member list - registering each member as an enum constant - and
reuses an identical inline enum if its members are already defined. Set
types compare structurally, so the synthetic enum name need only be unique.
Encoding the enum in the type string rather than carrying parser state makes
the feature work uniformly in every type position with no declaration-ordering
or ownership concerns.
Tests: cp.test.inlineset (parser/semantic) and cp.test.e2e.inlineset (compile
+ run on both backends). Grammar and language rationale documented.
A global variable declaration may now carry an initialiser whose value is
folded at compile time and emitted into the data section, so the variable
holds its initial value before the program body runs (matches FPC/Delphi):
var
G: Integer = 42;
S: string = 'hello';
A: array[0..2] of Integer = (10, 20, 30);
Implementation reuses the typed-constant pipeline end to end:
- Parser: the const value scanner is factored into ParseConstValue, shared by
const declarations and the new var-initialiser path. TVarDecl carries an
owned InitConst: TConstDecl (TConstDecl moved ahead of TVarDecl so no
forward declaration is needed).
- Semantic: AnalyseVarInitializer folds the value and type-checks it against
the declared type; array initialisers derive element type and bounds from
the resolved static-array type and mint a data label.
- Codegen: EmitGlobalVarInit (QBE) and EmitGlobalInitData (native) emit the
folded value as a typed data slot - a single field for scalars/strings (a
string global points at an immortal static header __sN + 12) and an inline
element list for arrays. Covers program- and unit-level globals on both
backends.
Restrictions, each rejected with a clear diagnostic: global scope only (no
local initialisers), a single name per initialised declaration, and arrays
only for aggregates (record and inline-set initialisers are deferred - no
record-constant machinery yet, and a set initialiser would clash with the
const symbol the set folder defines).
Tests: cp.test.varinit (IR + parser/semantic) and cp.test.e2e.varinit
(compile+run on both backends). Grammar and language rationale documented.
Add multi-dimensional static array syntax, both the comma form
(array[0..1, 0..2] of Integer; A[i, j]) and the equivalent nested/chained
form (array[0..1] of array[0..2] of Integer; A[i][j]). The comma forms are
syntactic sugar: the parser desugars array[a, b] of T into the nested
array[a] of array[b] of T, and A[i, j] into chained subscripts A[i][j], so
the two notations are fully interchangeable in every position.
Layers:
- uParser: comma loops in ParseTypeName and subscript reads; the statement
LHS now lowers A[i, j] := v and A[i][j] := v to a TStaticSubscriptAssign
carrying a new BaseExpr (the inner-array address expression). The previous
"chained base not yet supported" rejection is removed.
- uAST: TStaticSubscriptAssign.BaseExpr (owned); wired into CloneStmt and the
.bif encoder/decoder (uUnitInterfaceIO); bif-coverage status updated.
- uSemantic: BaseExpr branch resolves the inner static-array type and checks
the index and value element type.
- Codegen (both backends): a nested static-array element now evaluates to its
inline address (mirroring record/interface elements) so a further subscript
indexes into it; the static-subscript store reads its base from BaseExpr
when set. Nested arrays are a flat row-major contiguous block.
- OPDF: no new record needed - each dimension emits one recArray whose element
points at the next inner recArray; pdr follows the chain and renders the
value as a true multi-dimensional structure.
Tests: IR unit tests (cp.test.staticarray), e2e tests on both backends via
AssertRunsOnAll (cp.test.e2e.staticarray), and a nested-recArray OPDF test
(cp.test.opdf). Grammar and language rationale documented.
Blaise now guarantees zero-initialisation of every variable as a language
semantic — local variables, globals, record fields, static-array elements,
threadvars, and Result. The QBE backend already satisfied this; the native
x86-64 backend was the only gap (scalar locals were uninitialised on stack).
Native backend changes (blaise.codegen.native.x86_64.pas):
- Replaced the ARC-only zero-init loop in EmitFunctionDef with an
exhaustive case over all TTypeKind values, covering every scalar
type (integer family, float, boolean, pointer, enum, set, procedural)
in addition to the already-handled managed types. An else-raise clause
ensures any future new type kind is caught at compile time rather than
silently skipped.
- Fixed AddSlot to allocate 16 bytes for method-pointer locals (Code +
Data slots), matching the 16-byte allocation the QBE backend already
used. Previously only 8 bytes were reserved, which would corrupt an
adjacent frame slot if a local method-pointer was written.
10 new E2E tests run on both backends via AssertRunsOnAll, using a
Dirty() helper that pre-fills the stack with 0xDEADBEEF to prove zero-init
comes from the prologue and not from lucky stack layout:
TestRun_ZeroInit_ScalarIntegers, FloatLocals, BooleanAndChar,
PointerLocals, EnumLocal, SetLocal (QBE-only — native crashes on sets,
pre-existing bug), RecordWithMixedFields, StaticArray, ThreadVar,
GlobalVars.
Documented in docs/language-rationale.adoc (decision, alternatives
rejected, implementation notes, future noinit/definite-assignment roadmap).
- TAsmEncodingTests: byte-level regression tests against AssembleToBytes
(bare (%reg) operands, .quad symbol relocations, TLS prefix ordering,
PC32 addend with trailing immediates, branch relocations,
.note.GNU-stack presence, REX.X for extended index registers, imm16
width, unknown-directive/duplicate-label errors, line-numbered
diagnostics).
- TElfWriterTests: ELF header fields, section append/align/offset
tracking, symbol definition and lookup, BSS reservation.
- TInternalAsmE2ETests: compile->link->run through the compiler CLI
with --backend native --assembler internal, including class virtual
dispatch (vtable .quad relocations) and float arithmetic (SSE
spills). A compile failure now Fails the test rather than being
masked as a missing toolchain.
- docs/internal-linker-design.adoc: design for the next
toolchain-independence phase — an internal ELF linker built into the
blaise binary (PIE output, eager binding, .rela.dyn/R_X86_64_RELATIVE,
TLS, CRT discovery, FreeBSD/macOS outlook, OPDF pass-through).
The program/unit's own name and the names of directly used units can
no longer be redeclared by top-level declarations, matching FPC and
Delphi (Duplicate identifier / E2004). Previously the module name was
stored on the AST node and never entered the symbol table, so
'program P; var P: Integer;' compiled by accident of omission.
The semantic pass plants skModule marker symbols in the scopes that
receive top-level declarations (global + program-block scope for
programs, unit scope for units); the ordinary duplicate check on
TSymbolTable.Define then rejects redeclarations. Lookup treats a
marker hit as unresolvable, so the reserved name is not a value and
inner scopes can still shadow it, as in FPC. The const paths get an
explicit marker check because their Define-failure branch otherwise
tolerates the clash silently (cross-unit const shadowing).
One deliberate divergence: FPC accepts a procedure named after the
program (an accident of its overload machinery); Blaise rejects all
declaration forms uniformly.
Grammar is unchanged — this is a name-resolution rule, not syntax —
so grammar.ebnf is untouched; the decision is recorded in
language-rationale.adoc. 44 test programs named 'program P' that also
declared an identifier P are renamed to 'program Prg'.
Closes#84
pdr now resolves the ASLR slide correctly (load base from the
binary's offset-0 mapping), so the -no-pie guard in both native link
paths is no longer needed. Debug binaries are position-independent
again, matching the platform default.
Verified under live ASLR: breakpoints, var-param drilldown, captured
vars, dynamic arrays, TList<T> inspection, callstack and stepping all
work against PIE binaries.
Remove the 'PIE (ASLR) support in PDR' section from
future-improvements.adoc — implemented.
Resolves the TStringList.Objects / TList<T> retention question from the
bug backlog as an explicit design decision (language-rationale,
'Collection Ownership'):
- TList<T>/TStack<T>/TQueue<T> managed elements ARE retained on store —
this already works (generic stores lower to ARC-aware pointer writes);
TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope now
pins it on both backends (object survives its creating scope).
- TStringList.Objects[] stays NON-OWNING by design: the API type is
Pointer and the integer-cast idiom (TObject(PtrUInt(N)), used in 27
places in the compiler itself) makes blind retention impossible.
Convention documented at the declaration and in the rationale.
- Known limitation recorded: generic Clear/Destroy do not yet release
remaining managed elements (needs a Default(T)-style zero-store);
tracked in the leak backlog.
Suite: 2946 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Interfaces may declare properties (FPC/Delphi parity):
IValued = interface
function GetValue(): Integer;
procedure SetValue(AValue: Integer);
property Value: Integer read GetValue write SetValue;
end;
Accessors must be methods of the interface or an inherited parent
(interfaces have no fields), validated at registration. I.Value reads
lower to the existing zero-arg getter itab dispatch; I.Value := X
lowers to the setter dispatch with X as the single argument — pure
compile-time sugar, no itab slots, no layout change. Child interfaces
see inherited properties via the parent chain. Wired end to end:
parser, TInterfaceTypeDef.Properties (AST + clone), TInterfaceTypeDesc
property registry, semantic read/write resolution, both backends, .bif
serialisation and import registration. v1 limits (recorded in
language-rationale): plain interface-typed receivers; no indexed/
default array properties.
Two pre-existing linking bugs surfaced by the dogfood program and are
fixed alongside:
- Property accessor names written in a different case than the method
declaration (read getValue for GetValue) produced unresolved symbols —
accessor names are now normalised to the declared casing at
registration (classes and interfaces, compile and import paths).
- A program-level class implementing an interface failed to link
whenever the program had a uses clause: program-scope methods carry
bare symbol names (uSemantic.CurrentUnitPrefix) but itabs and
property-setter call sites prefixed them with the program name via
Sym.OwningUnit. ClassUnitPrefix (QBE) / ClassSymName (native) now
skip the prefix for program-owned classes (new FProgramName field).
Tests: 7 in cp.test.interfaces (parse, registration, accessor
validation, read-only enforcement, inheritance, IR dispatch), bif
round-trip in cp.test.unitinterface, 2 e2e suites on both backends in
cp.test.e2e.classes2 (interface read/write incl. compound assignment
and inherited dispatch; case-mismatch + uses regression). Suite: 2940
OK on working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Sin(12) and Tanh(I) now compile: the float compiler builtins (Sqrt,
Ceil/Floor/Round/Trunc, Ln/Log2/Log10, Power, the trig family, IsNaN/
IsInfinite) accept integer-family arguments and widen them to Double —
matching FPC/Delphi and Blaise's own implicit int→float assignment
rule. The trig builtins return Double for integer arguments (still
Single→Single / Double→Double for float arguments). Power previously
type-checked nothing and emitted invalid IR for integer (and Single)
arguments; it now validates and coerces both to double.
Double(I) / Single(I) typecasts were lowered as bit copies: QBE emitted
an integer temp into 'stored' (rejected by qbe), native stored an
integer register — so the Tanh(Single(I)) workaround failed too. Float
casts now emit real conversions on both backends (swtof/sltof/ultof/
uwtof + exts/truncd on QBE; cvtsi2sd/ss + cvtss2sd/cvtsd2ss natively),
including float→float width changes.
Fixing this exposed two pre-existing native float-width bugs: a Single
RHS assigned to a Double variable was stored without cvtss2sd, and
mixed-width binary operands (s * 1000 — integer literals are emitted as
.double constants) ran the Single binary path at the wrong width.
Added EmitXmm0WidthAdjust and applied it at assignment and to both
binary operands.
Decision recorded in docs/language-rationale.adoc (Implicit
Integer→Float Widening). Tests: 8 IR/semantic tests in cp.test.math
(the two RejectInteger tests inverted to acceptance) + 2 e2e tests on
both backends in cp.test.e2e.math. Suite: 2930 OK on working and
fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Array-typed FIELDS were second-class citizens for element access; this
lands the full variation family on both backends:
- Semantic: r.A[i] := v dropped the subscript from the LHS type — the
parser stores it in TFieldAssignment.PropIndexExpr (the indexed-
property slot) and semantic ignored it for real fields, demanding the
whole array type on the RHS. A subscript on a real dyn/static-array
field is now an ELEMENT write (new semantic-set IsElemWrite flag);
both backends emit the element store with the standard ARC and
record-copy rules.
- Parser: c.N.A[i] := v failed with "Expected 'end' but got '['" —
the chained L-value walker now accepts a terminating Field[idx] :=,
and the subscript-chain path accepts arr[i].A[j] := v (subscript
directly over another subscript stays a clear parse error).
- Bare implicit-Self: A[i] := v inside a method raised "Undeclared
variable 'A'" — TStaticSubscriptAssign now resolves array-typed
fields of Self (IsImplicitSelf + ImplicitFieldInfo).
- SetLength(r.A, n): QBE refused ("first argument must be a
variable"); native silently emitted NO code for field receivers and
mis-stored through var-param receivers. QBE routes through
EmitLValueAddr; native gains EmitLValueSlotAddr covering field,
var-param and implicit-Self receivers for dyn-array and string
SetLength.
- Read side: c.A[i] through a class variable computed the element base
as if c were an inline record (missing object-pointer load) and
segfaulted; implicit-Self bases had the same gap; native missed
chained reads (c.N.A[i]) entirely. All base shapes are handled in
the IsArrayAccess read paths of both backends now.
bif-coverage.status regenerated for the new semantic-set AST fields
(safe). Tests: 4 IR tests (cp.test.dynarray) + 6 e2e tests on both
backends (cp.test.e2e.records) covering record/class/implicit-Self
receivers, nested chains, static-array fields and string-element ARC.
Suite: 2922 OK on working and fixpoint binaries; FIXPOINT_OK (single
round); NATIVE_FIXPOINT_OK.
Dynamic arrays of records were broken in three interlocking ways:
- Parser: a[i].Field := v (and a[i].Method, chained a[i].F.G := v) on
the statement LHS raised "Expected ':=' but got '.'" — the subscript
statement branch only accepted ':=' directly after ']'. It now
builds a subscript-rooted postfix chain ending in a TFieldAssignment
(via ObjExpr) or TMethodCallStmt (via ObjExpr). grammar.ebnf gains
SubscriptFieldAssign / SubscriptMethodCall rules.
- Both backends: a[i] := r stored the ADDRESS of r into the element
instead of copying the record, and element reads loaded the first
8 bytes of the element as if it were a pointer. The two bugs masked
each other (elements aliased r — the TElfSection workaround comment
in uElfObject.pas documents the symptom). Record-element subscript
reads now yield the element address (dyn/open/static arrays) and
writes do an ARC-aware fieldwise copy: EmitRecordCopy on QBE,
retain-src/release-dest/memcpy on native. Native static arrays of
records had the same read/write bug and are fixed too.
- QBE backend: Exit inside the SECOND (or later) try block of a
function skipped _PopExcFrame, leaving a stale g_exc_top that
corrupted later raises/pops. EmitTryFinallyStmt/EmitTryExceptStmt
emitted normal and exception paths sequentially but decremented the
codegen-time FExcDepth on both (net -1 per try statement). Ported
the native backend's rebalancing (restore depth before emitting the
exception path). This is the likely root cause of the historical
'avoid bare Exit inside try' convention.
Tests: 3 IR tests (cp.test.dynarray), 4 e2e tests on both backends
(cp.test.e2e.records), IR pop-count + e2e regression for the exc-frame
bug (cp.test.exceptions, cp.test.e2e.exceptions). Suite: 2912 OK on
working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Records how the landed implementation differs from the plan: a single
unified hoist region (EmitArgHoist) for open-array literals,
record-call sret buffers and string pin slots, and shape-based
protection at unknown-signature sites instead of a TInterfaceTypeDesc
const-flag extension.
Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).
uCodeGen.pas → blaise.codegen.pas
uCodeGenQBE.pas → blaise.codegen.qbe.pas
Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
Document the for-in string iteration dual-mode semantics: Byte loop
variable iterates raw UTF-8 bytes, Integer iterates codepoints via
_Utf8DecodeAt. Remove "deferred to future Runes(S) iterator" notes
since CodePointAt and for-in Integer are now implemented.
Sets with 33–64 enum members now use 8-byte (QBE 'l' / x86-64 64-bit)
storage instead of silently truncating to 32 bits. Both QBE and native
x86-64 backends emit correct instructions for all set operations:
literals, in, Include/Exclude, union/difference/intersection, equality,
and for-in iteration. Enumerations with more than 64 members in a
set-of declaration are rejected with a clear semantic error.
Also fixes tkThreadvar missing from CheckUnitNamePart, which broke
parsing of unit names containing 'threadvar' in self-hosted builds.
Make parentheses mandatory on every function, procedure, method, and
constructor call — even those with zero arguments. A bare identifier
or field access is now unambiguously a variable/field/property read;
appending () makes it a call.
Mechanically migrated all 144 source files (compiler, runtime, stdlib,
tests, kanban tool). Fixed several latent bugs exposed by the AST node
transition from TFieldAccessExpr.IsMethodCall to TMethodCallExpr:
- IsBuiltinToString applied to record methods (added tyClass guard)
- IsVarParam not set for value record/static-array parameters in
AnalyseMethodCallExpr (extended to check skParameter + aggregate type)
- Native backend used movq (pointer load) for record receivers instead
of leaq (address-of) in EmitMethodCallExpr
- ResolveDiamond now handles TMethodCallExpr for diamond-operator
constructor calls
Updated grammar.ebnf (MethodCall, ProcCall, Factor rules) and
language-rationale.adoc with the design decision. Marked the
future-improvements.adoc entry as implemented.
All 2627 tests pass. Fixpoint verified (FIXPOINT_OK).
TThread.Create(False) appeared to silently skip execution (GitHub #73).
Root cause: Start took an extra _ClassAddRef so the trampoline could
release it on exit. This kept the refcount at 1 after Free, preventing
Destroy (and its WaitFor/pthread_join) from running — the main program
exited before the thread finished.
Fix: remove the trampoline's ARC reference entirely. The caller's
reference is the only one; releasing it triggers Destroy → WaitFor →
pthread_join, which blocks until the thread has fully exited. This is
safe because pthread_join guarantees the trampoline has returned before
Destroy frees the object.
Also remove FreeOnTerminate — ARC makes it redundant. In Delphi/FPC it
exists because manual Free is the only cleanup path; in Blaise, scope
exit or reassignment automatically joins and frees the thread. The
migration analyser can flag this for ported code.
Document the TThread ARC lifetime model in language-rationale.adoc and
add class/method documentation comments to classes.pas.
Add the `threadvar` keyword for declaring thread-local storage variables.
Each thread gets its own zero-initialised copy. Only allowed at program
or unit scope.
Lexer/Parser: new tkThreadVar token; ParseVarBlock accepts threadvar.
AST: IsThreadVar field on TVarDecl, TAssignment, TIdentExpr.
Symbol table: IsThreadVar on TSymbol.
Semantic: propagates IsThreadVar through analysis; rejects local scope.
QBE backend: emits `export thread data` declarations and `thread $Name`
references for correct TLS access (local-exec model via %fs:@tpoff).
Native backend: emits .tbss section and %fs:Name@tpoff addressing.
Unit interface: serialises IsThreadVar through .bif files.
Tests: 8 unit tests (parser, semantic, IR emission) + 2 E2E tests
(compile and run programs using threadvars).
The `not` operator previously only accepted Boolean operands. It now
accepts all integer types (Byte, SmallInt, Word, Integer, Int64, UInt64)
and performs bitwise complement, enabling bitmask patterns like
`Flags := Flags and (not MASK)`.
Semantic pass promotes narrow types to Integer, preserves Int64/UInt64.
QBE backend emits `xor -1` (w or l suffix by width). Native backend
emits `notl`/`notq`. Also adds missing bitwise binary ops (and, or,
xor, shl, shr) and nested record field read/write support to the
native backend.
Extend the generics system to support record types alongside classes and
interfaces. Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.
Unlike generic classes, generic records do not emit typeinfo, vtable, or
field-cleanup data — they are value types with no class metadata.
Parser, semantic, QBE codegen, and native codegen all updated. 15 new
unit tests (parser + semantic + codegen) and 4 E2E tests. Fixpoint OK.
Add _SysWriteBool to the runtime platform layer so WriteLn(Boolean)
prints 'True' or 'False' instead of '1' or '0', matching Delphi/FPC
behaviour. Both QBE and native x86_64 backends emit the new call for
tyBoolean arguments. Updated ~65 assertions across 13 test files.
Remove sLineBreak (Delphi alias for LineEnding) and PathDelim (Delphi
alias for DirectorySeparator). Blaise keeps exactly three platform
constants: LineEnding, DirectorySeparator, PathSeparator.
Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
Reapply the const-parameter ARC elision that was reverted in 03b59e8,
now paired with the caller-side transient retain that makes it sound.
The four entry/exit ARC loops skip IsConstParam again (a const parameter's
object is kept alive by the caller for the whole call, so the callee needs no
_StringAddRef/_StringRelease or _ClassAddRef/_ClassRelease pair). The earlier
revert was because that premise fails for a TEMPORARY bound to a const param
(e.g. `Use(A + ' ' + B)`): the concat result is +0, its only reference is the
argument slot, and with the callee retain elided it was freed mid-call.
The fix (Andrew Haines, cherry-picked from the llvm branch — commits 00fcf41 +
e94e70544) adds the missing reference at the CALL SITE: EnsureConstStringRef
emits _StringAddRef before the call and ReleaseConstStringArgs emits
_StringRelease after, for each value-mode argument to a const-string parameter.
The pair is a no-op on immortal literals and nets to zero on owned strings, so
the overhead falls only on the transients that actually need it.
Tests: restored the elision IR tests (string + interface const params),
Andrew's caller-retains-transient IR tests and valgrind e2e
(TestRun_ConstStringParam_TransientRetained_Valgrind), alongside the existing
TestRun_ConstStringTemp_StaysAlive_Valgrind. docs/future-improvements.adoc marks
the optimisation shipped. Full suite 0 failures; FIXPOINT_OK; the original
metaclass-ref crash (`C := TFoo`) compiles cleanly.
This reverts commit 5a5b5d4. The optimisation elided the callee-side
ARC retain/release for const string/class/interface value params on the
premise that the caller keeps the argument alive for the whole call. That
premise fails for a TEMPORARY bound to a const param (e.g. `Use(A + ' ' + B)`):
the concatenation result's only reference is the argument slot, so without the
callee-side retain its refcount hits zero at the call boundary and it is freed
before the callee reads it — a use-after-free.
This bit the RTL hardest: `_StringCopy` / `StrHead` take `const string` params
and are called with built-at-runtime temporaries, so the emitted RTL was
miscompiled. Under self-hosting the defect is self-reproducing and only
manifests at the SECOND generation (the compiler that emits the broken RTL is
itself fine), which is why a one-step fixpoint did not expose it and the
compiler's own sources did not reliably trigger it. The symptom was a
deterministic crash compiling any program that uses a metaclass reference
(`C := TFoo`) or HasClassAttribute — which is why TestRunner (via
blaise.testing.runner.text) could not be built, blocking the whole suite.
The original change's valgrind e2e test passed only because it bound a string
LITERAL (immortal) to the const param, not a temporary.
Removed the now-invalid IR/e2e tests that asserted the elided behaviour
(string const params, and the interface-const variant from 088d12f which
relied on this commit's IsConstParam guard). Added
TestRun_ConstStringTemp_StaysAlive_Valgrind, which passes a concatenation
result as a const string param and reads it in the callee — the exact case the
optimisation broke. docs/future-improvements.adoc records how to re-attempt the
elision safely (condition on the argument, not the parameter; retain temporaries
either caller- or callee-side).
Verified: stage-2 build clean, TestRunner builds, full suite 0 failures,
FIXPOINT_OK.
cdecl/stdcall/register/pascal/safecall directives were recognised by the
parser but silently discarded. Record them on TMethodDecl.CallingConv (both
the standalone-routine and class-method directive loops), copy through
CloneMethodDecl, propagate into TRoutineSig.CallingConv in BuildRoutineSig,
and persist in the free-routine .bif format (appended per routine record,
symmetric writer/reader) so the convention survives separate compilation.
Codegen is unchanged: every routine still emits the System V AMD64 convention
(which is the C ABI on Linux x86_64, so cdecl and the default already agree).
The directive is metadata only — the prerequisite for a future Windows/x86
target where stdcall and cdecl differ, and for faithful FFI/debugger tooling.
Replace the pending-placeholder TestCallingConv_Cdecl_Preserved with a real
assertion that 'procedure Beep; cdecl;' yields CallingConv='cdecl'.
Grammar and rationale updated: MethodDirective notes the retained conventions
and a new rationale subsection records the metadata-only decision.
The compiler test suite is now fully green (2518 tests, 0 failures).
'out' was parsed as a synonym for 'var' — by-reference, but indistinguishable
from 'var' afterwards. Add TMethodParam.IsOutParam, set alongside IsVarParam
when the 'out' keyword is present, and carry it through CloneMethodParam and
the .bif param-flags pack (new bit 3) so the loader's TRoutineSig and any
future tooling can recover the declared mode.
Codegen is unchanged: 'out' still lowers identically to 'var' (a pointer
parameter). The flag is metadata only — the prerequisite for a future
read-before-write lint and/or zero-on-entry semantics.
Replace the pending-placeholder TestParam_ModeOut_Preserved with a real
assertion that 'out' yields IsOutParam=True, IsVarParam=True, IsConstParam=False.
Grammar and rationale updated to match: ParamGroup gains the OUT alternative
and the out-parameter section documents the preserved-metadata decision.
Captures the rule of thumb that every AST shape change must touch
three places (uAST.pas / uSemantic.pas / uUnitInterfaceIO.pas), with
codegen as a fourth when the node has runtime semantics.
Worked example walks through adding a hypothetical 'when' statement
end-to-end: parser, semantic, QBE codegen, .bif encoder/decoder, and
test surface. Plus a shorter checklist for plain field additions,
common-pitfalls section, and the rationale behind the positional
(rather than tagged) .bif format.
Implement the sret calling convention for functions/procedures that
return a record type, matching the QBE backend's hidden-first-pointer
approach:
Callee side (EmitFunctionDef):
- FSretFunc flag set in BuildFrame when ResolvedReturnType.Kind = tyRecord
- Result slot becomes an 8-byte pointer slot (nil type = pointer-size)
- Prologue spills the hidden sret %rdi into the Result slot (IntIdx starts
at 1 so normal params continue at %rsi, %rdx, ...)
- No Result initialisation (caller's buffer is already zeroed by caller)
- Epilogue emits plain ret (no return value in %rax/%xmm0)
Field writes through Result (TFieldAssignment with RecordName='Result'):
- Load the sret pointer from the Result slot into %rcx
- Write through %rcx + field offset
Caller side (EmitSretCall):
- leaq dest → %r10 before arg evaluation (survives clobbers)
- Call memset(%r10, 0, TotalSize) to zero the destination buffer
- Reload %r10 after memset (caller-saves may be clobbered)
- Evaluate normal args and push; pop into %rsi/%rdx/... (index 1+)
- movq %r10, %rdi to place sret pointer as hidden first arg
- callq function
TAssignment detection: when LHS is a record and RHS is a record-returning
TFuncCallExpr, dispatch to EmitSretCall instead of EmitExprToEax.
TestRun_Native_RecordReturnFunction promoted from Ignore to AssertRunsOnBoth.
2383 tests pass; FIXPOINT_OK.
TFoo<>.Create on the RHS of an assignment infers all type arguments from
the declared type of the LHS variable, eliminating the redundant repetition:
var S: TStack<string>;
S := TStack<>.Create; { was: TStack<string>.Create }
var D: TDictionary<string, Integer>;
D := TDictionary<>.Create; { infers both K and V }
Works for any number of type parameters.
Implementation:
- Parser: detects IDENT tkNotEquals DOT (the lexer folds '<>' into a single
tkNotEquals token) in expression context and stores 'TFoo<>' as the sentinel
RecordName in TFieldAccessExpr.
- Semantic: ResolveDiamond() in AnalyseAssignment replaces the '<>' sentinel
with the full concrete type name from the resolved LHS type, before the
normal constructor-call analysis proceeds.
Docs: grammar.ebnf and language-rationale.adoc updated.
Tests: 6 IR-level tests in cp.test.generics (parser sentinel, semantic
inference for 1 and 2 type params, IR identity with explicit form);
2 E2E tests in cp.test.e2e.misc (single-arg and two-arg, compile+run).
Inside a function, Exit(X) now assigns X to Result and returns, matching
Delphi/FPC:
function Classify(n: Integer): Integer;
begin
if n < 0 then Exit(-1);
if n = 0 then Exit(0);
Result := 1
end;
Pipeline:
- AST: TExitStmt gains Value (the parsed X) and ResultAssign (a
synthesised 'Result := X' built by semantic). CloneStmt copies them.
- Parser: the tkExit branch parses an optional (Expr) after Exit.
- Semantic: Exit(X) is valid only inside a function (Result in scope);
it is rewritten into a 'Result := X' TAssignment that is analysed like
any assignment, so it inherits return-type compatibility checking and
the widening / ARC handling for string and class returns. Exit(X) in a
procedure, or with a type-incompatible X, is a clear error.
- Codegen: emits the synthesised assignment (via EmitAssignment) before
the normal exit jump; CollectAddressTakenStmt walks it too. Bare Exit
is unchanged.
Tests: 5 IR/semantic cases in cp.test.flowjumps (parse attaches value;
function OK; procedure + type-mismatch errors; codegen stores Result
then jumps) and an e2e in cp.test.e2e.controlflow covering int and
string (ARC) returns plus fall-through. Grammar (ExitStmt) and
language-rationale updated. Full suite 2341 tests pass; fixpoint clean.
A bracket literal can now be passed directly where a `set of <enum>`
parameter is expected, e.g. Configure([optA, optC]) or Report([]) — no
named intermediate variable needed.
Previously such a call failed overload resolution: a bare [a, b] is
analysed (lacking set context) as an open-array of the enum, which does
not match the set parameter. Now:
- ArgMatchScore matches a TArrayLiteralExpr argument against a set
parameter — non-empty when the members share the param's base enum,
and the empty [] against any set — checked before the nil-arg bail so
[] (which has no resolved type) is handled.
- AnalyseArrayLiteralExpr defers an empty [] to a nil type instead of
erroring outright; it is only meaningful with a set target.
- After overload resolution, RetypeSetLiteralArgs re-points each matched
set-literal argument's ResolvedType at the parameter's set type, so
codegen emits the bitmask (EmitArrayLiteralExpr dispatches on tySet).
- CheckTypesMatch gained a nil-actual guard (a clean "no value type"
error instead of a segfault on a stray []), and the assignment path
rejects an empty [] assigned to a non-set LHS.
Fixes a second, latent bug exposed by passing sets by value: a `set of`
value parameter of <=32 members (QBE type w) was spilled in the prologue
with storel, which QBE rejects. Added a tySet case to both param-spill
sites (storew for w sets, storel for l sets).
Tests: 6 IR/semantic cases in cp.test.sets (arg OK / empty / wrong-enum
/ empty-non-set-assign fail, bitmask fold, w-width param spill) and an
e2e in cp.test.e2e.misc. language-rationale updated. Full suite 2335
tests pass; fixpoint clean.