Recent

Author Topic: Useful Oxygene Features for FPC?  (Read 22664 times)

440bx

  • Hero Member
  • *****
  • Posts: 3921
Re: Useful Oxygene Features for FPC?
« Reply #15 on: August 20, 2018, 10:06:39 pm »
2) why the need of doing: "procedure ASMCODE; assembler" asm {asm code here..} end;
//why the extra assembler need? the "asm" should be sufficient here, shouldnt it?

This predates Free Pascal (TP already had it). I don't know exactly. Probably it was considered important to the parsing model to have procedure "end" match to something.
Just FYI, eliminating the "asm" keyword there would cause all kinds of problems/complications and ripple through the entire syntactic structure of the language.

This simple sequence of code from the documentation can be used to show some of the problems it would create:
Code: Pascal  [Select][+][-]
  1. procedure Move(const source;var dest;count:SizeInt);assembler;  
  2. var  
  3.   saveesi,saveedi : longint;  
  4. asm  
  5.   movl %edi,saveedi  
  6. end;
Without the "asm" keyword, there is no demarcation between the variable declaration and the code.  That would require a completely different parser than the current one. 

If someone where to make the case that "begin" would be sufficient to mark the end of the variable declaration and the start of the code, they would run into another problem.  Specifically, when the parser sees "begin", it expects a pascal statement not an assembly language statement.  if "begin" were to be used instead of "asm" then the parser needs to be able to parse pascal and assembly and, be able to infer the switch based on a single token (which may not always be possible - what if there is a Pascal function/procedure declared somewhere else in the code named "movl" ?).  It may be possible to implement something like that but, the parser code would be a lot more complicated and would put into question why "begin" is needed/required most everywhere else.

It's not always obvious but, what may look like a small change, can have repercussions in the entire grammar of the language.   Attempting to eliminate the "asm" keyword in the case above is a fairly good example of that.
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v3.2) on Windows 7 SP1 64bit.

soerensen3

  • Full Member
  • ***
  • Posts: 213
Re: Useful Oxygene Features for FPC?
« Reply #16 on: August 20, 2018, 10:17:12 pm »
2) why the need of doing: "procedure ASMCODE; assembler" asm {asm code here..} end;
//why the extra assembler need? the "asm" should be sufficient here, shouldnt it?
I think it was the assembler hint he wanted to drop, not the asm part.

:)
Quote
4) why doing this for extensions: "TExtension = Type helper for record(TRecord)" instead of: "TExtension = extension of(TMyRecord)" would actually do the same because the extensiontype is based on the type it extens obviously
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$modeswitch typehelpers}{$macro on}{$define extension := Type helper for}
  2. type
  3.   TMyExtension = Extension integer
  4.     function returntype:TTypekind;
  5.   end;
  6.  
  7.   function TMyExtension.returntype:TTypekind;
  8.   begin
  9.     Result := GetTypeKind(integer);
  10.   end;
  11.  
  12.  
  13.  begin
  14.    writeln(1000000.returntype);
  15.  end.
There you go... 8-)

I thought for sure that you hated macros! :P
Lazarus 1.9 with FPC 3.0.4
Target: Manjaro Linux 64 Bit (4.9.68-1-MANJARO)

440bx

  • Hero Member
  • *****
  • Posts: 3921
Re: Useful Oxygene Features for FPC?
« Reply #17 on: August 20, 2018, 10:59:11 pm »
2) why the need of doing: "procedure ASMCODE; assembler" asm {asm code here..} end;
//why the extra assembler need? the "asm" should be sufficient here, shouldnt it?
I think it was the assembler hint he wanted to drop, not the asm part.
Re-reading that, you may very well be right.  One reason the assembler keyword is still useful, maybe even needed in other cases I can't think of at this time, is that it informs the compiler that the whole of the function/procedure code can be directly output to the assembler file,  the asm keyword doesn't imply that since it may be found in a Pascal block.


(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v3.2) on Windows 7 SP1 64bit.

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 11351
  • FPC developer.
Re: Useful Oxygene Features for FPC?
« Reply #18 on: August 21, 2018, 03:09:13 am »
440bx: good point with the variable declarations.

