Documentation du langage

Introduction

Catnip est un langage de programmation interprété conçu pour être simple, expressif et performant. Il combine une syntaxe claire avec des fonctionnalités modernes comme le pattern matching et les lambdas.

Caractéristiques principales

  • Syntaxe claire et concise : inspirée par des langages modernes
  • Typage dynamique : les types sont déterminés à l'exécution
  • Pattern matching : pour un code plus expressif et sûr
  • Fonctions de première classe : les fonctions sont des valeurs comme les autres
  • Performance : optimisations via Cython pour les opérations critiques
  • REPL interactif : pour expérimenter et apprendre rapidement

Premiers pas

Premier programme

Catnip n’a jamais eu vocation à parler au monde.

Seulement à l’exécuter.

print("BORN TO SEGFAULT")

Types de données

Catnip supporte les types de données suivants :

Nombres

# Entiers
chaussette_gauche = 42
chaussette_droite = -17

# Flottants
pi = 3.14159
constante_de_cafe = 2.71828

# Notation scientifique
espace_infiniment_loin = 1.5e10

# Hexadécimal, binaire, octal
hex_val = 0xFF
bin_val = 0b1010
oct_val = 0o755

Chaînes de caractères

# Chaînes simples ou doubles guillemets
message = "BORN TO SEGFAULT"
name = 'Capitaine Whiskers'

# Chaînes multilignes
text = """
    Ceci est une chaîne
    sur plusieurs lignes
"""

Pas de concaténation implicite

Contrairement à Python, Catnip ne concatène pas automatiquement les chaînes adjacentes :

# Python : "hello" "world" → "helloworld"
# Catnip : erreur de syntaxe

# Utiliser l'opérateur + explicitement
message = "hello" + "world"

Pourquoi ce choix ?

La concaténation implicite est une source de bugs silencieux, notamment dans les listes :

# En Python, une virgule oubliée passe inaperçue :
items = [
    "foo",
    "bar"   # virgule oubliée
    "baz"
]
# items == ["foo", "barbaz"] — aucune erreur, bug silencieux

Forcer un opérateur explicite élimine cette catégorie de bugs. Une erreur de syntaxe visible vaut mieux qu'un comportement incorrect silencieux.

La concaténation implicite applique le principe du moindre effort au mauvais endroit : elle économise un caractère (+) mais coûte potentiellement des heures de débogage. Le ratio effort/bénéfice est défavorable d'un facteur d'environ 10⁴.

F-strings (chaînes formatées)

Les f-strings permettent d'interpoler des expressions directement dans les chaînes de caractères, évitant ainsi d'encombrer le contexte avec des fonctions de concaténation basiques.

Syntaxe : Préfixer la chaîne avec f ou F (insensible à la casse) et utiliser {expression} pour insérer des valeurs.

# Variables simples
astronaute = "Léonie"
age = 30
f"Je m'appelle {astronaute} et j'ai {age} ans"
# → "Je m'appelle Léonie et j'ai 30 ans"

# Expressions arithmétiques
x = 10
y = 20
f"La somme de {x} et {y} est {x + y}"
# → "La somme de 10 et 20 est 30"

# Appels de fonction
double_burrito = (n) => { n * 2 }
n = 5
f"Le double de {n} est {double_burrito(n)}"
# → "Le double de 5 est 10"

# Échappements standards
f"Ligne 1\nLigne 2"  # Retour à la ligne
f"Colonne 1\tColonne 2"  # Tabulation

# Majuscules ou minuscules
F"Valeur: {42}"  # Identique à f"Valeur: {42}"

Note : Les expressions dans les f-strings sont évaluées dans le scope courant. Les variables non définies ou les erreurs de syntaxe produisent des messages d'erreur clairs.

Évolution future : L'implémentation actuelle couvre les besoins basiques. Des fonctionnalités avancées (format specs comme :02d ou :.2f) pourront être ajoutées si nécessaire.

Booléens

vrai = True
faux = False
rien = None

Listes

Catnip supporte les littéraux de listes avec la syntaxe list(…) :

# Liste vide
empty = list()

# Liste de nombres
scores_de_licorne = list(1, 2, 3, 4, 5)

# Liste de chaînes
crew = list("Alice", "Bob", "Charlie")

# Liste avec expressions
computed = list(1 + 1, 2 * 3, 10 / 2)  # list(2, 6, 5.0)

# Listes imbriquées
matrix = list(
    list(1, 2, 3),
    list(4, 5, 6),
    list(7, 8, 9)
)

# Avec virgule finale (optionnel)
items = list(1, 2, 3,)

# Accès aux éléments (via __getitem__)
first = scores_de_licorne.__getitem__(0)  # 1
last = scores_de_licorne.__getitem__(4)   # 5

# Itération
for n in scores_de_licorne {
    print(n)
}

# Avec fonctions Python
total = sum(list(1, 2, 3, 4, 5))   # 15
length = len(crew)                 # 3

