Introduction

Ce didacticiel est destiné aux nouveaux utilisateurs de la Simple Virtual Machine.

Dans ce didacticiel, vous allez mettre en place des fonctions récursives.

Le temps de lecture de ce didacticiel est estimé à 15 minutes si les fonctions ont été abordées.

Mise en place

Pour commencer, créez le canevas de l'application dans le fichier exécutable recursion.svm en utilisant ce code :

#!/usr/bin/env svm
DESCRIPTION
Recursion example
END
LOG
DEBUG "Recursion" STYLE "default"
PLUGIN "svmcom.so"
PLUGIN "svmint.so"
ARGUMENT INT nb
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
	END
	MEMORY nb
END

Récursion simple

Modifiez le code pour ajouter une fonction récursive et son appel :

#!/usr/bin/env svm
DESCRIPTION
Recursion example
END
LOG
DEBUG "Recursion" STYLE "default"
PLUGIN "svmcom.so"
PLUGIN "svmint.so"
ARGUMENT INT nb
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
		:memory (PTR, INT, INT)/parameters
		[ , @&nb , ] -> parameters
		:call f parameters
		:com.message @(parameters/2)
		:shutdown
		# Function f: PTR, INT (input), INT (output)
	:label f
		:memory PTR, INT, INT, BLN -> &P
		:int.cmp @(P/1) < 2 -> (@&P/3)
		:goto f_end :when @(@&P/3) TRUE
		@(P/1) -> (@&P/1)
		:shift -1 (@&P/1)
		:call f &@&P*3
		:int.mul @(@&P/2) @(P/1) -> (P/2)
		:return
	:label f_end
		1 -> (P/2)
		:return
	END
	MEMORY nb
END

Avant de lancer l'application, observons la fonction :

Une telle fonction est dite "récursive".

Lancez l'application avec quelques valeurs :

./recursion.svm 0
1
./recursion.svm 1
1
./recursion.svm 2
2
./recursion.svm 3
6
./recursion.svm 4
24
./recursion.svm 5
120

Il s'agit de la célèbre fonction factorielle : elle renvoie le produit des nombres de 1 à la valeur d'entrée.

Lancez l'application avec la valeur 5 dans le débugueur et observez le déroulement de l'exécution instruction par instruction. Lorsque la fonction passe dans la branche sans appel récursif, la mémoire et le processeur ressemblent à :

Recursion
Memory - K main - P application
AddressTypeValue
&0INT5
&1PTR&4*4
&2INT5
&3INT
&4PTR&8*4
&5INT4
&6INT
&7BLNFALSE
&8PTR&12*4
&9INT3
&10INT
&11BLNFALSE
&12PTR&16*4
&13INT2
&14INT
&15BLNFALSE
&16PTR&20*4
&17INT1
&18INT
&19BLNFALSE
&20PTR
&21INT
&22INT
&23BLNTRUE
AliasPointer
nb&0*1
parameters&1*3
Address:
Display
Aliases
P
Focus
Back
Clear
Processor - K main - P application
State:
Next instruction: <main:1/15>
Current instruction: <main:1/14>
Code
Current memory: &16*3
Allocated memory: &20*4
Defined aliases:
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
Return stack:
State:
Next instruction: <main:1/12>
Current instruction: <main:1/11>
Code
Current memory: &12*3
Allocated memory: &16*4
Defined aliases:
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
State:
Next instruction: <main:1/12>
Current instruction: <main:1/11>
Code
Current memory: &8*3
Allocated memory: &12*4
Defined aliases:
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
State:
Next instruction: <main:1/12>
Current instruction: <main:1/11>
Code
Current memory: &4*3
Allocated memory: &8*4
Defined aliases:
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
State:
Next instruction: <main:1/12>
Current instruction: <main:1/11>
Code
Current memory: &1*3
Allocated memory: &4*4
Defined aliases:
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
State:
Next instruction: <main:1/4>
Current instruction: <main:1/3>
Code
Current memory: &0*0
Allocated memory: &1*3
Defined aliases: parameters
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
Global interruptions:
Waiting interruptions:
Code main - K main - P application
:debug BREAK
:memory ( PTR , INT , INT )/parameters
[  , @&nb ,  ] -> parameters
:call f parameters
:com.message @(parameters/2)
:shutdown
:label f