IIRC TP3 did not do inline asm the way it is done in lazarus. One did the declaration and then included an object file (asm source assembled to .o(bj) with the TASM assembler) with $L.  In that case there is no asm, and possibly the assembler directive signalled that option.

In newer TPs and Delphi this concept isn't used much.

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Re: Useful Oxygene Features for FPC?
« Reply #19 on: August 21, 2018, 03:52:34 am »
IIRC TP3 did not do inline asm the way it is done in lazarus. One did the declaration and then included an object file (asm source assembled to .o(bj) with the TASM assembler) with $L.

Nope! TP3 did *inline* "assembler" this  way:
Code: Pascal  [Select][+][-]
  1. { XMODEM-CRC calculation by Scott Murphy }
  2. procedure updcrc(a : byte);
  3. begin
  4.    inline( $8A/$46/$04/        {MOV     AL,[BP+04]}
  5.            $8B/$1E/crcval/     {MOV     BX,crcval}
  6.            $B9/$08/$00/        {MOV     CX,0008}
  7. {loop0}    $D0/$E0/            {SHL     AL,1}
  8.            $D1/$D3/            {RCL     BX,1}
  9.            $73/$04/            {JNC     loop1}
  10.            $81/$F3/$21/$10/    {XOR     BX,$1021}
  11. {loop1}    $E2/$F4/            {LOOP    loop0}
  12.            $89/$1E/crcval)     {MOV     crcval,BX}
  13. end;
  14.  

It was TP4 which introduced $L and .obj linking. The internal assembler, asm and the assembler directive weren't introduced until TP6 or 7. There were then units and programs which were more assembler with a Pascal veneer than Pascal proper  :D

ETA (for historians):
Checked the manuals and:
1) TP3 also allowed an external func/proc modifier to link in external code:
Code: Pascal  [Select][+][-]
  1. {for x86 platforms (CP/M-86 / DOS)}
  2. procedure MyExternal(var AnInt: Integer); external 'MYEXTERN.BIN';
  3.  
  4. {for CP/M-80}
  5. procedure MyExternal(var AnInt: Integer); external $0F83;
  6.  

2) The internal assembler and the associated keywords assembler and asm (a feature collectively known as BASM) did, indeed, appear with TP 6.0
« Last Edit: August 22, 2018, 08:44:00 pm by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

Shpend

  • Full Member
  • ***
  • Posts: 167
Re: Useful Oxygene Features for FPC?
« Reply #20 on: August 22, 2018, 04:57:45 pm »
@440bx:
Yes, I understand your concerns About some stuff I declared in my post.

And the stuff I declared was also not meant to be rude against FPC :D 

it was just some small syntactical Thing I mentioned but nevermind all in all ist a clean and strong language, hence my usage of it :D


@blaazen:
Sry but the suggestions you brought up are not really a clean and pascalish way for me and yes it is Shorter but thats the whole Thing About Pascal, sometimes longer but more cleaner in Expression and i persoanlly wouldnt want to loose that and, this reminds me more of a pure C/C#/C++ Syntax, but to be honest oxygene did that good, with : tuple of (), because you also say: Array of   and not [Var1, Var2, Var3], pls do not a JS-language out of Pascal

@Rusik:
I dont get that this is not good for Pascal, i dont wanna do it a JS language but async calls are AFAIK needed to work for hyperthreading models? otherwise how you wanna reach a good way to do paralell Things? (I am open ofc for suggestions, this is not a sarchastic line!)

Quote
Clean design is not the ultimate objective.  Compatibility is. Freepascal is not a language experiment, but a production level open source compiler with a large set of libraries and a long compatibility trackrecord.  Something you acknowledge in the "Reasons why I came back to FPC+Lazarus".

If you look at the forum, and see the  constant bickering over compatibility issues you might don't believe it, but those are relatively in the fringes. Actually the high level of compatibility makes people obsess about the details.
@marcov:

I understand that fully and I agree with that, but still its nice when you have some stuff which can be (actually) be done by the compiler and to express it with reserved keywords from the respected compiler would be nice I think. But the fortunate thing in FPC atleast is, that you can workaround these missing things relatively easily by yourself, thats good!

@Thaddy:
Nice, thx for the suggestions, but I tested your thing and it worked well, but i wanted to do the exact same but except, i wanted to change "Extension" to "Extensionof(<type>)" and he complains :O

here is how I did it  %)

Code: Pascal  [Select][+][-]
  1. program test;
  2.  
  3. {$mode objfpc}
  4. {$modeswitch typehelpers}
  5. {$macro on}
  6. {$define name}
  7. {$define extensionOf(name) := Type helper for name}
  8. type
  9.   TClass = class(TObject);
  10.  
  11.   TMyExtension = extensionOf(integer)
  12.     const value = 100000;
  13.   end;
  14.  
  15. begin
  16. end.
  17.  
« Last Edit: August 22, 2018, 05:07:21 pm by Shpend »

Blaazen

  • Hero Member
  • *****
  • Posts: 3237
  • POKE 54296,15
    • Eye-Candy Controls
Re: Useful Oxygene Features for FPC?
« Reply #21 on: August 22, 2018, 07:25:38 pm »
@Shpend
Sry but the suggestions you brought up are not really a clean and pascalish way for me and yes it is Shorter but thats the whole Thing About Pascal, sometimes longer but more cleaner in Expression and i persoanlly wouldnt want to loose that and, this reminds me more of a pure C/C#/C++ Syntax, but to be honest oxygene did that good, with : tuple of (), because you also say: Array of   and not [Var1, Var2, Var3], pls do not a JS-language out of Pascal

1) but this is already possible in Oxygen:
Code: Pascal  [Select][+][-]
  1. var s: String;
  2. var i: Int32;
  3. (s, i) := MyTupleMethod();

2) if I have to declare
Code: Pascal  [Select][+][-]
  1. var t: tuple of (String, Int32, Boolean)
and then I can do
Code: Pascal  [Select][+][-]
  1. var s := t[0];
  2. var i := t[1];

there's not a big difference to
Code: Pascal  [Select][+][-]
  1. type
  2.   TRec = record
  3.     s: string;
  4.     i: Integer;
  5.     b: Boolean;
  6.   end;
  7. var t: TMyRec;
  8.  
and then
Code: Pascal  [Select][+][-]
  1. s:=t.s;
  2. i:=t.i;
  3.  
and the benefit is that
Code: Pascal  [Select][+][-]
  1. i:=t.i;
is more readable than
Code: Pascal  [Select][+][-]
  1. i:=t[1];
Lazarus 2.3.0 (rev main-2_3-2863...) FPC 3.3.1 x86_64-linux-qt Chakra, Qt 4.8.7/5.13.2, Plasma 5.17.3
Lazarus 1.8.2 r57369 FPC 3.0.4 i386-win32-win32/win64 Wine 3.21

Try Eye-Candy Controls: https://sourceforge.net/projects/eccontrols/files/

Shpend

  • Full Member
  • ***
  • Posts: 167
Re: Useful Oxygene Features for FPC?
« Reply #22 on: August 22, 2018, 08:34:46 pm »
@blaazen:
Your sugeestions now are better and AFAIK different from before because you wrote before:


Code: Pascal  [Select][+][-]
  1. function GetTupleBack: integer, String;  //this was your former suggestion
  2.  
  3. //but i would rather have a strongly expression as I am expecting it from the pascal-compiler, like this here:
  4. function GetTupleBack: tuple of (integer, String);
  5.  
  6.  
  7. //or actually even more strict and how it actually needs to be done
  8. type
  9.   TMyGoodNamedTuple = tuple of (integer, String);
  10.  
  11. function GetTupleBack: TMyGoodNamedTuple
  12.  

this is what I want to force a developer to do:

always give a full tuple of back or declare it but not just its parts alone!
« Last Edit: August 22, 2018, 08:45:25 pm by Shpend »

Shpend

  • Full Member
  • ***
  • Posts: 167
Re: Useful Oxygene Features for FPC?
« Reply #23 on: August 22, 2018, 08:44:42 pm »
And to be honest with you guys:

I have actually an even better Idea than touples  >:D

why not making all records/classes/objects automatically behave like tuples.

This means that, whenever you have such a (normal) case:

