Recent

Author Topic: FPC Unleashed (async/await, parallel for, match, string interpolation & more)  (Read 47423 times)

flowCRANE

  • Hero Member
  • *****
  • Posts: 988
Aren't all thread vars naturally "threadstatic" anyway ?... the value of a threadvar isn't on the stack, i.e, it doesn't go out of scope when the function that uses it exits.

The difference is that a threadvar can only be a global or a static, meaning its scope is the same as that of global variables.

In contrast, a static variable—whether regular or thread-specific—is intended to be declared only inside a function, so that it can only be accessed from within that function. In other words, on the one hand, it has the scope of a local variable, but on the other hand, it exists in memory alongside global variables and retains its value between calls within a given thread.

So, the premise is that a variable declared as static can only be declared inside a function, its scope is limited to that function, and it exists in the process’s data segment. On the other hand, a variable declared as threadstatic can also only be declared inside a function and is visible only to that function, but it exists in the TLS.

I wonder if these assumptions can be implemented at all.
« Last Edit: June 06, 2026, 07:32:17 pm by flowCRANE »
Lazarus 4.6 with FPC 3.2.2, Windows 11 — all 64-bit

Working solo on a top-down retro-style action/adventure game (pixel art), programming the engine from scratch, using Free Pascal and SDL3.

440bx

  • Hero Member
  • *****
  • Posts: 6560
So, the premise is that a variable declared as static can only be declared inside a function, its scope is limited to that function, and it exists in the process’s data segment. On the other hand, a variable declared as threadstatic can also only be declared inside a function and is visible only to that function, but it exists in the TLS.

I wonder if these assumptions can be implemented at all.
Now, I understand, thank you for explaining.  As far as, being implementable, I don't see anything at this time that would prevent it but, that conclusion is off the top of my head, not a result of exhaustive analysis.
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

Thaddy

  • Hero Member
  • *****
  • Posts: 19498
  • Glad to be alive.
Note that the semantics only differ ever so slightly from local variables.
Furthermore there is nothing thread related to account for. Only local semantics apply and that goes for both unthreaded as threaded applications. That is as opposed to threadvar with all its overhead.
But implementation would be trivial. A better wording would be a localstatic modifier for local var. 99% is the same code as local var. It is only the record field semantics that makes it differ, because record fields are truly static.
Should be easy to implement because a record can be cast to the type of the first field and a var can be cast to the record.
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}
  2. type
  3.   TIntRec = record aField:integer; end;  
  4. var
  5.   a:TIntRec;
  6.   b:Integer = 100;
  7. begin
  8.   integer(a) := b;writeln(a.aField);
  9.   b := integer(a);writeln(b);
  10. end.
So if the compiler chooses record semantics if it sees the modifier it is fine.
Code: Pascal  [Select][+][-]
  1. procedure test;
  2. var
  3.   i:integer;localstatic;// folds in record is now true local static
  4. begin
  5. end;
The most important difference with normal local var is it is a true local static, so can't be used in e.g.
Code: Pascal  [Select][+][-]
  1. for i := 0 to 10 do; // this won't work because the variable i has become static.
  2. // casting.pas(11,8) Error: Illegal counter variable

The whole! implementation is just choose record semantics path when the compiler sees the modifier.
The rest should be automatic because of the above. No threading code is necessary.
And this the exact behavior that also occurs in many other languages.
It stays assignment compatible with normal vars.
« Last Edit: June 07, 2026, 10:14:20 am by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
@Thaddy

You've brought up the var-record a few times and I keep getting stuck on the same thing: in what way is that static?

A local var a: record ... end; lives on the stack - it's recreated on every call. So how does it persist between calls?
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

Thaddy

  • Hero Member
  • *****
  • Posts: 19498
  • Glad to be alive.
Read the above example: the record semantics turns the variable i into a static variable i.
It subsequently has automatic static behavior. As an example I gave that when the localstatic modifier is used the variable can't be used as a counter: true static.
It does not persist between calls. It persists until out of scope of the procedure.
It persists for the duration of the thread.
If you would use a thread class, you could use a field, but if you use threading without a thread class you can use a localstatic var to keep state.

