try blocks can transform an abort into a return value?

chown

Member
I don't think I get it:

Code:
int a() {
	try {
		abort("abort! abort!");
	} finally {
		print("finally");
	}
	return 5;
}

int b() {
	if (5==a()) print("five");
	int foo = a();
	print(foo);
	a();
	print("huh?");
	return 6;
}

void main() {
	b();
}

Code:
> whm_temp

abort!     abort!
finally
abort! abort!
finally
0
abort!     abort!
finally

So, the first two calls to a() return 0, and the last one aborts? (no "huh?" message after it.) But, without the try-finally, they would all abort. (well, okay, the first would abort and the others wouldn't be reached. sheesh, it's tough being a pedant, sometimes.) This seems to be intentional, but it also seems very strange to me, because I can't simply add try-finally to protect some code that needs to temporarily change some Mafia settings, without having to also rework the functional decomposition of my code. Is that right?
 

chown

Member
Uh, actually on further investigation, I don't think this is intentional. It seems that the behavior doesn't just affect the function that uses try-finally, but rather every function above it in the call stack. The try-finally changes the behavior of the abort so that it automatically gets transformed into a return value as soon as the stack unwinds to a function call where a return value is used.

edit: no, it's even worse than that. this happens at _every_ function call up the stack. I haven't figured out a solution yet that introduces less than two extra function calls....
 
Last edited:

fronobulax

Developer
Staff member
Are you saying that the scope of a finally appears to extend beyond the try block where it is first defined? I'd be inclined to call that an unexpected feature of ash and then wait for someone smarter than me to either agree or explain patiently what the intended behavior is and why that should be so.
 

xKiv

Active member
AFAICT, try/finally is only meant to ensure that the finally { ... } block runs regardless of wheter try { ... } aborted. The insides of finally { ... } are run in normal state, but then the abort-y state is restored (more precisely, it restores the state that the interpretter was in when it left the try {...} block).

Stopping abort propagation looks like this (capturing return value into a variable forces continue):
Code:
int a_inner() {
 abort("haha");
 return 0;
}

int a() {
 int ret=a_inner();
 return ret;
}

// also this, I think (I also think you don't need to capture the return value for this):
int a2() {
 string name="a_inner";
 call a();
 // would call "a_inner"(); work here?
}
 

fronobulax

Developer
Staff member
AFAICT, try/finally is only meant to ensure that the finally { ... } block runs regardless of wheter try { ... } aborted. The insides of finally { ... } are run in normal state, but then the abort-y state is restored (more precisely, it restores the state that the interpretter was in when it left the try {...} block).

After reading various Java practices for try/catch/finally it seems that the intention in ash is that the abort state is analogous to an exception that is thrown by a routine. In that case the intent is that the finally be executed after which the exception is thrown to the caller.
 

chown

Member
After reading various Java practices for try/catch/finally it seems that the intention in ash is that the abort state is analogous to an exception that is thrown by a routine. In that case the intent is that the finally be executed after which the exception is thrown to the caller.

Yeah, that's what I had expected. It's definitely not what I got. As I understand it, the presence of the try-finally makes the calling code behave as if there are two versions of a(), that are overloaded on whether the return value is used. The (conceptually) "void a()" version aborts. The "int a()" one does not.

xKiv, I can't make heads or tails of your response. I don't know what behavior you are trying to demonstrate, and your code doesn't validate.
 

Veracity

Developer
Staff member
Here is the code within the ASH Interpreter that handles try/finally:

Code:
		try
		{
			result = this.body.execute( interpreter );
		}
		finally
		{
			if ( this.finalClause != null )
			{
				String oldState = interpreter.getState();
				interpreter.setState( Interpreter.STATE_NORMAL );
				KoLmafia.forceContinue();
				if ( interpreter.isTracing() )
				{
					interpreter.trace( "Entering finally, saved state: " + oldState );
				}
				Value newResult = this.finalClause.execute( interpreter );
				if ( interpreter.getState() == Interpreter.STATE_NORMAL )
				{
					interpreter.setState( oldState );
				}
				else
				{
					result = newResult;
				}
			}
		}
As you can see it is (surprise) implemented in terms of Java's try/finally.

The way it restores the interpreter state is suspect. I do not understand why is doesn't ALWAYS restore the interpreter state - and I don't understand why it would ever return the value returned by the finally block in place of the value it was returning from the try block.

In particular, I don't see why it shouldn't just be this:

