FPC Unleashed is a community-driven fork of Free Pascal built for developers who want modern language features today, not after an official release that will likely never include them. The features added here were rejected, ignored, or shelved as "too experimental" by upstream - this fork is your only option for them.
- Features
- Unleashed Mode
- Statement Expressions
- Inline Variables
- Anonymous Tuples
- Match Statement
- Multi-Variable Initialization
- Flexible Array Members
- Scoped Cleanup (defer, autofree, scoped with)
- For-Step
- Tweaks
- Multiline Strings
- Array Equality
- Strip RTTI
- Indexed Labels
- Lazy Label Declarations
- Compound Assignment for Pascal Operators
- Custom Binary Metadata
- Extra Improvements
- Detailed Documentation
- Installation
- Contributing
Activate: {$mode unleashed} or -Munleashed
A modern Pascal mode based on objfpc with powerful enhancements enabled by default. Instead of toggling individual modeswitches, you get everything at once.
When using Lazarus Unleashed, this mode is enabled by default for all projects and full code completion is supported out of the box.
The following modeswitches are enabled automatically:
| Modeswitch | Description |
|---|---|
statementexpressions |
Use if, case, and try as expressions |
inlinevars |
Declare variables inline anywhere inside a begin..end block |
tuples |
Anonymous tuple types, literals, and destructuring |
match |
Pattern matching with first-match semantics |
multivarinit |
Initialize several variables of the same type with one value |
forstep |
step N clause in for loops to advance by N each iteration |
anonymousfunctions |
Anonymous procedures and functions |
functionreferences |
Function pointers that capture context |
advancedrecords |
Records with methods, properties, and operators |
arrayoperators + arrayequality |
Direct array comparisons with = and <> |
ansistrings |
Use AnsiString as the default string type |
underscoreisseparator |
Allow underscores in numeric literals (1_000_000) |
duplicatelocals |
Allow reusing identifiers in limited scopes |
multilinestrings |
Allow multi-line string literals without manual concatenation |
stringordcast |
Cast a string literal to an ordinal type (dword('RIFF')) |
autofree |
defer STATEMENT, autofree EXPR, scoped with var x := ... |
flexiblearrays |
C99-style flexible array member as last record field (array[] of T) |
Note
For the best code-completion experience, we recommend using Lazarus Unleashed - a fork of Lazarus with full support for unleashed mode. If you are using stock Lazarus, enable the mode via -Munleashed in the project's Custom Options instead of placing {$mode unleashed} directly in the source file, to avoid autocomplete issues and incorrect Code Insight behavior.
Activate: available in Unleashed mode.
Allows using if, case, and try as expressions that return a value, enabling a more functional and concise coding style. All branches must return values of the same type.
Traditionally, if, case, and try are statements - they perform actions but don't produce a value. With statement expressions, they can be used on the right side of an assignment, as function arguments, or anywhere an expression is expected.
var
s: string;
begin
s := if 0 < 1 then 'Foo' else 'Bar';
// s = 'Foo'
end.Chained if-expressions work as expected:
s := if x > 100 then 'large' else
if x > 10 then 'medium' else
'small';Only one branch is evaluated - side effects in the other branch are never triggered:
function loadFromDatabase: string;
begin
result := 'data';
end;
var useCache := false;
var s := if useCache then 'cached' else loadFromDatabase;
// expensive is only called when condition is falsetype
TMyEnum = (mefirst, mesecond, melast);
var
s: string;
begin
s := case mesecond of
mefirst: 'Foo';
mesecond: 'Bar';
melast: 'FooBar';
end;
// s = 'Bar'
end.s := case x of
0: 'zero';
1..9: 'single digit';
else 'large';Note
When using enums, all values must be covered. Otherwise, the compiler will reject the expression. When using integer or ordinal ranges, provide an else clause.
Evaluates a function call and returns a fallback value if an exception occurs:
function conditionalthrow(doraise: boolean): string;
begin
result := 'OK';
if doraise then raise TObject.Create;
end;
var
s: string;
begin
s := try conditionalthrow(false) except 'Error';
// s = 'OK'
s := try conditionalthrow(true) except 'Error';
// s = 'Error'
// match specific exception types:
s := try conditionalthrow(true) except on o: TObject do 'TObject' else 'Error';
// s = 'TObject'
end.Note
The try expression must contain a function call - try 'literal' except ... is not valid.
Activate: available in Unleashed mode.
Declare variables at the point of use inside begin..end blocks instead of in a separate var section at the top. Supports explicit types and type inference.
In standard Pascal, all variables must be declared in a var section before the begin keyword. Inline variables let you declare them exactly where they are needed, reducing visual distance between declaration and use, and enabling type inference from the initializer.
begin
// explicit type, no initializer
var x: integer;
x := 10;
// explicit type with initializer
var y: integer := 42;
// type inference - compiler deduces integer from the literal
var z := 100;
// string inference
var s := 'hello';
// multiple variables of the same type
var a, b: integer;
a := 1;
b := 2;
end.var
sum: integer;
begin
sum := 0;
// explicit type
for var i: integer := 1 to 5 do
sum := sum + i;
// type inference
for var j := 1 to 5 do
sum := sum + j;
end.var
arr: array[0..2] of integer = (10, 20, 30);
sum: integer;
begin
sum := 0;
for var item in arr do
sum := sum + item;
// sum = 60
end.Note
Inline variables have the same scope as regular local variables - they are visible from the point of declaration until the end of the enclosing routine. They are not block-scoped.
Note
Untyped numeric inline variables default to a 32-bit signed integer (integer).
Activate: available in Unleashed mode (modeswitch tuples).
Lightweight anonymous record types written in parentheses, with literals, destructuring, comparison, and full record semantics (managed types, copy by value, passing by var/const). Tuples are stored as ordinary internal records, so anything records can do, tuples can do.
function GetPair: (integer, integer);
begin
Result := (10, 20); // positional literal
end;
var
p: (integer, string) := (42, 'hello');
begin
writeln(p._1, ' ', p._2); // 42 hello
writeln(p[0], ' ', p[1]); // same, by constant index
end.function Coords: (x, y: integer);
begin
Exit(x: 10, y: 20); // shorthand inside Exit
end;var
a, b: integer;
begin
(a, b) := GetPair; // unpack tuple into existing vars
end.See unleashed/docs/tuples.md for the full grammar (named/positional mixing, tuple arrays, comparison operators, IDE hints).
Activate: available in Unleashed mode (modeswitch match).
Pattern matching with first-match semantics. Replaces case..of for non-ordinal subjects (tuples, strings, arbitrary expressions) and adds catch-all, fallthrough, condition-based branches, tuple wildcards, and an expression form.
match s of
'hello': writeln('greeting');
'bye': writeln('farewell');
_: writeln('unknown'); // catch-all
end;match
x > 100: writeln('big');
x > 10: writeln('medium');
x > 0: writeln('small');
_: writeln('non-positive');
end;var p: (integer, integer) := (0, 5);
match p of
(0, 0): writeln('origin');
(0, _): writeln('on Y axis'); // matches
(_, 0): writeln('on X axis');
_: writeln('other');
end;var label_: string := match x of
1: 'one';
2: 'two';
_: 'many';
end;See unleashed/docs/match.md for fallthrough mode (match all), leave, range patterns, and exhaustiveness rules.
Activate: available in Unleashed mode (modeswitch multivarinit).
Initialize several variables of the same type with a single value in one declaration. Works in var, typed constants, and inline var. Each variable gets its own independent copy.
var
a, b, c: integer = 42; // global var
ok, done: boolean = false;
const
MinX, MinY, MinZ: integer = 0; // typed constants
procedure Bar;
begin
var p, q: integer := 99; // inline var
var i, j := 10; // inline var with inference
end;The initializer is evaluated once and copied into each variable; a := 100 does not affect b or c. See unleashed/docs/multi-var-init.md for the full evaluation table.
Activate: available in Unleashed mode (modeswitch flexiblearrays).
C99-style records with a variable-length tail. The last field is declared with empty brackets and no upper bound; the record header has a fixed size, the tail extends as far as the allocation says, and sizeof(rec) reports only the fixed part.
type
PMessage = ^TMessage;
TMessage = packed record
code: integer;
length: integer;
data: array[] of byte; // flexible array member
end;
var
msg: PMessage;
begin
GetMem(msg, sizeof(TMessage) + 1024); // header + 1024-byte tail in one block
msg^.code := 42;
msg^.length := 1024;
msg^.data[1023] := $FF; // no range check fires under {$R+}
FreeMem(msg);
end;The compiler does not track the run-time length, so indexing skips both compile-time and runtime range checks even with {$rangechecks on} active. There is no separate buffer, no pointer chase, no managed lifetime.
The pattern is what Win32 headers usually express today as array[0..0] of T or ANYSIZE_ARRAY, with the well-known problems (range check fires, sizeof is one element too large, padding is implicit). FAM gives the inline layout, honest sizeof, and a working {$R+} in one feature. Common targets: TOKEN_GROUPS, BITMAPINFO, LOGPALETTE, network frames, file headers with inline payload.
Restrictions enforced at parse time: FAM must be the last field of a plain record with at least one preceding field; no FAMs in classes, objects, variant parts, or as class var / threadvar; FAM-records cannot be embedded in another type, used as array elements, declared on the stack, passed by value, or returned by value. Use PFamRec (a pointer) wherever a FAM-record would otherwise live by value.
See unleashed/docs/flexible-arrays.md for the full rule list, memory layout diagram, comparison with array of T, and PPU notes.
Activate: available in Unleashed mode (modeswitch autofree).
Three cooperating constructs for scope-based resource management without try..finally boilerplate.
Register a statement to fire when the enclosing begin..end block exits (normal exit, exit, break, continue, exception). Multiple defers fire in LIFO order. Argument expressions are evaluated at exit, not at registration.
procedure CopyFile(const src, dst: string);
begin
var fin := TFileStream.Create(src, fmOpenRead);
defer fin.Free;
var fout := TFileStream.Create(dst, fmCreate);
defer fout.Free;
fout.CopyFrom(fin, 0);
end;
// fout.Free runs first (LIFO), then fin.Free, even if CopyFrom raisesSugar that registers a nil-guarded Free defer for a class instance. Works on inline-var declarations and on assignments to existing locals. The cleanup uses if x<>nil then begin x.Free; x:=nil end, so manual x.Free; x := nil; earlier in the scope does not double-free.
procedure foo;
begin
var list := autofree TStringList.Create;
list.Add('hello');
list.Add('world');
WriteLn(list.Text);
end;
// list.Free called automatically here
// also works on existing variables
var
a, b: TFoo;
begin
a := autofree TFoo.Create(1);
b := autofree TFoo.Create(2);
// ... use a, b ...
end;
// b.Free, then a.Free (LIFO)The right-hand side must be a class derived from TObject. The LHS must be a plain local or inline variable.
The with statement accepts inline-var bindings, with optional autofree. Three forms:
// inline-var with autofree (cleanup at end of with-scope)
with var http := autofree TFPHTTPClient.Create(nil) do
s := http.Get('http://httpbin.org/ip');
// bind to an existing local
var http: TFPHTTPClient;
with http := autofree TFPHTTPClient.Create(nil) do
s := http.Get('http://httpbin.org/ip');
// hidden holder (no name; methods reachable through with-symtable)
with autofree TFPHTTPClient.Create(nil) do
s := Get('http://httpbin.org/ip');Multi-with works with any combination:
with var a := autofree TFoo.Create,
var b := autofree TBar.Create do
Use(a, b);
// b.Free, then a.Freedefer written inside a scoped-with body is scoped to that with (fires before the autofree cleanup), even when the body is a single statement without begin..end.
The classic with X do BODY (no inline-var, no autofree) is unchanged.
See unleashed/docs/autofree.md for the full grammar, lowering details, error catalogue, and edge cases.
Activate: available in Unleashed mode (modeswitch forstep).
Advance the loop counter by an arbitrary positive amount on each iteration with the step clause. Works with both to and downto, and with inline var.
step is a context-sensitive keyword - it is only recognized between the to/downto expression and do. Anywhere else (variable name, function name, record field) step stays an ordinary identifier, so existing code with a step symbol keeps compiling. Even mixed: for i := 0 to step step 1 do parses correctly - the upper bound is the step variable, the keyword step introduces the increment.
for i := 1 to 10 step 2 do
write(i, ' '); // 1 3 5 7 9
for i := 20 downto 1 step 3 do
write(i, ' '); // 20 17 14 11 8 5 2
for var k := 5 to 50 step 5 do
write(k, ' '); // 5 10 15 ... 50The step expression must be of an ordinal type and must be a positive integer. Use downto for descending loops; the step itself is always positive. The expression is evaluated once before the loop starts, so calls with side effects fire only one time:
for i := 0 to 12 step ComputeStep() do // ComputeStep called exactly once
...Constant step 1 folds back to a regular for-loop, so all the usual optimizations apply. break, continue, exit and raise work the same as in a regular for loop. step is rejected in for-in loops.
Activate: unleashed-mode-only (no separate modeswitch).
Small semantic adjustments to make existing Pascal constructs behave the way most people expect them to.
In standard Pascal the for-loop counter is undefined after the loop exits - the optimizer is free to leave any value behind, and Delphi/FPC docs explicitly warn not to rely on it. That bites every time you write for i := 1 to N do if X then break; and then try to use i.
In Unleashed mode the counter is guaranteed to keep its last assigned value:
for i := 1 to 100 do
if X[i] = target then
break;
{ i now holds the index of the match (or 100 if nothing matched) }
for i := 1 to 10 do ;
{ i = 10 (the last in-range value), not 11 (overshoot) }This matches the intuitive behavior of C, Python, JavaScript and Go. Cost is one extra assignment on the natural exit path; nothing on break/continue/exit.
Delphi-style shorthand for negated runtime type checks and set membership tests:
if Obj is not TFoo then ... // same as: if not (Obj is TFoo)
if x not in [Apple, Orange] then ... // same as: if not (x in [Apple, Orange])Compiles to the same node tree as the parenthesised form, so semantics and runtime cost are unchanged. Available in unleashed mode only.
See unleashed/docs/tweaks.md for the catalogue and the exact rules each tweak applies.
Activate: available in Unleashed mode (modeswitch multilinestrings).
Two delimiter forms let a string literal span multiple source lines without manual + or LineEnding.
const
banner =
`========================================
= FCF Fibonacci Demo =
========================================`;A normal string literal extended to tolerate embedded newlines.
const
sql =
'''
select id, name
from users
where active = 1
''';A Delphi-11-style textblock literal. The opener (''' followed by a newline) and the closer (''' on its own line) must each sit alone; the indentation of the closing delimiter defines the column that gets stripped from every content line.
The two forms differ in tokenization, indentation handling, and how they compose in expressions. See unleashed/docs/multiline-strings.md for the details. (Stock FPC actually accepts these too but never documented them.)
Activate: {$modeswitch arrayequality} (requires arrayoperators to also be active; both are enabled in {$mode unleashed})
Adds support for = and <> comparison operators between arrays.
Standard Free Pascal with arrayoperators allows + (concatenation) on dynamic arrays, but does not allow direct equality comparison. This modeswitch fills that gap - you can compare two arrays element-by-element using = and <>.
{$mode unleashed}
var
a, b: array of integer;
begin
a := [1, 2, 3];
b := [1, 2, 3];
if a = b then
writeln('Arrays are equal'); // this is printed
b := [1, 2, 4];
if a <> b then
writeln('Arrays are different'); // this is printed
end.Activate: {$modeswitch striprtti}
Important
This modeswitch is not enabled by default in unleashed mode. It must be opted into explicitly.
When enabled, all RTTI strings (type names of custom structures like records, classes, etc.) are stripped from the binary - they are replaced with empty strings. RTTI structures still exist and cannot be fully removed, but the most obvious fingerprint - plain-text type identifiers - is gone.
Sometimes one may want to avoid exposing an application's internal structure, especially when a simple ASCII dump can reveal type names and identifiers, and with them, the true purpose of the program.
For instance, in the context of game cheats, embedding a name like TGameWallhack in the binary can immediately reveal the nature of the software.
📄
|
| Standard | With {$modeswitch striprtti} |
|---|---|
Offset Size String acf0 10 0123456789ABCDEF af20 29 FPC 3.3.1 [2025/06/18] for x86_64 - Win64 b0d1 13 TMyAwesomeCheatBase b1c1 0b TGameAimbot b2a9 0d TGameWallhack b3b8 0c Enemy Player b3d8 0d Another Enemy b3ea 13 TMyAwesomeCheatBase b418 0b MyCoolCheat b47a 0b TTargetList b4aa 0b MyCoolCheat b4c2 0b TGameAimbot b512 0b TGameAimbot b538 0b MyCoolCheat b552 0d TGameWallhack b57a 0b MyCoolCheat b5bb 0b MyCoolCheat |
Offset Size String acf0 10 0123456789ABCDEF af20 29 FPC 3.3.1 [2025/06/18] for x86_64 - Win64 b398 0c Enemy Player b3b8 0d Another Enemy |
Type names like TGameAimbot, TGameWallhack, or MyCoolCheat are no longer present, making the binary significantly less identifiable at first glance. Only actual string data (like player names) remains.
Compiling a typical LCL application with striprtti enabled will likely result in a startup failure, because code such as:
application.createform(TForm1, form1);will search for "" (empty string) in the resources instead of TForm1, and fail.
Three ways are provided to selectively whitelist types that should keep their RTTI name:
1. expose keyword - placed directly before a type declaration in unleashed mode:
type
expose TForm1 = class(TForm)
// ...
end;The keyword is contextual and only recognized in {$mode unleashed}. It is parsed even when striprtti is off (no-op then), so you can leave the keyword in place while temporarily disabling stripping.
2. {$rttiexpose} directive - per-unit list of glob patterns. Whitespace and/or comma separated:
{$rttiexpose TForm* TButton*, TPanelMain}3. --rttiexpose= CLI flag - global list of glob patterns, applied to every compiled unit. Repeatable:
fpc --rttiexpose=TForm*,TButton* --rttiexpose=TPanelMain ...
Useful for whitelisting types you do not control (LCL, RTL).
CLI patterns and per-unit directive patterns are merged (union); the directive can only widen the whitelist for its own unit, never narrow CLI.
Note
The {$modeswitch striprtti} directive works on a per-unit basis. You can enable it only in the units where you want to hide type names, while leaving it disabled in others - for example, in units that contain forms or require RTTI to function correctly.
See unleashed/docs/strip-rtti.md for the full list of stripped fields, edge cases (forwards, generics, aliases), interaction with PPU, and implementation notes.
Labels now support indexes.
label
mylabel1,
mylabel2[1, 2, 3],
mylabel3[1..10],
mylabel4['foo', 'bar'];
begin
goto mylabel4['foo'];
writeln('you should not see this');
mylabel1:
mylabel2[2]:
mylabel3[10]:
mylabel4['foo']:
writeln('hello!');
end.Labels no longer need to be declared before use.
begin
goto mylabel;
writeln('you should not see this');
mylabel:
writeln('hello!');
end.Compound assignment is now supported for Pascal operators such as div, mod, and xor, without requiring {$COPERATORS ON}.
var
i: integer = 10;
begin
i div= 2; // equivalent to: i := i div 2
writeln(i); // prints "5"
end.Available: div=, mod=, and=, or=, xor=, shl= and shr= .
Three CLI flags override metadata that ends up in the produced binary. Useful for branding releases, hiding the toolchain you used to build the binary, or simply controlling what inspection tools display - it is your binary, set the fields to whatever you want.
--fpcsignature=<str>- replaces the ident string in the.fpc.versionsection. Cross-platform; every target emits this section. Default isFPC Unleashed <version> [<date>] for <cpu> - <target>. Passing an empty string (--fpcsignature=) drops the section entirely - the produced binary carries no FPC ident marker at all.--linkerversion=<Major.Minor>- setsMajorLinkerVersion/MinorLinkerVersionin the PE optional header. Windows PE only. Default derived from FPC version (e.g.3.31for FPC 3.3.1).--osversion=<spec>- setsMajorOperatingSystemVersion/MinorOperatingSystemVersionin the PE optional header. Windows PE only.specis either an OS name (XP,Win11,Vista,7,8.1, ...) resolved via a built-in table, or numericMajor.Minor(10.0,6.3). Default is4.0unless-WPis set.
Examples:
fpc --fpcsignature="MyApp 1.0" --linkerversion=14.39 --osversion=Win11 my_program.pas
fpc --fpcsignature="FPC" my_program.pas # keep "FPC", drop version + date
fpc --fpcsignature="" my_program.pas # no signature section in the binary
fpc --osversion=10.0 my_program.pas
fpc --osversion=XP --linkerversion=14.0 my_program.pas
The OS-name table is case-insensitive and accepts an optional Win prefix (Win11, WinXP work just like 11, XP).
Important
--linkerversion and --osversion are Windows PE only (targets win32, win64, wince). Other binary formats do not carry these fields:
- ELF (Linux, BSD, Solaris, Haiku, Android) - no linker version or OS version in the header.
- Mach-O (macOS, iOS) - has
LC_BUILD_VERSION/LC_VERSION_MIN_*but FPC delegates linking to the systemld, which fills these from the SDK. - NE / OMF / WASM / NLM / AmigaOS hunk / Atari TOS - either no such field or hardcoded for compatibility.
Passing the flags on a non-PE target compiles cleanly but the values are silently ignored. --fpcsignature works on every target.
See unleashed/docs/binary-metadata.md for full per-flag rationale, the OS-name table, and cross-platform notes.
Smaller, targeted improvements that unlock Pascal patterns standard FPC modes reject. Each is gated on its own modeswitch (some are on by default in unleashed, others must be opted into):
stringordcast- cast a string literal to an ordinal type at compile time, e.g.dword('RIFF')orword('MZ'). Useful for signature checks. On by default in unleashed.typehelpers-type helper for Ton any named type, not just classes and records.multihelpers- several helpers for the same type visible in one scope (instead of "last one wins").implicitgenerics- Delphi-style implicitgeneric/specializesyntax (TList<T>without keywords). Stock FPC locks this to{$mode delphi}; the modeswitch makes it usable in any mode.
Full descriptions and examples in unleashed/docs/extra-improvements.md.
Each feature has a dedicated reference page in unleashed/docs/ with the full grammar, semantics, edge cases, and IDE notes. Start at the index: unleashed/docs/README.md.
- Download fpcupdeluxe and run it once to generate the
fpcup.inifile. - Edit
fpcup.iniand add the following under[ALIASfpcURL]:
[ALIASfpcURL]
unleashed.git=https://github.com/fpc-unleashed/freepascal.gitAnd, for Lazarus Unleashed (with autocomplete support for some of the new features), add the following under [ALIASlazURL]:
[ALIASlazURL]
unleashed.git=https://github.com/fpc-unleashed/lazarus.git- Reopen fpcupdeluxe, uncheck GitLab, and select
fpc-unleashed.gitas your FPC version. - Choose any Lazarus version you like.
- Click Install/update FPC+Lazarus.
- Optionally install cross-compilers via the
Crosstab.
- Make sure your existing FPC + Lazarus installation was created with fpcupdeluxe.
- In your installation directory, delete or rename the
fpcsrcfolder. - Clone the FPC Unleashed repo into the
fpcsrcdirectory:
git clone https://github.com/fpc-unleashed/freepascal.git fpcsrc- In fpcupdeluxe, go to Setup+, check FPC/Laz rebuild only, and confirm.
- Click Only FPC to rebuild the compiler and RTL.
- Optionally install cross-compilers via the
Crosstab.
We welcome bold ideas and experimental features that push Pascal forward.
FPC Unleashed is a home for innovation. If you have built a language feature that was considered too experimental or not standard enough for upstream, this is where it belongs.
- New language ideas - Propose modeswitches, syntax extensions, or compiler enhancements via GitHub Issues or Discussions. Even if you do not have an implementation yet, a well-described idea with clear use cases is valuable.
- Complete, high-quality implementations - We accept pull requests for new language constructs, compiler enhancements, and RTL improvements. We expect production-grade code: clean implementation, proper test coverage, and clear documentation of the feature.
We do not accept minor convenience patches, trivial reformats, or small tweaks that only scratch a personal itch. Every change to a compiler carries weight - if you are contributing code, it should be a meaningful feature or fix that benefits the broader community.

