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 :

  1. Scope local courant : context.locals
  2. Scopes parents : Remonte la chaîne parent jusqu'à trouver
  3. Scope global : context.globals
  4. Erreur : NameError si 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 C
  • registry_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