Extension de Catnip

Guide pour ajouter de nouvelles fonctionnalités à Catnip.

Ajouter une nouvelle opération

Pour ajouter une nouvelle opération au langage :

1. Définir l'opcode

Ajouter l'opcode dans catnip_rs/src/ir/opcode.rs (Rust est la source de vérité) :

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum OpCode {
    // ... existing opcodes
    MyOp = 99,
}

Puis regénérer le fichier Python :

python catnip_rs/gen_opcodes.py

2. Ajouter la règle de grammaire

Modifier catnip_rs/grammar/grammar.js pour définir la syntaxe :

// Exemple : opérateur binaire @@
my_op: $ => prec.left(PREC.my_op, seq(
    field('left', $._expression),
    '@@',
    field('right', $._expression)
)),

// Ajouter dans _expression
_expression: $ => choice(
    // ... existing choices
    $.my_op,
),

Regénérer le parser :

make grammar-deps

3. Ajouter le transformer

Créer ou modifier un fichier dans catnip_rs/src/parser/transforms/ :

pub fn transform_my_op(
    py: Python,
    node: Node,
    source: &str,
    transformer: &TreeSitterParser,
) -> PyResult<PyObject> {
    let left_node = node.child_by_field_name("left").unwrap();
    let right_node = node.child_by_field_name("right").unwrap();

    let left = transformer.transform_node(py, left_node, source)?;
    let right = transformer.transform_node(py, right_node, source)?;

    let opcode = OpCode::MyOp as i32;
    let args = PyTuple::new(py, &[left, right])?;

    create_ir(py, opcode, args.into_any(), py.None())
}

Enregistrer dans catnip_rs/src/parser/core.rs :

"my_op" => transform_my_op(py, node, source, self),

4. Ajouter l'implémentation dans le Registry

Ajouter le handler dans catnip_rs/src/core/registry/ (nouveau module ou existant) :

// Dans arithmetic.rs ou nouveau fichier
impl Registry {
    pub fn op_my_op(&self, py: Python, args: &Bound<PyTuple>) -> PyResult<PyObject> {
        let left = self.exec_stmt(py, args.get_item(0)?)?;
        let right = self.exec_stmt(py, args.get_item(1)?)?;

        // Implémentation de l'opération
        let result = my_implementation(left, right)?;

        Ok(result.into_py(py))
    }
}

Ajouter le dispatch dans execution.rs :

fn try_rust_dispatch(&self, py: Python, opcode: i32, args: &Bound<PyTuple>) -> PyResult<Option<PyObject>> {
    match opcode {
        // ... existing cases
        x if x == OpCode::MyOp as i32 => Some(self.op_my_op(py, args)),
        _ => None,
    }
}

5. Compiler et tester

uv pip install -e .
make test

Ajouter un opcode VM

Pour ajouter un opcode au niveau bytecode de la VM :

1. Définir l'opcode VM

Dans catnip_rs/src/vm/opcode.rs :

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum VMOpCode {
    // ... existing opcodes
    MyVMOp = 71,
}

Regénérer Python :

python catnip_rs/gen_opcodes.py

2. Implémenter le dispatch

Dans catnip_rs/src/vm/core.rs :

match opcode {
    // ... existing cases
    VMOpCode::MyVMOp => {
        let arg = frame.pop()?;
        let result = my_vm_operation(arg);
        frame.push(result);
    }
}

3. Ajouter au compiler

Dans catnip_rs/src/vm/compiler.rs :

fn compile_node(&mut self, py: Python, node: &Bound<PyAny>) -> PyResult<()> {
    match opcode {
        // ... existing cases
        x if x == OpCode::MyOp as i32 => {
            self.compile_node(py, &args.get_item(0)?)?;
            self.compile_node(py, &args.get_item(1)?)?;
            self.emit(VMOpCode::MyVMOp, 0);
        }
    }
    Ok(())
}

Étendre le contexte

Ajouter des fonctions ou variables globales disponibles dans Catnip :

from catnip import Catnip
from catnip.context import Context

# Créer un contexte personnalisé
ctx = Context()

# Ajouter une fonction Python
def my_func(x, y):
    return x + y

