Syntaxe définition propres opérateurs + ordre de prioritée

a marqué ce sujet comme résolu.

Bonjour,

Comme vous l'avez peut être vu, je suis en train de développer un langage et je suis en train de chercher une syntaxe pour définir ses propres opérateurs.

Voilà la syntaxe pour surcharger un opérateur existant:

1
2
3
4
5
6
7
8
class A { # Définition classe
    Value_: int! = 0 # Création variable membre privée de type int

    pub implicit A(Value:int) { Value_ = Value } # Déclaration constructeur publique appelable implicitement
    pub operator +(Rhs: A) -> A! { # Déclaration opérateur qui prend qui additionne les valeurs
        Value_ + Rhs::Value_
    }
}

Mais maintenant, je cherche une syntaxe pour que l'utilisateur définisse un opérateur qui n'existe pas comme il veut. Mais du coup, il faut lui assigner un ordre de priorité et là, je trouve pas de syntaxe satisfaisante.
Pour définir un opérateur perso, j'ai pensé comme ça:

1
2
3
4
5
6
operator Plus(Lhs:A, Rhs:A) -> A! {
    Lhs + Rhs # Opérateur '+' défini avant
}
TestA:A = 10 # Constructeur déclaré "implicitement" donc pas besoin de faire = A 10
TestB:A = 10 # Idem
Print (TestA Plus TestB) # Affiche 20

Là, rien n'indique l'ordre de priorité. Auriez vous une manière élégante de l'indiquer ?

Merci,
Ardakaniz

Je n'ai pas forcément de solution prête à l'emploi à te proposer, mais si tu ne connais pas Prolog, ça peut être intéressant de t'y pencher. Je ne crois pas connaître de langage qui soit aussi fin pour la définition des opérateurs.

En Prolog donc, pour définir un opérateur, on utilise le prédicat op/3 de la manière suivante :

1
op(Precedence, Type, Name).

Avec Precedence qui est un entier indiquant la priorité (Les opérateurs de base sont définis de manière très large pour permettre d'en intercaler d'autres au milieu. Par exemple, le + binaire est défini avec une précédence de 500, et le * avec une précédence de 400). Type quant à lui sert à définir l'associativité de l'opérateur (yfx pour associatif à gauche, fx pour un opérateur préfixe, xf pour postfixe, et d'autres variantes). Enfin, Name est le nom de l'opérateur en question. À noter que le choix retenu en Prolog est donc de séparer la déclaration de la définition de l'opérateur (op/3 dit seulement que Name est un opérateur, mais ne dit pas ce qu'il fait).

Ça me paraît difficile de faire ça proprement avec une syntaxe du style de ce que tu utilises. Peut être quelque chose de ce style pour les opérateurs binaires ?

1
2
3
operator<500,LEFT> Plus(Lhs:A, Rhs:A) -> A! {
    Lhs + Rhs
}

Je n'ai pas une réponse à ta question exactement, mais j'ai peut-être des pistes. Au passage, il y a aussi l'associativité qu'il faut définir.

Je connais deux langages qui permettent de définir des opérateurs et ils fonctionnent tous les deux différemment : - D'un côté, il y a Ocaml pour lequel un opérateur hérite de la priorité de l'opérateur correspondant à son premier caractère. Du coup, +, +., +: et ++ ont la même priorité (et associativité). C'est implicite, ce qui permet d'éviter d'éviter de devoir le spécifier soit-même. En revanche, il n'est plus possible de faire des opérateurs "symétriques" (ou palindromes) tels que :+:, <+> ou .+. parce que la priorité ne serait pas celle que l'on attend. - De l'autre côté, il y a Haskell qui définit une priorité par défaut (la plus haute) et qui permet de spécifier soit-même la priorité avec infixl 5 ++ par exemple. Cela permet plus de liberté, mais a l’inconvénient de forcer l'utilisateur à indiquer lui-même la priorité et de connaître la priorité des autres opérateurs.

Les deux solutions ont aussi un autre inconvénient : celui d'avoir un nombre fini de priorités possibles. C'est-à-dire que (pour le Haskell) il n'est pas possible de définir un opérateur ayant une priorité entre + (6) et * (7).

Une dernière chose : tu laisses la possibilité à l'utilisateur de définir des opérateur ayant un nom exactement comme une variable. Cela risque de te poser des problèmes pour le compilateur : en effet, tu ne peux plus vérifier durant l'analyse syntaxique que l'utilisateur n'a pas écrit quelque chose équivalent à 5 + * 8 *, qui n'a que très peu de chances d'être syntaxiquement correct. Pour cela, il te faut des informations de l'analyse sémantique, qui est sensé se faire après, notamment parce qu'il est difficile de donner du sens à quelque chose qui n'est pas forcément syntaxiquement correcte.

Le Haskell résout le problème en permettant l'utilisation de n'importe quel variable en tant qu'opérateur avec ` . Par exemple, `plus` est un opérateur infixe. Au passage, on peut définir la priorité de ce genre d'opérateur de la même manière que pour les opérateurs habituels infixl 5 `plus` .

Pour ton langage, on pourrait imaginer quelque chose comme :

1
2
3
operatorl<5> +(Lhs:A, Rhs: A) -> A! {
   Lhs + Rhs;
}