Note : La syntaxe list(…) évite la confusion avec la notation de broadcast .[…].

Sets

Les sets sont des collections non ordonnées sans répétition, ils utilisent la syntaxe set(…) :

# Set vide
empty = set()

# Set avec valeurs
numbers = set(list(1, 2, 3, 4, 5))

# Les doublons sont automatiquement supprimés
unique = set(list(1, 2, 2, 3, 3, 3))  # {1, 2, 3}

# Opérations sur les sets (via Python)
a = set(list(1, 2, 3, 4))
b = set(list(3, 4, 5, 6))
union = a.union(b)           # {1, 2, 3, 4, 5, 6}
intersection = a.intersection(b)  # {3, 4}
difference = a.difference(b)      # {1, 2}

Dictionnaires

Les dictionnaires utilisent des paires (clé, valeur) :

# Dictionnaire vide
empty = dict()

# Dictionnaire simple
pirate = dict(("name", "Capitaine Whiskers"), ("age", 7), ("city", "Paris"))

# Clés numériques
mapping = dict((1, "un"), (2, "deux"), (3, "trois"))

# Valeurs calculées
stats = dict(("sum", 1 + 2 + 3), ("product", 2 * 3 * 4))

# Structures imbriquées
data = dict(
    ("numbers", list(1, 2, 3)),
    ("info", dict(("x", 10), ("y", 20)))
)

# Accès aux valeurs (via __getitem__)
nom_capitaine = pirate.__getitem__("name")  # "Capitaine Whiskers"

# Avec virgule finale (optionnel)
config = dict(("debug", True), ("port", 8080),)

Note : La syntaxe dict((key, value), …) nécessite des paires car {…} est réservé pour les blocs de code.

Autres collections (via Python)

# Tuples
coords_lune = tuple(list(10, 20))

# Sets
unique = set(list(1, 2, 2, 3, 3, 3))

# Ranges
numbers = list(range(1, 10))

Variables et assignation

Assignation simple

x = 10
nom = "Alice"
actif = True

Assignation en chaîne

# Assigner la même valeur à plusieurs variables
a = b = c = 42

Référence à la dernière valeur

x = 10
y = 20
x + y        # Résultat: 30
print(_)     # _ contient la dernière valeur calculée: 30

Expressions multilignes

Les expressions peuvent s'étendre sur plusieurs lignes en utilisant des parenthèses. Les newlines sont ignorés à l'intérieur des parenthèses, permettant de formater le code de manière lisible.

Les parenthèses créent une zone de micro-apesanteur syntaxique où les newlines flottent librement sans jamais toucher le sol du parser. Un espace où les expressions longues se détendent naturellement.

Expressions arithmétiques

# Expression multiligne avec parenthèses
resultat = (
    10 + 20 +
    30 + 40 +
    50
)  # 150

# Calcul complexe
moyenne = (
    valeur1 + valeur2 + valeur3
    + valeur4 + valeur5
) / 5

# Avec indentation libre
total = (
    prix_ht
    * quantite
    * (1 + tva)
)

Appels de fonction