Code:
		try
		{
			result = this.body.execute( interpreter );
		}
		finally
		{
			if ( this.finalClause != null )
			{
				String oldState = interpreter.getState();
				interpreter.setState( Interpreter.STATE_NORMAL );
				KoLmafia.forceContinue();
				if ( interpreter.isTracing() )
				{
					interpreter.trace( "Entering finally, saved state: " + oldState );
				}
				this.finalClause.execute( interpreter );
				interpreter.setState( oldState );
			}
		}
 

Veracity

Developer
Staff member
Well, it was a bit more complicated.

Code:
		try
		{
			result = this.body.execute( interpreter );
		}
		finally
		{
			if ( this.finalClause != null )
			{
				String oldState = interpreter.getState();
				boolean userAborted = StaticEntity.userAborted;
				MafiaState continuationState = StaticEntity.getContinuationState();

				KoLmafia.forceContinue();

				if ( interpreter.isTracing() )
				{
					interpreter.trace( "Entering finally, saved state: " + oldState );
				}

				interpreter.setState( Interpreter.STATE_NORMAL );
				this.finalClause.execute( interpreter );
				interpreter.setState( oldState );

				StaticEntity.setContinuationState( continuationState );
				StaticEntity.userAborted = userAborted;
			}
		}
lets this program:

Code:
int abc()
{
    try {
	print( "Try block" );
	return 10;
    }
    finally {
	print( "Finally block" );
	return 20;
    }
}

print( "abc returned " + abc() );
do this:

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

Try block
Finally block
abc returned 10
and this program:

Code:
int abc()
{
    try {
	print( "Try block" );
	abort( "Aborting" );
    }
    finally {
	print( "Finally block" );
	return 20;
    }
}

print( "abc returned " + abc() );
do this:

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

Try block
[color=red]Aborting[/color]
Finally block
With "Aborting" in the status line and GUI red.

That seems correct to me.

Revision 16296
 

chown

Member
Hmm. Now I'm even more confused. Isn't a return statement in a finally block supposed to convert the abort to a regular return value? As in, put the interpreter back into STATE_NORMAL, or whatever? I would have expected (based on my many-years-stale experience with Java) Veracity's programs to both return 20. I didn't understand xKiv's explanation of how abort states can be converted to return values without try-finally. (If that's what it is; I really just don't understand.)
 

chown

Member
I'm also a bit confused as to how the Mafia code that Veracity quotes (before her changes) is changing return values. Is it the "result = newResult"? It looks to me like that should only execute if there is an abort from inside the finally block. Are there other cases where interpreter.getState() != STATE_NORMAL?
 

chown

Member
Also, maybe it's not quite as bad as I think. I found the exact code that I've been trying to write in CounterChecker.ash:
Code:
// Handle the 0-turn lights out adventure. Make sure to reset original preferences
void advLightsOut(location hauntedLoc, string choice) {
	string originalValue = get_property("lightsOutAutomation");
	try {
		set_property("lightsOutAutomation", choice);
		(!adv1(hauntedLoc, 0, ""));
	} finally
		set_property("lightsOutAutomation", originalValue);
}

So, if I understand correctly, I can ignore a return value inside the try block, rather than creating a separate function....
 

xKiv

Active member
xKiv, I can't make heads or tails of your response. I don't know what behavior you are trying to demonstrate,

You are missing several things:
1) finally is not catch. If you wan't try-catch behaviour, you have to emulate it with what ASH has.

2) ASH does not have exceptions; abort() simply sets mafia's [1] state to "do not continue".

3) There's no converting of abort into return values.

4) There's "magic" that happens when you assign function's return value into variable, and doesn't happen when you throw away the return value.
Code:
int dummy = a();
explicitly resets mafia's state to "DO continue" (unless there was a really hard abort, like user pressing ESC).
*this* is ASH's version of try/catch.

Code:
a();
does not change that state.

[1] per-interpretter? I don't think it's global, because there can be several scripts executing in parallel (relay script, chat script, CLI-initiated script), and I don't think they stop each other


and your code doesn't validate.

Well, I am not firing up mafia just to check if I am missing a return, or wrote an incorrect call.



======================================================================

The way it restores the interpreter state is suspect. I do not understand why is doesn't ALWAYS restore the interpreter state - and I don't understand why it would ever return the value returned by the finally block in place of the value it was returning from the try block.

I think it's so that
Code:
try {
  something that doesn't abort;
} finally {
 abort();
}
aborts.