:memory PTR , INT , INT , BLN -> &P
:int.cmp @(P/1) < 2 -> (@&P/3)
:goto f_end :when @(@&P/3) TRUE
@(P/1) -> (@&P/1)
:shift -1 (@&P/1)
:call f &@&P*3
:int.mul @(@&P/2) @(P/1) -> (P/2)
:return
:label f_end

1 -> (P/2)
:return
Auto-scroll to
Current
with above
Display

Lorsque les appels récursifs se terminent, vous pouvez observer les différentes valeurs de la factorielle dans la mémoire.

Une fonction est dite récursive lorsqu'elle s'appelle elle-même. La récursion se termine lorsque la fonction passe par une branche de code qui ne l'appelle plus.

Les règles habituelles des fonctions s'appliquent toutes aux fonctions récursives.

Récursion interrompue

Modifiez le code pour obtenir :

#!/usr/bin/env svm
DESCRIPTION
Recursion example
END
LOG
DEBUG "Recursion" STYLE "default"
PLUGIN "svmcom.so"
PLUGIN "svmint.so"
ARGUMENT INT nb
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
		:memory (PTR, INT, INT)/parameters
		[ , @&nb , ] -> parameters
		:flag "main"
		:interruption CASCADE NUMERIC stop
		:call f parameters
		:goto error :unless (parameters/2) INITIALISED
		:com.message @(parameters/2)
		:shutdown
	:label error
		:com.message "Error"
		:shutdown
		# Function f: PTR, INT (input), INT (output)
	:label f
		:memory PTR, INT, INT, INT, BLN -> &P
		:int.cmp @(P/1) = 0 -> (@&P/4)
		:goto f_end :when @(@&P/4) TRUE
		:int.mod @(P/1) 2 -> (@&P/3)
		:int.cmp @(@&P/3) = 0 -> (@&P/4)
		:goto f_alter :when @(@&P/4) TRUE
		:int.sub @(P/1) 1 -> (@&P/1)
		:call f &@&P*3
		:int.div 100 @(@&P/2) -> (P/2)
		:return
	:label f_alter
		:int.div @(P/1) 4 -> (@&P/1)
		:call f &@&P*3
		:int.add @(P/1) @(@&P/2) -> (P/2)
		:return
	:label f_end
		0 -> (P/2)
		:return
		# Interruption handler
	:label stop
		:return 0 :unless "main" RAISED
	END
	MEMORY nb
END

Puis lancez l'application avec quelques valeurs, en mode débugueur à votre convenance :

./recursion.svm 0
0
./recursion.svm 2
2
./recursion.svm 3
50
./recursion.svm 8
10
./recursion.svm 10
12

Dans tous ces cas, la fonction se déroule correctement, et les appels récursifs aboutissent pour donner le résultat.

Maintenant, lancez l'application avec ces autres valeurs :

./recursion.svm 1
Error
./recursion.svm 4
Error
./recursion.svm 6
Error
./recursion.svm 17
Error
./recursion.svm 70
Error

Cette fois, pour chacune de ces valeurs, un des appels récursif tombe sur une division par 0, ce qui génère une interruption NUMERIC.

Pour rattrapper cette interruption, le code appellant la fonction :

  1. Lève un drapeau "main" sur son niveau de pile de retour de fonction,
  2. Place un gestionnaire d'interruption cascadé sur l'interruption NUMERIC.

Et la fonction associée au gestionnaire d'interruption contient une seule instruction qui permet de ressortir de tous les appels récursifs (jusqu'à détecter le drapeau "main") en une seule fois en libérant la mémoire.

Lancez l'application en mode débugueur avec la valeur 17, puis placez un point d'arrêt éphémère sur l'instruction :return 0 :unless "main" RAISED et lancez l'exécution du code :