Les arguments de fonction peuvent être répartis sur plusieurs lignes. Note : le premier argument doit être sur la même ligne que (, les arguments suivants peuvent être sur des lignes séparées.

# Appel multiligne
resultat = fonction_complexe(argument1,
    argument2,
    argument3,
    argument4)

# Avec arguments nommés
configurer(host="localhost",
    port=8080,
    debug=True,
    timeout=30)

# Mélange positionnel et nommé
creer_serveur("192.168.1.1",
    3000,
    ssl=True,
    workers=4)

Lambdas multilignes

Les paramètres de lambda peuvent aussi s'étendre sur plusieurs lignes. Note : le premier paramètre doit être sur la même ligne que (.

# Paramètres multiligne
transformer = (x,
    y,
    z,
    scale=1.0) => {
    x * scale + y * scale + z * scale
}

# Lambda variadique
combiner = (prefix,
    *items) => {
    result = list(prefix)
    for item in items {
        result = result + list(item)
    }
    result
}

Expressions conditionnelles

Les conditions peuvent être formatées sur plusieurs lignes :

# Condition complexe
if (
    age >= 18
    and permis == True
    and experience > 2
) {
    print("Peut conduire")
}

# Expression booléenne longue
valide = (
    x > 0
    and x < 100
    and y > 0
    and y < 100
    and z > 0
)

Cas d'usage

Sans parenthèses (erreur de parsing) :

# ✘ ERREUR : newline termine l'expression
x = 1 + 2 +
    3 + 4  # Parsing error!

Avec parenthèses (valide) :

# ✔ OK : newlines ignorés dans les parenthèses
x = (1 + 2 +
     3 + 4)  # 10

Note : Les collections (list(), dict(), etc.) et les blocs {} supportent déjà les newlines naturellement sans nécessiter de parenthèses supplémentaires.


Opérateurs

Opérateurs arithmétiques

# Addition, soustraction, multiplication, division
resultat = 10 + 5 - 2 * 3 / 4

# Division entière et modulo
quotient = 17 // 5     # 3
reste = 17 % 5         # 2

# Exponentiation
puissance = 2 ** 10    # 1024

# Opérateurs unaires
negatif = -42
positif = +42

Opérateurs de comparaison

# Égalité et inégalité
egal = 5 == 5          # True
different = 5 != 3     # True

# Comparaisons
plus_petit = 3 < 5     # True
plus_grand = 10 > 2    # True
inf_egal = 5 <= 5      # True
sup_egal = 10 >= 5     # True

# Comparaisons en chaîne
dans_intervalle = 1 < x < 10

Opérateurs logiques

# AND, OR, NOT
condition1 = True and False    # False
condition2 = True or False     # True
condition3 = not True          # False

# Court-circuit
resultat = x > 0 and y / x > 2

Opérateurs binaires

# AND, OR, XOR binaire
masque = 0xFF & 0x0F           # 0x0F
union = 0xF0 | 0x0F            # 0xFF
exclusif = 0xFF ^ 0x0F         # 0xF0

# Inversion binaire
inverse = ~0xFF

Structures de contrôle

Blocs

Les blocs regroupent plusieurs instructions et retournent la valeur de la dernière expression.

# Bloc simple
resultat = {
    x = 10
    y = 20
    x + y          # Valeur retournée: 30
}

# Bloc vide
vide = {}          # Retourne None

Conditions (if/elif/else)

# simple if
if x > 0 {
    print("x is positive")
}

# if/else
if x > 0 {
    print("positive")
} else {
    print("negative or zero")
}

# if/elif/else
if x > 0 {
    print("positive")
} elif x < 0 {
    print("negative")
} else {
    print("zero")
}

# nested conditions
if x > 0 {
    if x > 100 {
        print("large number")
    } else {
        print("small number")
    }
}

elif est du sucre syntaxique pour un else contenant un if.

if comme expression

if est une expression, pas un statement.

TODO : if est assignable a = if cond { A } else { B }

Un bloc { … } a pour valeur la valeur de sa dernière expression.

La valeur d’un if est définie par les règles suivantes.

Forme sans else

if cond { A }

est une expression dont la valeur est :

  • si cond est vrai → la valeur de A
  • si cond est faux → None

Forme avec else

if cond { A } else { B }

est une expression dont la valeur est :

  • si cond est vrai → la valeur de A
  • si cond est faux → la valeur de B

Forme avec elif

if cond1 { A } elif cond2 { B } else { C }

est équivalent à :

if cond1 {
    A
} else {
    if cond2 {
        B
    } else {
        C
    }
}

et suit les mêmes règles de valeur.

Interaction avec les fonctions

Le corps d’une fonction est un bloc expression :

f = (x) => {
    if x > 0 {
        "positive"
    } else {
        "non-positive"
    }
}

ici, la valeur de la fonction est la valeur de l’expression if.

return expr sort explicitement de la fonction avec la valeur expr.

si aucun return explicite n’est exécuté, la fonction renvoie la valeur de la dernière expression du bloc.

Note pour les exilés des autres langages

Dans Catnip, if est une expression, et renvoie une vraie valeur.

Et un if sans else renvoie None quand la condition est fausse.

TODO : production d'un valeur par if

Boucle while

# Boucle while simple
i = 0
while i < 5 {
    print("i =", i)
    i = i + 1
}

# Condition complexe
somme = 0
n = 1
while somme < 100 {
    somme = somme + n
    n = n + 1
}

Boucle for

# Itération sur une séquence
for i in range(1, 6) {
    print("i =", i)
}

# Itération sur une liste
for nom in list("Capitaine Whiskers", "Docteur Latte", "Agent Photon") {
    print("BORN TO SEGFAULT,", nom)
}

# Avec enumerate
for idx in range(len(nombres)) {
    print("Index:", idx, "Valeur:", nombres.get(idx))
}

Contrôle de flux dans les boucles : break et continue

break - Sortir d'une boucle

Le mot-clé break permet de sortir immédiatement d'une boucle while ou for.

# Recherche dans une liste
found = False
for i in list(1, 5, 10, 15, 20) {
    if i == 10 {
        found = True
        break  # Sort de la boucle dès qu'on trouve
    }
}

# Boucle infinie avec break
count = 0
while True {
    count = count + 1
    if count == 5 {
        break  # Sort après 5 itérations
    }
}

# Dans des boucles imbriquées, break ne sort que de la boucle interne
for i in list(1, 2, 3) {
    for j in list(1, 2, 3) {
        if j == 2 {
            break  # Sort uniquement de la boucle interne
        }
        print(i, j)
    }
}

continue - Passer à l'itération suivante

Le mot-clé continue passe directement à l'itération suivante de la boucle, en ignorant le reste du code.

# Ignorer les nombres pairs
for i in range(10) {
    if i % 2 == 0 {
        continue  # Passe à l'itération suivante
    }
    print(i)  # Affiche uniquement les impairs
}

# Filtrage dans une boucle while
i = 0
sum = 0
while i < 10 {
    i = i + 1
    if i % 3 == 0 {
        continue  # Ignore les multiples de 3
    }
    sum = sum + i
}

# Validation de données
ages = list(25, -5, 30, 150, 42)
valid_ages = list()
for age in ages {
    if age < 0 or age > 120 {
        continue  # Ignore les valeurs invalides
    }
    valid_ages = valid_ages + list(age)
}

Combiner break et continue

# Recherche avec filtrage
numbers = list(10, 20, 35, 40, 55, 60)
result = None

for num in numbers {
    if num % 10 == 0 {
        continue  # Ignore les multiples de 10
    }
    if num > 50 {
        result = num
        break  # Trouve le premier > 50 (non multiple de 10)
    }
}
# result = 55

Notes importantes :

  • break et continue ne fonctionnent que dans les boucles (while, for)
  • Utiliser break ou continue en dehors d'une boucle lève une exception
  • Dans des boucles imbriquées, ils n'affectent que la boucle la plus interne
  • Pour sortir d'une fonction, utiliser return (qui fonctionne même dans une boucle)

Fonctions

Définition de fonctions

# Fonction sans paramètres
fn saluer() {
    print("BORN TO SEGFAULT!")
}

# Fonction avec paramètres
fn additionner_tacos(a, b) {
    a + b
}

# Fonction avec valeurs par défaut
fn saluer_passager(nom="Monde") {
    print("BORN TO SEGFAULT,", nom, "!")
}

# Fonction avec plusieurs paramètres et défauts
fn configurer_navette(host="localhost", port=8080, debug_mode=False) {
    print("Serveur intergalactique:", host, ":", port)
    print("Mode debug:", debug_mode)
}

Appel de fonctions

# Appel simple
saluer()

# Avec arguments positionnels
resultat = additionner_tacos(10, 20)

# Avec arguments nommés
saluer_passager(nom="Alice")

# Mixte
configurer_navette("192.168.1.1", port=3000, debug_mode=True)

Valeur de retour

La dernière expression d'une fonction est automatiquement retournée :

fn calculer_moyenne(a, b, c) {
    somme = a + b + c
    somme / 3          # Valeur retournée
}

moyenne = calculer_moyenne(10, 20, 30)  # 20.0

Lambdas et blocs anonymes

Les lambdas sont des fonctions anonymes qui peuvent être passées comme valeurs.

Syntaxe

# Lambda sans paramètres
action = () => {
    print("Action exécutée!")
    42
}

# Lambda avec paramètres
doubler = (x) => { x * 2 }
additionner = (a, b) => { a + b }

# Lambda multiligne
calculer = (x, y) => {
    intermediaire = x * 2
    resultat = intermediaire + y
    resultat
}

# Lambda avec valeurs par défaut
saluer = (nom="Monde") => {
    print("BORN TO SEGFAULT,", nom, "!")
}

Fonctions variadiques

Les lambdas supportent les paramètres variadiques avec *args :

# Lambda variadique simple
collect = (*items) => { items }
collect(1, 2, 3)  # list(1, 2, 3)

# Somme variadique
somme = (*nums) => {
    total = 0
    for n in nums {
        total = total + n
    }
    total
}
somme(1, 2, 3, 4, 5)  # 15

# Paramètres mixtes : réguliers + variadiques
prefix_list = (prefix, *items) => {
    result = list(prefix)
    for item in items {
        result = result + list(item)
    }
    result
}
prefix_list(0, 1, 2, 3)  # list(0, 1, 2, 3)

# Avec valeurs par défaut et variadiques
make_list = (prefix=0, *items) => {
    list(prefix) + list(items)
}
make_list(100, 1, 2, 3)  # list(100, 1, 2, 3)
make_list(1, 2, 3)       # list(1, 2, 3)

Utilisation

# Appel direct
valeur = action()           # Exécute la lambda, retourne 42

# Passage en argument
resultat = doubler(21)      # 42

# Avec plusieurs paramètres
somme_result = additionner_tacos(10, 32) # 42

# Avec valeur par défaut
saluer()                   # "BORN TO SEGFAULT!"
saluer_passager("Alice")   # "BORN TO SEGFAULT, Alice !"

Lambdas dans les collections

# Liste de fonctions
operations = list(
    (x) => { x + 1 },
    (x) => { x * 2 },
    (x) => { x ** 2 }
)

# Application
nombre = 5
apply_first = operations.__getitem__(0)
resultat = apply_first(nombre)  # 6

Tail Calls (Appels en position terminale)

Les tail calls (appels en position terminale) sont des appels de fonction qui sont la dernière opération avant le retour d'une fonction. Catnip détecte automatiquement ces appels durant l'analyse sémantique et peut les optimiser pour éviter la croissance de la pile d'appels.

Qu'est-ce qu'un tail call ?

Un appel est en position terminale si : 1. C'est la dernière expression dans une fonction 2. Son résultat est directement retourné (sans opération supplémentaire) 3. C'est dans la dernière branche d'un if/match 4. C'est la dernière expression d'un bloc

✔ Exemples de tail calls

# Factorielle tail-recursive (avec accumulateur)
factorial = (n, acc=1) => {
    if n <= 1 { acc } else { factorial(n-1, n*acc) }  # ✔ Tail call
}

# Compteur à rebours
countdown = (n) => {
    if n == 0 {
        "Terminé!"
    } else {
        countdown(n - 1)  # ✔ Tail call
    }
}

# Avec return explicite
chercher = (liste, valeur) => {
    if len(liste) == 0 {
        return False
    } else {
        if liste[0] == valeur {
            return True
        } else {
            return chercher(liste[1:], valeur)  # ✔ Tail call
        }
    }
}

✘ Exemples de NON-tail calls

# Factorielle classique (opération après l'appel)
factorial_bad = (n) => {
    if n <= 1 { 1 } else { n * factorial_bad(n-1) }  # ✘ NOT tail: multiplication après
}

# Fibonacci (deux appels + addition)
fib = (n) => {
    if n <= 1 { n } else { fib(n-1) + fib(n-2) }  # ✘ NOT tail: addition après
}

# Opération après l'appel
double_sum = (n) => {
    2 * sum_to(n)  # ✘ NOT tail: multiplication après
}

Détection automatique

Catnip détecte automatiquement les tail calls pendant l'analyse sémantique :

  • Les nœuds Op avec ident='call' reçoivent un attribut tail=True s'ils sont en position terminale
  • Cette annotation permet à l'exécuteur d'optimiser l'appel (réutilisation du cadre d'exécution)
  • Seuls les appels récursifs (à la même fonction) sont annotés comme tail calls

Positions terminales

Structure Position terminale
Fonction/Lambda Dernière expression du corps
Block Dernière expression du bloc
If/Elif/Else Dernière expression de chaque branche
Match Dernière expression de chaque case
Return Expression retournée
Opérations ✘ Les arguments d'opérations ne sont PAS en position terminale

Conversion non-tail → tail

Pour profiter de l'optimisation, transformez vos fonctions récursives en tail-recursive avec un accumulateur :

Avant (non-tail) :

sum_list = (liste) => {
    if liste == [] {
        0
    } else {
        liste.get(0) + sum_list(liste.slice(1))  # ✘ Addition après
    }
}

Après (tail-recursive) :

sum_list = (liste, acc=0) => {
    if liste == [] {
        acc
    } else {
        sum_list(liste.slice(1), acc + liste.get(0))  # ✔ Tail call
    }
}

Pattern Matching

Le pattern matching permet de faire correspondre des valeurs à des motifs de manière déclarative et sûre, en regroupant tous les cas au même endroit.

Syntaxe de base

match valeur {
    motif1 => { action1 }
    motif2 => { action2 }
    _ => { action_par_defaut }
}

Correspondance de valeurs littérales

code_http = 404

match code_http {
    200 => { print("OK") }
    404 => { print("Non trouvé") }
    500 => { print("Erreur serveur") }
    _ => { print("Code inconnu") }
}