Code: Pascal  [Select][+][-]
  1.   type
  2.     TRecord = record
  3.        a: String[30];
  4.        b: integer;
  5.        c: boolean;  
  6.     end;
  7.  
  8. so when you do this:
  9.  
  10. a, b, c: TRecord;
  11. res: boolean;
  12.  
  13. res := (a = b) or (b = c) or (a = c)  //just some senseless Code...
  14.  

You should be able to have it working like tuples by Default, that you can do all comparison stuff automaticall.

this is not possible by Default, you have to override all These Operators: [<>, =, <=, >= etc...] for it to work!

and then you have implicit tuples without the Need of creating a senseless new Concept :)
« Last Edit: August 22, 2018, 09:55:29 pm by Shpend »

asdf121

  • New Member
  • *
  • Posts: 35
Re: Useful Oxygene Features for FPC?
« Reply #24 on: August 22, 2018, 10:07:43 pm »
MULTITHREADING
  1) Lock
  2) Parallelism
  3) future types
  4) await + async

I just remembered that I read a feature request about something like that some time ago. I guess that would cover all the things from your list but someone has to implement it.  :D

PascalDragon

  • Hero Member
  • *****
  • Posts: 5444
  • Compiler Developer
Re: Useful Oxygene Features for FPC?
« Reply #25 on: August 23, 2018, 07:38:58 am »
why not making all records/classes/objects automatically behave like tuples.

This means that, whenever you have such a (normal) case:

Code: Pascal  [Select][+][-]
  1.   type
  2.     TRecord = record
  3.        a: String[30];
  4.        b: integer;
  5.        c: boolean;  
  6.     end;
  7.  
  8. so when you do this:
  9.  
  10. a, b, c: TRecord;
  11. res: boolean;
  12.  
  13. res := (a = b) or (b = c) or (a = c)  //just some senseless Code...
  14.  

You should be able to have it working like tuples by Default, that you can do all comparison stuff automaticall.

this is not possible by Default, you have to override all These Operators: [<>, =, <=, >= etc...] for it to work!

and then you have implicit tuples without the Need of creating a senseless new Concept :)
Not a good idea as the compiler's rule is to not allow custom operators when there is a build-in operator. And there are cases where you don't want to restrict what e.g. = compares as some information in the record might not be important for equality.

@Thaddy:
Nice, thx for the suggestions, but I tested your thing and it worked well, but i wanted to do the exact same but except, i wanted to change "Extension" to "Extensionof(<type>)" and he complains :O

here is how I did it  %)

Code: Pascal  [Select][+][-]
  1. program test;
  2.  
  3. {$mode objfpc}
  4. {$modeswitch typehelpers}
  5. {$macro on}
  6. {$define name}
  7. {$define extensionOf(name) := Type helper for name}
  8. type
  9.   TClass = class(TObject);
  10.  
  11.   TMyExtension = extensionOf(integer)
  12.     const value = 100000;
  13.   end;
  14.  
  15. begin
  16. end.
  17.  
FPC does not support macros with parameters. And before you ask for them: that idea was last rejected a few weeks ago.

1) Generics: why introduce a "generic/specialize" keyword, its actually unneseccarry to introduce because what we see as generic is the syntax: "TType<T>" and what we see as specialize is the syntax: "var field: TType<String>"
The use might not be apparent in type and variable declarations, but definitely in inline specializations. The following works in mode ObjFPC, but not in mode Delphi (if the specialize are removed) currently:
Code: [Select]
somevar := specialize SomeFunc<LongInt>(42) + specialize SomeOtherFunc<LongInt>(21);This is because types and routines can have variables and constants with the same name either in the same unit (not yet fully supported by FPC) or in different units. Thus in mode Delphi the compiler needs to decide whether SomeFunc< starts a generic or a comparison which it will only know for sure once it parses the final > which can be many tokens away. Especially if midway through it should realize that parsing it as a generic was an error than it needs to backtrack without any errors visible for the user and try again as an expression. This makes parsing such code really ugly.

OOP-MODEL:
  1) Class Contracts:
  2) Sequences and Queries
  3) Tuples + their behaviour more important
  4) Duck Typing + Soft Interfaces
  5) Cirrus/Aspect oriented Programming
  6) Nullable Types + the ":" operator