qui permettrait de définir la priorité (et l'associativité) d'un opérateur. Si tu veux aller plus loin et permettre n'importe quel priorité sans nécessiter que l'utilisateur connaisse celle des opérateurs usuels, on pourrait imaginer quelque chose comme operator<+> qui indique un opérateur de même priorité que + ou operator<+,*> qui indique un opérateur dont la priorité est entre + et *.

Cependant, tu risques d'avoir du mal à bien définir le comportement d'un code qui contient ce genre de chose

1
2
3
4
5
6
7
operator<+,*> +.
operator<+,*> *.
operator<+.,*> ++

// on a + < +. < ++ < *
// et + < *. < *
// donc on a une ambiguïté bien plus difficile à résoudre que celle que l'on a avec uniquement les deux premiers opérateurs
+1 -0

Merci beaucoup !

Je pense que je vais partir sur la syntaxe de Berdes et faire quelque chose comme:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
operator<+> `Plus`(Lhs: A, Rhs: A) -> A! { // Même priorité que l'opérateur +
    Lhs + Rhs
}
operator<higest> ++(Lhs: void, Rhs: int!&) -> int! { // Prioritée la plus grande (par exemple 50000)
    Rhs = Rhs + 1 # On additionne Rhs à 1
    Rhs # Et on le renvoi
}
operator<smallest> ++(Lhs: int!&, Rhs: void) -> int! { // Prioritée la plus petite (0) {
    Lhs = Lhs + 1
    Lhs
}
operator<||, &&> &|(Lhs: bool, Rhs: bool) -> bool { // Priorité entre && et ||
    (Lhs || Rhs) && !(Lhs && Rhs) // Y'a moyen de faire plus court, mais je ne m'en rappelle plus.
}

Var: int = 4
PrintLine (Var `Plus` 5) // 9
PrintLine ++Var // 5 car plus grand priorité
PrintLine Var++ // 5 car plus petite priorité mais:
PrintLine Var // vaut maintenant 6
PrintLine (true &| false) // true
PrintLine (true &| true) // false
PrintLine (false &| false) // false

Après, j'ai rien d'implanter encore donc à voire, mais je pense que c'est pas mal

Sincèrement, mon idée de définir la priorité d'un opérateur en fonction comme étant entre deux autres opérateurs peut semble élégante et pratique. Mais en réalité, ça risque de causer bien plus de soucis que prévu en ajoutant (potentiellement non intentionnellement) beaucoup d’ambiguïté.

En fait, le problème est d'arriver à transformer un ordre partiel en ordre total. Et ce, de manière non ambiguë. Et ça, ça me semble pas simple (pour ne pas dire impossible).

Après, tu fais ce que tu veux. D'une certaine manière, ça permettrait aussi d'expérimenter et de voir si les problématiques théoriques que je te montrent se traduisent en réels problèmes à l'utilisation.

Edit : par contre, il pourrait être possible de définir de manière non ambiguë une priorité relative en indiquant qu'un opérateur à la priorité la plus petite strictement plus grande que + et inférieur à *. Dans un tel cas, mon premier exemple avec +., *. et ++ n'est plus ambiguë puisque l'on obtient l'ordre + < +. = *. < ++ < *, en supposant que les priorités sont les plus faibles possibles. Par contre, ça ne résout peut-être pas toutes les ambiguïtés.

+1 -0

Ah ouais.

Pffff, c'est beaucoup plus compliqué que ce que je pensais.... Je vais y réfléchir. Mais si vraiment, c'est trop compliqué, je pense que je vais pas l'implanter en fait. Mais ce serait dommage :'(

EDIT:

1
2
3
operator<+,*> +.                
operator<+,*> *.                   
operator<+.,*> ++    

Imaginos que + est une priorité de 1 et * de 2. operator<+,> donne une priorité de (1 + 2) / 2 = 1.5. Donc les priorités de +. et de . sont les mêmes.
= + < +. = *. < ++ < *

+0 -0

L'opérateur puissance par exemple est associatif à droite. En Python :

1
2
3
4
>>> 2**3**2
512
>>> (2**3)**2
64

On peut imaginer d'autres trucs, comme par exemple un opérateur a_mangé, qui s'utiliserait : oiseau a_mangé serpent a_mangé rongeur. Ici, c'est bien le serpent qui a mangé le rongeur, et pas l'oiseau qui a mangé le serpent et le rongeur (c'est un exemple bête, juste histoire d'illustrer).

J'avais un sujet, mais j'ai changé de nom et de syntaxe et ajoute des features. Bref plus du tout le meme. J'ai donc signale le sujet obsolète. Sinon le code est sur mon git, mais encore une fois c'est l'ancien. Et puis rien n'est en place pour l'instant, j'attends le tuto sur LLVM (oui, j'ai le temps…)

j'attends le tuto sur LLVM (oui, j'ai le temps…)

Ardakaniz

Ne te sens pas forcé d'attendre un tuto LLVM pour faire un compilateur qui l'utilise. Pour en avoir aussi écrit un sans connaitre LLVM avant, la documentation et les présentation sont vraiment bien faites. Au final, je n'ai pas eu besoin de plus que quelques exemples et de la doc pour faire un truc complet. Certes, probablement pas parfait, mais complet quand même.

Tu as une présentation qui contient un nombre incroyable de ressources différentes : LLVM Overview. Il suffit de regarder une ou deux intro pour pouvoir commencer à faire quelque chose d'intéressant. Et tu as la doc LLVM Language Reference Manual pour quand tu cherches une instruction particulière ou que tu veux une info précise sur un point.

+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