Capture de variable

# Capturer la valeur dans une variable
nombre = 42

match nombre {
    0 => { print("Zéro") }
    n => { print("Le nombre est:", n) }
}

Wildcard (joker)

match x {
    1 => { print("Un") }
    2 => { print("Deux") }
    _ => { print("Autre chose") }  # Correspond à tout
}

Guards (conditions)

age = 25

match age {
    n if n < 18 => { print("Mineur") }
    n if n < 65 => { print("Adulte") }
    n => { print("Senior") }
}

# Plusieurs conditions
score = 85

match score {
    n if n >= 90 => { print("Excellent") }
    n if n >= 75 => { print("Bien") }
    n if n >= 60 => { print("Passable") }
    n => { print("Insuffisant") }
}

Pattern OR (alternatives)

jour = 6

match jour {
    1 | 2 | 3 | 4 | 5 => { print("Jour de semaine") }
    6 | 7 => { print("Weekend") }
    _ => { print("Jour invalide") }
}

# Avec capture
symbole = "+"

match symbole {
    "+" | "-" => { print("Opérateur additif") }
    "*" | "/" => { print("Opérateur multiplicatif") }
    op => { print("Opérateur inconnu:", op) }
}

Match complexe

# Classification de nombres
fn classifier(n) {
    match n {
        0 => { "zéro" }
        n if n < 0 => { "négatif" }
        n if n < 10 => { "petit positif" }
        n if n < 100 => { "moyen positif" }
        n => { "grand positif" }
    }
}

print(classifier(-5))   # "négatif"
print(classifier(7))    # "petit positif"
print(classifier(150))  # "grand positif"

Propriétés du Pattern Matching

Le système de pattern matching garantit plusieurs propriétés observables :

