PRPL+ a PRPL language with local variables, script including and more (v0.2.1)

Started by kajacx, December 26, 2016, 07:55:10 AM

Previous topic - Next topic

kajacx

UPDATE to v0.2.1: now includes a %relinclude command, that includes a file from a location relative to the current file (as opposed from the PF directory)
UPDATE to v0.1.2: warning when a variable is only read from or only writen to (prevents misspellings) + end of comment bug fix

I have been missing a lot of features in the PRPL programming language, so I created a new language with those features added. Needless to say, it compiles to PRPL, and has a nice property that any PRPL will be compiled without change, so only new symbols have special meaning.

Video tutorial
TL;DR list from the tutorial

How to install:
    1) Install Java 8
    2) Download and extract the .zip file from the forums
    3) Run the .jar file from the extracted zip
    4) Click the "Compile every second" checkbox, and minimize the window
    5) Rename your files from .prpl to .prpl+
    6) Download and set the PRPL+ syntax highlighter
    7) Re-open the PRPL+ window to check for errors

Local variables:
    - +>varname to write to, <+varname to read from a local varaible
    - compiled by adding unique prefix for each funtion (->f1__varanme, <-f2__varname)
    - ~>varname, <~varname for script-local varaibles when including a script
    - works with delete, exists and all reference varaints, even when using warp notation
    - checks varaible usage (helps to prevent mispellings)

Script including:
    - include a differnt script by specifing it's path relative to the current file
    - usefull for re-usable utility functions and libraries
    - use %library to not compile the file on it's own to avoid script cluttering
   
Other features:
    - multi-line comments
    - easier list creation
    - %blockstart, %blockend, hexadecimal constant
[close]

Local variables
Spoiler

The idea is simple. Instead of worring if you will get a variable name collision somewhere, or putting prefixes everywhere, just use "<+varname" to read from and "+>varname" to write to a local variable, and the prefixes will be generated automaticly.

This isn't a true local variable, in the sense that it will not be auto-destroyed when you leave the function, and it will not work if you call your function recursively. It merely automaticly adds a unique prefix before your variable name, which is same inside one function.

You can also use "<~varname" to read from and "~>varname" to write to a script-local variable. PRPL+ supports script including, you can put multiple PRPL+ scripts together to create one PRPL script. In such scenario, you might consider using these script-local variables, as they are local variables to one PRPL+ script. They get compile with a unique prefix as well, however the prefix is unique to one PRPL+ script, and not to one function.

Table of all variable-related operators supported by PRPL+, with explanation:
Spoiler