Your simplified version looks to me like it won't honor aborts from inside the finally { ... } block.
 

Veracity

Developer
Staff member
I think it's so that
Code:
try {
  something that doesn't abort;
} finally {
 abort();
}
aborts.

Your simplified version looks to me like it won't honor aborts from inside the finally { ... } block.
Yeah, good point. That should be easy enough to deal with.
 

chown

Member
You are missing several things:
1) finally is not catch. If you wan't try-catch behaviour, you have to emulate it with what ASH has.
Right. That's exactly why I _DON'T_ want it doing that. Go re-read my initial post?
3) There's no converting of abort into return values.
Sigh. Okay, but there is. How do you explain the behavior I described in my original post?
4) There's "magic" that happens when you assign function's return value into variable, and doesn't happen when you throw away the return value.
Code:
int dummy = a();
explicitly resets mafia's state to "DO continue" (unless there was a really hard abort, like user pressing ESC).
*this* is ASH's version of try/catch.
But, why does it do that _only_ when there's a try-finally involved?
 

Veracity

Developer
Staff member
Yeah, good point. That should be easy enough to deal with.
In revision 16298:

Code:
int abc()
{
    try {
	print( "Try block" );
	return 10;
    }
    finally {
	print( "Finally block" );
	abort( "Aborting" );
    }
}

print( "abc returned " + abc() );
Does this:

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

Try block
Finally block
[color=red]Aborting[/color]
 

xKiv

Active member
Right. That's exactly why I _DON'T_ want it doing that. Go re-read my initial post?

I did. It says that
1) you don't understand the difference between catch and finally
2) you don't understand the interaction between abort and capturing return values in ASH

You should stop wanting finally to do what catch does. If you want ash to implement catch, ask for catch.

Sigh. Okay, but there is. How do you explain the behavior I described in my original post?

The only thing I didn't explain is why you printed 0 instead of 5.
Do you want step-by-step?
first a():
- abort sets mafia to abort state (and prints abort! abort!)
- finally prints finally
- mafia is still in abort state after that, so it "returns" from the function (without having executed a return statement, so there's no return value
- 5==a() captures the return value, but there's no return value set, so it's interpretted as 0 (and 0!=5), so nothing prints
- thanks to the capture, mafia is no longer in abort state (the "exception" got "caught")

second a():
- same inside
- int foo=a() captures return value, but it gets again interpretted as 0, so you print 0
- again, capture the return, so mafia is no longer in abort state

third a():
- same inside
- a() does NOT capture the return value (it's not assigned to a variable, even a temporary one)
- "the exception was not caught" - mafia continues to be in abort state
- so the function ends without executing the rest (is this so hard to understand?)
- this of course propagates recursively


But, why does it do that _only_ when there's a try-finally involved?
It does that when abort is involved. try-finally has nothing to do with it, except that it confused you into thinking it does.

Maybe you just don't understand exceptions in the first place? I assumed you do ...
 

xKiv

Active member
So, after rereading the change that sprung from this (which includes one thing that was not explicitly mentioned in this thread, namely that it doesn just restore interpretter.state, but also StaticEntity.continuationState), I think I need a clarification.

Was the issue that
- Try.execute did not reset continuationState to pre-finally value after leaving the finally block
- but it did call KolMafia.forceContinue() before executing the finally block, which reset StaticEntity.continuationState to CONTINUE
- therefore, passing through a try/finally block effectively downgraded an abort to non-abort (because, unless the user explicitly aborted, KoMafia.refusesContinue() can only be true of StaticEntity.continuationState is ABORT)
- because of this, capturing return values at any point up the call stack would incorrectly stop the abort ... which I wrongly assumed to be the intended behavior [1]
?

(and this no longer happens, because now even continuationState is restored)

[1] why did nobody correct me on this?
 

Veracity

Developer
Staff member
It's fine to catch the value up the call stack, but ABORT is explicitly defined to always stop a script. Kolmafia.refusesContinue() checks both ABORT state and a user-forced abort (ESC key). KoLmafia.forceContinue resets both of them. If the user hits the ESC key to force the script to stop, it is not ok to downgrade that to a not-abort because we passed through a finally block.

So, yes, what you describe is exactly what I was trying to fix.
 

Crowther

Active member
If the user hits the ESC key to force the script to stop, it is not ok to downgrade that to a not-abort because we passed through a finally block.
I've been following this closely as a script writer without realizing that this fix might being an end to pounding escape over an over and over trying to get a script to stop.
 
Top