PIF - Introduction

Salutations camarades, amis proches ou éloignés, de tous bords, de tous genres, de toutes origines, de tout penchants religieux ou politiques, de toutes orientations sexuelles, et bien plus que ça.

Je suis actuellement en train de travailler sur un nouveau langage de programmation, non pas pour faire un langage populaire, mais plutôt comme une expérience personnelle. J’ai choisi le chemin facile, à savoir que je laisse le gros de l’optimisation et de la compilation à LLVM.

Il y a plusieurs objectifs à un tel projet :

  • Créer un langage qui soit beau, lisible, facile à comprendre
  • Créer un langage qui soit simple à utiliser et à comprendre
  • Créer un langage qui soit plutôt rapide – donc compilé
  • Créer un environnement de programmation unifié, permettant de se mettre à coder rapidement des petites applications inutiles

Je l’ai déjà dit, mais il y a aussi un non-objectif : celui de concurrencer les langages existants.

Mais j’oublie un point important : ce langage s’appelle PIF, ce qui est un acronyme pour Programming Is Fun – ce qui montre bien l’état d’esprit du projet. Il s’agit d’un langage pour apprendre la programmation en s’amusant dans son coin, mais pas particulièrement prévu pour des gros projets. Comprendre aussi : c’est mon langage, c’est mon compilateur, alors je fait ce que je veux (et je vous emmerde – sauf votre respect).

Mais dans la pratique, ça ressemble à quoi ?

J’ai commencé à écrire les spécifications sur mon bloc-notes ici (document principal).

Types de base

Il n’y a que six types de bases : void, bool, byte, int, float, string (cf les spécifications). On pourra aussi utiliser des types de taille différentes pour les nombres entiers, mais globalement j’encourage à la simplicité et donc à n’utiliser que les six types de base, les autres n’ayant leurs utilité que lorsqu’il s’agit de communiquer avec le monde extérieur (comprendre : appel de fonctions de bibliothèques en C par exemple).

Il y aura aussi des types pour les tableaux. Ceux-ci fonctionneront d’une manière similaire aux tableaux de Go. Mais chaque chose en son temps, les tableaux ne sont pas du tout dans mes priorités pour l’instant. Les string non plus, d’ailleurs, parce que c’est vraiment un gros morceau.

Syntaxe

Je m’attend à ce que personne ne comprenne pourquoi j’ai choisi une telle syntaxe. En gros, il y a toujours à peu près deux raisons pour tel ou tel choix :

  • C’est lisible, compréhensible et joli pour un humain
  • C’est organisé de manière à ce que le compilateur puisse le comprendre sans trop de complications

Voici donc, sans attendre, des exemples de code :

################# MATH LIBRARY ###################
#       BECAUSE YOU LOVE MATH AND SO DO I
##################################################

######### BASIC MATH #########
## packages/pif/math/math.pif

Attention : non seulement s’agit-il ici d’un extrait de code de la bibliothèque pour faire des maths – qui en fait fait essentiellement appel aux fonctions de la libm – mais c’est surtout du code non réaliste, qui concrètement sera écrit autrement. C’est vraiment juste pour l’exemple. En passant, remarquez que les commentaires commencent par un #.

const pi : float = 3.1415926536
let tau = 2 * pi

Les deux lignes précédentes étaient assez explicites, je laisse le soin au lecteur de les comprendre. Remarquez l’utilisation du mot-clef let pour déclarer ses variables. Je l’ai choisi car var et def sonnaient très mal, défaut rédhibitoire pour mon idée – let est tout aussi conventionnel et compréhensible à mon avis. Remarquez aussi l’organisation de la déclaration de pi : le signe deux-points (:) indiquera toujours que ce qui suit est le type à considérer pour l’objet qui précède – il est donc utilisé dans toutes les déclarations, définitions de structures, etc.

func abs : (x : float) -> float {
    if x >= 0 then return x
              else return -x
}

C’est un dilemme pour moi : dois-je écrire la fonction abs qui prend un float pour renvoyer un float, ou dois-je l’écrire avec des int ? Vais-je devoir en écrire deux ? En tout cas, il n’est pas question d’implémenter de surcharge des fonctions, c’est bien trop lourd et anti-principes-fondamentaux-décrits-plus-haut.