Recursion
Processor - K main - P application
State:
Next instruction: <main:1/28>
Current instruction: <main:1/27>
Code
Current memory: &14*3
Allocated memory:
Defined aliases:
Local interruptions:
Cascaded local interruptions: NUMERIC => <main:1/27>
Code
Flags:
Cascaded flags:
Return stack:
State:
Next instruction: <main:1/20>
Current instruction: <main:1/19>
Code
Current memory: &14*3
Allocated memory: &19*5
Defined aliases:
Local interruptions:
Cascaded local interruptions: NUMERIC => <main:1/27>
Code
Flags:
Cascaded flags:
State:
Next instruction: <main:1/23>
Current instruction: <main:1/22>
Code
Current memory: &9*3
Allocated memory: &14*5
Defined aliases:
Local interruptions:
Cascaded local interruptions: NUMERIC => <main:1/27>
Code
Flags:
Cascaded flags:
State:
Next instruction: <main:1/23>
Current instruction: <main:1/22>
Code
Current memory: &4*3
Allocated memory: &9*5
Defined aliases:
Local interruptions:
Cascaded local interruptions: NUMERIC => <main:1/27>
Code
Flags:
Cascaded flags:
State:
Next instruction: <main:1/19>
Current instruction: <main:1/18>
Code
Current memory: &1*3
Allocated memory: &4*5
Defined aliases:
Local interruptions:
Cascaded local interruptions: NUMERIC => <main:1/27>
Code
Flags:
Cascaded flags:
State:
Next instruction: <main:1/6>
Current instruction: <main:1/5>
Code
Current memory: &0*0
Allocated memory: &1*3
Defined aliases: parameters
Local interruptions:
Cascaded local interruptions: NUMERIC => <main:1/27>
Code
Flags: main
Cascaded flags:
Global interruptions:
Waiting interruptions:
Memory - K main - P application
AddressTypeValue
&0INT17
&1PTR&4*5
&2INT17
&3INT
&4PTR&9*5
&5INT16
&6INT
&7INT1
&8BLNFALSE
&9PTR&14*5
&10INT4
&11INT
&12INT0
&13BLNTRUE
&14PTR&19*5
&15INT1
&16INT
&17INT0
&18BLNTRUE
&19PTR&24*5
&20INT0
&21INT0
&22INT1
&23BLNFALSE
AliasPointer
nb&0*1
parameters&1*3
Address:
Display
Aliases
P
Focus
Back
Clear
Code main - K main - P application
:debug BREAK
:memory ( PTR , INT , INT )/parameters
[  , @&nb ,  ] -> parameters
:flag "main"
:interruption CASCADE NUMERIC stop
:call f parameters
:goto error :unless (parameters/2) INITIALISED
:com.message @(parameters/2)
:shutdown
:label error

:com.message "Error"
:shutdown
:label f

:memory PTR , INT , INT , INT , BLN -> &P
:int.cmp @(P/1) = 0 -> (@&P/4)
:goto f_end :when @(@&P/4) TRUE
:int.mod @(P/1) 2 -> (@&P/3)
:int.cmp @(@&P/3) = 0 -> (@&P/4)
:goto f_alter :when @(@&P/4) TRUE
:int.sub @(P/1) 1 -> (@&P/1)
:call f &@&P*3
:int.div 100 @(@&P/2) -> (P/2)
:return
:label f_alter

:int.div @(P/1) 4 -> (@&P/1)
:call f &@&P*3
:int.add @(P/1) @(@&P/2) -> (P/2)
:return
:label f_end

0 -> (P/2)
:return
:label stop

:return 0 :unless "main" RAISED
Auto-scroll to
Current
with above
Display

Avant de poursuivre l'exécution du code, notez :

  1. sur quel niveau de la pile de retour se trouve le drapeau "main",
  2. le nombre d'états dans la pile de retour du processeur,
  3. le nombre d'adresses définies dans la mémoire.

Puis exécutez uniquement l'instruction :return 0 :unless "main" RAISED et constatez le nouvel état : en une seule instruction, tous les appels récursifs sont immédiatement terminés, et la mémoire allouée par chaque appel est libérée.

L'instruction :return 0 associée à une condition permet d'interrompre proprement l'exécution d'une fonction récursive.

Les drapeaux peuvent être utilisés pour simplifier la condition de sortie de la fonction récursive.

Lorsqu'il s'agit d'une sortie anticipée suite à une interruption, ce mécanisme peut être mis en place indépendamment de la fonction récursive elle-même et peut varier d'un appel de la fonction à l'autre.

Conclusion

Vous venez de voir comment écrire des fonctions récursives dans une application.

Les fonctions récursives sont parfois un moyen élégant pour écrire certains calculs, mais comportent souvent une gestion d'erreur relativement complexe.

Avec l'argument 0 de l'instruction de retour de fonction et une condition, une fonction récursive peut être aisément terminée sans crainte de fuite mémoire.