• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Recursion Safety

Recursion Safety

Table of Contents

The Problem

Recursion-safety is often needed in cases where recursive execution of a function is possible
and harmful to an array of system data. Consider this example:

JASS:
function OnDamage takes nothing returns nothing
    set source = GetEventDamageSource()
    set target = GetTriggerUnit()
    set amount = GetEventDamage()
    
    call damageEvent.fire()

    set source = null
    set target = null
    set amount = 0
endfunction

This is a function inside a damage detection system that runs whenever damage is dealt.
Take note of the 5th line of code that fires the event. This line of code will execute
all functions registered to the system to run upon a damage event. What if one of those
functions dealt damage to a unit? You guessed it, this OnDamage function would be called
again and it would refire all the functions registered to the system.

Sometimes, if there are no conditions for dealing damage in the registered functions,
this would lead to an infinite loop. When there are conditions, this leads to a recursion
that would repeat a finite number of times.

Do note that upon the first recursion, the source, target and amount variables will be
changed, so all the functions that still haven't been executed by the damageEvent.fire()
call would be using different source/target/amount information. This also affects the
function that dealt the damage. By dealing damage, you are changing the source, the
target and the amount.

The Solution

The solution to keep the data valid is to preserve the previous values before setting new ones
and restoring them after the functions are executed. We have to wind and unwind a stack. There are
two approaches:

1) The array approach: Instead of using regular variables to store our data, we can use
arrays. We'd have to have an index to represent the stack depth that we increment each time we
execute the function. We'd use that index when writing data to the arrays and subsequently,
reading data from the arrays:

JASS:
function OnDamage takes nothing returns nothing
    set stackDepth = stackDepth + 1
    set source[stackDepth] = GetEventDamageSource()
    set target[stackDepth] = GetTriggerUnit()
    set amount[stackDepth] = GetEventDamage()

    call damageEvent.fire()

    set source[stackDepth] = null
    set target[stackDepth] = null
    set amount[stackDepth] = 0
    set stackDepth = stackDepth - 1
endfunction

This works quite well, but unfortunately, it's not the best solution. In fact, it's a bad one.

2) The locals approach: We can avoid arrays and stack depths altogether by taking advantage
of the automatic storage facilities provided by JASS. Yes, that's a fancy phrase for local variables.

JASS:
function OnDamage takes nothing returns nothing
    local unit previousSource = source
    local unit previousTarget = target
    local real previousAmount = amount

    set source = GetEventDamageSource()
    set target = GetTriggerUnit()
    set amount = GetEventDamage()

    call damageEvent.fire()

    set source = previousSource
    set target = previousTarget
    set amount = previousAmount

    set previousSource = null
    set previousTarget = null
endfunction

This is quite an effective solution because the winding and unwinding is abstracted. The handle
pointers are managed by the JASS VirtualMachine rather than our JASS code.

Wrap-Up

This concludes my tutorial on Recursion-safety in JASS. Stay in school.

~Magtheridon96
 
Top