Documentation de référence du langage Jaguar telle qu'implémentée par
jcc.py.Important : cette documentation décrit l'implémentation actuelle du compilateur, pas une spécification future. Certaines fonctionnalités peuvent être volontairement limitées.
Jaguar est un langage compilé qui est traduit en C par JaguarCC (jcc.py), puis compilé avec GCC.
Le pipeline est :
Code Jaguar (.ja)
↓
Lexer
↓
Parser
↓
Resolver
↓
CodeGen
↓
C (.c)
↓
GCC
↓
Exécutable
Jaguar reprend volontairement plusieurs idées du C/C++ tout en simplifiant certains aspects :
jcc ;.jah) ;python jcc.py main.ja -o game
Le compilateur génère :
game.c
game
Puis appelle GCC automatiquement.
Le nom de sortie ne doit pas se terminer par .c :
python jcc.py main.ja -o game
et non :
python jcc.py main.ja -o game.c
Sans -o, le C est écrit sur la sortie standard :
python jcc.py main.ja > main.c
python jcc.py main.ja --c89 -o game
En mode normal, les déclarations peuvent apparaître après des instructions, comme en C99+.
Avec --c89, Jaguar déplace les déclarations au début des blocs et laisse l'initialisation à son emplacement logique.
La sémantique Jaguar reste identique.
Jaguar utilise principalement deux extensions :
.ja : fichier source Jaguar ;.jah : Jaguar Header, un fichier d'interface Jaguar.Techniquement, un .jah utilise la même syntaxe que Jaguar. L'extension indique simplement qu'il s'agit d'une interface destinée à être réutilisée avec using.
Exemple :
include/
math.jah
src/
main.ja
Puis :
using math;
void main(string param) {
math:add(10, 20);
}
Les .jah sont particulièrement utilisés pour les bindings d'API C générés par BindGen.
Jaguar peut appeler des fonctions définies dans une bibliothèque C avec @extern.
@extern
int add(int a, int b);
Pour les API C plus complexes, BindGen génère automatiquement un .jah à partir du header C.
Les types d'interopérabilité bas niveau comprennent notamment :
cptr
cstr
cfuncptr
ainsi que l'opérateur & pour transmettre l'adresse d'une variable à une API C :
u32 buffer = 0;
gl:glGenBuffers(1, &buffer);
cstr représente une chaîne C (const char *) et cptr représente un pointeur C opaque. cfuncptr représente un pointeur de fonction C, notamment utile pour les callbacks et les APIs comme GLAD.
Jaguar n'est pas sensible aux retours à la ligne.
Les blocs utilisent {} et les instructions se terminent généralement par ;.
Exemple :
int main(string param) {
int x = 10;
int y = 20;
if (x < y) {
sys:print("x est plus petit");
}
return 0;
}
// Ceci est un commentaire
int x = 10;
/*
Commentaire
sur plusieurs lignes
*/
Les commentaires de documentation /** ... */ sont également supprimés par le lexer.
Jaguar conserve certaines directives du préprocesseur C et les transmet au C généré. #include n'est pas supporté : les headers C doivent être intégrés avec BindGen/Jaguar headers.
Les directives supportées sont :
#define
#undef
#if
#ifdef
#ifndef
#elif
#else
#endif
#pragma
#error
#warning
#line
Exemple :
#define PI 3.14159265
#define GAME_VERSION 1
#ifdef JAGUAR_DEBUG
// code de debug
#elif defined(JAGUAR_RELEASE)
// code de release
#else
// autre configuration
#endif
Jaguar accepte également #elseif comme écriture alternative de #elif; elle est normalisée vers #elif dans le C généré.
Les conditions sont évaluées par le préprocesseur C lors de la compilation GCC. Les blocs peuvent être imbriqués.
#include est volontairement interdit :
#include <stdio.h> // invalide
Pour utiliser une API C, utilisez un fichier .jah généré par BindGen et using.
Les macros ne sont pas des variables Jaguar et leur type n'est pas inféré par le compilateur.
#define START_HEALTH 100
int default_health = START_HEALTH;
Les macros ne sont pas des variables Jaguar et leur type n'est pas inféré par le compilateur Jaguar.
| Jaguar | Alias | Type C approximatif |
|---|---|---|
void |
— | void |
int |
i32 |
int |
uint |
u32 |
unsigned int |
short |
i16 |
short |
ushort |
u16 |
unsigned short |
long |
i64 |
long long |
ulong |
u64 |
unsigned long long |
char |
i8 |
signed char |
uchar |
u8 |
unsigned char |
sbyte |
i8 |
signed char |
byte |
u8 |
unsigned char |
float |
f32 |
float |
double |
f64 |
double |
bool |
— | _jBool |
string |
— | objet string Jaguar |
Les alias sont normalisés par le compilateur. Par exemple :
int a = 10;
i32 b = 20;
utilisent tous deux le type logique i32.
int x;
f64 speed;
bool alive;
string name;
int x = 42;
f64 speed = 12.5;
bool alive = true;
string name = "Oscar";
Une variable locale peut être déclarée avec n'importe quel type connu.
constconst rend une variable ou un paramètre non modifiable.
const int max_health = 100;
Un paramètre peut également être const :
void print_value(const int value) {
sys:print(value);
}
Les méthodes peuvent également être déclarées const :
int get_health() const {
return health;
}
Une variable const locale doit être initialisée immédiatement.
const int x = 10;
autoauto permet de déduire le type d'une variable locale depuis son initialisation.
auto x = 42;
auto value = 3.14;
auto name = "hello";
Une variable auto doit obligatoirement être initialisée.
auto x; // invalide
auto est destiné aux variables locales et aux constructions de boucle prévues par Jaguar.
0
42
-10
3.14
0.5
10.0
true
false
"Hello world"
stringstring est le type chaîne natif de Jaguar.
string name = "Jaguar";
Il ne s'agit pas d'un simple char* exposé au programme Jaguar.
length()int n = name.length();
empty()bool empty = name.empty();
equals()if (name.equals("Jaguar")) {
sys:print("OK");
}
contains()bool found = name.contains("gua");
starts_with()bool result = name.starts_with("Jag");
ends_with()bool result = name.ends_with("ar");
concat()string full = name.concat("CC");
substring()string part = name.substring(0, 3);
char_at()i32 c = name.char_at(0);
Le résultat est le code du caractère, ou -1 si l'index est invalide.
to_upper() / to_lower()string upper = name.to_upper();
string lower = name.to_lower();
+ - * / %
Exemple :
int result = (10 + 5) * 2;
== != < > <= >=
Les comparaisons produisent un bool.
bool result = health <= 0;
&& || !
Exemple :
if (alive && health > 0) {
sys:print("vivant");
}
La priorité est proche du C :
||
&&
== !=
< > <= >=
+ -
* / %
- !
Les parenthèses peuvent être utilisées pour rendre une expression explicite.
Jaguar utilise volontairement la syntaxe de cast C :
f64 value = (f64)10;
i32 integer = (i32)3.14;
La forme suivante n'est pas un cast Jaguar :
i32(3.14); // ce n'est pas la syntaxe de cast
Le resolver peut utiliser des conversions numériques simples pour choisir une surcharge :
Exemple :
void test(i32 value) {
sys:print(value);
}
void test(f64 value) {
sys:print(value);
}
test(10);
test(3.14);
Si plusieurs surcharges restent équivalentes, l'appel est considéré comme ambigu.
int add(int a, int b) {
return a + b;
}
int result = add(10, 20);
voidvoid say_hello() {
sys:print("Hello");
}
Un paramètre peut avoir une valeur par défaut :
int add(int a, int b = 10) {
return a + b;
}
Les deux appels suivants sont valides :
add(5);
add(5, 20);
Les arguments peuvent être transmis par nom :
int move(int x, int y, int speed = 1) {
return x + y + speed;
}
move(y = 20, x = 10);
move(x = 10, y = 20, speed = 5);
Une fois qu'un argument nommé est utilisé, un argument positionnel ne peut plus suivre.
move(x = 10, 20); // invalide
Les fonctions intégrées jcc:* et sys:* n'acceptent pas les arguments nommés.
Une fonction peut être déclarée sans corps :
int add(int a, int b);
Puis définie plus loin :
int add(int a, int b) {
return a + b;
}
Le prototype et la définition doivent avoir exactement la même signature.
Deux prototypes identiques ou deux définitions identiques provoquent une erreur.
Jaguar permet plusieurs fonctions de même nom avec des paramètres différents :
void print_value(i32 value) {
sys:print(value);
}
void print_value(f64 value) {
sys:print(value);
}
Le compilateur choisit la surcharge appropriée.
Le name mangling utilise les types des paramètres lorsque le nom est réellement surchargé.
@extern@extern empêche le name mangling.
@extern
int my_native_function(int value);
Le nom C reste exactement my_native_function.
Cela permet notamment d'appeler des fonctions définies ailleurs dans le projet ou dans une bibliothèque native.
mainLa forme spéciale recommandée est :
void main(string param) {
sys:print("Hello");
}
ou une forme retournant un entier selon les fonctions supportées par le programme.
Jaguar traduit cette fonction vers un main C standard.
Le compilateur évite d'émettre un main(string) C non standard.
returnint square(int x) {
return x * x;
}
Pour une fonction void :
void stop() {
return;
}
ifif (health > 0) {
sys:print("alive");
}
elseif (health > 0) {
sys:print("alive");
} else {
sys:print("dead");
}
else ifif (health > 75) {
sys:print("high");
} else if (health > 25) {
sys:print("medium");
} else {
sys:print("low");
}
Les accolades sont obligatoires.
whileLe while Jaguar est volontairement différent du C : il ne prend pas de condition.
while {
if (health <= 0) {
break;
}
health = health - 1;
}
La sortie de boucle se fait avec break ou return.
La forme suivante est interdite :
while (health > 0) {
}
break et continuewhile {
if (value == 10) {
break;
}
if (value == 5) {
continue;
}
}
Ils ne sont valides qu'à l'intérieur d'une boucle.
for_loopJaguar fournit une boucle numérique simplifiée :
int i = for_loop(0, 10) {
sys:print(i);
}
La borne finale est incluse.
Donc :
0 1 2 3 4 5 6 7 8 9 10
Si le début est supérieur à la fin, aucune itération n'est effectuée.
Le type de l'index doit être entier :
int / i8 / u8 / i16 / u16 / i32 / u32 / i64 / u64
L'index est en lecture seule dans la boucle.
Chaque bloc possède sa propre portée.
void test() {
int x = 10;
if (true) {
int y = 20;
sys:print(y);
}
// y n'existe plus ici
}
Deux blocs frères peuvent utiliser le même nom :
if (true) {
int x = 10;
}
if (true) {
int x = 20;
}
En revanche, masquer une variable visible depuis un scope englobant est interdit.
Une variable peut être déclarée au niveau global :
int max_health = 100;
f64 gravity = 9.81;
Un initialiseur global doit être une expression constante :
#define ;Ceci est valide :
#define MAX_HEALTH 100
int max_health = MAX_HEALTH;
Ceci n'est pas valide comme initialisation globale :
int x = get_value();
Les variables globales doivent également être déclarées avant leur utilisation.
Les list, map et container ne sont actuellement pas utilisables comme variables globales.
structLes structures Jaguar sont des structures de données simples.
struct Vector2 {
f32 x;
f32 y;
}
Utilisation :
Vector2 position;
position.x = 10.0;
position.y = 20.0;
Elles sont générées comme des typedef struct C.
Jaguar possède un système de classes avec :
override ;Exemple :
class Player {
$int health = 100;
void take_damage(int amount) {
health = health - amount;
}
int get_health() const {
return health;
}
}
Les membres sont privés par défaut.
$class Player {
$int health;
}
%class Player {
%int health;
}
Sans marqueur :
class Player {
int secret_value;
}
Le contrôle d'accès est vérifié par le compilateur.
class Player {
$int health;
void damage(int amount) {
health = health - amount;
}
}
Appel :
Player player = Player();
player.damage(10);
Une méthode possède implicitement un self dans le C généré.
thisDans une classe, this désigne l'instance courante.
class Player {
$int health;
void reset() {
this.health = 100;
}
}
Dans le C généré, this correspond à self.
this n'est disponible que dans une classe.
constrclass Player {
$int health;
constr(int initial_health) {
health = initial_health;
}
}
Création :
Player player = Player(100);
Le compilateur génère également l'allocation de l'objet.
destrclass Player {
destr() {
sys:print("destroyed");
}
}
Chaque classe possède un point d'entrée destructeur généré par le compilateur, même si aucun destr() n'est écrit.
Avec l'héritage, le destructeur de la classe de base est appelé automatiquement.
La syntaxe actuelle est :
class Enemy {
$int health;
}
class Zombie, Enemy {
void attack() {
health = health - 10;
}
}
La classe dérivée contient la partie correspondant à sa classe de base.
Une méthode peut être virtuelle :
class Enemy {
virtual void attack() {
sys:print("enemy attack");
}
}
Une classe dérivée peut l'override :
class Zombie, Enemy {
void attack() override {
sys:print("zombie attack");
}
}
override doit correspondre à une méthode virtual de la classe de base avec une signature exacte.
constclass Player {
$int health;
int get_health() const {
return health;
}
}
Les paramètres peuvent également être const.
newJaguar possède l'expression new :
new Player(100)
Elle peut être utilisée dans les mécanismes d'ownership de container.
container<T>container<T> représente une valeur dont le container possède la donnée.
container<int> value = new int(42);
Une syntaxe courte est disponible :
container<int> value => 42;
Lecture :
int x = value.get();
Pour une classe :
container<Player> player = new Player(100);
player.get().damage(10);
Le container détruit automatiquement l'objet qu'il possède.
Les container ne peuvent pas être indexés avec [].
list<T>list<T> est une collection homogène.
list<int> numbers = {1, 2, 3, 4};
Ajouter un élément :
numbers.push(5);
Lire un élément :
int value = numbers[2];
L'indexation est en lecture seule.
Cette syntaxe n'est pas autorisée :
numbers[2] = 10;
Pour modifier/ajouter des valeurs, utilisez les opérations prévues par la collection.
map<K,V>map<K,V> est une collection clé/valeur.
map<string, int> scores = {
"alice", 10,
"bob", 20
};
Ajouter ou remplacer une entrée :
scores.emplace("charlie", 30);
Lire une valeur :
int score = scores["alice"];
L'indexation est en lecture seule :
scores["alice"] = 100; // invalide
Pour écrire dans la map, utilisez emplace().
La clé string dispose d'une comparaison par contenu.
pair<K,V>Une paire contient deux valeurs : first et second.
pair<string, int> result = {"score", 42};
sys:print(result.first);
sys:print(result.second);
pair est notamment utilisé par loop_map.
dynamic_listdynamic_list permet de stocker plusieurs types dans une même collection.
dynamic_list values = {
42,
"hello",
true,
3.14
};
Ajouter :
values.push(123);
values.push("world");
Taille :
int count = values.size();
Type d'une valeur :
string type = values.type(1);
Accès brut :
auto value = values.get(0);
L'indexation est également disponible :
sys:print(values[0]);
Pour extraire une valeur primitive avec un type connu, un cast peut être utilisé :
i32 number = (i32)values[0];
f64 decimal = (f64)values[3];
Pour les objets/classes, le cast conserve le pointeur vers l'objet stocké.
loop_listloop_list parcourt une list<T>.
list<int> numbers = {1, 2, 3, 4};
auto item = loop_list(numbers) {
sys:print(item);
}
Pendant la boucle, la collection est considérée comme en lecture seule.
Ceci est donc interdit :
auto item = loop_list(numbers) {
numbers.push(10); // interdit
}
loop_list ne fonctionne pas sur dynamic_list.
loop_maploop_map parcourt une map<K,V> et fournit un pair<K,V>.
map<string, int> scores = {
"alice", 10,
"bob", 20
};
auto item = loop_map(scores) {
sys:print(item.first);
sys:print(item.second);
}
La map est en lecture seule pendant la boucle.
Jaguar possède une syntaxe signal: pour associer un bloc à une variable :
signal:health {
sys:print("Health changed");
}
Cette fonctionnalité est destinée à associer une réaction aux modifications d'une variable.
Note : le comportement exact dépend de la génération actuellement implémentée dans
jcc.py. Cette syntaxe fait partie de l'AST du compilateur et doit être considérée comme une fonctionnalité spécifique de Jaguar, pas comme une construction C standard.
Les namespaces utilisent :.
namespace math {
int add(int a, int b) {
return a + b;
}
}
Appel :
int value = math:add(10, 20);
Les namespaces peuvent être imbriqués :
namespace engine {
namespace math {
int add(int a, int b) {
return a + b;
}
}
}
Appel :
engine:math:add(10, 20);
Le nom C généré est manglé pour éviter les collisions.
Les namespaces supportent actuellement les fonctions, structs, classes et namespaces imbriqués selon les règles du compilateur. Les variables globales directement dans un namespace ne sont pas supportées.
Une expression d'appel peut être placée au niveau global :
my_function();
Le compilateur peut alors générer l'appel correspondant dans le C produit lorsque le contexte le permet.
syssys fournit les fonctions système directement intégrées à JaguarCC.
sys:printsys:print("Hello");
sys:print(42);
sys:print(3.14);
sys:print(true);
Types imprimables automatiquement :
stringinti8u8i16u16i32u32i64u64floatf32f64booldynamic_list.Chaque appel imprime une valeur suivie d'un retour à la ligne.
sys:console:set_colorsys:console:set_color("\\033[31m");
sys:print("Red");
sys:console:reset_color();
La fonction attend une séquence ANSI.
sys:console:reset_colorsys:console:reset_color();
int result = sys:execute("program", ".");
Le deuxième argument représente le répertoire de travail.
Le résultat correspond au code retourné par l'exécution système.
string data = sys:fs:read("save.txt");
sys:fs:write("save.txt", "hello");
Ces fonctions utilisent l'API fichier C en interne sans exposer FILE* au langage Jaguar.
jccjcc est la bibliothèque standard simplifiée de JaguarCC.
Elle expose des fonctions inspirées de la libc, mais adaptées aux types et conventions Jaguar.
Les pointeurs et les API libc trop bas niveau ne sont volontairement pas exposés directement.
jcc:exitjcc:exit(0);
Termine le programme avec le code fourni.
jcc:abortjcc:abort();
Arrête immédiatement le programme.
jcc:abs_i32i32 x = jcc:abs_i32(-42);
jcc:min_i32i32 x = jcc:min_i32(10, 20);
jcc:max_i32i32 x = jcc:max_i32(10, 20);
jcc:clamp_i32i32 x = jcc:clamp_i32(value, 0, 100);
jcc:random_i32i32 x = jcc:random_i32(0, 100);
jcc:time_msi64 now = jcc:time_ms();
Retourne un temps en millisecondes.
jcc:assertjcc:assert(health >= 0, "health must not be negative");
jccToutes les fonctions suivantes utilisent principalement f64.
| Fonction | Signature |
|---|---|
sqrt |
f64 -> f64 |
pow |
f64, f64 -> f64 |
sin |
f64 -> f64 |
cos |
f64 -> f64 |
tan |
f64 -> f64 |
asin |
f64 -> f64 |
acos |
f64 -> f64 |
atan |
f64 -> f64 |
atan2 |
f64, f64 -> f64 |
floor |
f64 -> f64 |
ceil |
f64 -> f64 |
round |
f64 -> f64 |
log |
f64 -> f64 |
log10 |
f64 -> f64 |
exp |
f64 -> f64 |
fmod |
f64, f64 -> f64 |
Exemple :
f64 distance = jcc:sqrt(x * x + y * y);
f64 angle = jcc:atan2(y, x);
f64 power = jcc:pow(2.0, 8.0);
jccjcc expose également des wrappers simples autour des opérations de chaînes.
string a = "Hello";
string b = " World";
int length = jcc:string_length(a);
bool same = jcc:string_equals(a, b);
string result = jcc:string_concat(a, b);
Fonctions :
string_length
string_equals
string_compare
string_contains
string_starts_with
string_ends_with
string_concat
string_substring
string_char_at
string_find
string_to_upper
string_to_lower
Exemple :
if (jcc:string_contains("Hello world", "world")) {
sys:print("found");
}
String → nombre :
i32 a = jcc:string_to_i32("42");
i64 b = jcc:string_to_i64("100000");
f64 c = jcc:string_to_f64("3.14");
Nombre → string :
string a = jcc:i32_to_string(42);
string b = jcc:i64_to_string(100000);
string c = jcc:f64_to_string(3.14);
Les fonctions inspirées de ctype.h utilisent des codes de caractères entiers.
bool digit = jcc:is_digit('0');
Fonctions disponibles :
is_digit
is_alpha
is_alnum
is_space
is_upper
is_lower
to_upper_char
to_lower_char
Elles peuvent être utilisées sur le résultat de string.char_at().
Exemple :
i32 c = name.char_at(0);
if (jcc:is_alpha(c)) {
sys:print("letter");
}
jccfile_existsbool exists = jcc:file_exists("save.dat");
remove_filebool success = jcc:remove_file("save.dat");
rename_filebool success = jcc:rename_file("old.txt", "new.txt");
env_getstring home = jcc:env_get("HOME");
Ces fonctions évitent de manipuler directement FILE*, char*, etc.
Jaguar possède un système de réflexion volontairement limité.
Une classe peut être enregistrée avec @register :
@register
class Player {
$int health = 100;
}
Un champ public peut être exposé avec @exposed :
@register
class Player {
@exposed
$int health = 100;
}
@exposed ne peut être utilisé que sur un champ public.
factory:constructUne classe enregistrée peut être construite dynamiquement avec son nom :
string class_name = "Player";
auto object = factory:construct(class_name);
La classe doit être enregistrée avec @register.
Le nom est résolu à l'exécution.
Si la classe n'est pas enregistrée, le runtime produit une erreur.
GetMemberUn membre exposé peut être récupéré dynamiquement :
string member = "health";
int health = (int)object.GetMember(member);
Le nom du membre est une string.
Si le membre n'existe pas ou n'est pas exposé, le runtime affiche une erreur.
Le cast est important pour obtenir une valeur typée.
MemberExistsAvant un accès dynamique :
string name = "health";
if (object.MemberExists(name)) {
int health = (int)object.GetMember(name);
}
Cela permet de vérifier si un membre est exposé.
SetMemberUn membre exposé peut être modifié dynamiquement :
object.SetMember("health", 50);
Les valeurs primitives et string sont prises en charge par le runtime de réflexion.
@register
class Enemy {
@exposed
$int health = 100;
$string name = "Enemy";
}
void main(string param) {
string class_name = "Enemy";
auto enemy = factory:construct(class_name);
if (enemy.MemberExists("health")) {
sys:print((int)enemy.GetMember("health"));
enemy.SetMember("health", 25);
sys:print((int)enemy.GetMember("health"));
}
}
Seuls les membres marqués @exposed sont accessibles via la réflexion.
Les attributs actuellement reconnus par le compilateur sont :
@extern
@register
@exposed
Leur utilisation dépend du contexte :
| Attribut | Utilisation |
|---|---|
@extern |
fonction |
@register |
classe |
@exposed |
champ public de classe |
Un attribut inconnu produit une erreur de compilation.
{}Les accolades peuvent créer des littéraux de collection.
Liste :
list<int> values = {1, 2, 3};
Map :
map<string, int> values = {
"one", 1,
"two", 2
};
Pair :
pair<string, int> value = {"score", 42};
dynamic_list :
dynamic_list values = {1, "hello", true};
[] est disponible sur certaines collections.
int x = numbers[0];
int score = scores["player"];
auto value = values[0];
Elle est volontairement en lecture seule.
Les container<T> ne sont pas indexables.
Jaguar ne donne volontairement pas accès à toute la libc mémoire directement.
Le runtime gère notamment :
Les structures internes utilisent C malloc, calloc, realloc, free, etc., mais ces pointeurs ne sont pas exposés comme API Jaguar générale.
Cela permet de conserver une syntaxe Jaguar plus sûre et plus simple.
Le compilateur connaît plusieurs noms libc afin d'éviter les collisions avec les fonctions Jaguar.
Par exemple, les noms suivants sont réservés lorsqu'ils sont utilisés comme fonctions Jaguar ordinaires :
malloc
free
memcpy
memset
printf
scanf
fopen
fclose
strlen
strcmp
strcpy
qsort
rand
srand
system
...
L'objectif est d'éviter qu'une fonction Jaguar génère accidentellement un symbole C entrant en collision avec la libc.
Lorsqu'une fonctionnalité libc est utile, elle doit idéalement être exposée par une API Jaguar jcc:* adaptée plutôt que par les pointeurs C bruts.
Jaguar génère des noms C compatibles avec les namespaces et la surcharge.
Une fonction simple :
int add(int a, int b) {
return a + b;
}
reste généralement add côté C lorsqu'elle n'est pas surchargée.
Une fonction dans un namespace :
namespace math {
int add(int a, int b) {
return a + b;
}
}
est manglée pour éviter les collisions, par exemple sous une forme du type :
math_add
Une surcharge utilise également les types des paramètres dans son nom C.
Lorsqu'un appel est rencontré, Jaguar recherche :
Un appel ambigu provoque une erreur.
Pour appeler une fonction C externe, la forme prévue est @extern.
@extern
int native_function(int value);
Cela évite d'exposer arbitrairement toutes les fonctions de la libc au langage.
Jaguar génère du C destiné principalement à GCC et à des compilateurs C modernes.
Le mode :
--c89
demande au générateur de produire une organisation compatible avec C89.
Le runtime évite notamment d'utiliser _Bool pour bool et génère son propre type _jBool.
Le langage Jaguar général n'expose pas directement plusieurs mécanismes bas niveau du C/C++ :
malloc/free comme API Jaguar générale ;FILE* ;memcpy et autres primitives mémoire comme API Jaguar générale ;Cependant, l'interopérabilité C possède un ensemble contrôlé de types et opérations bas niveau :
cptr
cstr
cfuncptr
&
Ils existent pour permettre d'utiliser des bibliothèques C réelles sans transformer Jaguar en langage de pointeurs général.
Les détails de ces types sont documentés dans la section dédiée à l'interopérabilité C.
Les fonctionnalités nécessitant des mécanismes C plus complexes peuvent être implémentées dans le runtime C ou exposées par BindGen.
Jaguar peut utiliser des bibliothèques C externes. Le principe est :
Header C (.h)
↓
jbg.py
↓
Jaguar Header (.jah)
↓
using
↓
Code Jaguar
↓
JCC
↓
Code C
↓
GCC
↓
Bibliothèque C
@externUne fonction C externe peut être déclarée manuellement :
@extern
int add(int a, int b);
@extern conserve le nom C de la fonction et empêche le name mangling Jaguar.
cptrcptr représente un pointeur C opaque.
Il est destiné aux handles, contextes, structures C et autres pointeurs dont Jaguar n'a pas besoin de connaître la représentation.
cstrcstr représente une chaîne C, typiquement const char *.
Exemple :
cstr version = gl:get_string(GL_VERSION);
Il s'agit d'une chaîne C et non du type string propriétaire de Jaguar.
cfuncptrcfuncptr représente un pointeur de fonction C.
Il est utile pour les callbacks et les APIs qui exposent des pointeurs de fonctions, notamment les chargeurs d'API graphiques.
&L'opérateur & permet de transmettre l'adresse d'une variable à une fonction C :
u32 buffer = 0;
gl:glGenBuffers(1, &buffer);
Il est particulièrement utile pour les paramètres C de type T *.
jbg.py)jbg.py transforme un header C en Jaguar Header (.jah).
Utilisation directe :
python jbg.py glad/glad.h -o glad.jah
Le résultat peut ensuite être utilisé avec :
using glad;
BindGen est conçu pour les headers d'API C et peut notamment représenter :
Les fonctions externes gardent leur nom C.
Un header C peut contenir des fonctions comme :
void glClear(unsigned int mask);
void glGenBuffers(int count, unsigned int *buffers);
const char *glGetString(unsigned int name);
Après BindGen, le code Jaguar peut utiliser l'API via son .jah :
using glad;
void main(string param) {
u32 buffer = 0;
gl:glGenBuffers(1, &buffer);
cstr version = gl:glGetString(GL_VERSION);
gl:glClear(GL_COLOR_BUFFER_BIT);
}
Les constantes provenant du header peuvent également être utilisées lorsqu'elles sont générées par BindGen.
.jah)Un .jah est un fichier Jaguar destiné à être utilisé comme header.
Exemple :
glad.jah
math.jah
window.jah
Puis :
using glad;
using math;
using window;
Un .jah peut contenir des prototypes, namespaces, déclarations @extern, typedefs/bindings et autres déclarations Jaguar nécessaires à l'interface.
Le .jah ne constitue pas une nouvelle syntaxe : c'est une convention d'organisation du code.
JaguarCC possède un build system séparé : JBS.
Un fichier .jbs décrit les sources, bibliothèques, includes et targets.
Exemple minimal :
version 1.0
out build
compile Main {
main.ja
}
Cela produit l'exécutable Main dans build.
outout définit le répertoire de sortie :
out build
Les targets sont écrites dans ce répertoire.
L'option CLI peut surcharger ce choix :
python jbs.py game.jbs -o other_build
compileCompile une target Jaguar en exécutable :
compile Main {
main.ja
player.ja
}
JBS effectue :
.ja
↓
JCC
↓
.c
↓
GCC
↓
exécutable
Plusieurs targets peuvent être définies dans le même fichier. Sans --target, JBS les construit dans l'ordre du fichier.
Pour ne construire qu'une target :
python jbs.py game.jbs --target Main
compile_staticConstruit une bibliothèque statique :
compile_static MyLib {
math.ja
player.ja
}
Le résultat est typiquement :
libMyLib.a
Cette target ne nécessite pas de fonction main.
compile_sharedConstruit une bibliothèque dynamique :
compile_shared MyLib {
math.ja
player.ja
}
Sur les plateformes supportées, JBS produit le format de bibliothèque dynamique correspondant, par exemple .so sous Linux ou .dll avec import library sous MinGW/Windows.
linkLes bibliothèques externes peuvent être ajoutées à une target :
link Main {
MyLib.a
SDL2
}
JBS utilise GCC pour effectuer le lien final.
Les bibliothèques C peuvent donc être utilisées directement.
Des chemins de bibliothèques peuvent également être utilisés avec les directives de chemins du build system.
includeAjoute un répertoire de recherche pour les headers/bindings :
include include
Les répertoires d'include sont utilisés notamment pour résoudre les using et les headers nécessaires aux bindings.
libpathAjoute un répertoire de recherche de bibliothèques :
libpath lib
Cela permet à JBS/GCC de rechercher les bibliothèques externes dans lib.
defineDéfinit une macro de compilation :
define PI 3.14
define MY_GAME_VERSION 2
Les définitions sont disponibles dans le C généré :
#define PI 3.14
#define MY_GAME_VERSION 2
Elles peuvent ensuite être utilisées dans Jaguar :
f64 x = PI;
bindgenJBS peut lancer automatiquement BindGen :
bindgen glad {
glad/glad.h
}
Cela génère automatiquement :
glad.jah
et permet ensuite :
using glad;
Les paramètres peuvent être indiqués sur la directive :
bindgen glad namespace=gl {
glad/glad.h
}
Le principe est donc :
glad.h
↓
bindgen
↓
glad.jah
↓
using glad
JBS possède plusieurs modes prédéfinis :
mode debug
mode release
mode relwithdebinfo
mode minsizerel
Ils configurent principalement les flags GCC et définissent une macro correspondante :
| Mode | Macro |
|---|---|
debug |
JAGUAR_DEBUG |
release |
JAGUAR_RELEASE |
relwithdebinfo |
JAGUAR_RELWITHDEBINFO |
minsizerel |
JAGUAR_MINSIZEREL |
Cela permet notamment :
#ifdef JAGUAR_DEBUG
sys:print("Debug build");
#elif defined(JAGUAR_RELEASE)
sys:print("Release build");
#endif
Le mode peut également être sélectionné depuis la ligne de commande :
python jbs.py game.jbs --mode release
c89JBS peut demander à JCC de générer du C compatible C89 :
c89
keep_cPar défaut, JBS peut supprimer les fichiers C intermédiaires.
Pour les conserver :
keep_c
Cela est utile pour inspecter le C généré par JaguarCC ou diagnostiquer une erreur GCC.
Un projet utilisant une API C peut être organisé ainsi :
MyGame/
├── example.jbs
├── src/
│ └── main.ja
├── include/
│ └── ...
└── lib/
└── ...
Fichier example.jbs :
version 1.0
out build
mode debug
include include
libpath lib
c89
keep_c
define GAME_VERSION 1
bindgen glad {
glad/glad.h
}
compile Main {
src/main.ja
}
link Main {
glad
}
Le pipeline complet devient :
glad.h
↓
jbg.py
↓
glad.jah
↓
using glad
↓
main.ja
↓
JCC
↓
Main.c
↓
GCC + bibliothèques C
↓
Main
Voici un petit programme utilisant plusieurs fonctionnalités du langage :
#define MAX_HEALTH 100
@register
class Player {
@exposed
$int health = MAX_HEALTH;
constr(int initial_health = MAX_HEALTH) {
health = initial_health;
}
void damage(int amount) {
health = health - amount;
}
bool alive() const {
return health > 0;
}
}
int add(int a, int b = 0) {
return a + b;
}
void main(string param) {
Player player = Player(100);
list<int> values = {10, 20, 30};
auto item = loop_list(values) {
sys:print(item);
}
player.damage(25);
sys:print(player.health);
if (player.alive()) {
sys:print("Player alive");
}
f64 distance = jcc:sqrt(3.0 * 3.0 + 4.0 * 4.0);
sys:print(distance);
string text = jcc:i32_to_string(add(10, b = 20));
sys:print(text);
}
class Player {
$f32 x = 0.0;
$f32 y = 0.0;
$int health = 100;
void move(f32 dx, f32 dy) {
x = x + dx;
y = y + dy;
}
void damage(int amount) {
health = health - amount;
}
bool alive() const {
return health > 0;
}
}
void main(string param) {
Player player = Player();
int i = for_loop(0, 4) {
player.move(1.0, 0.5);
sys:print(i);
}
player.damage(20);
if (player.alive()) {
sys:print("Player is alive");
}
sys:print(player.x);
sys:print(player.y);
}
// commentaire
/* commentaire */
#define NAME VALUE
const int x = 10;
auto y = 20;
int add(int a, int b) {
return a + b;
}
int add(int a, int b);
if (condition) {
} else if (other) {
} else {
}
while {
break;
continue;
}
int i = for_loop(0, 10) {
}
struct Vector2 {
f32 x;
f32 y;
}
class Player {
$int health;
%int protected_value;
int private_value;
constr() {
}
virtual void update() {
}
void update_child() override {
}
destr() {
}
}
namespace game {
int start() {
return 0;
}
}
list<int> values = {1, 2, 3};
map<string, int> scores = {"a", 10};
pair<string, int> result = {"score", 42};
dynamic_list data = {42, "hello", true};
container<int> value => 42;
sys:print("Hello");
jcc:sqrt(25.0);
void
int / i32
uint / u32
short / i16
ushort / u16
long / i64
ulong / u64
char / i8
uchar / u8
sbyte / i8
byte / u8
float / f32
double / f64
bool
string
list<T>
map<K,V>
pair<K,V>
container<T>
dynamic_list
if / else
while
for_loop
break
continue
return
class
constr
destr
virtual
override
this
$
%
const
namespace
namespace:function()
@extern
@register
@exposed
cptr
cstr
cfuncptr
&
#define
#undef
#if
#ifdef
#ifndef
#elif
#else
#endif
#pragma
#error
#warning
#line
#elseif est accepté comme alias Jaguar de #elif.
#include n'est pas supporté.
list.push()
list[index]
map.emplace()
map[key]
pair.first
pair.second
container.get()
dynamic_list.push()
dynamic_list.get()
dynamic_list.type()
dynamic_list.size()
dynamic_list[index]
loop_list()
loop_map()
sys:print()
sys:console:set_color()
sys:console:reset_color()
sys:execute()
sys:fs:read()
sys:fs:write()
jccjcc:exit()
jcc:abort()
jcc:abs_i32()
jcc:min_i32()
jcc:max_i32()
jcc:clamp_i32()
jcc:random_i32()
jcc:time_ms()
jcc:assert()
jcc:sqrt()
jcc:pow()
jcc:sin()
jcc:cos()
jcc:tan()
jcc:asin()
jcc:acos()
jcc:atan()
jcc:atan2()
jcc:floor()
jcc:ceil()
jcc:round()
jcc:log()
jcc:log10()
jcc:exp()
jcc:fmod()
jcc:string_length()
jcc:string_equals()
jcc:string_compare()
jcc:string_contains()
jcc:string_starts_with()
jcc:string_ends_with()
jcc:string_concat()
jcc:string_substring()
jcc:string_char_at()
jcc:string_find()
jcc:string_to_upper()
jcc:string_to_lower()
jcc:string_to_i32()
jcc:string_to_i64()
jcc:string_to_f64()
jcc:i32_to_string()
jcc:i64_to_string()
jcc:f64_to_string()
jcc:is_digit()
jcc:is_alpha()
jcc:is_alnum()
jcc:is_space()
jcc:is_upper()
jcc:is_lower()
jcc:to_upper_char()
jcc:to_lower_char()
jcc:file_exists()
jcc:remove_file()
jcc:rename_file()
jcc:env_get()
Jaguar cherche à garder un compromis entre :
Le compilateur s'occupe autant que possible des détails bas niveau tout en laissant les fonctionnalités essentielles du langage explicites.
Cette documentation correspond au comportement actuellement présent dans jcc.py.
Les points à considérer comme particulièrement spécifiques à l'implémentation actuelle sont :
while sans condition ;for_loop avec borne finale incluse ;loop_list / loop_map ;container<T> ;dynamic_list ;@register / @exposed / factory:construct ;sys:* ;jcc:* ;Pour toute fonctionnalité non décrite ici, il faut se référer au comportement réel de jcc.py plutôt que supposer qu'elle est héritée du C ou du C++.
version
out
include
libpath
define
mode
c89
keep_c
bindgen
compile
compile_static
compile_shared
link
CLI :
python jbs.py project.jbs
python jbs.py project.jbs --target Main
python jbs.py project.jbs --mode release
Le build system utilise JCC pour Jaguar → C et GCC pour les étapes de compilation et de linkage.
Une organisation typique peut être :
Project/
├── build/
├── include/
│ ├── engine.jah
│ └── glad.jah
├── lib/
│ ├── libMyEngine.a
│ └── ...
├── src/
│ ├── main.ja
│ └── game.ja
└── project.jbs
Les interfaces réutilisables sont placées dans des .jah, les implémentations dans des .ja, et les API C externes sont généralement introduites par BindGen.
┌──────────────┐
│ API C .h │
└──────┬───────┘
│
jbg.py
│
▼
┌──────────────┐
│ API .jah │
└──────┬───────┘
│
using
│
┌──────────────┐ ▼
│ source .ja │ ──────────► JCC
└──────────────┘ │
▼
.c
│
┌──────┴──────┐
│ GCC │
└──────┬──────┘
│
┌──────┴──────┐
│ C libraries │
└──────┬──────┘
▼
executable
JBS orchestre l'ensemble du processus.