Recent

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

Thaddy

  • Hero Member
  • *****
  • Posts: 19498
  • Glad to be alive.
I was afraid of that....   :'(
Anyway, all new features tested and work, now going to shoot holes in 'm.. :D :D
BTW: ARM/AARCH64? Any progress?
« Last Edit: July 03, 2026, 09:42:41 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
What were you afraid of? ;) Syncing? Still possible.

Did you compare threadstatic against your own solution yet?

As for ARM/AARCH64 - what do you expect me to do? This is just a fork; every target FPC works on, this fork works on too. Last I heard, exception handling on Windows AARCH64 was broken - someone (retrofoxed? I see his account's been removed, what happened?) fixed it with AI and opened an MR, and now it's just hanging :) 5 months and counting.

If you really wish, we could kind of "detach" from FPC upstream altogether and merge the upstream fixes ourselves, years faster. Even my own MRs: 5 merged, 9 open - easy small fixes for reported issues, hanging for 2 months and counting.

Maybe instead of fixing upstream I should just commit to Unleashed? But if I do, syncing with mainstream will eventually become impossible and it'll be a completely separate fork. And who's going to maintain it once I "get bored"? :-X Mainstream may be slow, but it delivers.
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 I am fine with all that. I am still testing... ;)
(Frankly, a lot: I grow - not grew - into liking it.)
« Last Edit: July 03, 2026, 02:40:50 pm 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
Your other 15 cores were unemployed. Until today.

New feature: Futures - async & await

On by default in {$mode unleashed}. Outside: {$modeswitch asyncawait}.

Docs: docs/async-await.md

async runs a call or a block on a fresh worker thread and hands back a future; await blocks until the worker finishes and reads its result. This is the std::async model from C++, not the C# one - no function coloring, no event loop, no scheduler. One async is one thread, one await is one join. Simple to reason about.

Code: Pascal  [Select][+][-]
  1. program async_demo;
  2.  
  3. {$mode unleashed}
  4.  
  5. uses SysUtils;
  6.  
  7. function Add(a, b: Integer): Integer;
  8. begin
  9.   sleep(300);                    // pretend this is expensive
  10.   result := a + b;
  11. end;
  12.  
  13. var a: Integer;
  14. begin
  15.   a := 2;
  16.   var sum := async Add(a, 3);    // spawns a worker; a is snapshotted as 2, type inferred: future of Integer
  17.   a := 100;                      // too late - does not affect the call
  18.   writeln('main thread keeps going...');
  19.   writeln('sum = ', await sum);  // sum = 5
  20.   readln;
  21. end.

A future of T is a first-class, reference-counted value: store it, pass it around, return it from a function - it stays alive as long as anything holds it. Spawn without keeping it and the work still runs (fire and forget):

Code: Pascal  [Select][+][-]
  1. program async_future;
  2.  
  3. {$mode unleashed}
  4.  
  5. uses SysUtils;
  6.  
  7. function FetchName: string;
  8. begin
  9.   sleep(50);                    // pretend it's a network call
  10.   result := 'Bob';
  11. end;
  12.  
  13. procedure LogToFile(const s: string);
  14. begin
  15.   writeln('log: ', s);
  16. end;
  17.  
  18. function StartFetch: future of string;
  19. begin
  20.   result := async FetchName;    // the future outlives this function
  21. end;
  22.  
  23. begin
  24.   async LogToFile('done');      // fire and forget - nobody waits
  25.   var f := StartFetch;          // a future returned from a function
  26.   writeln('hi ', await f);      // hi Bob
  27.   readln;
  28. end.

Two spawn forms

