Scopes et Variables dans Catnip
Ce guide explique en détail comment fonctionnent les scopes (portées) et les variables dans Catnip.
Vue d'ensemble
Catnip utilise un système de scopes lexicaux (lexical scoping) où la portée d'une variable est déterminée par sa position dans le code source. Chaque fonction crée un nouveau scope, et les variables peuvent être résolues en remontant la chaîne de scopes parents.
Concepts clés
- Scope global : Le scope de niveau racine où les variables sont accessibles partout
- Scope local : Un scope créé par une fonction, bloc ou lambda
- Pile de scopes : Une structure de pile qui gère les scopes imbriqués
- Shadowing : Quand une variable locale masque une variable de même nom dans un scope parent
Scope global vs scope local
Scope global
Le scope global contient :
- Les builtins Python (
print,len,range, etc.) - Les variables définies au niveau racine du programme
- Les fonctions définies au niveau racine
# Au niveau racine - variables globales
x = 10
y = 20
print(x, y) # 10 20
Comportement important : Quand il n'y a qu'un seul scope (le scope global), toutes les assignations vont dans context.globals.
Scope local
Un nouveau scope local est créé pour :
- Les fonctions (
name = () => { … }) - Les lambdas (
(params) => { … }) - Les blocs (indirectement via les fonctions)
x = 10 # Variable globale
f = () => {
x = 20 # Variable locale, masque la globale
print(x) # 20
}
f()
print(x) # 10 - la globale n'a pas changé
Pile de scopes
Catnip maintient une pile de scopes dans context._scopes :
[Scope global] # Scope 0 (base)
[Scope fonction 1] # Scope 1
[Scope fonction 2 (imbriquée)] # Scope 2
Détection du scope global
Le code utilise len(context._scopes) == 1 pour détecter si on est au scope global :
# Dans registry_core.pyx:_set_locals
is_global_scope = len(self.ctx._scopes) == 1
if is_global_scope:
self.ctx.globals[name] = value # Variable globale
else:
self.ctx.locals._set(name, value) # Variable locale
Résolution de variables
Ordre de résolution
Quand Catnip cherche une variable, il suit cet ordre :
- Scope local courant :
context.locals - Scopes parents : Remonte la chaîne
parentjusqu'à trouver - Scope global :
context.globals - Erreur :
NameErrorsi non trouvé
Exemple de résolution
x = 100 # Global
f1 = () => {
y = 200 # Local à f1
f2 = () => {
z = 300 # Local à f2
# Résolution :
# - z : trouvé dans scope local de f2
# - y : trouvé dans scope parent (f1)
# - x : trouvé dans globals
print(x, y, z) # 100 200 300
}
f2()
}
f1()
Shadowing (masquage)
Une variable locale peut masquer une variable de même nom dans un scope parent :
x = 10 # Global
f = () => {
x = 20 # Local, masque la globale
print(x) # 20
g = () => {
x = 30 # Local à g, masque celle de f
print(x) # 30
}
g()
print(x) # 20 - celle de f
}
f()
print(x) # 10 - la globale
Fonctions et scopes
Création de scope
Chaque appel de fonction crée un nouveau scope :
compteur = (start) => {
count = start # Variable locale
increment = () => {
count = count + 1 # Accède au count du scope parent
count
}
increment
}
c1 = compteur(0)
c2 = compteur(100)
print(c1()) # 1
print(c1()) # 2
print(c2()) # 101
print(c2()) # 102
Note : Dans l'exemple ci-dessus, chaque lambda increment a accès au count de son scope parent, créant ainsi une closure.
Paramètres de fonction
Les paramètres sont des variables locales au scope de la fonction :
f = (a, b, c) => {
# a, b, c sont dans le scope local
result = a + b + c
result # Aussi dans le scope local
}
f(1, 2, 3) # 6
# a, b, c, result ne sont pas accessibles ici
Paramètres variadiques
Les paramètres variadiques *args créent une liste dans le scope local :
somme = (*nums) => {
# nums est une liste locale
total = 0
for n in nums {
total = total + n
}
total
}
somme(1, 2, 3, 4, 5) # 15
Exemples pratiques
Compteur avec état
# Closure qui maintient un état privé
make_counter = () => {
count = 0 # Variable privée
# Retourne un dict de fonctions
dict(
("increment", () => {
count = count + 1
count
}),
("decrement", () => {
count = count - 1
count
}),
("get", () => { count })
)
}
counter = make_counter()
increment = counter.__getitem__("increment")
decrement = counter.__getitem__("decrement")
get = counter.__getitem__("get")
print(increment()) # 1
print(increment()) # 2
print(decrement()) # 1
print(get()) # 1
Fabrique de fonctions
# Crée une fonction qui multiplie par n
make_multiplier = (n) => {
(x) => { x * n } # n est capturé du scope parent
}
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Variables globales explicites
# Au niveau racine
config = dict(("debug", True), ("version", "1.0"))
# Les fonctions peuvent lire les globales
get_config = (key) => {
config.__getitem__(key)
}
print(get_config("version")) # "1.0"
# Mais attention : assigner crée une locale
# (sauf au niveau racine)
set_config = (key, value) => {
# Ceci crée une NOUVELLE variable locale 'config'
# qui masque la globale
config = dict((key, value))
}
Note : Pour modifier une globale depuis une fonction, il faut actuellement utiliser des méthodes qui modifient l'objet en place (pas de mot-clé global comme en Python).
Optimisations internes
Cython et performance
Catnip utilise Cython pour optimiser les opérations de scope :
scope.pyx: Résolution rapide avec lookups Cregistry_core.pyx: Gestion optimisée des scopes- Chaînage de scopes avec pointeurs parents
Tail-Call Optimization (TCO)
Les fonctions tail-récursives utilisent un seul scope et rebindent les paramètres au lieu de créer de nouveaux scopes :
# Factorielle tail-récursive
factorial = (n, acc=1) => {
if n <= 1 {
acc
} else {
factorial(n - 1, n * acc) # Tail call
}
}
# Un seul scope utilisé même pour factorial(1000)
factorial(1000) # Pas de stack overflow !
Résumé
| Concept | Description |
|---|---|
| Scope global | Niveau racine, accessible partout |
| Scope local | Créé par fonction/lambda, limité à cette fonction |
| Résolution | Local → Parents → Global |
| Shadowing | Variable locale masque parent de même nom |
| Closure | Fonction capture variables de son scope parent |
| TCO | Un seul scope pour tail-récursion |