Directives pragma

Les pragmas règlent la compilation et l'exécution d'un fichier sans modifier son résultat fonctionnel. Un programme correct sans pragma doit rester correct avec un réglage différent ; les directives portent sur les optimisations, le scheduler et le diagnostic.

Règles communes

  • le scope est le fichier entier ;
  • en REPL, l'état persiste entre les évaluations ;
  • la ligne de commande prend priorité sur le fichier, puis viennent les valeurs par défaut ;
  • les directives d'un fichier sont appliquées dans l'ordre, donc la dernière valeur gagne ;
  • chaque directive exige une valeur et les noms inconnus sont rejetés pendant l'analyse sémantique.
pragma("tco", True)
pragma("optimize", 3)
pragma("nd_mode", ND.thread)

La forme générale accepte des arguments supplémentaires pour les directives qui ciblent un nom :

pragma(directive, value)
pragma(directive, value, option=value)

Les booléens tco, jit, cache, debug, warning et nd_memoize n'acceptent que True ou False, sauf jit qui accepte aussi "all". optimize, nd_workers et nd_batch_size exigent des entiers. Une valeur d'un autre type lève CatnipPragmaError.

La priorité se constate sur la configuration effective : si le fichier contient pragma("tco", False) et que le CLI lance catnip -o tco:on, la TCO reste active. Deux directives du fichier peuvent servir à poser une valeur générale puis à la remplacer plus bas ; seule la dernière atteint l'exécution. Un pragma ne possède pas de portée de bloc : le placer dans un if ne le rend pas conditionnel, car l'analyse sémantique le traite avant l'exécution du branchement.

Référence

Pragma Valeurs Défaut Effet
tco True, False True Optimisation des appels terminaux
jit True, False, "all" False Compilation JIT
optimize entier 0 à 3 2 Passes d'optimisation
cache True, False True Cache de compilation
debug True, False False Mode debug
pure nom de fonction Marquage d'une fonction pure
inline "always", "never", "auto" "auto" Indication d'inlining
warning True, False True Émission des warnings
nd_mode ND.sequential, ND.thread, ND.process ND.sequential Backend d'exécution ND
nd_workers entier positif, 0 pour auto 0 Nombre de workers ND
nd_memoize True, False False Mémoïsation ND
nd_batch_size entier positif, 0 pour auto 0 Taille des lots ND

TCO

tco contrôle l'optimisation des appels par nom en position terminale : auto-récursion, récursion mutuelle et appel terminal vers une autre fonction.

pragma("tco", True)

countdown = (n) => {
    if n <= 0 {
        "done"
    } else {
        countdown(n - 1)
    }
}

countdown(100000)
# ⇒ "done"

La TCO est activée par défaut et exécute ces chaînes d'appels en pile constante. La désactiver conserve les frames intermédiaires et fournit donc une trace récursive plus détaillée :

pragma("tco", False)

Les critères exacts d'une position terminale sont documentés dans Fonctions.

Exécution ND

Les pragmas nd_* règlent les opérateurs de ND-récursion ~~ et d'application ~>.

Forme Effet
~~(seed, lambda) Exécute une récursion ND depuis une graine
~~ lambda Construit une fonction ND-récursive réutilisable
~>(data, function) Applique une fonction à des données dans le contexte ND
~> function Construit une fonction levée dans le contexte ND
data.[~~ lambda] Diffuse une récursion ND sur une collection
data.[~> function] Diffuse une application ND sur une collection
list(5, 6, 7).[~~ (n, recur) => {
    if n <= 1 { 1 }
    else { n * recur(n - 1) }
}]
# ⇒ [120, 720, 5040]

Mode

Mode Backend Mémoïsation Frontière principale
ND.sequential appel local locale aucune
ND.thread threads Rayon partagée mémoire partagée, GIL pour Python
ND.process pool de workers + IPC par processus captures et globals copiées
pragma("nd_mode", ND.process)
pragma("nd_workers", 8)

ND.sequential minimise le coût de planification et simplifie le debug. ND.thread partage les données et le cache, mais le GIL limite le code Python CPU-bound. ND.process fournit du parallélisme CPU ; les captures doivent alors franchir une frontière de sérialisation et les mutations ne reviennent pas au parent.

Le backend processus utilise le pool Rust quand la fonction et ses captures sont freezables. Une capture non sérialisable, par exemple un callback Python, déclenche un fallback automatique vers le pool de processus Python.

Workers, mémoïsation et lots