Call form async SomeCall(args) evaluates the arguments now, snapshots them by value, and runs the routine on the worker from those snapshots (that's why sum above is 5, not 103). Block form async begin ... end captures referenced locals by reference on the heap, so the worker sees later mutations and the future may safely outlive the spawning routine.

Controlling the worker

The future carries a small control surface: Cancel, Cancelled, Done (never blocks - poll it for progress), ThreadID (plugs straight into the RTL thread API). Cancellation is cooperative - inside a block the flag is visible as a read-only Cancelled boolean:

Code: Pascal  [Select][+][-]
  1. program async_cancel;
  2.  
  3. {$mode unleashed}
  4.  
  5. uses SysUtils;
  6.  
  7. begin
  8.   var ticks := 0;
  9.   var h := async begin           // block form: captures locals by reference
  10.     while not Cancelled do begin // cooperative cancel flag, visible in the block
  11.       inc(ticks);
  12.       sleep(10);
  13.     end;
  14.   end;
  15.   sleep(100);
  16.   writeln('running: ', not h.Done);   // running: TRUE
  17.   h.Cancel;                           // ask the worker to stop
  18.   await h;
  19.   writeln('done: ', h.Done, ', ticked: ', ticks > 0);   // done: TRUE, ticked: TRUE
  20.   readln;
  21. end.

Exceptions cross the thread boundary

An exception raised on the worker is captured and re-raised on the caller at the first await - a background failure surfaces as an ordinary exception, exactly where you read the result:

Code: Pascal  [Select][+][-]
  1. program async_exceptions;
  2.  
  3. {$mode unleashed}
  4.  
  5. uses SysUtils;
  6.  
  7. begin
  8.   var bad := async begin
  9.     raise Exception.Create('boom');
  10.   end;
  11.  
  12.   try
  13.     await bad;                        // worker exception re-raised here
  14.   except
  15.     on E: Exception do writeln('caught: ', E.Message);   // caught: boom
  16.   end;
  17.  
  18.   readln;
  19. end.

Notes
  • await is repeatable - the result is cached in the future, only the first await actually waits. Reading a future's value without await is a compile error.
  • The call form rejects var/out arguments (a by-value snapshot would silently drop the writes) and nested routines (they outlive the parent frame) - both with dedicated errors pointing at the block form.
  • Shared mutable state is your responsibility: snapshots protect the call form's arguments, but block captures and object fields are shared - synchronize yourself (lock works).
  • On Unix add cthreads as the first unit, as for any threaded FPC program; on Windows it works out of the box.


New feature: parallel for

On by default in {$mode unleashed}. Outside: {$modeswitch parallelfor}.

Docs: docs/parallelfor.md

for parallel var i := lo to hi do runs the loop body across a pool of worker threads instead of one after another. Iterations are handed out dynamically through an atomic counter (uneven work balances itself), the loop is a barrier - control continues only when every iteration has finished - and the body is the same statement you'd write in a classic for:

Code: Pascal  [Select][+][-]
  1. program pfor_demo;
  2.  
  3. {$mode unleashed}
  4.  
  5. begin
  6.   var total := 0;
  7.   for parallel var i := 1 to 10000 do
  8.     InterlockedExchangeAdd(total, i);   // shared variable -> atomic
  9.   writeln(total);                       // 50005000, every time
  10.   readln;
  11. end.

What it buys you - counting primes up to 4 million, same IsPrime, one line changed:

Code: Pascal  [Select][+][-]
  1. program pfor_primes;
  2.  
  3. {$mode unleashed}
  4.  
  5. uses SysUtils;
  6.  
  7. function IsPrime(n: Integer): Boolean;
  8. var d: Integer;
  9. begin
  10.   if n < 2 then exit(false);
  11.   d := 2;
  12.   while d * d <= n do begin
  13.     if n mod d = 0 then exit(false);
  14.     inc(d);
  15.   end;
  16.   result := true;
  17. end;
  18.  
  19. const N = 4000000;
  20. var
  21.   count: Integer;
  22.   t0: QWord;
  23. begin
  24.   count := 0;
  25.   t0 := GetTickCount64;
  26.   for var i := 2 to N do
  27.     if IsPrime(i) then Inc(count);
  28.   writeln('sequential: ', count, ' primes, ', GetTickCount64 - t0, ' ms');
  29.  
  30.   count := 0;
  31.   t0 := GetTickCount64;
  32.   for parallel var i := 2 to N do
  33.     if IsPrime(i) then InterlockedIncrement(count);
  34.   writeln('parallel:   ', count, ' primes, ', GetTickCount64 - t0, ' ms');
  35.   readln;
  36. end.
  37.  
  38. // sequential: 283146 primes, 734 ms
  39. // parallel:   283146 primes, 109 ms

The header composes

Pool size, downto, step and chunk (how many iterations one atomic grab claims - keeps cheap bodies from paying a contended atomic per index) all fit in the usual places:

Code: Pascal  [Select][+][-]
  1. program pfor_forms;
  2.  
  3. {$mode unleashed}
  4.  
  5. begin
  6.   var total := 0;
  7.  
  8.   for parallel var i := 1 to 1000 do                 // pool = CPU count
  9.     InterlockedExchangeAdd(total, i);                // +500500
  10.  
  11.   for parallel(4) var i := 100 downto 1 do           // explicit 4 workers
  12.     InterlockedExchangeAdd(total, i);                // +5050
  13.  
  14.   for parallel var i := 1 to 999 step 2 chunk 64 do  // 1,3,5,... claimed 64 at a time
  15.     InterlockedExchangeAdd(total, i);                // +250000
  16.  
  17.   writeln(total);   // 755550
  18.   readln;
  19. end.

Without chunk the block size defaults to about four grabs per worker; parallel(1) degenerates to a plain sequential loop, and the calling thread is always one of the workers - it doesn't sit idle.

WorkerIndex / WorkerCount

Inside the body two implicit read-only locals identify the executing worker - the clean way to keep per-worker private state (a partial sum, a scratch buffer) with no locking at all:

Code: Pascal  [Select][+][-]
  1. program pfor_worker;
  2.  
  3. {$mode unleashed}
  4.  
  5. var
  6.   acc: array[0..3] of Integer;    // one private slot per worker
  7.   total: Int64;
  8.   w: Integer;
  9. begin
  10.   for parallel(4) var i := 1 to 100000 do
  11.     acc[WorkerIndex] := acc[WorkerIndex] + i;   // private slot - no atomics needed
  12.   total := 0;
  13.   for w := 0 to 3 do
  14.     total := total + acc[w];    // combine after the barrier
  15.   writeln(total);               // 5000050000
  16.   readln;
  17. end.

Notes
  • The loop variable must be declared inline (for parallel var i := ...) - each worker owns its copy; a shared pre-existing counter is rejected.
  • An exception in a body is caught, the pool joins, and the first one is re-raised on the calling thread - a fault surfaces at the loop, not as a crash on a helper thread.
  • continue works as usual; break cancels cooperatively (no new iterations start, running ones finish). exit and goto out of the body are compile errors.
  • Nested parallel loops don't oversubscribe: an inner for parallel on a worker runs sequentially unless you opt in with an explicit parallel(N).
  • parallel is context-sensitive - only recognized between for and the header, so existing code using it as an identifier keeps compiling. Same for chunk.
  • On Unix add cthreads as the first unit (the compiler hints about it); on Windows it works out of the box.
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

schuler

  • Sr. Member
  • ****
  • Posts: 376
I have not properly tested this PR. This for you to know what I am experimenting with:
https://github.com/fpc-unleashed/freepascal/pull/13
« Last Edit: July 09, 2026, 08:33:05 pm by schuler »

schuler

  • Sr. Member
  • ****
  • Posts: 376
Quote
for parallel var i := 1 to N do ...

Do you have benchmarks (single thread / threaded)?

Fibonacci

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

Had a look - that's a big one: the whole -O4 pass suite (loop opts, vectorization, x86 tail calls), ~8k lines (not counting tests). Nice work - and I see it's AI-assisted (Fable 5 in the commit trailers). No problem here, I use AI myself; appreciate the effort either way. Did you submit this to the official FPC repo too, or Unleashed only? If it's Unleashed-only, maybe better to hang it off a new optimization level (-O5) instead of -O4?

Heads-up: I'm planning to sync with upstream soon. That AARCH64 SEH MR that sat for 5 months finally got merged 2 or 3 days ago, so there's finally a "reason" to pull mainstream in.

One thing though - I see your changes are branched off feat/lightweight-generics. If I do the sync that could get messy, and you won't be able to tell what actually broke. Next time better branch off main or devel and put your work there. You've still got Fable 5 in the plan until July 12 before it goes paid per-prompt - maybe enough to squeeze in a rebase onto main? :)



