Langage de capture de code

a marqué ce sujet comme résolu.

Salut à tous.

Je réfléchis en ce moment sur une sorte de successeur spirituel de coccinelle pour le C++. Si vous ne connaissez pas, coccinelle est un logiciel qui permet de faire du pattern matching avancé sur du code C et opérer des transformations suite à ces détections.

Par exemple, le snippet ci dessous va détecter le pattern kmalloc + test de nulité + mise à zéro via memset (les … signifient n'importe quel chemin d'execution possible), puis remplacer kmalloc par par kzalloc et supprimer l'appel à memset.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@test @
expression S,F;
identifier x;
statement S1;
@@
- x = kmalloc(S,F)
+ x = kzalloc(S,F)
...
if(!x) 
 S1
...
- memset(x,0,...);

L'outil est assez puissant mais je trouve la syntaxe peu agréable à utiliser. Elle ressemble (par choix) beaucoup à celle de patch et l'attention qu'on doit porter à l'écriture des patchs (exemple dans le cas ci dessus, si x est une expression ou un identifiant, ca ne va pas matcher les mêmes choses).

Je suis en pleine réflexion pour un langage de capture & transformation. Il faudrait qu'il soit capable d'exprimer des propriétés sur le code et aussi les transformations qu'on souhaite y appliquer.

j'ai donc quelques questions pour vous :

  • A quel niveau voudriez vous exprimer les fragments à capturer ?
  • Quel type de transformation souhaitez vous ?
  • Comment voudriez vous exprimer les opérations à réaliser ?

C'est complètement libre, vous pouvez écrire ce que vous voulez.

Pour vous aider, voici une liste de chose que vous pouvez essayer de résoudre

  • Passage d'un argument d'une fonction 'Foo::bar' de pointeur à référence + changement de la syntaxe correspondante dans le code de la fonction+ des sites d'appels
  • Détection des codes où on utilise un pointeur puis plus loin on le teste pour savoir s'il est nul et vous voulez ramener le test avant la première utilisation.

J'ai un certain nombre d'idées en tête, mais je voudrais avoir le plus de retour possible de la part de gens autre.

Merci ! David.

+4 -0