MULTITHREADING
  1) Lock
  2) Parallelism
  3) future types
  4) await + async

Well, there are some features that I indeed do plan to at least play around with. Most importantly tuples and class contracts.
The multithreading mechanisms would probably be provided by library code once we have support for anonymous routines cause we can then port the Spring4D framework.

Shpend

  • Full Member
  • ***
  • Posts: 167
Re: Useful Oxygene Features for FPC?
« Reply #26 on: August 23, 2018, 02:15:33 pm »
@PascalDragon

Tuples

Ok, I got that, but when you want to allow tuples (hopefully with the Syntax: tuple of (….)) you Need to make the Default comparison (=, <>) possible, so that I can write instantly without any extra line!


Code: Pascal  [Select][+][-]
  1.   var tuple1, tuple2: tuple of (String, Integer);
  2.  
  3.   tuple1 = tuple2;
  4.   tuple1 <> tuple2;  
  5.  

and btw: would you also consider, if its well possible in Terms of complextity, to introduce by Default:
Value named tuples, which has fields and not properties!

So the tuples I personally prefere are:

* being allocated on a stack, thus they do usually just contain fields
* have named Fields so you can do:
Code: Pascal  [Select][+][-]
  1.  TCar = tuple of (Price: Double; Name: Stringf[15]; Company: String[10])
* not readonly, so you can get the adress of their fields, while in other languages the fields are properties and thus not allowed for pointer stuff

Code: Pascal  [Select][+][-]
  1. var fieldAddr: PChar;  fieldAddr := @myCar.Name

Quote
  5) Cirrus/Aspect oriented Programming
  6) Nullable Types + the ":" operator

Nullable Types + the ":" operator

What would you say to those:

Again, I like how oxygene did this:

Code: Pascal  [Select][+][-]
  1. var
  2.   myNullable: nullable Integer;
  3.   tmp1, tmp2: integer;
  4.  
  5. //now u can use the "myNullable" as you would use an usual integer, with the exception that you can assign nil to it.
  6.  
  7.  tmp1 := 1000;
  8.  mynullable := if tmp 1 < 1000 then nil else (tmp1 * tmp2);  //btw "assignment expressions" also an interesst thing to consider if you like this model ofc
  9.  
  10.  
  11. writeln(if myNullable = nil then 'OK' else 'NOT OK');
  12.  
  13. //or if the nullable is a nullable TRecord you can do for instance:
  14. myField1 := myRecordNullable:Field1  //note the ":" Operator, which will try to Access the field and if it is Nil it assigns Nil and doesnt raise a: "AccessViolation"
  15.  

Would be awesome I think, without the burden of always do consiously: "myNullable.HasValue", "myNullable.Value"


Aspect Oriented Programming

this model extends the use of "Seperation of concerns" so that all code which really doesnt belong to the original module-logic

for ex: Logging, DebuggingOutput, SecurityChecks etc... can be injected at runtime in the specified region of the function without to uglify the actuall code logic.


Code: Pascal  [Select][+][-]
  1.  //oxygene way just for show
  2. type
  3.   [aspect: Logging]
  4.   Foo = class
  5.     …
  6.   end;
  7.  

« Last Edit: August 23, 2018, 02:22:35 pm by Shpend »

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9754
  • Debugger - SynEdit - and more
    • wiki
Re: Useful Oxygene Features for FPC?
« Reply #27 on: August 23, 2018, 03:00:10 pm »
Maybe I am overlooking something.... But it seems that Tuples boil down to one purpose: Breaking type safety.

Let have a look:

1) You can access by index. But that really rather seems a side effect? I mean accessing by name (as in records) is much better.

2) You have inlined constants (1, 'a', true)
   And also inlined constructing from other values: (i, s, b)
   But those could same as easily be introduced for records (and be more readable, because you would give the names for the fields):
   (num: i; text: s; flag: b)

3) if I understand correctly: You can specify only some fields (leaving out fields at the end)
  var t: tuple of (integer, string, boolean);
  t:= (1,'ab');