Do you have benchmarks (single thread / threaded)?

No benchmarks beyond the one in the announcement above (primes to 4M: 734 -> 109 ms). But faster is a given - by default, if you don't pass a thread count, it uses as many threads as you have cores, so the boost scales straight with your CPU. And it's dead simple: one keyword on an ordinary for loop, no setup.
« Last Edit: July 09, 2026, 05:23:00 pm by Fibonacci »
FPC Unleashed: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub

ALLIGATOR

  • Sr. Member
  • ****
  • Posts: 457
  • I use FPC [main] 💪🐯💪
I have not properly tested this PR. This for you to know what I am experimenting with:
https://github.com/fpc-unleashed/freepascal/pull/11

Wow! This really looks like something interesting! It would be really cool if we could merge some part of it - or even all of it - with the main branch of the original FPC.
I may seem rude - please don't take it personally

Fibonacci

  • Hero Member
  • *****
  • Posts: 1046
  • Behold, I bring salvation - FPC Unleashed
Synced with upstream

FPC Unleashed just pulled the latest upstream on both sides: the compiler and RTL from FPC trunk, and the IDE from Lazarus main (as of today, 2026-07-09).

So every mainline fix and improvement since the last sync now sits underneath the full Unleashed feature set - you get the newest FPC and Lazarus and {$mode unleashed} at the same time, no divergence lag.
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.
Good job, now let's see if the installer works again ;)
Yes. This time it worked. ;D
« Last Edit: July 09, 2026, 05:20:07 pm 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
New types: Int128 / UInt128

