Feature - Implemented Varargs in ASH

Veracity

Developer
Staff member
Is this cool, or what?

Code:
void test( int ... args )
{
    print( "arg count = " + count( args ) );
    foreach index, arg in args {
	print( "arg(" + index + ") = " + arg );
    }
}

// Test calling the vararg function with an array

int [] zero = {};
int [] one = { 1 };
int [] two = { 1, 2 };
int [] three = { 1, 2, 3 };

test( zero );
test( one );
test( two );
test( three );
print( "----" );

// Test calling the vararg function with multiple arguments

test();
test( 1 );
test( 1, 2 );
test( 1, 2, 3 );
print( "----" );

int min( int first, int ... args )
{
    int retval = first;
    foreach index, arg in args {
	if ( arg < retval ) {
	    retval = arg;
	}
    }
    return retval;
}

print( min( 100 ) );
print( min( 100, 50 ) );
print( min( 100, 50, 200, 25  ) );
yields

Code:
[color=green]> varargs.ash[/color]

arg count = 0
arg count = 1
arg(0) = 1
arg count = 2
arg(0) = 1
arg(1) = 2
arg count = 3
arg(0) = 1
arg(1) = 2
arg(2) = 3
----
arg count = 0
arg count = 1
arg(0) = 1
arg count = 2
arg(0) = 1
arg(1) = 2
arg count = 3
arg(0) = 1
arg(1) = 2
arg(2) = 3
----
100
50
25
It only works for user defined functions, so far. I may submit it so you-all can play with it, but I'd like it to work for built-in functions, too. I'd like a min() and max(), for example, that work with any number of args - just like I demonstarted with the ASH program.
 

Veracity

Developer
Staff member
Another example. It looks for exact type matches for parameters/types, and then non-exact - including varargs.
Perhaps exact varargs should take precedence over functions that require coercion. Maybe not.
Here's how it works now:

Code:
int mymax( int arg1, int arg2 )
{
    print( "mymax(" + arg1 + "," + arg2 + ")" );
    return max( arg1, arg2 );
}

int mymax( int arg1, int arg2, int arg3 )
{
    print( "mymax(" + arg1 + "," + arg2 + "," + arg3 + ")" );
    return mymax( arg1, mymax( arg2, arg3 ) );
}

int mymax( int arg1, int ... rest )
{
    print( "mymax " + arg1 + " ... " + count( rest ) + " more args " );
    int retval = arg1;
    foreach i, arg in rest {
	retval = mymax( retval, arg );
    }
    return retval;
}

print( mymax( 1, 2 ) );
print( mymax( 1, 2, 3 ) );
print( mymax( 1, 2, 10, 20, 4, 100, 12 ) );
yields

Code:
[color=green]> varargs2.ash[/color]

mymax(1,2)
2
mymax(1,2,3)
mymax(2,3)
mymax(1,3)
3
mymax 1 ... 6 more args
mymax(1,2)
mymax(2,10)
mymax(10,20)
mymax(20,4)
mymax(20,100)
mymax(100,12)
100
 

Veracity

Developer
Staff member
I ran VMF (which does not use this, but uses almost everything else in the ASH language) on a character. It worked fine.
I submitted this (experimental) feature in revision 19880.

I'm leaving this open for comment and suggestions and bug reports.
 

AlbinoRhino

Active member
This is pretty interesting. Does it only apply to arrays/multiples of ints? (I haven't updated yet or I would just try it with some strings.)
 

Veracity

Developer
Staff member
I used int, because it was simple, but you can use literally any type there. int, string, maps, records, whatever.
 

AlbinoRhino

Active member
Even sweeter. I am thinking most people would expect an exact type match function to take precedence over a more loose match? I could be wrong.
 

zarqon

Well-known member
This is exciting! Thanks Veracity. I'll have to update ZLib's min() and max() to accept unlimited arguments now.

I suppose something like

Code:
int myfunction(int ... goodints, int ... badints) {

should be illegal because there's no way of knowing when the good ints break bad. But consecutive varargs of different types should be fine? Just thinking of possible ways to break it while on my lunch break. :)

Perhaps exact varargs should take precedence over functions that require coercion. Maybe not.

I would expect that and be mildly surprised if it were otherwise, probably. Easy enough to work around, though.
 

Veracity

Developer
Staff member
I keep it simple. You get a single vararg argument, which must be the last.

In Java, your vararg can get an array of generic Objects. You get one, at the end, because it takes all the remaining arguments and you can figure them out on your own.

In ASH, they have to be the same type, since we don't have generic Objects (or null). I suppose I could have multiple vararg arguments, grouping together different types, but that seems complicated to code and to understand. SO, I think not.
 

xKiv

Active member
Unless varargs cannot be empty, you can't even do multiple vargargs of the same type even with another type in between. Not to mention, what if the you do two types where one can be coerced to another, or pass arguments that can be coerced to one or another ... doing anything else than other languages do would be a nightmare.
 

Veracity

Developer
Staff member
Revision 19888 whacks Function lookup for function calls at compile time based on function name and the list of parameters in the call.
Considering overloading, it wants to find the best - correct - function with the specified name which will handle the arguments.
It makes 12 (!) passes over the symbol table before giving up and throwing an undefined function, although it short circuits at the first match.

There are 3 kinds of comparison:

1) EXACT - The parameter type and the argument type are the same - including typedefs. This allows the following to work:

typedef int t1;
typedef int t2;

void f( t1 arg ) {}
void f( t2 arg ) {}
void f( int arg ) {}

Three functions, same name, single int argument - but given a typedef argument, it will find the correct function

2) BASE - compare the "base" types of arguments and parameters. For typedefs, the type it names.