nd_workers = 0 choisit le nombre de workers automatiquement. Une valeur positive impose ce nombre :

pragma("nd_workers", 4)

nd_memoize mémorise les résultats d'une récursion ND. Il convient aux fonctions pures dont les sous-problèmes se répètent :

pragma("nd_memoize", True)

fibonacci = ~~(n, recur) => {
    if n <= 1 { n }
    else { recur(n - 1) + recur(n - 2) }
}

fibonacci(30)
# ⇒ 832040

En mode thread, le cache est partagé. En mode process, chaque worker possède son cache : des sous-problèmes exécutés par deux processus peuvent donc être recalculés.

nd_batch_size fixe le nombre d'éléments par lot. 0 utilise ceil(collection_length / (workers * 4)), soit environ quatre lots par worker :

pragma("nd_batch_size", 0)   # calcul automatique
pragma("nd_batch_size", 10)  # dix éléments par lot

Des petits lots répartissent mieux une charge irrégulière, au prix de plus de planification. Des lots plus grands réduisent ce coût mais augmentent le risque qu'un worker termine bien après les autres.

Le choix du backend, la sérialisation des closures et les méthodes de mesure sont développés dans Concurrence ND. Les exemples complets sont dans ND-récursion.

JIT

jit contrôle la compilation native :

Valeur Effet
False désactive le JIT
True active la détection des boucles et fonctions chaudes
"all" tente une compilation immédiate de toutes les fonctions compatibles
pragma("jit", True)

i = 0
total = 0
while i < 100000 {
    total = total + i
    i = i + 1
}

En VM, une boucle devient candidate après environ cent itérations. Les fonctions récursives terminales simples de un à trois paramètres entiers sont aussi compilables. Si une fonction ou une trace ne correspond pas au sous-ensemble supporté, le runtime continue avec la VM ou l'interpréteur ; le pragma ne transforme pas ce cas en erreur.

Pour cibler une seule fonction, utilise le décorateur @jit :

@jit factorial = (n, accumulator=1) => {
    if n <= 1 { accumulator }
    else { factorial(n - 1, accumulator * n) }
}

Les seuils, les traces et les opérations compilables sont détaillés dans JIT et Optimisations.

Optimisation et cache

optimize règle les passes pour le fichier :

pragma("optimize", 0)  # désactive les passes
pragma("optimize", 3)  # ajoute le tier inter-blocs CFG+SSA

0 désactive les passes. 1 et 2 activent les passes locales — elles ne se distinguent pas encore l'une de l'autre. 3 ajoute le tier inter-blocs CFG+SSA (LICM, DSE globale, GVN), décrit dans Optimisations ; le défaut étant 2, ce tier ne s'obtient que sur demande. L'option CLI -o level:N a priorité sur le pragma.

cache active ou désactive le cache de compilation :

pragma("cache", False)

Son effet est actuellement limité ; il ne contrôle pas le cache de mémoïsation ND, réglé séparément par nd_memoize.

Debug et warnings

pragma("debug", True)
pragma("warning", False)

debug active les informations de diagnostic disponibles pour le runtime. warning contrôle tous les warnings ; une cible précise peut être fournie :

pragma("warning", False, name="unused")

Désactiver un warning réduit le signal du compilateur mais ne change ni l'analyse ni l'exécution.

Pureté et inlining

pure déclare qu'une fonction est déterministe et sans effet de bord observable. Cette propriété autorise des optimisations dans le broadcasting et le JIT :

pragma("pure", "double")
pragma("pure", "double", enable=False)

Le décorateur est la forme recommandée, car le contrat reste attaché à la définition :

@pure double = (x) => { x * 2 }

Un marquage incorrect peut rendre une optimisation invalide ; Catnip ne prouve pas automatiquement la pureté.

inline donne une indication pour une fonction précise :

pragma("inline", "always", function="double")
pragma("inline", "never", function="trace")
pragma("inline", "auto", function="compute")

"auto" laisse le compilateur décider. "always" et "never" expriment un choix d'optimisation, sans modifier le contrat fonctionnel de la fonction.

Un pragma est un bouton de la machine, pas une nouvelle règle du langage. Si le résultat change, le bouton ou le programme ment.

Voir aussi

  • Fonctions — TCO, fonctions pures et décorateurs
  • Concurrence ND — choix du scheduler et frontière processus
  • Optimisations — pipeline de compilation
  • JIT — traces et sous-ensemble compilable