Always available as types; integer literals wider than 64 bits are gated by {$modeswitch int128}, on by default in {$mode unleashed}.

Docs: docs/int128.md

The same step Int64 once took over Integer, taken once more: native 128-bit signed and unsigned integers that behave like any other ordinal. Arithmetic, div/mod, shifts, bitwise, comparisons, inc/dec/succ/pred, abs/odd/sqr, for loops, case, range (-Cr) and overflow (-Co) checking, Str/Val and Write/Read - all of it, no unit to add.

Code: Pascal  [Select][+][-]
  1. program int128_demo;
  2.  
  3. {$mode unleashed}
  4.  
  5. var
  6.   a, b: Int128;
  7.   u: UInt128;
  8. begin
  9.   a := 10000000000000000000;   // 1e19 - past high(Int64), a real 128-bit literal
  10.   b := a * a div 3;            // full 128-bit multiply and divide
  11.   writeln(b);                  // 33333333333333333333333333333333333333
  12.   writeln(high(Int128));       // 170141183460469231731687303715884105727
  13.   u := not UInt128(0);
  14.   writeln(u);                  // 340282366920938463463374607431768211455 (2^128 - 1)
  15.   writeln(u shr 100);          // 268435455 (2^28 - 1)
  16.   writeln(a mod 7);            // 3
  17.   readln;
  18. end.

Registers, not calls

On x86_64 a 128-bit value lives in a pair of 64-bit registers and gets the same treatment Int64 gets on 32-bit CPUs. The everyday operations compile to inline instruction sequences, not helper calls: add/adc and sub/sbb carry chains, one instruction per half for the bitwise ops, shld/shrd shifts, and multiplication as one mul plus two cross-term imuls. Parameters and results stay in registers; on SysV targets the calling convention matches C's __int128 exactly, so cdecl interop with gcc/clang works out of the box. Only the genuinely large operations go through RTL helpers - div/mod, overflow-checked multiply, Str/Val, float conversions - mirroring exactly what Int64 does on i386.

The other 64-bit CPUs get the same register-pair codegen through the generic layer: aarch64 (adds/adc), powerpc64, riscv64, loongarch64, mips64 and sparc64 with their native carry idioms. Everything else - down to the 8- and 16-bit targets - runs on the portable helper model (the same approach gcc uses with __divti3 and friends), so the types work everywhere, they're just fastest where the hardware helps.

Notes
  • Wide literals work in every base ($ hex, % binary, & octal, decimal with any digit count). One gotcha inherited from the language, not new here: a 16-digit hex literal follows the existing Int64 literal rules, so $ffffffffffffffff is -1 and sign-extends to all 128 bits, not 2^64 - 1. The compiler warns when it lands in an unsigned slot - the same warning QWord has always given for that literal - and decimal 18446744073709551615 gives you the low-64 mask.
  • The type names are usable from objfpc/delphi too; only the oversized literal needs {$modeswitch int128}. Existing code is unaffected - without the switch, oversized literals keep the historical real-constant behavior.
  • Run-tested on x86_64 and i386; the register-pair codegen is compile-verified on the other 64-bit targets.
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.
Can I just use the installer anew? Because that is a neat feature.
Any "programmer" that knows only one programming language is not a programmer

440bx

  • Hero Member
  • *****
  • Posts: 6560
I have a question regarding the new int128...

"Normal FPC", when dealing with mixed signed expressions promotes types as needed up to int64.  This causes problems when the expression uses qwords because there is no int128 "superset type" that could be used to "homogenize" the expression.

My question is: in unleashed, if the modeswitch int128 is enabled and an expression has item types int64 and qword in it, will the compiler automatically promote the types to int128 to do the calculations ?
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

Thaddy

  • Hero Member
  • *****
  • Posts: 19498
  • Glad to be alive.
I have a question regarding the new int128...

"Normal FPC", when dealing with mixed signed expressions promotes types as needed up to int64.  This causes problems when the expression uses qwords because there is no int128 "superset type" that could be used to "homogenize" the expression.

My question is: in unleashed, if the modeswitch int128 is enabled and an expression has item types int64 and qword in it, will the compiler automatically promote the types to int128 to do the calculations ?
To add: does the compiler optimize to avx2 instructions? Because that is the only benefit when using 128 bit integer types......
« Last Edit: July 17, 2026, 06:10:14 pm by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12959
  • FPC developer.
  • sse2 has 128-bit registers
  • avx(2) has 256-bit registers

But both only support integers up to 64-bit as base type.

 

TinyPortal © 2005-2018