I was editing the example.
« Last Edit: June 07, 2026, 10:22:56 am by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
It persists for the duration of the thread.

... and only as long as you stay inside that same procedure - it's gone the moment the procedure returns. That's just an ordinary local variable. So what is the record even for? A plain var n: Integer; does exactly the same: on the stack, thread-local, alive until the proc exits. The record wrapper adds nothing. Am I missing something?



In case you disagree with "and only as long as you stay inside that same procedure":

Code: Pascal  [Select][+][-]
  1. {$mode objfpc}
  2.  
  3. uses SysUtils;
  4.  
  5. procedure Thaddy;
  6. var
  7.   a: record b: integer; end;
  8. begin
  9.   a.b += 1;
  10.   writeln(a.b);
  11. end;
  12.  
  13. procedure stackmess;
  14. var
  15.   a: record b: integer; end;
  16. begin
  17.   a.b := 1000;
  18.   writeln(a.b);
  19. end;
  20.  
  21. function thr(param: Pointer): PtrInt;
  22. begin
  23.   Thaddy;    // 1
  24.   Thaddy;    // 2
  25.   Thaddy;    // 3
  26.   stackmess; // 1000
  27.   Thaddy;    // 1001
  28.   Thaddy;    // 1002
  29. end;
  30.  
  31. begin
  32.   BeginThread(@thr);
  33.   readln;
  34. end.
« Last Edit: June 07, 2026, 10:47:46 am by Fibonacci »
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

Thaddy

  • Hero Member
  • *****
  • Posts: 19498
  • Glad to be alive.
No it is not an ordinary local variable as per my example. You can only use it as a static variable.
It has the same semantics as the field of a record/class/object but at procedure/function level,
static vars can only be changed by explicit reads or writes, thus capturing state, just like fields do.
Any "programmer" that knows only one programming language is not a programmer

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
I've opened a dedicated discussion for the atomic feature on GitHub Discussions - easier to dig into a single feature there than in this one big thread.

Join in / leave feedback: Feature: atomic (critical-section sugar)
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
New feature: String Interpolation

On by default in {$mode unleashed}. Elsewhere: {$modeswitch interpolatedstrings}.

Docs: docs/string-interpolation.md

Put a $ before a string literal and drop expressions inside { }. Each placeholder is type-checked, formatted, and concatenated at compile time - no more + chains, IntToStr, or Format calls.

Basics

Anything that yields a printable value goes in the braces - variables, full expressions, calls, even an if-expression:

Code: Pascal  [Select][+][-]
  1. WriteLn($'[{level}] {count} items in {ms} ms');
  2. WriteLn($'hi {name}, {Length(name)} chars, status {if ok then 'ready' else 'busy'}');
  3. // [INFO] 42 items in 17 ms
  4. // hi Bob, 3 chars, status ready

Masks - {expr:mask}

A % mask goes through Format; any other mask is type-driven - FormatFloat for numbers, FormatDateTime for dates, xN for hex:

Code: Pascal  [Select][+][-]
  1. WriteLn($'v{major}.{minor:00}.{patch:00}');   // v2.05.09
  2. WriteLn($'progress: {pct:0.0}%');             // progress: 42.5%
  3. WriteLn($'total: {amount:#,##0.00}');         // total: 1,234.50
  4. WriteLn($'flags = ${flags:x2}');              // flags = $0A
  5. WriteLn($'started at {t:hh:nn:ss}');          // started at 09:07:03

Masks default to the invariant locale (. decimal, English names); prefix with L for the system locale ({1234.5:L0.00} -> 1234,50).

Auto-format by type

A bare {expr} formats by its type - booleans as TRUE/FALSE, enums by name, class/record instances via ToString:

Code: Pascal  [Select][+][-]
  1. WriteLn($'ok={ok} mode={m}');     // ok=TRUE mode=mRun
  2. WriteLn($'point = {p}');          // point = (3, 4)   <- p.ToString

Escaping & notes
  • '' -> literal '; {{ / }} -> literal { / }
  • Inside { } it's normal Pascal (single-quote strings), and interpolations can nest
  • Bare {expr} needs no units; masks pull in SysUtils (Format/FormatFloat/FormatDateTime/IntToHex) - a missing unit is a clear compile error naming the function
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
New directive: {$incfile <varname> '<path>'}

A directive, not a modeswitch - always available, regardless of {$mode}.

Docs: docs/incfile.md

Embed a file's bytes into your program at compile time as a String constant. The file is read as raw bytes and baked into the binary right where the directive sits - no runtime file I/O, no external file dependency at run time.

Code: Pascal  [Select][+][-]
  1. program project1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. {$incfile mysource 'project1.lpr'}   // embed this very file at compile time
  6.  
  7. begin
  8.   writeln('I am ', Length(mysource), ' bytes of source:');
  9.   writeln(mysource);                 // ...and print it right back out
  10.   readln;
  11. end.

mysource is a String const holding the file's bytes byte-for-byte. Text or binary alike - the real use is baking assets into a single-file executable:

Code: Pascal  [Select][+][-]
  1. {$incfile icon 'assets/app.ico'}   // binary, byte-exact

Path resolution matches {$I}: an absolute path is used verbatim, a relative one is searched in the source-file directory, then the local and global include paths.

Notes
  • It expands to a const declaration, so it must sit where a const is valid: unit/program scope (after uses), or any local const position.
  • The bytes are fixed at compile time - repointing the path needs a recompile. Large files inflate compile time, so keep it to reasonably-sized assets.


New intrinsic: Type()

On by default in {$mode unleashed}. No separate modeswitch.

Docs: docs/type-intrinsic.md

Type(expr) yields the static type of an expression at compile time - Pascal's answer to C++ decltype and D typeof. The operand is never evaluated (like SizeOf): no code runs, no memory is read, no side effects fire, no range checks. It's valid anywhere a type is expected - declarations, typecasts, type-taking intrinsics (Default, SizeOf, High, Low, ...), generic arguments, and derived types (array of, ^, set of).

Code: Pascal  [Select][+][-]
  1. {$mode unleashed}
  2.  
  3. var
  4.   x: Double;
  5.   y: Type(x);          // y is Double
  6.   b: Byte;
  7.  
  8. begin
  9.   b := 7;
  10.   y := Type(x)(b);     // typecast - cast b to whatever type x has
  11.   writeln('b = ', b, ', y = ', y:0:4); // b = 7, y = 7.0000
  12.   readln;
  13. end.

Because the operand isn't evaluated, you can name the type of storage that doesn't exist at runtime - an element of an empty array, a null dereference, a not-yet-allocated field:

Code: Pascal  [Select][+][-]
  1. {$mode unleashed}
  2.  
  3. var
  4.   a: array of Integer;         // empty; A[0] would range-check at runtime
  5.  
  6. begin
  7.   writeln(High(Type(a[0])));   // 2147483647 - element type only, a[0] never read
  8.   readln;
  9. end.

It composes with type inference, so one inference site can drive several declarations without ever spelling the type out:

Code: Pascal  [Select][+][-]
  1. {$mode unleashed}
  2.  
  3. uses Classes;
  4.  
  5. function MakePoint(x, y: Integer): TPoint;
  6. begin
  7.   result.X := x;
  8.   result.Y := y;
  9. end;
  10.  
  11. begin
  12.   var z := MakePoint(3, 4); // z inferred as TPoint
  13.   var a: array of Type(z);  // array of TPoint
  14.   var n: Type(z);           // TPoint
  15.   // change MakePoint's result type and all of these follow
  16. end.

Notes
  • The operand must still type-check - only its evaluation is skipped. An undeclared identifier or illegal expression is still an error.
  • On a constant expression you get the smallest type that fits: Type(1+2) is a byte-sized subrange (SizeOf = 1), not Integer. Anchor it with a cast or a typed variable - Type(Integer(5)).
  • Why not typeof: that name already means an RTTI/VMT operator in Object Pascal, and type is a keyword. The ( disambiguates Type( from the type keyword; other modes reject it exactly as before, so existing code is unaffected.
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

creaothceann

  • Sr. Member
  • ****
  • Posts: 394
New directive: {$incfile <varname> '<path>'}
Embed a file's bytes into your program at compile time as a String constant.
...
For a non-empty input file, the directive emits a const declaration whose initializer is the bytes of the file, concatenated as Pascal string fragments

Code: Pascal  [Select][+][-]
  1. const <varname>: String =
  2. '<printable run 1>'+#$nn+#$nn+'<printable run 2>'
  3. +'<printable run 3>'+...;

TBH I'd rather have an array[...] of byte, or even word/dword/qword. (Some CPU architectures, like the 68k, have a multi-byte data bus, so e.g. program data wouldn't even be ever accessed as single bytes.)

Though if I could do this:

Code: Pascal  [Select][+][-]
  1. const
  2.         {$incfile Test_ROM_STR '../roms/test.bin'}
  3.         Test_ROM : array[SizeOf(Test_ROM_STR) div 2] of u16 absolute Test_ROM_STR;
  4.  

... then that'd do the job too.

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
@creaothceann

Looks like this calls for a separate directive - {$incfilebytes} - emitting an array of bytes instead of a String.

On a side note: on the devel branch {$incfile} already got an upgrade - besides the two-argument form it now also takes a single argument (just the path) and pastes the string literal right where the directive sits, so you can write things like call({$incfile 'path'}). It'll be a while before that reaches main, though.

Meanwhile I'll think through {$incfilebytes}. If you have any thoughts on the name, behavior, element type and so on - go for it.
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

Khrys

  • Sr. Member
  • ****
  • Posts: 471
Meanwhile I'll think through {$incfilebytes}. If you have any thoughts on the name, behavior, element type and so on - go for it.

You could always borrow more from Rust...


Function   |   Rust   |   FPC Unleashed
Pull file content straight into source code   |   include!   |   {$include}  or  {$i}
Create string constant   |   include_str!   |   {$includestring}
Create byte array constant   |   include_bytes!   |   {$includebytes}


Rust's macros expand into expressions, which makes them wonderfully flexible. It would be possible for  {$includestring}  to mostly mimic this by expanding into a string literal, but  {$includebytes}  on the other hand... I'm not sure if anything can be done about that.

Thaddy

  • Hero Member
  • *****
  • Posts: 19498
  • Glad to be alive.
Forgot about three things???, which makes me a bit wary if this include feature is necessary at all.
I have been using three tools for ions:
- A resourcescript with e.g. RT_RCDATA ( does the same, rc automatically compiled to res and links. Nowadays Xplatform )
- Data2Inc, converts any data to an include file with fixed array semantics.
- Bin2Obj, converts any data to an obj file that links in with fixed array semantics.

If anything, make it a TByteArray ( or the same as Bin2Obj)

I can't find any improvement over those 35 + year old concepts.
(The latter two are also standards in UNIX distributions, all concepts come with fpc as standard. )

I like the type() intrinsic, though.
« Last Edit: June 16, 2026, 03:57:53 pm by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

Khrys

  • Sr. Member
  • ****
  • Posts: 471
I have been using three tools for ions:
- A resourcescript with RT_RCDATA ( does the same, rc automatically compiled to res and links. Nowadays Xplatform )
- Data2Inc, converts any data to an include file with fixed array semantics.
- Bin2Obj, converts any data to an obj file that links in with fixed array semantics.

All valid approaches, but compiler builtins are simply more convenient (and don't come with gotchas like  windres  padding with garbage or external tools not running for some reason). Even C has  #embed  now!

Also it's spelled EONS ffs

 

TinyPortal © 2005-2018