Déterminisme : Pour une valeur donnée, le matching produit toujours le même résultat - Le premier pattern qui matche est toujours choisi - L'ordre d'évaluation est prévisible (gauche à droite pour les OR patterns) - Un seul parcours de la liste de cases, pas de backtracking

Composition : Les OR patterns se composent de manière associative - a | (b | c) produit le même résultat que (a | b) | c - La recherche est court-circuitée au premier succès - Réduction du nombre de cas à traiter (un seul case au lieu de multiples)

Isolation : Les guards n'ont pas d'effet de bord sur le scope principal - Chaque guard évalue dans un scope temporaire - Les bindings du pattern sont visibles dans le guard - Le scope principal reste intact si le guard échoue - Cette localité facilite le raisonnement (toute la logique au même endroit)

Note théorique : Ces propriétés correspondent à celles d'un morphisme discriminant dans un topos (voir Johnstone, Sketches of an Elephant, vol. 1, D1.3). Le pattern matching construit une fonction partielle (valeur → bindings) avec des garanties de décision unique et prévisible. Cette structure explique pourquoi :

  • Il n'y a pas d'ambiguïté possible (un seul chemin d'exécution)
  • La composition des patterns préserve ces garanties
  • L'ajout de guards correspond à une restriction de domaine sans changer la structure
  • L'exhaustivité peut être vérifiée mécaniquement (car le domaine est bien défini)

Ces fondements catégoriques garantissent que les propriétés observables (déterminisme, prévisibilité) ne sont pas accidentelles mais découlent de la structure mathématique sous-jacente.


Fonctions intégrées

Catnip donne accès à plusieurs fonctions Python intégrées :

Fonctions de base

# write : écrire sur stdout (bas niveau, sans séparateur ni newline)
write("BORN")                             # Écrit "BORN" sans newline
write("TO", " ", "SEGFAULT")              # Écrit "TO SEGFAULT"

# write_err : écrire sur stderr (bas niveau, sans séparateur ni newline)
write_err("Error: ", code)                # Écrit sur stderr

# print : fonction de haut niveau (builtin, implémentée en Python)
# Joint les arguments avec des espaces et ajoute un newline
# Implémentée en utilisant write() sous le capot
print("Message:", valeur)                 # Écrit "Message: <valeur>\n"
print("A", "B", 42)                       # Écrit "A B 42\n"
print()                                   # Écrit juste "\n"

# range : générer une séquence de nombres
for i in range(10) { print(i) }           # 0 à 9
for i in range(5, 10) { print(i) }        # 5 à 9
for i in range(0, 10, 2) { print(i) }     # 0, 2, 4, 6, 8

# len : longueur d'une séquence
taille = len(list(1, 2, 3, 4, 5))         # 5

Conversion de types

# Conversions
entier = int("42")                         # 42
flottant = float("3.14")                   # 3.14
texte = str(42)                            # "42"

# Collections
ma_liste = list(range(5))                  # [0, 1, 2, 3, 4]
mon_tuple = tuple(1, 2, 3)                 # (1, 2, 3)
mon_dict = dict()                          # {}
mon_set = set(1, 2, 2, 3)                  # {1, 2, 3}

Fonctions mathématiques

# Valeur absolue
valeur = abs(-42)                          # 42

# Minimum et maximum
petit = min(3, 7, 2, 9)                    # 2
grand = max(3, 7, 2, 9)                    # 9

# Arrondi
arrondi = round(3.14159, 2)                # 3.14

# Somme
total = sum(list(1, 2, 3, 4, 5))              # 15

Fonctions sur les séquences

# Tri
trie = sorted(list(3, 1, 4, 1, 5, 9))         # [1, 1, 3, 4, 5, 9]

# Inversion
inverse = reversed(list(1, 2, 3))              # [3, 2, 1]

# Énumération
for pair in enumerate(list("a", "b", "c")) {
    print(pair)  # (0, "a"), (1, "b"), (2, "c")
}

# Zip (combiner des séquences)
for pair in zip(list(1, 2, 3), list("a", "b", "c")) {
    print(pair)  # (1, "a"), (2, "b"), (3, "c")
}

Fonctions d'ordre supérieur

# Map (appliquer une fonction à chaque élément)
doubler = (x) => { x * 2 }
doubles = map(doubler, list(1, 2, 3, 4, 5))

# Filter (filtrer selon une condition)
est_pair = (x) => { x % 2 == 0 }
pairs = filter(est_pair, list(1, 2, 3, 4, 5, 6))

Accès aux attributs et chaînage

Catnip supporte l'accès aux attributs et le chaînage de méthodes :

# Accès à un attribut
valeur = objet.attribut

# Appel de méthode
resultat = objet.methode()

# Appel avec arguments
resultat = objet.methode(arg1, arg2)

# Chaînage
resultat = objet.methode1().methode2().attribut

# Chaînage complexe
valeur = objet.get_data().process(param).to_string()

Indexation et Slicing

Catnip supporte l'indexation et le slicing avec la syntaxe […], compatible avec Python.

Indexation simple

# Accès par index (listes)
nombres = list(10, 20, 30, 40, 50)
premier = nombres[0]      # 10
troisieme = nombres[2]    # 30
dernier = nombres[4]      # 50

# Avec variable ou expression
idx = 3
valeur = nombres[idx]           # 40
autre = nombres[1 + 1]          # 30

# Dictionnaires
personne = dict(("nom", "Alice"), ("age", 30))
nom = personne["nom"]           # "Alice"
age = personne["age"]           # 30

# Chaînes de caractères
texte = "bonjour"
premiere_lettre = texte[0]      # "b"

Slicing (extraction de sous-séquences)

Le slicing utilise la syntaxe [start:stop:step] où tous les paramètres sont optionnels.

Slicing basique

# start:stop - du start (inclus) au stop (exclu)
nombres = list(10, 20, 30, 40, 50)
sous_liste = nombres[1:4]       # [20, 30, 40]

# start: - du start jusqu'à la fin
fin = nombres[2:]               # [30, 40, 50]

# :stop - du début jusqu'à stop
debut = nombres[:3]             # [10, 20, 30]

# : - copie complète
copie = nombres[:]              # [10, 20, 30, 40, 50]

Slicing avec pas (step)

# ::step - tous les step éléments
chiffres = list(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
pairs = chiffres[::2]           # [0, 2, 4, 6, 8]
impairs = chiffres[1::2]        # [1, 3, 5, 7, 9]

# start:stop:step - syntaxe complète
nombres = list(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
extrait = nombres[1:8:2]        # [10, 30, 50, 70]

Indices négatifs

Les indices négatifs comptent depuis la fin (-1 = dernier élément).

liste = list(1, 2, 3, 4, 5, 6, 7, 8, 9)

# Accès négatif
dernier = liste[-1]             # 9
avant_dernier = liste[-2]       # 8

# Slicing avec indices négatifs
fin = liste[-3:]                # [7, 8, 9]
milieu = liste[-5:-2]           # [5, 6, 7]
sans_dernier = liste[:-1]       # [1, 2, 3, 4, 5, 6, 7, 8]

Inversion et pas négatif

Un pas négatif parcourt la séquence en sens inverse.

nombres = list(1, 2, 3, 4, 5)

# Inversion complète
inverse = nombres[::-1]         # [5, 4, 3, 2, 1]

# Un élément sur deux en partant de la fin
extrait = nombres[::-2]         # [5, 3, 1]

# Chaînes inversées
texte = "bonjour"
reverse = texte[::-1]           # "ruojnob"

Slicing sur chaînes

Le slicing fonctionne également sur les chaînes de caractères.

message = "hello world"

# Extraction
debut = message[0:5]            # "hello"
fin = message[6:]               # "world"
un_sur_deux = message[::2]      # "hlowrd"

# Palindrome check
mot = "kayak"
est_palindrome = mot == mot[::-1]  # True

Notation .[:] (fullslice)

La syntaxe .[start:stop:step] permet le slicing avec notation explicite à point, utile après des expressions ou pour chaîner avec d'autres opérations.

# Équivalent à [:]
data = list(1, 2, 3, 4, 5, 6)
data.[:]                        # [1, 2, 3, 4, 5, 6]

# Sur expressions
(list(1, 2, 3) + list(4, 5, 6)).[1:5]  # [2, 3, 4, 5]

# Chaînage avec broadcast
list(10, 20, 30, 40, 50).[:3].[* 2]    # [20, 40, 60]

La notation .[:] fonctionne exactement comme [:] mais avec la syntaxe membre. Elle s'avère particulièrement pratique quand l'objet source est une expression complexe ou quand on souhaite chaîner plusieurs opérations de manière fluide.

Cas particuliers

# Slice hors bornes (pas d'erreur, liste vide)
liste = list(1, 2, 3)
vide = liste[10:20]             # []
aussi_vide = liste[5:]          # []

# Step de 0 lève une erreur
# liste[::0]  # ValueError!

# Slicing d'une liste imbriquée
matrix = list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9))
premiere_ligne = matrix[0]      # [1, 2, 3]
sous_matrice = matrix[0:2]      # [[1, 2, 3], [4, 5, 6]]

Indexation et slicing imbriqués

# Accès dans une liste de listes
matrix = list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9))
element = matrix[1][2]          # 6 (2ème ligne, 3ème colonne)