Remplacements :

  • lock/unlock, *open/close, etc par la capsule RAII équivalente qui va bien
  • localisation au maximum de variables non encore utilisées (ce qui peut être compliqué dans le cas de x = o.f(); y=o.g(); z(y); u(x); -> quid si f() altère l'état de o et donc le résultat de g)
  • passage de l'opérateur ternaire à un if-else et vice-versa
  • transformation d'un cast C vers le cast C++ qui va bien
  • passage de f.begin() à begin(f)
  • for (int ${var} = 0; ${var} != (ou <) ${truc: truc >0} ; ++i) -> size_t
  • soyons fous: le extract method: l'utilisateur sélectionne une bout de code, l'analyse déduit les I/O du bout extrait et génère la fonction et son appel qui vont bien.

Besoins :

  • de hooks pour spécifier des politiques de renommage si besoin – surtout si la transformation doit introduire de nouvelles variables ou constantes ou …
  • de pouvoir spécifier des conditions sur les trucs extraits

Pour la syntaxe. Boudiou. C'est une bonne question. Peut-être s'inspirer de bash: ${expression matchée/remplacement}. Et prévoir des moyens pour :

  • exprimer des contraintes
  • identifier une expression, pour la réutiliser.

Pour mes exemples, on aurait:

1
2
3
4
5
6
- ${1|rvalue}.begin()
+ begin(${1})

// pas terrible pour la gestion des espaces variables
// il faudrait prévoir quelque chose de plus souple
~ for\s*(\s*${0/int/std::size_t)\s+${1|lvalue}\s*=\s*0;\s*${1}<${2:$2 >= 0}\s*;\s*++${1}\s*)
  • lock/unlock, *open/close, etc par la capsule RAII équivalente qui va bien

C'est le genre de chose qui sont AMA presque déjà faisable avec coccinelle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@test @
expression L;
@@
- lock(L)
+ RAIILock(l)
...
if(!L) 
 return
...
- unlock(L);

Patch non testé (encore moins sur du C++), mais l'idée y est.

Et c'est globalement toujours la même chose : détection d'une paire de fonction et remplacement de la première par autre chose.

  • localisation au maximum de variables non encore utilisées (ce qui peut être compliqué dans le cas de x = o.f(); y=o.g(); z(y); u(x); -> quid si f() altère l'état de o et donc le résultat de g)

Pas compris.

  • passage de l'opérateur ternaire à un if-else et vice-versa

Une des difficultés c'est qu'un ternaire renvoie une expression alors qu'un if modifie le flot de contrôle. Il faut en plus détecter la potentielle affectation à gauche. Et quid si c'est du const ? const int a = (E) ? E1 : E2 ; ?

  • transformation d'un cast C vers le cast C++ qui va bien

Clang tidy les repere, après je ne sais pas s'il est assez intelligent pour les changer seul.

  • passage de f.begin() à begin(f)

Assez facile à priori

  • for (int ${var} = 0; ${var} != (ou <) ${truc: truc >0} ; ++i) -> size_t

Assez facile à priori

  • soyons fous: le extract method: l'utilisateur sélectionne une bout de code, l'analyse déduit les I/O du bout extrait et génère la fonction et son appel qui vont bien.

https://github.com/iZotope/cpp-tools/tree/master/extract-method

Besoins :

  • de hooks pour spécifier des politiques de renommage si besoin – surtout si la transformation doit introduire de nouvelles variables ou constantes ou …

C'est noté.

  • de pouvoir spécifier des conditions sur les trucs extraits

Tu entends quoi par là ?

Pour la syntaxe. Boudiou. C'est une bonne question.

C'est aussi le but du sujet ^^.

Peut-être s'inspirer de bash: ${expression matchée/remplacement}.

Je suis vraiment pas fan. Je trouve bash encore plus abscons que patch… mon but n'est pas de transformer du code cryptique par un outils cryptique. Idéalement, il faudrait que la simple lecture suffise à avoir une idée de ce que va faire la transformation (ce que ne permet pas ton second exemple).

Et prévoir des moyens pour :

  • exprimer des contraintes

Contraintes sur quoi ? De quel nature : Typage (statique/dynamique) ? Valeur ?

  • identifier une expression, pour la réutiliser.

Ouais, j'avais déjà ca dans ma liste.

+0 -0

[localisation]

L'idée est de transformer

1
2
T v;
v = val;

en ce qui va bien. Et bien sûr que cela marche si d'autres lignes sont au milieu.

La difficulté, c'est les éventuelles interdépendances, ou les volontés d'organisation des données, pour une meilleure gestion de l'utilisation de la pile.

[ternaire]

Tout à fait, il faut prévoir le const. Cela fait parti de la transformation.

[contraintes]

Il peut y en avoir plein:

  • lvalue/rvalue
  • pattern sur le nom, ou le type
  • liste finie de types
  • concept auquel l'expression doit adhérer (comme: est un random-access conteneur, qui permet d'employer std::sort au lieu de conteneur.sort)
  • est entier et positif
  • etc

Je ne sais pas si la question a un autre but que « imaginez l'outil de vos rêves » (si tu travailles vraiment sur un successeur de coccinelle, est-ce que tu pourrais en raconter un peu plus sur les points théoriques et techniques ?), mais je pense que la syntaxe doit venir en dernier dans ce genre d'outils. La bonne question, et tu la poses, c'est d'abord « qu'est ce que je veux pouvoir exprimer ? ».

Partir d'exemples concrets comme le fait lmghs est une première piste dans cette direction : avec une liste un peu variée de choses concrètes qu'on pourrait vouloir faire, on peut espérer en sortir une description des opérations que l'outil doit fournir. Ça doit aussi aller de paire avec des détails techniques : par exemple, est-ce qu'on travaille uniquement sur la syntaxe ou est-ce qu'on autorise les transformations à bénéficier d'informations sur les types ? Le deuxième choix permet d'exprimer plus de choses, mais il demande aussi un travail nettement plus conséquent.

À mon avis, quand on conçoit ce genre d'outils, le principal danger à éviter est donc de réfléchir de manière trop ad-hoc, en énumérant simplement une liste de transformations à pouvoir faire plus ou moins complexes qui ne se situent pas au même niveau. C'est le meilleur moyen de se retrouver avec un outil bancal bourré de hacks, qu'il est très difficile d'étendre proprement ou même de s'assurer qu'il fonctionnera convenablement au bout de quelques ajouts supplémentaires.

Cela dit, que ça ne vous empêche pas de lister des choses que vous aimeriez avoir. Je n'ai pas d'idée en tête, donc je n'apporterai pas grand chose à ce niveau là, mais j'essaye quand même un peu de guider la discussion d'une façon à mon avis plus pertinente que la direction qu'elle semble emprunter (par exemple, en parlant des points de détail d'une transformation en particulier). Quant à la syntaxe, elle viendra après : elle est là avant tout pour exprimer les idées sous-jacentes, pas pour faire des jolis fichiers agréables à l'oeil. Si elle est conçue en fonction de ce qu'elle doit exprimer, elle sera bonne (et la question de son choix ne devrait même pas se poser, à part quelques détails comme « $ ou % ? » qui n'ont pas un grand intérêt).

Cela dit, c'est un projet intéressant. On aimerait en savoir plus :-)

Edit :

Pour donner un exemple de transformations qui ne demandent pas du tout le même travail :

  • passage de f.begin() à begin(f) : c'est extrêmement facile, on pourrait presque y arriver rien qu'avec un sed.
  • transformation d'un cast C vers le cast C++ qui va bien : je ne connais pas assez la sémantique du C++ pour en être sûr, mais j'ai l'impression qu'il « suffit » de disposer d'un AST typé (ou de son équivalent). C'est plus compliqué que la précédente parce qu'on ne peut pas résoudre ça uniquement avec des informations sur la syntaxe, mais si on s'autorise à inclure un typeur (attention, ça complique quand même beaucoup le programme, surtout avec un langage comme C++), ça ne devrait pas poser de problème.
  • contrainte est entier et positif : C'est très (très) au delà de ce qu'on attend de ce genre d'outil. Selon les exigences qu'on a (accepter les programmes concurrents par exemple), vue la complexité qu'on peut facilement avoir avec le C++, on peut même à mon avis arriver à la limite de ce que la recherche en anayse statique sait faire actuellement.
+0 -0

SNIP

Eusèbe

Le but du sujet est vraiment de récolter des avis/idées en vrac sur ce que les gens voudraient. Ca fait un pot pourri d'idées dans lequel je fais je peux faire un tri. Sur les fondements théoriques, ca sera assez proche à priori assez proche de coccinelle à base de CTL + d'autres trucs avec du model checking par dessus le CFG (obtenu par clang). L'avantage d'utiliser clang est qu'on dispose d'un vrai compilateur et donc de tous les types des éléments qu'on manipule. Le 2 gros soucis majeurs que je vois sont :

  • Les exceptions, qui en théorie complexifie le CFG.
  • Les macros, qui empêche une bijection AST-code source, même si là encore, clang dispose de quelques éléments de réponses.

Reste après la question de l'expression des contraintes symboliques sur les variables, l'analyseur statique de clang peut être un début de piste.

C'est dans le cadre d'une thèse (que j'ai débuté il y'a peu), donc il n'a y a pas beaucoup à présenter pour le moment, mais je tiendrai au courant.

+0 -0

Lu'!

Du point de vue syntaxe, tu peux regarder ce qui se fait du côté de Nanopass. J'ai retrouvé ça dans mes notes de séminaire d'y a quelques mois et c'était un truc noté à la va vite, donc je peux difficilement donner des détails, mais si je me souviens bien, ils avaient un truc plutôt sympa.

A noter par contre qu'il me semble qu'eux c'était la manipulation directe de l'AST qui se faisait, mais si je me souviens bien la syntaxe permet d'exprimer le même genre de chose que ce que tu veux exprimer toi.

EDIT : ça se trouve un peu milieu sur un moteur de recherche quand on rajoute "compiler" à "nanopass".

Un truc probablement un peu tendu a faire, c'est de supprimer les informations explicites du type et des qualifiers lorsque l'inference de type peut-etre faite. Le but etant d'avoir un nombre de declarations de type le plus faible possible pour favoriser le refactoring, la genericite et eventuellement une approche plus 'semantique' des types variables utilises (passer de 't a le type X parce que c'est le type de v' a directement 't a le type de v').

Exemple:

1
2
3
std::vector<int> v;
...
std::vector<int> t = v; // Remplace par auto t = v ou par decltype(v)

Encore plus chaud, on pourrait penser a extraire les caracteristiques utilisees par un type pour en creer une version template avec les meme contraintes verifies en compile-time via metaprogrammation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct A {
    void operator() {};
};

void f(A a) {
    a()
}

// Transforme en:
template<class T, typename std::enable_if<std::is_function<T()>::value>::type>
void f(T a) {
    a()
}

Je suis bien conscient qu'il faut tout de meme une intervention de l'utilisateur pour selection telle ou tell contrainte a remplacer au final, mais comme je developpe actuellement un framework qui essaye de supprimer autant que possible de l'heritage pour faire du vrai sous-typage. Ca m'interesserait de pouvoir faire ce travail automatiquement, surtout vu la syntaxe degueulasse de la metaprogrammation en C++.

Un truc genre: 1) extraction du plus de caracteristiques possibles avec un label comprehensible, 2) choix par l'utilisateur du jeu de contraintes a appliquer, 3) remplacer.

Desole pour le vocabulaire approximatif.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte