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

creaothceann

  • Sr. Member
  • ****
  • Posts: 471
Quote from: site
We welcome bold ideas and experimental features that push Pascal forward. [...] New language ideas - propose modeswitches, syntax extensions, or compiler enhancements via GitHub Issues or Discussions. Even if you don't have an implementation yet, a well-described idea with use cases is valuable.
Assuming the RTTI feature is not for size reasons but for hiding identifiers: Instead of removing RTTI, they could be stored in an obfuscated or encrypted form (e.g. via cipher, embedded password, distributing 2 nybbles into 2 bytes, etc.), and fixed in-memory right before unit initializations.

- - -

I agree that a fallthrough feature/keyword (disabled by default of course) would be nice in some situations.

- - -

I work a lot with state machines, specifically emulators of 8/16-bit CPUs. There's usually a case BytecodeValue of that jumps to a particular opcode implementation, and there are hundreds of them (256 cases for a MOS 6502, up to 512 cases for a WDC 65c816).

In addition to the already existing function pointers (and procedure pointers), for performance reasons it would be nice if there was a computed goto. This would allow emulator writers to craft a custom "handmade" case-of in which each case can jump to another case, depending on the next bytecode instruction. (The technique was discussed for example here.)

In addition to the ability to declare an array of labels, it'd look like this:

Code: Pascal  [Select][+][-]
  1. unit MOS_6502;
  2.  
  3. {$ModeSwitch AdvancedRecords}
  4. {$Modeswitch DuplicateLocals}
  5.  
  6. interface
  7.  
  8. type
  9.         T_6502 = record
  10.                 var
  11.                         A, IR, X, Y      : byte;  // accumulator, instruction register (opcode), indices
  12.                         PC, S            : word;  // program counter, stack pointer
  13.                         c, z, i, d, v, n : byte;  // flags
  14.  
  15.                 procedure Run(var stop : boolean);
  16.                 end;
  17. var
  18.         Memory : array[word] of byte;
  19.  
  20. implementation
  21.  
  22. procedure T_6502.Run(var stop : boolean);  // in this version, stop is checked only on interrupts
  23. label
  24.         Opcode[$00..$FF],  // declare 256 labels at once
  25.         Resume, Restore;
  26. var
  27.         A, IR, X, Y      : byte;
  28.         PC, S            : word;
  29.         c, z, i, d, v, n : byte;
  30. begin
  31.         // initialize local variables
  32.         A := Self.A;  IR := Self.IR;  X := Self.X;
  33.         Y := Self.Y;  PC := Self.PC;  S := Self.S;
  34.         c := Self.c;  z  := Self.z;   i := Self.i;
  35.         d := Self.d;  v  := Self.v;   n := Self.n;
  36.         goto Resume;
  37.  
  38.         Opcode[$00]:  // software interrupt (BRK) or hardware interrupt (RESET, NMI, IRQ)
  39.                 if stop then goto Restore;
  40.                 Resume:
  41.                 {...}
  42.                 IR := Memory[PC];  Inc(PC);
  43.                 goto Opcode[IR];
  44.  
  45.         Opcode[$01]:  // ORA zeropage[$nn + X]
  46.                 {...}
  47.                 IR := Memory[PC];  Inc(PC);
  48.                 goto Opcode[IR];
  49.  
  50.         Opcode[$02]:  // KIL (undocumented opcode)
  51.                 {halt CPU until RESET}
  52.                 goto Opcode[00];
  53.  
  54.         { $03..$FF }
  55.  
  56.         Restore:  // save local variables to global state
  57.         Self.A := A;  Self.IR := IR;  Self.X := X;
  58.         Self.Y := Y;  Self.PC := PC;  Self.S := S;
  59.         Self.c := c;  Self.z  := z;   Self.i := i;
  60.         Self.d := d;  Self.v  := v;   Self.n := n;
  61. end;
  62.  
  63. end.
« Last Edit: March 19, 2026, 03:11:11 pm by creaothceann »

Fibonacci

  • Hero Member
  • *****
  • Posts: 1103
  • Behold, I bring salvation - Unleashed Pascal
    • fibo.gg
Two updates!

1. Fork synced with upstream

FPC Unleashed is now synced with FPC trunk. The fork is up to date.

2. Multiline strings enabled by default in {$mode unleashed}

I noticed a nice modeswitch in the FPC codebase and enabled it by default in unleashed mode. Sample code:

Code: Pascal  [Select][+][-]
  1. program app;
  2.  
  3. {$mode unleashed}
  4.  
  5. begin
  6.   randomize;
  7.   var s := if random(10) >= 5 then '''
  8.   line 1
  9.   line 2
  10.   '''
  11.   else 'single line';
  12.  
  13.   writeln(s);
  14.   readln;
  15. end.
  16.  



@440bx: Thanks for the long post, I really appreciate such detailed feedback. I read it, but let me respond later because it's a lot to address and I'm fried for today.

One general note though: if anyone wants a feature in Unleashed, the best approach is to describe it with concrete use cases and example code. We can then discuss and find a reasonable design, and figure out who implements it - currently that's just me running and maintaining this, all implementation and merging lands on my shoulders, so bandwidth is limited. And remember - it's just a toy hobby project :D

I can merge things that upstream won't take (or will take, but years will pass), for example Gareth's Pure Functions MR looked interesting, but it broke after the recent upstream sync. If Gareth fixes it I'll gladly pull it in. Same goes for anyone who has a ready implementation - PRs are welcome.

FYI, @Warfley is the second admin on the fpc-unleashed GitHub - maybe he'll wake up and start contributing. He knows his stuff and has done some excellent FPC features. Actually, that's how fpc-unleashed started - it began with me merging @Warfley's work. I was particularly interested in his Record Composition feature, which has been waiting for upstream merge for years and probably won't make it in. At some point it broke along with upstream bug fixes  >:( and couldn't be merged anymore - maybe I'll revisit it, or maybe @Warfley himself will pick it up again.
Unleashed Pascal: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub ⭐

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12760
  • Debugger - SynEdit - and more
    • wiki
On a general note (about some of the ideas that are now coming up in this thread)...

Whenever the idea comes from "in some language you can do..." => evaluate it by getting opinions from people who never used that some language.

I myself have switched between language (who hasn't), and each language has its own style/flair/approach. When I switch, I have to pay attention to switch the way I think and approach stuff. But that is a good thing.

Example: there are language where you can define your apps logic in data (constant data, in structures, that define what the app does). However, that only works for those languages, and it certainly wouldn't make any other language better. But when switching, the brain wants to do it that way. Yet it does not mean, it should be done.


Disclosure, I probably am not going to use this fork, because I don't need, nor want any of those features.

But, for those who need them. Consider how they affect code readability (they may not affect your ability to read the code, if you are more fluent in some other language, and find that you often misread Pascal as if it were that other language / but consider if you would read the code based on the current Pascal definition(s))

An inline var would likely not dampen readability. (in terms of being crossly misleading on what it does)

A change to case (labels vs conditions / or fallthrough) => both of those changes are a different feature. They have nothing to do with case as it is in Pascal. They should never use the "case" keyword. (well at least the first / the fallthrough is just a new way to have a "goto" statement)
If they were done, they need a new keyword.

Case in Pascal is a sort-of "jump table" lookup, it guarantee uniqueness of the labels. Reading the "case" statement gives that  guarantee. Breaking that, isn't an addition, breaking that is breaking the language.
If the label should be an expression, then make it an entirely new statement with its own new keyword (not just a modifier, but a new keyword).

That said, IMHO the "case val of expression" only would be ok, if the compiler could guarantee at runtime that only one expression will be matched.

That all is said, in case this is considered at all. Which is not my call, as clearly this is for those who want a different approach that I would chose.

creaothceann

  • Sr. Member
  • ****
  • Posts: 471
(well at least the first / the fallthrough is just a new way to have a "goto" statement)

[...]

Case in Pascal is a sort-of "jump table" lookup, it guarantee uniqueness of the labels. Reading the "case" statement gives that  guarantee. Breaking that, isn't an addition, breaking that is breaking the language.

Under the hood fallthrough is definitely not a goto though, it's the absence of a goto, and the fastest way to continue at another case. Every case without fallthrough has an implicit goto (except for the last case), which does make a difference (consuming an entry in the CPU's Branch Predictor & Branch Target Buffer, unless optimized down to a RET instruction). Case also isn't guaranteed to be a jump table; it can compile to the equivalent of several if-else statements, each with its own Branch allocations.

Zvoni

  • Hero Member
  • *****
  • Posts: 3574
On a general note (about some of the ideas that are now coming up in this thread)...

Whenever the idea comes from "in some language you can do..." => evaluate it by getting opinions from people who never used that some language.

I myself have switched between language (who hasn't), and each language has its own style/flair/approach. When I switch, I have to pay attention to switch the way I think and approach stuff. But that is a good thing.

Example: there are language where you can define your apps logic in data (constant data, in structures, that define what the app does). However, that only works for those languages, and it certainly wouldn't make any other language better. But when switching, the brain wants to do it that way. Yet it does not mean, it should be done.
*snip*

Case in Pascal is a sort-of "jump table" lookup, it guarantee uniqueness of the labels. Reading the "case" statement gives that  guarantee. Breaking that, isn't an addition, breaking that is breaking the language.
If the label should be an expression, then make it an entirely new statement with its own new keyword (not just a modifier, but a new keyword).

That said, IMHO the "case val of expression" only would be ok, if the compiler could guarantee at runtime that only one expression will be matched.

That all is said, in case this is considered at all. Which is not my call, as clearly this is for those who want a different approach that I would chose.
Everything true you said.
And i was in the same boat coming from vb/vba to Pascal some 8 years ago.

And don't get me wrong: I said "i miss that feature".
it doesn't mean i haven't made my peace with it. I have found my workflow if i encounter such a situation, and am happy with it

OTOH, i still have to do a lot of stuff with VBA in Office (work-related), and i don't know how many times i have cursed VBA into next week by NOT supporting constant arrays (always have to use static-arrays with an "InitThisArray"-Procedure-Call.
But: It's the same: I have made my peace with it, of never having that feature in VBA
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

Zvoni

  • Hero Member
  • *****
  • Posts: 3574
(well at least the first / the fallthrough is just a new way to have a "goto" statement)

[...]

Case in Pascal is a sort-of "jump table" lookup, it guarantee uniqueness of the labels. Reading the "case" statement gives that  guarantee. Breaking that, isn't an addition, breaking that is breaking the language.

Under the hood fallthrough is definitely not a goto though, it's the absence of a goto, and the fastest way to continue at another case. Every case without fallthrough has an implicit goto (except for the last case), which does make a difference (consuming an entry in the CPU's Branch Predictor & Branch Target Buffer, unless optimized down to a RET instruction). Case also isn't guaranteed to be a jump table; it can compile to the equivalent of several if-else statements, each with its own Branch allocations.

Don't forget in a Fallthrough by condinitonally changing the value for the Variable within a case-label, that way even forcing additional case-executions
Code: C  [Select][+][-]
  1. c=5;
  2. Switch(c){
  3.    Case 1: Printf('1'); Break;
  4.    Case 5: Printf('5'); c=10;  /*!!!!!*/
  5.    Case 6: If(c=10){Result=6; Break;}
  6.    Case 10: Printf('unreachable');
  7. }
Though since i'm not a c-guy, i have no idea if above is utter nonsense  :P :P :P
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

440bx

  • Hero Member
  • *****
  • Posts: 6619
Personally, I believe that the "base" for the "unleashed" fork should be the current FPC "fixes".

The reason I would choose that version is because:

1. ensure that it is based on what is hopefully, currently, the most bug-free version.
2. leave open for discussion what features are to be implemented and, most importantly, HOW they will be implemented.

Specifically about (2) above.  IMO, the current implementation of inline variables having global scope is inherently incorrect because inline variables should essentially be "throw away" variables that only apply to a few statements.  In addition to that, locality should also apply to constants and types.

That's just a one example of a feature I believe has a deficient implementation.  There are other features in trunk that, IMO, are also deficient in one way or another.

The underlying point is: before implementing a new feature, it should be _very carefully_ analyzed and designed because once it becomes part of the language, there will be code that uses it which makes correcting/modifying the design difficult or impossible.  As a result, the corrected design has to coexist with the old design (in order not to break code) which is undesirable.  There should be a strong emphasis on attempting to get it right the first time, in order to minimize design fixes that need to coexist with deficient implemtations.
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

Fibonacci

  • Hero Member
  • *****
  • Posts: 1103
  • Behold, I bring salvation - Unleashed Pascal
    • fibo.gg
Do you mean this, @Zvoni and @Martin_fr?

Code: Pascal  [Select][+][-]
  1. var i := 2;
  2.  
  3. case true of
  4.   (i = 1): writeln('i eq 1');
  5.   (i = 2): writeln('i eq 2'); // execute
  6.   (i > 0): writeln('i gt 0'); // should NOT execute
  7.   (i = 3): writeln('i eq 3');
  8.   (i = 4): writeln('i eq 4');
  9.   else writeln('bar');
  10. end;

EDIT: I forgot: in Visual Basic it's possible to use multiple Boolean-Expressions inside a Case-label
Code: [Select]
Dim i As Long
    Select Case True
        Case i = 0
            Debug.Print "i equals zero"
        Case i > 100 And User="Zvoni"  //<-- THIS ONE
            Debug.Print "i is greater than 100 and the User=Zvoni"
        Case i < -50 Or User="Laura"   //<-- THIS ONE
            Debug.Print "i is smaller than -5 or the User=Laura"
        Case Else
            Debug.Print "Doh!"
    End Select
Now imagine the usecase of this feature

Like this?

Code: Pascal  [Select][+][-]
  1. var i := 2;
  2. var s := 'hi';
  3.  
  4. case true of
  5.   (i = 1): writeln('i eq 1');
  6.   (i = 2) and (s = 'hi'): writeln('i eq 2 and s eq "hi"'); // execute
  7.   (i = 123) or (s = 'hi'): writeln('i eq "123" or s eq "hi"'); // should NOT execute
  8.   (i = 3): writeln('i eq 3');
  9. else
  10.   writeln('no match');
  11. end;

Is this okay for the "case" keyword?



Personally, I believe that the "base" for the "unleashed" fork should be the current FPC "fixes".

Does it have anon functions? If not, I don't see much point in using it.
Unleashed Pascal: async/await, parallel for, match, tuples, string interpolation, inline vars, autofree, no-RTTI & tons more. Star on GitHub ⭐

creaothceann

  • Sr. Member
  • ****
  • Posts: 471
Speaking of "case": I just remembered another feature whose absence I noticed recently, the ability to have a "variant block" anywhere in the definition of a record.

This would be impossible in current Free Pascal without having to write getter & setter methods:

Code: Pascal  [Select][+][-]
  1. {$ModeSwitch AdvancedRecords}
  2.  
  3. type
  4.         MyRecord = packed record
  5.                 i, j, k : Int64;
  6.                
  7.                 private
  8.                
  9.                 case integer of
  10.                         0: (_a, _b, _c, _d : byte );
  11.                         1: (_Value         : dword);
  12.                 end;
  13.                
  14.                 public
  15.                
  16.                 var x, y : word;
  17.                
  18.                 property a : byte read _a write _a;
  19.                 property b : byte read _b write _b;
  20.                 property c : byte read _c write _c;
  21.                 property d : byte read _d write _d;
  22.                 end;

- - -

Code: C  [Select][+][-]
  1. c = 5;
  2. switch (c)  {
  3.         case  1:  printf('1');              break;
  4.         case  5:  printf('5');  c = 10;                /*!!!!!*/
  5.         case  6:  if (c = 10) {Result = 6;  break;}
  6.         case 10:  printf('unreachable');
  7. }
Though since i'm not a c-guy, i have no idea if above is utter nonsense  :P :P :P

The value in c would only be evaluated once.
« Last Edit: March 19, 2026, 04:31:29 pm by creaothceann »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 13018
  • FPC developer.
This is something that should go into the main fpc, but maybe if developed on the fork could be merged back? That is if the creator(s) of the fork have time and interest to work on that.

It would all be about the dwarf generated by FPC. (and some to be added to FpDebug, but that goes to my todo list).

Current unmerged revisions with "dwarf" in the log entry (either filename or text, so includes things like linfodwarf)

https://www.stack.nl/~marcov/mergelogs32/dwarf.html

440bx

  • Hero Member
  • *****
  • Posts: 6619
Personally, I believe that the "base" for the "unleashed" fork should be the current FPC "fixes".

Does it have anon functions? If not, I don't see much point in using it.
I don't believe it does but, those could be "brought down" into it.  The point is to select a code base that is as bug free as reasonably possible.

I understand that for some people, probably includes you, new features are important.  While I have nothing against new and useful features, I have a _very strong_ preference for an as-bug-free-as-reasonably-possible compiler.  I believe the current fixes would provide that.

Porting new features from trunk into it would give (and necessitate) _thorough_ testing of the feature which I believe would be a plus because, as I understand it, trunk's raison d'etre isn't eliminating bugs, it's adding features which will hopefully be eventually bug free and included in some release.

Here is the _crucial_ point: a fork is utterly useless if the features it offers are bug ridden and/or its features have significant deficiencies. 

I don't know exactly how inline variables are implemented in Delphi but, I believe they are scoped, IOW, not function global.  If that is the case, having an implementation where they are function global is useless and confusing.   That's a step backwards which is the last thing Pascal needs.

I understand the desire for new features, there are some features that are dear to my heart that I want to see but, FIRST, I'd like to have a compiler with as many bugs fixed as reasonably possible and, THEN, add features (and appropriate test cases) to it which will be the next release (which hopefully would only be a few months later, not years.)

Lastly, to put all my cards on the table, I'd really like to see features added that make Pascal more parallel to C.  For instance, I would strongly support a form of record composition that would make porting into Pascal C structure definitions easier and more intuitive.
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

n7800

  • Hero Member
  • *****
  • Posts: 800
  • Lazarus IDE contributor
    • GitLab profile
As Aircode in Pascal it would look like this
Code: Pascal  [Select][+][-]
  1. Var i:Integer; //or whatever other Datatype
  2. //......
  3.   Case True Of
  4.     i=0    : Writeln('i equals zero');
  5.     i>100 : Writeln('i is greater than 100');
  6.     i<-50 : Writeln('i is smaller than -50');
  7.     Else Writeln('Doh!');
  8.   End;

Free Pascal already allows some things:

Code: Pascal  [Select][+][-]
  1. case i of
  2.   0..5      : writeln('0-5');
  3.   7         : writeln('7');;
  4.   6, 8..9   : writeln('6, 8, 9');
  5.   10..MaxInt: writeln('greater than 9');
  6.   else        writeln('negative');
  7. end;

Your example doesn't look very good, but still:

Code: Pascal  [Select][+][-]
  1. case i of
  2.   0              : Writeln('i equals zero');
  3.   101..MaxInt    : Writeln('i is greater than 100');
  4.   -MaxInt-1..-51 : Writeln('i is smaller than -50');
  5.   else             Writeln('Doh!');
  6. end;
  7. end;

To obtain boundary values, you can use certain constants ("MaxInt"), the functions ("high(i)" or "high(integer)"), type helpers ("i.MaxValue"), and so on.

n7800

  • Hero Member
  • *****
  • Posts: 800
  • Lazarus IDE contributor
    • GitLab profile

n7800

  • Hero Member
  • *****
  • Posts: 800
  • Lazarus IDE contributor
    • GitLab profile
In particular, it is interesting what is described for the loop variable.

440bx

  • Hero Member
  • *****
  • Posts: 6619
Actually, the loop variable should _always_ be "for" local. 

It is a flaw in Pascal that the value of the "for" index variable is undefined whenever the "for" ends normally.  Why do I say that ? ... very simple: that's the only case when a function global variable loses its value.  Consistency alone should require the loop index NOT to lose the value when it is defined as a function global variable (why should it lose it ???  there is NO reason for that semantic exception, it exists only to give room to the compiler to optimize the location of the index, i.e, put it in a register.)

if the loop index was declared in the "for" statement instead of globally in the function then it would make sense for the loop index to lose its value since outside the "for" the indexing variable no longer exists.

if the programmer wants to know on what value a particular loop was broken out of, the programmer should simply code something along the lines of "MyFunctionGlobalVar := LoopIndex; break;" 
And use MyFunctionGlobalVar instead of the loop variable which currently may or may not be valid depending on how the loop was exited.

For those who don't like to type, maybe "break" could have a variation valid only in "for" loops, something along the lines of "break(MyFunctionGlobalVar);".  Objections to that construction would be misplaced as it already exists for "exit" which allows specifying a value when it is used in a function but not in a procedure.

This would provide the best of both worlds because it would allow the compiler to always optimize the use of the loop index and make the programmer responsible for saving the loops exit value if it is desired all while making it crystal clear the value will or won't be used after the loop exits.


 
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

 

TinyPortal © 2005-2018