Feature - Implemented Let ASH scripts have "static" data

Veracity

Developer
Staff member
When KoLmafia wants to execute an ASH script, it looks for an Interpreter pre-loaded with that script. If it finds one and neither the script nor any of its included scripts have changed since the Interpreter was created, it uses it. Otherwise, it makes a new Interpreter and parses the script into it.

Every time a script is executed, the Interpreter does the following:

- executes all the top-level commands in the script and included libraries
- executes the "main" function of the script, if any.

There are currently three exceptions to this:

- The forms of adventure() and adv1() that specify a filter function execute a single function in the context of the currently executing script. It will not execute the top-level commands.
- KoLmafiaASH.getScriptHTML() will execute a single function in the specified relay script. It will not execute the top-level commands. This function is never called.
- You can execute a single ASH function from the current "namespace". This will execute the top-level commands the first time the namespace file is executed, but not again unless it is reloaded.

Every other script - before battle scripts, recovery scripts, counter scripts, consult scripts - operates as described: execute top-level commands followed by "main".

What can go into top-level commands?

- global variables
- assignment statements
- arbitrary function calls or other ash constructs

Note that declaring a global variable implicitly assigns it an initial value, whether or not you specify one.

int a = 5;

assigns the value 5 to variable a.

int a;

assigns the value 0 to a. Similarly:

string [int] map;
map[ 1 ] = "one";
map[ 2 ] = "two"

assigns an empty map to variable "map" and then creates two mappings in it.

It seems to me that there are two kinds of global data that scripts want to use:

- Real variables
- Constant data, initialized by hand or via file_to_map, and never changed thereafter.

The former is like Java "static" data. The latter is like Java "static final" data.

I believe that we could allow that distinction for the use of ASH scripts, too: top-level commands are executed every time a script is executed, but the script could designate some of those commands as "final" to specify that they will be executed exactly once within a particular interpreter. The next time the script runs in the same interpreter, those declarations/assignments/function calls/whatever will be skipped.

Obviously, the programmer has to be careful deciding which commands should be "final"; most likely, only commands that initialize data that will really never change from execution to execution, although I can imagine scripts that "learn" over the course of multiple executions and want to augment or change their permanent data each time without having to write it out to disk and reload it.

I propose the following:

Code:
// The following will be executed every time the script executes:
int level = my_level();
string path = my_path();

// The following will be executed the first time a script executes in a session:
final
{
    string [int] map;
    map[ 1 ] = "one";
    map[ 2 ] = "two";

    string [int] map2;
    file_to_map( "bigfile.txt", map2 );
}

Thoughts?
 
Last edited:
That's kinda awesome. It would save some scripts a lot of execution time if they only had to run some subroutines a single time each session. For a combat script or recovery script that is run for every single action it would really add up.
 
I just checked in Revision 10765 which does what I proposed - complete with "final" as the keyword. It will be easy to change, but I wanted to let people try it out first.

main1.ash:
Code:
import <lib1.ash>;
import <lib2.ash>;

final
{
    print( "final data in main1" );
}

print( "executing main1" );
lib1.ash:
Code:
final
{
    print( "final data in lib1" );
}

print( "loading lib1" );

final
{
    print( "more final data in lib1" );
}
lib2.ash:
Code:
final
{
    print( "final data in lib2" );
}

print( "loading lib2" );

final
{
    print( "more final data in lib2" );
}
yields:

> main1

final data in lib1
loading lib1
more final data in lib1
final data in lib2
loading lib2
more final data in lib2
final data in main1
executing main1

> main1

loading lib1
loading lib2
executing main1
I'll change the keyword, after further discussion, if we so decide, but I was shocked at how easy this was and I wanted to get it out. :)
 
OK, still under development... the variables do need to go into the parent's scope. Stay tuned.
 
Got it.

This:

Code:
final
{
    string [ int ] numbers;
    numbers[ 1 ] = "one";
    numbers[ 2 ] = "two";
    numbers[ 3 ] = "three";

    print( "final data in main1" );
}

print( "executing main1" );

foreach key, val in numbers
    print( "numbers[" + key + "] = " + val );
yields:

> main1

final data in main1
executing main1
numbers[1] = one
numbers[2] = two
numbers[3] = three

> main1

executing main1
numbers[1] = one
numbers[2] = two
numbers[3] = three

Revision 10769
 
This sounds really cool.
Just wondering what actions, exactly, cause a new Interpreter to be formed. So far it looks like the data will be executed once per mafia session, or again if the file changes.
Are there any other actions that would cause a new interpreter to be created? And does logging out/in without actually closing mafia count as a new session?
 
We create a new Interpreter when we execute an ASH script. We cache it, with the modification time of the file. Within the Interpreter, we list all imported scripts and THEIR modification dates.