(Don't mind the space in "< -", Open office Writer kept replacing it with a wierd arrow)

All the global-scoped operators are compiled without change, except "--?" that gets compiled to "--!" (I was using the old CRPL wiki that used "--?") and "--%" that gets compiled to the string constant "prpl_plus__", i'll get to what that is used for later.

The script-local operands are compiled exactly as the local operands, except with different prefix, so I will not cover them. The non-ref local operands ("<+", "+>", "+?" and "++") are compiled by simply adding a prefix to the variable name. For example "+>a" becomes "->foo__a" or something like that.

Finally, things start to get interesting when we get to the reference local operands ("<+!", "+>!", "+?!" and "++!"). If we want to remove a local variable "a" by reference, [ "a" ++! ] needs to be compiled as [ 'a' ->prpl_plus__varname "foo__" <-prpl_plus__varname Concat --! ]. You can think it over why such workaround is nessesary.

This will also work if the ref operand is followed by an opening parenthesis. In such case, the compiler will find the matching closing parenthesis, and put the concat workaround there, even if multiple parenthesies are stacked in-between. Example: [ ++! ( Concat ( "a" "b" ) ) ] gets compiled into [ --! ( Concat ( "a" "b" ) ->prpl_plus__varname "foo__" <-prpl_plus__varname Concat ) ]. This is acctually generated from the compiler, and as you can see, the prefix-adding work-around is inserted at the right location.

Next, "++?" gets compiled exactly as "++!", it's just there because I was following the CRPL wiki. Finally, the "++%" operand compiles into the local prefix string constant, so "foo__" for example. This is useful when you need to pass variable name somewhere, for example to a function that swaps 2 variables or something. The "--%" compiles to "prpl_plus__" string constant. It's there just so I can have a nice table.
[close]

One note on how the local variables are prefixed: for each scope (function or PRPL+ script), a unique name is generated. After that name is then inserter 2 underscores. This is so that even if you use underscores in variable names (for example in ALL_CAPS), you have a guarantee that there will not be a variable name collision.

Disscussion with Good Morning about local variables
Spoiler

Good Morning has spoke againts local variables in the past when I said I wanted to implement them, and for a good reason. It can lead to lazy naming conventions (I'm also to blame here, in one function, I have 2 variables for lists, named "list1" and "list2") that simply wouldn't be possible without local variables, since you would get a name collision. This would force you to use unique and descriptive variable names (for example: "origModulesList" and "movedModulesList") instead, making the code more readable.

However, I have a different ideology: encapsulation. When I think about a function I want to make, I:

  • Decide what the header should look like (name + input parameters + output parameters)
  • Write the header
  • Write the body
  • Forget the body
  • Remember just the header
This frees space in my mind for ther things, for example, I don't have to remember if I used a while-repeat loop or do-loop one. This would be needed to remembered without local variables, and now I don't have to. I guess I just prefer the "set it and forget it" attitue, maybe because I'm lazy, maybe because it would be impossible to keep track of every single variable in a 1000+ lines code.

However, you don't have to use local variables, there is plenty of other stuff that PRPL+ offers.
[close]
[close]

Script including
Spoiler

You can include a PRPL+ script into another PRPL+ script using the "%include" keyword. The keyword has to be followed by a string constant, containing the path to the included script, relative from the PFDirectory directory (see [How to install]). If the file exists, the content of that script (except functions) will be inserted at the place where the "%include" keyword was. The functions will be imported later, after the functions from your current script.

If you include an already included script, nothing will happen. However, if a cycle is detected when including scripts (for example A includes B, B includes C, and C includes A), you will receive a warning, because the order of the includes cannot be guaranteed.

Also, if the first non-comment symbol is "%library", that sript will not be compiled, unless it was included from elsewhere. This serves both as comment for programmers that this script is meant to be included elsewhere as a library and prevent useless ".prpl" files from being generated and polluting the "add script" select box in level editor.

You can also include a vanilla ".prpl" script, but I wouldn't recommend it, here's why: currently, the program only compiles the scripts if it detects a change in any of the ".prpl+" scripts. So, if you include "b.prpl" from "a.prpl+" and then change "b.prpl", "a.prpl+" will not get re-compiled and therefore will not register the change. Solution: rename "b.prpl" to "b.prpl+", any valid PRPL code will remain unchanged when compiled. Then just import the orignal "b.prpl+", and not the compiled "b.prpl".
[close]

Other stuff (multi-line comments, ...)
Spoiler

This some random stuff that I added because why not.

  • Multi-line comments: "/*" to start and "*/" to end a multi-line comment.
  • Block folding: write "%blockstart" and "%blockend" around a block to make it foldable in Notepad++ with prpl+syntaxer.
  • Hexadecimal constants: write 0x before a number to write a hexadecimal constant.
  • Use `{ 1 2 3 ... }` to easily create a list. You can stack lists recursively into one another with this as well.
[close]

How to install & run
Spoiler

Download the PRPL Toolset zip and extract it anywhere. Then, go to settings.properties and set PFDirectory to your particle fleet documents directory (just replace my username with yours). This will replace every ".prpl+" file with the compiled ".prpl" file inside the "editor" subfolder. If you don't want to compile directly insode your PF folder, just set PFDirectory to any folder that has a sub-folder named "editor" and place your scripts there. However, then you will have to manually move your comiled ".prpl" scripts back to the PF folder.

Once you have done that, open the ".jar" program or open the "start_with_console.bat" program to show console - you can see a more detailed error logs in the console. Make sure you have Java 8 installed. Next, just check the "Compile every 2 seconds" checkbox and you are good to go.

You can click the "Show status display" to display a small window with compile status - it will show how many errors/warning did you have on last compilation, if any.
[close]

Example of usage
Spoiler

You can download the example map that I was testing this on. It's about merging ships together. To merge ships, just move 5 ships you want to merge and hit "Compile" in level editor. The merged ship will appear in your ship inventory.

The code is split into multiple PRPL+ scripts, the libraries (except ship_merge) work as stand-alone, that means you can use them in your code as well without worry. The main script, "ship_spawn.prpl+", is implemented as a library as well, this is so that you can easily reuse the same script inside multiple maps, without moving the script from file to file - just include it with one line each time you want to use it. Clever, huh?
[close]

Screenshots
Spoiler

How many times have you misspelled a variable name? I know I have a lot of the times, but now I get a warning when I do so!
Unused/undefined variable warning
[close]

When scrolling up and down in 1000+ lines file is too much work...
Foldable code
[close]

[close]

Happy coding and merry chrismas!
Why do work yourself, when you can write a program that will do the work for you.

GoodMorning

Somebody else with too much free time on their hands. :)

As long as this all becomes PRPL, it is good to have. If I ever want a scripting compiler, you may hear from me again. Thank you for putting in the time.

Be aware, however, that most scripts are too small to use this, for good reason. PRPL is running at another level of abstraction, using command lists, and string comparison in each Core upon variable read/write. (A similar concern to your larger ship editor.) This means that any script sufficiently major to need your tools is also likely to cause lag. Also note that PRPL has an existing definition of "Global Variable", using ->* for map-global variables.

Also, does your .zip include your Java source code?
A narrative is a lightly-marked path to another reality.

kajacx

Quoteusing string comparison in each Core upon variable read/write
Well I hope virgilw wasn't lazy and dind't implement all variables in a single hashmap. Here is what I would have done: Have 2 kinds of variables: named and unnamed. A named varaible is any variable whose name appears with one of the value (non-reference) opperands ( "<-", "->", "-?" and "--" ). Such variables can be implemented by storing their values in an array, however you would need an extra value type for undefined named variables. Then, using value opperands on named variables is truly in constant time, no hashmap needed, it gets compiled into dirrectly accessing the named varaible array via a pre-computed index.

Then, all variables can be stored in a hashmap. Named variabled will have their index stored there, unnamed will have their value stored. You can add unnamed variabled by the "->!" operand. This will still result in a single hash map operation per reference operand.

So you pay with slightly slower refernce operands, but you gain significantly faster value opperands. And that is a payoff worth having. However, knowing that virgilw was lazy to create a serializable triplet of integers, so he store the triplet (modileID, positionX, positionY) as a Vector3f instead, I doubt he used this implemetation instead of a single hash map.


Also I thought the "->*" operands were global for one PRPL Core, however it doesn't really matter for the purpouse of the compiler, it leaves such operands without change anyway.

No, the .zip doesn't include the sources, but the project is open sourced on GitHub

Finally: yes, I agree that uber large code likely to benefit from this probably too large for what PRPL was intended to be - a short custom functionality code, and not a full-fledged programming language. However I wrote it for fun more than anything else, also I tried to solved the most problematic issued that I had when I was programming in PRPL, and I managed to do jsut that, so I'm happy.
Why do work yourself, when you can write a program that will do the work for you.

GoodMorning

And this is good. It's all in fun, and I can see that PRPL or CRPL might sometimes feel like using C instead of Python. Enjoy.

I think that V has mentioned that all of the PRPL variables are stored in a dictionary. This may be because V has GetCoresWithVar and [GS]etScriptVar implemented.
A narrative is a lightly-marked path to another reality.

kajacx

QuoteThis may be because V has GetCoresWithVar and [GS]etScriptVar implemented.
I thought these were for the argument variables only (declared with $). Well anyway, that's what the hashmap is for. If you set variable "a" to 5 (eighter by "SetScriptVar" or by "->!") the program would look in the hashmap for key "a". The if it found that "a" is a named variable, it would take the index stored in the hashmap and write the value 5 to the named variables array at that index. Otherwise it would put the ("a", value(5)) pair into the hashmap, eighter overriding the old value or creating a new unnamed variable.
Why do work yourself, when you can write a program that will do the work for you.

GoodMorning

I already understand the function of the hashmap in your construction. I assume that V's reason was to build it in the simplest way possible, but speculated as to a possible additional reason.

It's difficult to discuss some things without knowing the exact level of prior knowledge possessed.
A narrative is a lightly-marked path to another reality.

kajacx

I just looked at the decompiled code and yea, it's just a single hashmap :(
(Also the classes are still named CrplCompiler and CrplCore :D )
Why do work yourself, when you can write a program that will do the work for you.

GoodMorning

I'm fairly sure that it's at least moderately illegal (here) to decompile proprietary software without permission.

However, listening to what V has said has given the same answer.

Now, back on topic, let us see what you can do with PRPL. :)
A narrative is a lightly-marked path to another reality.