Mais examinons ces lignes un peu plus en détail, car elles sont très révélatrices. D’abord, constatez que la fonction abs est du type noté (x : float) -> float, à comprendre qu’elle prend en argument un float appelé x, et renvoie un float. C’est plutôt clair, non ? Le nom de l’argument (ici x) prend son importance lorsqu’on a une fonction qui prend vraiment tout plein de paramètres, et que ceux-ci sont optionnels, et que l’on veut en préciser juste quelques-uns. Sinon, c’est pas trop grave.

Ensuite, il y a le if. Peut-être le classique if...then...else... vous fait penser trop à du vieux QBasic foireux – dans ce cas, tout peut s’arranger, parce que dès que l’on veut mettre plus qu’une seule ligne dans le corps du if, il est nécessaire d’utiliser des accolades pour délimiter un bloc de code, ce qui nous dispense alors d’écrire le then (mais vous pouvez aussi le mettre si ça vous fait plaisir, franchement je m’en fous). Dans ce cas précis, ça peut donner un truc comme ça :

# AUTRE VERSION, JUSTE POUR L'EXEMPLE
func abs : (x : float) -> float {
    let ret = x
    if ret < 0 {
        ret = -ret
    }
    return ret
}

C’est mieux ? Bien, passons à la suite :

func sqrt : (x : float) -> float
    extern sqrt

func cos : (x : float) -> float
    extern cos

Ces deux exemples illustrent typiquement la référence à une fonction externe, en l’occurrence on définit les fonctions sqrt et cos, toutes les deux du même type, et faisant références aux fonctions du même nom dans la libm.

func sin : (x : float) -> float {
    return (extern sin : (x : float) -> float) (x)
}

La fonction ci-dessus montre une autre façon de référencer une fonction externe. Ici c’est tout à fait inutile, mais c’est pour l’exemple. Remarquez que l’on a tout simplement importé la fonction sin en précisant son type, sans lui donner de nom particulier dans notre programme (c’est donc une fonction anonyme), puis on l’a appelée directement.

Maintenant, parlons un peu des pointeurs de fonction. Attention : les pointeurs sur fonction, c’est pas encore au point et je suis pas convaincu que ça va se faire exactement comme ça au niveau syntaxe, même si ça a l’air plutôt pas mal. Ce n’est pas encore rentré dans les spécifications.

func tan : (x : float) -> float {
    let externTanPtr : (x : float) -> float = extern tan
    return externTanPtr(x)
}

Cette fois c’est un peu pareil, sauf qu’on a utilisé un pointeur de fonction, auquel on a attribué une référence à la fonction externe tan. On a ensuite utilisé ce pointeur pour appeler la fonction.

Finalement, pour illustrer les fonctions anonymes et les pointeurs de fonctions, du code cette fois vraiment inutile :

let func_ref = func (val : float) -> float {
    return sqrt(val)
}

let sqrtOf2 = (func (val : float) -> float { return sqrt(val) }) (2)

Dans ce premier exemple, on définit un pointeur de fonction et on lui donne comme valeur une fonction qui a été créée sur le champ. Si le let avait été un const, cela aurait été l’équivalent approximatif d’une définition de fonction classique. Mais c’est moche et inutile, en fait, donc on préfère faire comme tout le monde et déclarer ses fonctions normalement.

Dans le deuxième exemple, on crée une fonction qui renvoie la racine carrée de l’argument, que l’on applique immédiatement avec l’argument 2, pour donner la valeur récupérée à la variable sqrtOf2.

Conclusion

Je ne suis moi-même pas tout à fait convaincu de ce nouveau langage que j’imagine aujourd’hui – et dont j’avais déjà commencer à coder un bout de compilateur, mais la syntaxe a changé depuis, alors je vais devoir modifier plein de trucs. Bref, voilà.

Dans le prochain épisode, s’il y a lieu, on parlera un peu des structures, des classes, des méthodes, etc.