Each time we want to execute the same script, we look up that File and compare its modification date to what we had cached. If it is unmodified, we look at the modification dates of all imported scripts and compare those. If nothing has been changed, we reuse the same Interpreter. If either the script or any of its imports have changed, we throw away the interpreter and make a new one.

Logging out and logging in does not clear the Interpreter cache; it is all based on file modification dates.

Now, you certainly could declare a final variable with user id and compare that each time you execute the script to the current user id and reinitialize some of the data.

You can also do a file_to_map to initialize a final variable, modify it within your script, and do a map_to_file to write out the updates at the end of script execution, if you want to read a file once and update it multiple times during a session.

I found something I'd hoped would work, but which doesn't yet.

Code:
void test()
{
    final {
	print( "initializing calls" );
	int calls = 3;
    }
    calls += 1;
    print( "test function called " + calls + " times." );
}

test();
test();
yields:

> main2

initializing calls
test function called 4 times.
test function called 1 times.
I had hoped individual function scopes could have their own private final data, but not yet. That is because we save variable bindings when entering a function and restore them when exiting. I'm going to have to think about this.

But, it should work fine for global data.
 
One more thing: Revision 10772 lets you use either a block (in {}) or a single command or declaration following the "final" keyword. So, this script:

Code:
void test()
{
    final int calls;
    calls += 1;
    print( "test function called " + calls + " times." );
}

test();
test();
test();
test();
test();
test();
Yields:

> main2

test function called 1 times.
test function called 2 times.
test function called 3 times.
test function called 4 times.
test function called 5 times.
test function called 6 times.
And, having done that, "final" looks really wrong to a Java programmer as the name for the keyword.

More suggestions? :)
 
Looking at it, I answered my own question: it's really much more like Java "static" data. So, Revision 10774 renames "final" to "static" and I am declaring this done, modulo bugs.
 
So far, I love this. Not sure if this would be an extension or a new feature, but could there be an "in between" static for function data? Declarations that occur once per invocation of the script, instead of once per creation of Interpreter.

I have absolutely no idea how difficult this would be to add given the current development, so if it's not worth the effort I can just find a workaround.
 
Looking at it, I answered my own question: it's really much more like Java "static" data. So, Revision 10774 renames "final" to "static" and I am declaring this done, modulo bugs.

The Java programmer in me thanks you, this naming makes much more sense to me :)

One distinction between these static blocks and Java's is how the compiler handles them. In Java, you can have multiple static blocks peppered throughout your code and the compiler will pull them all out, concatenate them, and execute them before anything else in the class. I find this behavior somewhat arbitrary and unhelpful, so I find ash's implementation to be superior.
 
You mean make a variable work as if it were declared in the top scope without actually declaring it there?
but, perhaps, not be accessible except in the function's scope. I suppose the workaround would be to simply declare variables in the top-scope but only use a particular variable in a particular function.

That doesn't sound like too painful a workaround to me.

Unless he means something else.
 
but, perhaps, not be accessible except in the function's scope. I suppose the workaround would be to simply declare variables in the top-scope but only use a particular variable in a particular function.

That doesn't sound like too painful a workaround to me.

Unless he means something else.

Nope, that's exactly what I mean. And yeah, the workaround is simple, but can get pretty ugly for large programs with large amounts of data. Probably the rare case for ASH though.
 
Not sure if intentional or bug, but I did some playing around. I have 2 little scripts, pail.ash, and pot.ash.
pail.ash:
Code:
static int calls; 
void test()
{
    calls += 1;
    print( "test function called " + calls + " times." );
}

test();
test();
test();
test();
test();
test();
The static is outside the function
pail.ash
Code:
import pail.ash;
test();
Output:
Code:
> pail

test function called 1 times.
test function called 2 times.
test function called 3 times.
test function called 4 times.
test function called 5 times.
test function called 6 times.
calls=6

> pot

test function called 1 times.
test function called 2 times.
test function called 3 times.
test function called 4 times.
test function called 5 times.
test function called 6 times.
calls=6
test function called 7 times.
calls=7

> pail

test function called 7 times.
test function called 8 times.
test function called 9 times.
test function called 10 times.
test function called 11 times.
test function called 12 times.
calls=12

> pot

test function called 8 times.
test function called 9 times.
test function called 10 times.
test function called 11 times.
test function called 12 times.
test function called 13 times.
calls=13
test function called 14 times.
calls=14
If I'm reading everything correctly, Mafia is creating 2 separate instances of calls, one for pail.ash and one for pot.ash (which has it's own static because it imported pail's static calls). This is intentional, correct?
 
That is correct. Each script that you execute from the command line gets its own interpreter and statics are per-interpreter.

Now, if we were trying to emulate DLLs, say, perhaps multiple scripts importing the same library would all link to the same copy of the library, which would have a single set of statics shared by all the scripts that use it, but that's now import works in ASH; it as is each imported script has its code inserted right there at the point of import.
 
Back
Top