But that would lead to one of 2 possibilities
a) the boolean is simply uninitialized: you can still read it => same for records
b) the tuple has an internal field to remember this, and throws an exception or similar (that would be new)

With records similar solutions could be introduced, if inlined constants/constructs were allowed.
Partial constants/constructs could be allowed
  var r: record num: integer; text: string; flag: boolean; end
  r := ( num: i; text: s)
The advantage: You could also leave out fields in the middle.
For case (b) you could use null-able types. (not the same but similar)


4) assignment and comparison
Well you can assign and compare records too.

The new part in tuples is
  var t: tuple of (integer, string, boolean);
  (i,s) := t;
Assigning only part of a tuple.

For comparison that leaves an interesting question " if (i,s) = t then" => can that ever be true, because the tuple (i,s) has a different amount of elements. This would only make sense, if tuples store there "length", i.e. how many of their elements actually got assigned.

Back to assignment:
  var t: tuple of (integer, string, boolean);
  var x: tuple of (integer, string);
  x := t;
This would break type safety. There are  2 different types, so they should not be assignment compatible.

If this is forbidden, then any assigning of a partial tuple needs to be forbidden
  (i,s) := t;
Because (i,s) is a type "tuple of (integer, string)" created on the fly.
Well it could also be seen as a partial specified tuple (similar to partial specified record, if that were introduced). Which means that probably specifing an inline constant/construct, needs some syntax to specify the type (rather than having the compiler inferring it).
  TMyTuple(i,s) or TMyTuple.create(i,s)
The latter would be odd for left hand side expressions.

---------------------------
Out of interest how would nested tuples work?
  var t: tuple of (integer, string, boolean);
  var x: tuple of (integer, string);
 
would that allow
  (i,s,f,i,s) := (t,x);
  (t, x) := (i,s,f,i,s);  // the first tuple must be completely filled, before values for the 2nd can be specified?)
    (t, x) := ( (i,s), (f,i,s)); // f goes into t ?
that is would nested tuples just be flattened?

What about:
  type t = tuple of (integer, string, boolean);
  var x: tuple of (t, string);
Does that even make sense?


marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 11351
  • FPC developer.
Re: Useful Oxygene Features for FPC?
« Reply #28 on: August 23, 2018, 03:15:54 pm »
Maybe I am overlooking something.... But it seems that Tuples boil down to one purpose: Breaking type safety.

Depends. You can also see a tuple as an reference to an implicit record that is automatically  type compatible to tuples that have the same declaration. Add in some rtti for iteration purposes,  interface style refcounting maybe.

Then you have a pseudo free "tuple" type based on old tech.

But, like any language addition, to make good decisions you need good descriptions of what should be achieved,
and not the typical 10 line examples in other languages feature bulletlist explanations.

E.g. the above description fits many examples, but not e.g. using an array of tuples as dataset (since that would be possibly not a compiletime defined tuple)

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9754
  • Debugger - SynEdit - and more
    • wiki
Re: Useful Oxygene Features for FPC?
« Reply #29 on: August 23, 2018, 03:59:56 pm »
Well yes, as I said most (if not all) could be archived by extending records (not that I advertise doing any of this).

One of the big question is what happens if you nest tuples? Are they flattened? Or are they kept as a typed entry?
With that goes what happens if you concatenate them, do you get one larger flattened tuple, or on tuple with 2 nested tuples? (This affects what you may be able to do in LHS expressions)

It seems (but info is missing), that maybe tuples are not really a type at all, but rather a loose list of typed entries.

How different are they from a sequence? Except there entries can be of different types.
And if there was an operator/syntax to transform a record to a sequence, would that provide the same?

Could it be said that in some way tuples are to sequences what duck typing is to structures?
In other words (some may find this exaggerated) tuples and duck typing just remove type safety from other features?

---------------
And yes of course you could make all tuples assignment compatible, the same like different numeric types (byte, word, int).

One possible consequence would be, that if duck typing were implemented (please not), then all classes/records/... could be assignment compatible too. So long as they have a subset of duck-type-able fields...

I find the prohibition of those things a useful feature. A useful feature that apparently oxygen does not (completely) have. ;)

 

TinyPortal © 2005-2018