ctx.globals._set('my_func', my_func)

# Ajouter une constante
ctx.globals._set('PI', 3.14159)

# Utiliser avec Catnip
cat = Catnip(context=ctx)
cat.parse('my_func(1, 2) + PI')
result = cat.execute()  # 6.14159

Décorateurs

@pure

Marque une fonction comme pure (sans effets de bord) pour permettre des optimisations :

from catnip import pure

@pure
def square(x):
    return x ** 2

ctx.globals._set('square', square)

Les fonctions pures peuvent être optimisées par le broadcast et potentiellement mémoïsées.

@pass_context

Passe le contexte d'exécution comme premier argument :

from catnip import pass_context

@pass_context
def inspect_scope(ctx):
    return list(ctx.current_scope._symbols.keys())

ctx.globals._set('inspect_scope', inspect_scope)

Créer des passes d'optimisation

Créer une nouvelle passe d'optimisation en Rust :

1. Créer le module

Dans catnip_rs/src/semantic/my_pass.rs :

use pyo3::prelude::*;
use super::OptimizationPass;

pub struct MyOptimizationPass;

impl OptimizationPass for MyOptimizationPass {
    fn name(&self) -> &'static str {
        "my_pass"
    }

    fn visit_ir(&self, py: Python, node: &Bound<PyAny>) -> PyResult<PyObject> {
        // Visiter d'abord les enfants
        let node = self.visit_children(py, node)?;

        // Appliquer l'optimisation
        let opcode = node.getattr("ident")?.extract::<i32>()?;

        if opcode == OpCode::MyTargetOp as i32 {
            // Transformer le nœud
            return Ok(optimized_node.into_py(py));
        }

        Ok(node.into_py(py))
    }
}

2. Enregistrer la passe

Dans catnip_rs/src/semantic/mod.rs :

pub fn create_default_passes() -> Vec<Box<dyn OptimizationPass>> {
    vec![
        // ... existing passes
        Box::new(MyOptimizationPass),
    ]
}

Utilisation Python

from catnip.semantic import Optimizer, ConstantFoldingPass

# Optimiseur personnalisé avec passes spécifiques
optimizer = Optimizer(passes=[
    ConstantFoldingPass(),
    # autres passes...
])

semantic = Semantic(registry, context)
semantic.optimizer = optimizer

Ajouter une commande CLI

Les commandes CLI utilisent un système de plugins via entry points.

1. Créer la commande

# my_plugin/commands.py
import click

@click.command()
@click.argument('file')
def mycommand(file):
    """Ma commande personnalisée."""
    click.echo(f"Processing {file}")

2. Enregistrer via entry points

Dans pyproject.toml du plugin :

[project.entry-points."catnip.commands"]
mycommand = "my_plugin.commands:mycommand"

3. Installer et utiliser

pip install my-plugin
catnip mycommand file.cat

Workflow de développement

# 1. Modifier le code Rust
vim catnip_rs/src/...

# 2. Tests Rust rapides
make rust-test-fast

# 3. Recompiler
uv pip install -e .

# 4. Tests Python complets
make test

# 5. Après modification de grammar.js
make grammar-deps

Structure des fichiers importants

catnip_rs/src/
├── ir/opcode.rs           # OpCodes IR (source de vérité)
├── vm/opcode.rs           # OpCodes VM (source de vérité)
├── parser/
│   ├── core.rs            # TreeSitterParser principal
│   └── transforms/        # Transformateurs par catégorie
├── semantic/
│   ├── analyzer.rs        # Semantic analyzer
│   └── *.rs               # Passes d'optimisation
└── core/registry/
    ├── mod.rs             # Registry struct
    ├── execution.rs       # Dispatch principal
    └── *.rs               # Implémentations par catégorie

Étendre Catnip revient à ajouter une nouvelle pièce à un puzzle qui ne sait pas encore qu'il est incomplet. La pièce doit s'intégrer parfaitement à toutes les couches : grammaire, transformation, sémantique, exécution. Si une seule couche refuse la pièce, le puzzle explose. C'est de l'architecture en oignon, mais avec des tests unitaires pour chaque pelure.