# Slicing puis indexation
liste = list(10, 20, 30, 40, 50)
sous = liste[1:4]               # [20, 30, 40]
element = sous[1]               # 30

# Équivalent en une ligne
element = liste[1:4][1]         # 30

Exemple pratique : recherche dichotomique

binary_search = (liste, cible, gauche=0, droite=None) => {
    if droite == None { droite = len(liste) - 1 }

    if gauche > droite {
        -1
    } else {
        milieu = (gauche + droite) // 2
        valeur = liste[milieu]      # Indexation

        if valeur == cible {
            milieu
        } elif valeur < cible {
            binary_search(liste, cible, milieu + 1, droite)
        } else {
            binary_search(liste, cible, gauche, milieu - 1)
        }
    }
}

liste_triee = list(1, 3, 5, 7, 9, 11, 13, 15)
index = binary_search(liste_triee, 7)  # 3

Exemples complets

Calcul de factorielle

fn factorielle(n) {
    match n {
        0 | 1 => { 1 }
        n => {
            resultat = 1
            i = 2
            while i <= n {
                resultat = resultat * i
                i = i + 1
            }
            resultat
        }
    }
}

print("10! =", factorielle(10))

Nombres de Fibonacci

fn fibonacci(n) {
    match n {
        0 => { 0 }
        1 => { 1 }
        n => {
            a = 0
            b = 1
            i = 2
            while i <= n {
                temp = a + b
                a = b
                b = temp
                i = i + 1
            }
            b
        }
    }
}

