Types de données
Sommaire
- Nombres
- Grands entiers
- Décimales exactes
- Partir d'une chaîne
- Nombres complexes
- Chaînes de caractères
- Pas de concaténation implicite
- F-strings (chaînes interpolées)
- Spécificateurs de format
- Conversion flags
- Debug syntax
- Limitation
- Chaînes de bytes
- Booléens
- Truthiness (valeur de vérité)
- Nil-coalescing (??)
- Enums
- Listes
- Sémantique des collections
- Sets
- Dictionnaires
- Tuples
- Ranges (via Python)
- Introspection de type
- Différences avec Python
- list() / [...] / tuple() / set() : littéraux purs
- Topos ND (~[])
- Namespaces builtin
- META
- ND
- RUNTIME
- Voir aussi
Nombres
integer = 42
float_value = 3.14159
scientific = 1.5e10
hex_val = 0xFF
bin_val = 0b1010
oct_val = 0o755
Grands entiers
L'arithmétique sur les entiers est en précision arbitraire. Les petits entiers (47-bit signés, de -2^46 à 2^46-1) sont
stockés inline sans allocation. Au-delà, la VM promeut automatiquement en BigInt (Arc<GmpInt> via rug/GMP). La
démotion inverse se fait si le résultat retombe dans la plage SmallInt.
Les littéraux suivent la même règle, quelle que soit leur base : ils restent exacts sans passer par un flottant.
2 ** 100
# ⇒ 1267650600228229401496703205376
0xFFFFFFFFFFFFFFFF
# ⇒ 18446744073709551615
(2 ** 100) + 1
# ⇒ 1267650600228229401496703205377
Toutes les opérations arithmétiques (+, -, *, //, %, **) et de comparaison (<, >, ==, etc.)
fonctionnent de manière uniforme sur SmallInt et BigInt. La division / promeut en float.
Les entiers ne débordent pas. Leur prochaine limite est une limite de ressources.
Décimales exactes
Le suffixe d (ou D) crée un nombre décimal base-10 exact (Python decimal.Decimal, 28 chiffres significatifs).
Résout le problème classique IEEE 754 : 0.1 + 0.2 != 0.3 en float.
# Littéraux décimaux
prix = 99.99d
taxe = 0.08d
total = prix + prix * taxe
# ⇒ 107.9892d
# Le test canonique
0.1d + 0.2d == 0.3d
# ⇒ True
# Promotion entier → Decimal
2 + 0.5d
# ⇒ 2.5d
Le suffixe d ne demande aucun import.
Partir d'une chaîne
Le suffixe s'applique à un littéral écrit dans le source. Une valeur lue à l'exécution — un montant dans du JSON, une
colonne de base — arrive en chaîne, et il n'existe pas de nom Decimal dans le langage : la conversion passe par le
module Python, qui rend exactement le même type.
dec = import('decimal', protocol='py')
montant = dec.Decimal("19.99")
montant * 3 # ⇒ 59.97d
montant + 0.01d # ⇒ 20.00d
Multiplier une chaîne par 1d ne convertit pas : "3.14" * 1d lève TypeError, la répétition de séquence n'acceptant
qu'un entier. Depuis un entier, en revanche, la promotion suffit : n * 1d ou n + 0d.
Convertir un float est refusé exprès (Decimal op Float → TypeError) : 0.1 est déjà inexact en binaire, et le
promouvoir figerait l'erreur au lieu de la corriger. Si la source est un float, c'est en amont qu'il faut la lire comme
chaîne.
Un float converti en décimal est une erreur d'arrondi à laquelle on a donné 28 chiffres pour s'exprimer.
Règles de mélange :
Decimal op Decimal→DecimalInt op Decimal→Decimal(promotion exacte)Decimal op Float→ TypeError (pas de coercion implicite)
La division arrondit au contexte décimal Python (28 chiffres significatifs). 10d / 2d donne 5d, 1d / 3d donne
0.3333...d (28 chiffres).
Les décimales ne mentent pas. Elles arrondissent poliment à 28 chiffres, ce qui suffit pour la plupart des réalités.
Nombres complexes
Le suffixe j (ou J) crée un nombre imaginaire pur. La construction d'un complexe complet passe par l'addition
standard.
# Imaginaire pur
2j
# → 2j
# Complexe via addition
1 + 2j
# → (1+2j)
# Arithmétique
(1+2j) * (3+4j)
# → (-5+10j)
# Attributs
(1+2j).real
# → 1.0
(1+2j).imag
# → 2.0
(1+2j).conjugate()
# → (1-2j)
abs(3+4j)
# → 5.0
# Builtin complex()
complex(1, 2)
# → (1+2j)
Règles de mélange :
int op complex→complexfloat op complex→complexcomplex op complex→complex
L'égalité (==, !=) fonctionne. Les comparaisons d'ordre (<, <=, >, >=) lèvent un TypeError - un nombre
complexe n'a pas d'ordre total.
Les complexes vivent dans un plan, pas sur une droite. Leur demander qui est le plus grand, c'est demander à un point de la carte de se justifier.
Chaînes de caractères
# Chaînes simples ou doubles guillemets
message = "BORN TO SEGFAULT"
name = 'Capitaine Whiskers'
# Chaînes multilignes (doubles ou simples guillemets)
text = """
Ceci est une chaîne
sur plusieurs lignes
"""
text2 = '''
Même chose avec
des guillemets simples
'''
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 économise un caractère (
+) et peut fusionner deux littéraux après une virgule oubliée. Le ratio se dégrade au premier bug silencieux.
F-strings (chaînes interpolées)
Les f-strings permettent d'interpoler des expressions directement dans les chaînes, sans multiplier les concaténations.
Syntaxe : préfixer la chaîne avec f ou F (insensible à la casse) et utiliser {expression} pour insérer des
valeurs.
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"
x = 10
y = 20
f"La somme de {x} et {y} est {x + y}"
# ⇒ "La somme de 10 et 20 est 30"
F"Valeur: {42}" # le préfixe est insensible à la casse
Les expressions sont évaluées dans le scope courant. Les échappements \n et \t gardent leur sens habituel.
Spécificateurs de format
Les f-strings supportent les spécificateurs de format Python standard via la syntaxe {expression:format_spec}.
n = 42
pi = 3.14159
ratio = 0.856
text = "chat"
f"{n:05}" # ⇒ "00042"
f"{n:x}" # ⇒ "2a"
f"{pi:8.2f}" # ⇒ " 3.14"
f"{ratio:.1%}" # ⇒ "85.6%"
f"{text:*<10}" # ⇒ "chat******"
f"{text:_^10}" # ⇒ "___chat___"
Référence complète : tous les spécificateurs de la Format Specification Mini-Language Python sont supportés.
Conversion flags
Les flags !r, !s et !a appliquent une conversion avant le formatage :
!rappellerepr()sur la valeur (utile pour afficher les guillemets autour des chaînes)!sappellestr()(comportement par défaut)!aappelleascii()
name = "Alice"
f"{name!r}" # ⇒ "'Alice'"
f"{name!r:>15}" # ⇒ " 'Alice'"
f"{42!s}" # ⇒ "42"
Debug syntax
La syntaxe = après une expression affiche à la fois le code source et le résultat, ce qui facilite le débogage sans
dupliquer le nom de la variable :
x = 42
f"{x=}" # ⇒ "x=42"
f"{x=:.2f}" # ⇒ "x=42.00"
a = 5
b = 3
f"{a + b=}" # ⇒ "a + b=8"
Limitation
Les f-strings imbriquées (nested f-strings) ne sont pas supportées. En Python 3.12+, les f-strings peuvent contenir d'autres f-strings grâce à la réécriture du parser en PEG récursif (PEP 701). Catnip utilise un parser Tree-sitter dont le tokenizer ne supporte pas la récursion à l'intérieur des interpolations.
# OK
f"{x:.2f}"
# Pas supporté
# f"{x:{'.2f' if precise else '.0f'}}"
# f"{f'{x}'}"
Chaînes de bytes
Les chaînes de bytes utilisent le préfixe b et produisent un objet bytes Python :
data = b"hello world"
data.decode("utf-8")
# ⇒ "hello world"
binary = b"\x48\x65\x6c\x6c\x6f"
# ⇒ b"Hello"
Les formes multilignes et les séquences d'échappement fonctionnent comme pour les chaînes. Les bibliothèques Python peuvent recevoir et retourner ces objets directement.
Booléens
vrai = True
faux = False
rien = None
Truthiness (valeur de vérité)
Catnip utilise les mêmes règles de truthiness que Python. Toute valeur peut être évaluée dans un contexte booléen (if,
while, and, or, not).
Valeurs falsy (évaluées à False) :
FalseNone0(entier zéro)0.0(flottant zéro)""(chaîne vide)list()(liste vide)tuple()(tuple vide)set()(set vide)dict()(dictionnaire vide)~[](topos vide ND)
Pour les valeurs natives Catnip, tout le reste est truthy : nombres non nuls, chaînes non vides, collections non vides, structs, fonctions.
x = 0
if x { "jamais" } else { "zéro est falsy" }
# → "zéro est falsy"
s = ""
if s { "jamais" } else { "chaîne vide est falsy" }
# → "chaîne vide est falsy"
data = list(1)
if data { "liste non vide est truthy" }
# → "liste non vide est truthy"
# Court-circuit : and/or retournent un booléen (pas la valeur opérande)
0 or "fallback" # → True (pas "fallback" comme en Python)
"ok" and 42 # → True (pas 42 comme en Python)
False and "nope" # → False
Les objets fournis par l'hôte délèguent leur truthiness au protocole Python (
__bool__/__len__). Un objet qui n'apparaît pas dans la liste peut donc être falsy, voire lever une erreur pendant le test. Les structs Catnip sont toujours truthy, sauf si quelqu'un implémente un jour un struct quantique dans un état superposé vrai-faux, ce qui n'est pas prévu.
La réduction au booléen s'arrête à and/or. Les comparaisons d'ordre (<, <=, >, >=) rendent ce que
l'opérande répond, sans le passer par un test de vérité :
np = import('numpy')
a = np.array(list(1, 2, 3))
b = np.array(list(3, 2, 1))
a < b # [ True False False] -- le tableau numpy, pas un booléen
Sans cette règle, un type qui compare élément par élément n'est pas utilisable : numpy lève
ValueError: ambiguous truth value quand on force un tableau de résultats vers un booléen unique, et une classe dont
__bool__ ne lève pas verrait ses résultats réduits à un seul True sans rien signaler. == et != restent booléens.
Une comparaison chaînée redevient booléenne, parce qu'elle est réécrite en and :
1 < 2 < 3 # True -- équivaut à (1 < 2) and (2 < 3)
Nil-coalescing (??)
a ?? b retourne a si a n'est pas None, sinon évalue et retourne b.
42 ?? 0 # → 42
None ?? 0 # → 0
None ?? None ?? 3 # → 3
?? teste uniquement None, pas la truthiness. Les valeurs falsy sont conservées :
0 ?? 99 # → 0
False ?? 99 # → False
"" ?? 99 # → ""
Trois niveaux distincts de sélection de valeur :
and/or- logique pure, retourne un booléen??- nil-check, retourne la valeur si non-None, sinon le RHSif x { x } else { y }- contrôle de flux, teste la truthiness
Code équivalent explosé :
# a ?? b
{ v = a; if v is None { b } else { v } }
??est le seul opérateur qui distingueNonedeFalse. Les autres s'en remettent à la truthiness, qui ne fait pas de différence entre les deux.
?? et les clés absentes
?? coalesce une valeur, il n'intercepte pas les exceptions : dans d['k'] ?? defaut, le subscript s'évalue d'abord et
lève KeyError si la clé est absente — l'opérateur ne voit jamais passer de valeur. Il ne couvre que la clé présente
valant None. La forme qui couvre les deux cas est d.get('k') ?? defaut (le linter signale le pattern fragile : règle
W304).
d = dict(a=None)
d['a'] ?? 1 # → 1 (clé présente, valeur None)
d.get('x') ?? 1 # → 1 (clé absente, couverte par .get)
# d['x'] ?? 1 # KeyError : le subscript lève avant que ?? n'évalue quoi que ce soit
Ce n'est pas un oubli. Faire capturer KeyError par ?? rendrait le périmètre de capture ambigu — dans
f(d['k']) ?? x, un KeyError levé à l'intérieur de f serait avalé aussi — et masquerait les fautes de frappe sur
les clés. Traiter spécialement le subscript sous ?? donnerait deux sémantiques à la même expression selon son
contexte : extraire v = d['k'] avant le ?? changerait le comportement du programme. Dans les langages où
x[k] ?? defaut fonctionne (JavaScript, Swift, Kotlin), c'est le subscript qui est total — il renvoie
undefined/Optional/null — jamais l'opérateur qui capture une exception. Ici le subscript garde la sémantique
Python, et la composition .get() + ?? fait le travail avec deux constructions existantes.
Un opérateur qui rattrape les exceptions de son opérande gauche ne sait pas où sa gauche commence.
Enums
Les enums déclarent un type avec un ensemble fini de variantes nommées. Chaque variante est une valeur distincte,
accessible par qualification (Color.red). Les variantes sont toujours truthy et supportent le pattern matching.
enum Color { red; green; blue }
c = Color.red
c == Color.red
# → True
Pour la syntaxe complète (déclaration, matching, limitations), voir ENUMS.
Listes
Catnip supporte les littéraux de listes avec la syntaxe list(…) :
empty = list()
scores_de_licorne = list(1, 2, 3, 4, 5)
crew = list("Alice", "Bob", "Charlie")
matrix = list(
list(1, 2, 3),
list(4, 5, 6),
list(7, 8, 9)
)
scores_de_licorne[-1]
# ⇒ 5
scores_de_licorne[1:3]
# ⇒ [2, 3]
for n in scores_de_licorne {
print(n)
}
Note : La syntaxe list(…) évite la confusion avec la notation de broadcast .[…].
Sémantique des collections
list(), tuple() et set() sont des littéraux purs : chaque argument devient un élément.
Règle déterministe :
- 0 argument : collection vide
- 1+ arguments : un argument = un élément (pas de consommation implicite d'itérable)
list() # → []
list(range(5)) # → [range(0, 5)]
list(list(1, 2, 3)) # → [[1, 2, 3]]
list("hello") # → ["hello"]
list(42) # → [42]
list(1, 2, 3) # → [1, 2, 3]
list("hello", "world") # → ["hello", "world"]
Même principe pour tuple() et set().
L'expansion est explicite via * (et ** pour dict) :
list(*list(1, 2), 3, *tuple(4, 5)) # → [1, 2, 3, 4, 5]
tuple(*list(1, 2), 3) # → (1, 2, 3)
set(*list(1, 2, 2), 3) # → {1, 2, 3}
dict(**dict(a=1), ("b", 2), c=3) # → {"a": 1, "b": 2, "c": 3}
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(1, 2, 3, 4, 5)
# Les doublons sont automatiquement supprimés
unique = set(1, 2, 2, 3, 3, 3) # → {1, 2, 3}
# Opérations sur les sets (via Python)
a = set(1, 2, 3, 4)
b = set(3, 4, 5, 6)
u = a.union(b) # → {1, 2, 3, 4, 5, 6}
inter = a.intersection(b) # → {3, 4}
diff = a.difference(b) # → {1, 2}
Dictionnaires
Les dictionnaires supportent deux notations : paires (clé, valeur) et kwargs clé=valeur.
# Dictionnaire vide
empty = dict()
# Notation kwargs (clés string implicites)
pirate = dict(name="Capitaine Whiskers", age=7, city="Paris")
# Notation paires (clés arbitraires)
mapping = dict((1, "un"), (2, "deux"), (3, "trois"))
# Mixte : paires et kwargs dans le même appel
mixed = dict((1, "un"), name="Alice", (2, "deux"))
# 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
nom_capitaine = pirate["name"] # → "Capitaine Whiskers"
# Avec virgule finale (optionnel)
config = dict(debug=True, port=8080,)
Note : La syntaxe dict(…) utilise des paires ou des kwargs car {…} est réservé pour les blocs de code. Les
kwargs convertissent l'identifiant en clé string au parse time.
Tuples
Les tuples sont des séquences immutables, avec la syntaxe tuple(…) :
# Tuple vide
empty = tuple()
# Tuple de coordonnées
coords_lune = tuple(10, 20)
# Accès par index
coords_lune[0] # → 10
coords_lune[-1] # → 20
# Unpacking dans for
for (x, y) in list(tuple(1, 2), tuple(3, 4)) {
print(f"{x}, {y}")
}
Note : la syntaxe (a, b) est réservée aux appels de fonction et au groupement d'expressions. Les tuples utilisent
tuple(…) pour lever l'ambiguïté.
Ranges (via Python)
range() est un builtin Python disponible directement. C'est un itérable, consommable avec for...in :
for i in range(1, 10) {
print(i) # 1 à 9
}
list(range(5)) # → [range(0, 5)] (littéral à 1 élément)
Introspection de type
typeof(expr) retourne le nom du type comme chaîne de caractères. Contrairement au type() Python qui retourne un
objet classe, Catnip retourne directement une string exploitable.
typeof(42)
# ⇒ "int"
struct Point { x; y }
typeof(Point(1, 2))
# ⇒ "Point"
| Valeur | Retour |
|---|---|
| entier | "int" |
| flottant | "float" |
| décimal | "decimal" |
| booléen | "bool" |
None |
"nil" |
| chaîne | "string" |
| liste | "list" |
| tuple | "tuple" |
| dictionnaire | "dict" |
| set | "set" |
| fonction / lambda | "function" |
| instance de struct | nom du type |
| variante d'enum | nom de l'enum |
| objet Python | nom de classe (lowercase) |
typeof() est un intrinsic du langage, pas une fonction first-class. L'expression f = typeof ne fonctionne pas. Pour
accéder au type Python original : import('builtins').type.
Les grands et petits entiers retournent tous deux "int" : leur représentation interne ne change pas leur type logique.
Différences avec Python
Quelques types et syntaxes Python qui n'existent pas en Catnip :
- Pas de séparateur
_dans les nombres :1_000_000n'est pas reconnu, écrire1000000 - Pas de raw strings : pas de préfixe
r"...", les séquences d'échappement sont toujours interprétées - Pas de concaténation implicite de chaînes adjacentes (voir plus haut)
list() / [...] / tuple() / set() : littéraux purs
En Catnip, list(...), tuple(...) et set(...) sont des littéraux variadiques. [...] est une seconde syntaxe de
littéral pour les listes, et non l'appel au constructeur Python :
[1, 2, 3] == list(1, 2, 3)
# ⇒ True
Le broadcast s'écrit cible.[...] : le point distingue les deux syntaxes. La règle d'arité vaut pour les deux formes de
littéral :
| Arité | Comportement | Exemple |
|---|---|---|
| 0 | collection vide | list() → [] |
| 1+ | littéral (argument encapsulé tel quel) | list(range(5)) → [range(0, 5)] |
Un argument reste un élément : list("hello") donne ["hello"], pas une liste de caractères. Il n'existe aucune
branche implicite selon __iter__.
Topos ND (~[])
~[] est un singleton vide utilisé par les opérateurs ND. Il est falsy, itérable vide, et sa longueur vaut 0.
empty = ~[]
len(empty) # → 0
list(empty) # → [~[]]
if empty { 1 } else { 2 } # → 2
Namespaces builtin
Catnip fournit des namespaces en lecture seule, accessibles sans import. Ils suivent la convention CAPS (META, ND,
RUNTIME).
META
Métadonnées du module en cours d'exécution.
META.file-- chemin du fichier source (ou"<input>")META.main--Truesi exécuté directement,Falsesi importé
ND
Constantes pour les modes d'exécution ND-récursion. Évite les fautes de frappe sur les strings.
ND.sequential # → "sequential"
ND.thread # → "thread"
ND.process # → "process"
pragma("nd_mode", ND.thread)
RUNTIME
Constantes internes de la runtime Catnip.
RUNTIME.smallint_max # → 70368744177663 (2^46 - 1)
RUNTIME.smallint_min # → -70368744177664 (-2^46)
RUNTIME.smallint_max + 1 # BigInt, toujours exact
Voir aussi
Les types ci-dessus peuvent aussi annoter des paramètres, des champs et des retours (annotations optionnelles, frontière de types nominaux) :
- TYPE_ANNOTATIONS - référence complète : positions, coercions, contrôles statiques et frontières
- FUNCTIONS - annotations de paramètres (
(x: int)), compositeslist[T]/dict[K, V] - UNIONS - unions de types (
int | str,Point | None) et unions taggées (ADT)