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
condest vrai → la valeur deA - si
condest faux →None
Forme avec else
if cond { A } else { B }
est une expression dont la valeur est :
- si
condest vrai → la valeur deA - si
condest faux → la valeur deB
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 :
breaketcontinuene fonctionnent que dans les boucles (while,for)- Utiliser
breakoucontinueen 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
Opavecident='call'reçoivent un attributtail=Trues'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 |