# Afficher les 10 premiers nombres de Fibonacci
for i in range(10) {
    print("fib(", i, ") =", fibonacci(i))
}

Tri à bulles

fn trier_bulles(liste) {
    n = len(liste)
    i = 0
    while i < n - 1 {
        j = 0
        while j < n - i - 1 {
            if liste.get(j) > liste.get(j + 1) {
                # Échanger
                temp = liste.get(j)
                liste.set(j, liste.get(j + 1))
                liste.set(j + 1, temp)
            }
            j = j + 1
        }
        i = i + 1
    }
    liste
}

nombres = list([64, 34, 25, 12, 22, 11, 90])
trie = trier_bulles(nombres)
print("Liste triée:", trie)

Calculatrice simple

fn calculer(operation, a, b) {
    match operation {
        "+" => { a + b }
        "-" => { a - b }
        "*" => { a * b }
        "/" => {
            match b {
                0 => { print("Erreur: division par zéro"); None }
                _ => { a / b }
            }
        }
        op => {
            print("Opération inconnue:", op)
            None
        }
    }
}

resultat = calculer("+", 10, 5)   # 15
resultat = calculer("*", 7, 6)    # 42
resultat = calculer("/", 10, 0)   # Erreur: division par zéro

FizzBuzz

fn fizzbuzz(n) {
    for i in range(1, n + 1) {
        match i {
            i if i % 15 == 0 => { print("FizzBuzz") }
            i if i % 3 == 0 => { print("Fizz") }
            i if i % 5 == 0 => { print("Buzz") }
            i => { print(i) }
        }
    }
}

fizzbuzz(20)

Conventions et bonnes pratiques

Nommage

# Variables et fonctions : snake_case
ma_variable = 42
ma_fonction = (x) => { x * 2 }

# Constantes : MAJUSCULES (convention, pas imposé)
PI = 3.14159
MAX_VALEUR = 100

Commentaires

# Commentaire sur une ligne

# Commentaires
# sur plusieurs
# lignes

Organisation du code

# 1. Constantes en haut
MAX_ITERATIONS = 1000
SEUIL = 0.001

# 2. Définitions de fonctions
fn fonction_helper() {
    # …
}

fn fonction_principale() {
    # …
}

# 3. Code principal
resultat = fonction_principale()
print(resultat)

Astuces et pièges à éviter

Portée des variables

Les variables dans les blocs ne sont pas accessibles à l'extérieur.

{
    x = 10
}
print(x)
✘ Error: Unknown identifier 'x'

Si tu veux qu'une variable survive, déclare-la avant:

x = 0
{
    x = 10  # réassigne la variable du scope parent
}
print(x)  # → 10

Toutes les structures de contrôle créent un scope local (if, while, for, {}):

for i in range(5) {
    # i appartient au scope interne créé par le for
    …
}
# i n'existe plus ici
print(i)
✘ Error: Unknown identifier 'i'

Note technique: Les blocs et boucles appellent ctx.push_scope() avant d'exécuter le corps, ce qui isole les variables locales. Contrairement à Python où les variables "fuient" dans le scope parent (comportement historique controversé), Catnip applique le principe de moindre surprise: une variable locale reste locale.

Évaluation court-circuit

# AND s'arrête au premier False
resultat = False and fonction_couteuse()  # fonction_couteuse() n'est PAS appelée

# OR s'arrête au premier True
resultat = True or fonction_couteuse()    # fonction_couteuse() n'est PAS appelée

Match exhaustif

# Toujours prévoir un cas par défaut
match valeur {
    1 => { "un" }
    2 => { "deux" }
    _ => { "autre" }  # IMPORTANT : évite les cas non gérés
}

Annexes

Priorité des opérateurs

du plus fort au plus faible

Opérateur Description
() Parenthèses
** Exponentiation
+x, -x, ~x Unaires
*, /, //, % Multiplication, division
+, - Addition, soustraction
& AND binaire
^ XOR binaire
| OR binaire
<, <=, >, >=, ==, != Comparaisons
not NOT logique
and AND logique
or OR logique