typedef int t3;
f( t3 );

... will call the foo(int) version, since there is not one specialized for the typedef

3) COERCE - if you can coerce the function call argument to the type of the function argument, cool.

f( true );

... because you can coerc a boolean to an int.

There are two kinds of check - no varargs, varargs.
There are two places to look - current scope (and recursively up through parent to global scope), built-in library functions.

3 * 2 * 2 = 12 searches in this order

EXACT, no varargs, scope
EXACT, no varargs, library
EXACT, varargs, scope
EXACT, varargs, library
BASE, no varargs, scope
BASE, no varargs, library
BASE, varargs, scope
BASE, varargs, library
COERCE, no varargs, scope
COERCE, no varargs, library
COERCE, varargs, scope
COERCE, varargs, library

I imagine this could be optimized to make one pass through each source (scope, library) checking/collecting all six search types (EXACT, BASE, COERCE)(no varargs, varargs) and pick the first result from the potential 12 results.

I also set up the RuntimeLibrary (built-in functions) to be able to handle varargs. I updated the built-in min() and max() functions to take any number of arguments:

int min( int, int... )
float min( float, float... )
int max( int, int... )
float max( float, float... )

Because we can coerce, you can mix ints and floats in the argument list and you'll get a float result.

There some other things I want to consider:

1) compile-time checking:

void func( int, int, int... )
void func( int, int... )

Whichever is first, the second has to be considered a redefinition, since we cannot decide which one would handle the call: func( 1, 2, 3)

2) There is some run-time usage of looking up a function in a particular scope. In particular, for the "call" construct. BasicScope.findFunction has the old search logic in it. Perhaps I can move the new logic into a utility which is usable both by the Parser and at runtime.

3) More of the same, Function.paramsMatch is used by the BasicScope search and also by UserDefinedFunction to determine if you are overriding a library function.

There is no end...
 

fredg1

Member
Did this update change the way functions need to be defined or something? A lot of pre-existing scripts now generate errors upon being launched
 

fredg1

Member
Mayyyyyyyybe... >_>

(19888 was the latest at the time of writing this; had no way of really knowing if this was a bug or not, so it's why I "phrased" it like that, as I know it would've been rude to phrase it as if it was a bug, if it had been a "change of the required syntax")

Btw, not really that informed in informatics, but that's a function that can be given a variable amount of arguments? Wow!
 

Veracity

Developer
Staff member
No Problem.

I most certainly did not intend to change function declaration syntax in a non-backwards-compatible way; everything I did was intended to be an extension. If existing programs break, that is unintended and I'll get on it as soon as possible to fix.
 

Veracity

Developer
Staff member
I just ran into this with 19888, rolled back to 19886, works.
You are referring to "bugs" in KoLmafia.
Perhaps you need to update to 19889 or later?
That was the post immediately following the one you cited. ;)

It makes 12 (!) passes over the symbol table before giving up and throwing an undefined function, although it short circuits at the first match.
...
I imagine this could be optimized to make one pass through each source (scope, library) checking/collecting all six search types (EXACT, BASE, COERCE)(no varargs, varargs) and pick the first result from the potential 12 results.
I changed the search to make a single pass over the functions in the scope and a single pass over the functions in RuntimeLibrary, extracting only the functions with the name in question. It then makes (up to) six passes over each of the vastly smaller - and frequently empty, since you are calling EITHER a user function OR a built-in function - lists.

That's probably good enough.

1) compile-time checking:

void func( int, int, int... )
void func( int, int... )

Whichever is first, the second has to be considered a redefinition, since we cannot decide which one would handle the call: func( 1, 2, 3)
This looks like a pain. Still important to minimize surprises, but I'll put this off for a bit.

2) There is some run-time usage of looking up a function in a particular scope. In particular, for the "call" construct.
3) More of the same, Function.paramsMatch is used by the BasicScope search and also by UserDefinedFunction to determine if you are overriding a library function.
Yeah. 19888 moved all the code to more logical places:

Operator has the "is this coercable?" stuff
Function has the "does this function's arguments match those parameters?" stuff.
BasicScope has the "find the function (using up to 12 searches)" stuff.

So I got rid of all the old-style stuff and thereby fixed issues #2 and #3.

That's as much time as I want to spend on this right now. Time to fix more bugs - like the ones I bumped from the top 12 pages or so of the Bug Reports - and spin a new point release before the end of the month. Hopefully, that will give THIS feature enough soak time.
 

Aventuristo

Member
To answer the original question: Yes, way cool. I have a library that allows one to create a script that performs steps in ascending numerical sequence, and if the script is interrupted, it can be rerun starting with the step that was interrupted. The steps are functions that have a particular signature; it would be cool if the step functions could take arbitrary numbers of arguments. This looks fun to play with, thanks!
 

Veracity

Developer
Staff member
Revision 19911 adds the aforementioned checks for vararg clashes:

f( int, int, int...)
f( int, int... )

clash with each other, since it is indeterminate which should be called with f( 1, 2 ).

As submitted either also clashes with

f( int, int)

I'm going to think more about that. Perhaps the exact match with the non-vararg function should win?
That is how I had it before.
Hmm.
 

Veracity

Developer
Staff member
Perhaps.

f( int )
f( int, int )
f( int, int...)

can coexist.

f( 1 ) is an exact match for the first function, f( 1, 2 ) is an exact match for the second functionf, and f( 1, 2, 3 ) is a vararg match for the third function.

Revision 19912
 
Top