Les premiers pas
Ada est un langage de programmation conçu conformément aux exigences définies par le ministère de la Défense des États-Unis : les exigences dites Steelman. Dans l'ensemble, ces exigences exigent un langage de programmation doté d'une puissance d'expression considérable couvrant un large domaine d'application. Par conséquent, le langage comprend des fonctionnalités offertes par des langages classiques tels que Pascal ainsi que des fonctionnalités que l'on ne trouve souvent que dans des langages spécialisés. Ainsi, le langage est un langage algorithmique moderne avec les structures de contrôle habituelles et avec la capacité de définir des types et des sous-programmes. Il répond également au besoin de modularité, grâce auquel les données, les types et les sous-programmes peuvent être empaquetés. Il traite également de la modularité au sens physique, avec une fonctionnalité permettant de prendre en charge la compilation séparée. En plus de ces aspects, le langage de programmation couvre la programmation en temps réel, avec des fonctionnalités pour modéliser des tâches parallèles et gérer les exceptions. Il couvre également la programmation système, ce qui nécessite un contrôle précis de la représentation des données et l'accès aux propriétés dépendantes du système. Enfin, les entrées-sorties au niveau de l'application et au niveau de la machine sont définies.
Portée de la norme
Cette norme spécifie la forme et la signification des unités de programme écrites en Ada. Son objectif est de promouvoir la portabilité des programmes Ada vers une variété de systèmes de traitement de données.
Étendue de la norme
Cette norme spécifie :
- La forme d'une unité de programme écrite en Ada.
- L'effet de la traduction et de l'exécution d'une telle unité de programme.
- La manière dont les unités de programme peuvent être combinées pour former des programmes Ada.
- Les unités de programme prédéfinies qu'une implémentation conforme doit fournir.
- Les variations autorisées dans la norme et la manière dont elles doivent être spécifiées.
- Les violations de la norme qu'une implémentation conforme est tenue de détecter, et l'effet de la tentative de traduction ou d'exécution d'une unité de programme contenant de telles violations.
- Les violations de la norme qu'une implémentation conforme n'est pas tenue de détecter.
Cette norme ne spécifie pas :
- Les moyens par lesquels une unité de programme écrite en Ada est transformée en code objet exécutable par un microprocesseur.
- Les moyens par lesquels la traduction ou l'exécution des unités de programme est invoquée et les unités d'exécution sont contrôlées.
- La taille ou la vitesse du code objet, ou la vitesse d'exécution relative de différentes constructions de langage de programmation.
- La forme ou le contenu de toute liste produite par les implémentations ; en particulier, la forme ou le contenu des messages d'erreur ou d'avertissement.
- L'effet de l'exécution d'une unité de programme contenant une violation qu'une implémentation conforme n'est pas tenue de détecter.
- La taille d'un programme ou d'une unité de programme dépassant la capacité d'une implémentation conforme particulière.
Lorsque la présente norme spécifie qu'une unité de programme écrite en Ada a un effet exact, cet effet est la signification opérationnelle de l'unité de programme et doit être produit par toutes les implémentations conformes. Lorsque la présente norme spécifie des variations admissibles dans les effets des constituants d'une unité de programme écrite en Ada, la signification opérationnelle de l'unité de programme dans son ensemble est comprise comme étant la gamme des effets possibles résultant de toutes ces variations, et une implémentation conforme est autorisée à produire n'importe lequel de ces effets possibles. Voici des exemples de variations admissibles :
- Les valeurs représentées de quantités numériques fixes ou flottantes, ainsi que les résultats des opérations sur celles-ci.
- L'ordre d'exécution des instructions dans différentes tâches parallèles, en l'absence de synchronisation explicite.
Conformité d'une implémentation avec la norme
Une implémentation conforme est celle qui :
- traduit et exécute correctement les unités de programme légales écrites en Ada, à condition qu'elles ne soient pas trop grandes pour dépasser la capacité de l'implémentation.
- rejette toutes les unités de programme étant trop grandes pour dépasser la capacité de l'implémentation.
- rejette toutes les unités de programme contenant des erreurs dont la détection est requise par la norme.
- fournit toutes les unités de programme prédéfinies requises par la norme.
- ne contient aucune variation, sauf lorsque la norme le permet.
- spécifie toutes les variations autorisées de la manière prescrite par la norme.
Structure de la norme
Les exemples sont destinés à illustrer les formes possibles des constructions décrites. Les notes sont destinées à souligner les conséquences des règles décrites dans la section ou ailleurs. Les références sont destinées à attirer l'attention des lecteurs sur un terme ou une expression ayant une signification technique définie dans une autre section.
Objectifs de conception et sources
Le langage de programmation Ada a été conçu avec trois préoccupations principales : la fiabilité et la maintenance du programme, la programmation en tant qu'activité humaine et l'efficacité.
Le besoin de langages de programmation favorisant la fiabilité et simplifient la maintenance est bien établi. Par conséquent, l'accent a été mis sur la lisibilité du programme plutôt que sur la facilité d'écriture. Par exemple, les règles du langage de programmation exigent que les variables du programme soient explicitement déclarées et que leur type soit spécifié. Le type d'une variable étant invariant, les compilateurs peuvent garantir que les opérations sur les variables sont compatibles avec les propriétés prévues pour les objets du type. De plus, les notations sujettes aux erreurs ont été évitées et la syntaxe du langage évite l'utilisation de formes codées au profit de constructions plus proches de l'anglais. Enfin, le langage de programmation offre un support pour la compilation séparée des unités de programme d'une manière facilitant le développement et la maintenance du programme, et fournissant le même degré de vérification entre les unités qu'au sein d'une unité.
Le souci du programmeur humain a également été souligné lors de la conception. Surtout, une tentative a été faite pour garder le langage de programmation aussi petit que possible, compte tenu de la nature ambitieuse du domaine d'application. Nous avons tenté de couvrir ce domaine avec un petit nombre de concepts sous-jacents intégrés de manière cohérente et systématique. Néanmoins, nous avons essayé d'éviter les pièges d'une involution excessive et, dans la recherche constante de conceptions plus simples, nous avons essayé de fournir des constructions de langage correspondant intuitivement à ce que les utilisateurs attendent normalement. Comme beaucoup d'autres activités humaines, le développement de programmes devient de plus en plus décentralisé et distribué. Par conséquent, la capacité d'assembler un programme à partir de composantes logiciels produits indépendamment a été une idée centrale dans cette conception. Les concepts de paquets, de types privés et d'unités génériques sont directement liés à cette idée, ayant des ramifications dans de nombreux autres aspects du langage.
Aucun langage ne peut éviter le problème de l'efficacité. Les langages nécessitant des compilateurs trop élaborés ou conduisant à une utilisation inefficace d'entreposage ou du temps d'exécution imposent ces inefficacités sur toutes les machines et sur tous les programmes. Chaque construction du langage a été examinée à la lumière des techniques d'implémentation actuelles. Toute construction proposée dont l'implémentation n'était pas claire ou nécessitant des ressources machine excessives a été rejetée.
Aucun des objectifs de conception ci-dessus n'a été considéré comme réalisable après coup. Les objectifs de conception ont guidé l'ensemble du processus de conception depuis le début.
Une difficulté perpétuelle dans la conception d'un langage de programmation est qu'il faut à la fois identifier les capacités requises par le domaine d'application et concevoir les fonctionnalités du langage qui fournissent ces capacités. La difficulté existait dans cette conception, bien qu'à un degré moindre que d'habitude en raison des exigences de Steelman. Ces exigences simplifiaient souvent le processus de conception en lui permettant de se concentrer sur la conception d'un système donné fournissant un ensemble bien défini de capacités, plutôt que sur la définition des capacités elles-mêmes.
Une autre simplification significative du travail de conception résultait de l'expérience antérieure acquise par plusieurs dérivés réussis de Pascal développés avec des objectifs similaires. Il s'agit des langages de programmation Euclid, Lis, Mesa, Modula et Sue. De nombreuses idées clefs et formes syntaxiques développées dans ces langages ont des équivalents dans Ada. Plusieurs langages existants tels qu'Algol 68 et Simula, ainsi que des langages de recherche récents tels qu'Alphard et Clu, ont influencé ce langage à plusieurs égards, bien que dans une moindre mesure que la famille Pascal.
Enfin, les rapports d'évaluation reçus sur une formulation antérieure (le langage vert) et sur des propositions alternatives (les langages rouge, bleu et jaune), les révisions de langage ayant eu lieu à différentes étapes de ce projet et les milliers de commentaires reçus de quinze pays différents pendant les étapes préliminaires de la conception d'Ada et pendant le sondage ANSI, ont tous eu un impact significatif sur la définition standard du langage de programmation.
Résumé du langage
Un programme Ada est composé d'une ou plusieurs unités de programme. Ces unités de programme peuvent être compilées séparément. Les unités de programme peuvent être des sous-programmes (définissant des algorithmes exécutables), des unités de paquetage (définissant des collections d'entités), des unités de tâche (définissant des calculs parallèles) ou des unités génériques (définissant des formes paramétrées de paquetages et de sous-programmes). Chaque unité se compose normalement de deux parties : une spécification, contenant les informations devant être visibles par les autres unités, et un corps, contenant les détails d'implémentation, n'ayant pas besoin d'être visibles par les autres unités.
Cette distinction entre la spécification et le corps, et la possibilité de compiler des unités séparément, permet à un programme d'être conçu, écrit et testé comme un ensemble de composantes logiciels largement indépendants. Un programme Ada utilisera normalement une bibliothèque d'unités de programme d'utilité générale. Le langage fournit des moyens par lesquels les organisations individuelles peuvent construire leurs propres bibliothèques. Le texte d'une unité de programme compilée séparément doit nommer les unités de bibliothèque dont elle a besoin.
Unités de programme
Un sous-programme est l'unité de base pour exprimer un algorithme. Il existe deux types de sous-programmes : les procédures et les fonctions. Une procédure est le moyen d'appeler une série d'actions. Par exemple, elle peut lire des données, mettre à jour des variables ou produire une sortie. Elle peut avoir des paramètres, pour fournir un moyen contrôlé de transmettre des informations entre la procédure et le point d'appel.
Une fonction est le moyen d'appeler le calcul d'une valeur. Elle est similaire à une procédure, mais en plus, elle renvoie un résultat.
Un paquet est l'unité de base pour définir une collection d'entités liées logiquement. Par exemple, un paquet peut être utilisé pour définir un bassin commun de données et de types, une collection de sous-programmes liés ou un ensemble de déclarations de type et d'opérations associées. Des parties d'un paquet peuvent être cachées à l'utilisateur, ce qui permet d'accéder uniquement aux propriétés logiques exprimées par la spécification du paquet.
Une unité de tâche est l'unité de base pour définir une tâche dont la séquence d'actions peut être exécutée en parallèle avec celles d'autres tâches. De telles tâches peuvent être implémentées sur des multi-ordinateurs, des multiprocesseurs ou avec une exécution entrelacée sur un seul processeur. Une unité de tâche peut définir soit une tâche à exécution unique, soit un type de tâche permettant la création de n'importe quel nombre de tâches similaires.
Déclarations et instructions
Le corps d'une unité de programme contient généralement deux parties : une partie déclarative, définissant les entités logiques à utiliser dans l'unité de programme, et une séquence d'instructions, définissant l'exécution de l'unité de programme.
La partie déclarative associe des noms aux entités déclarées. Par exemple, un nom peut désigner un type, une constante, une variable ou une exception. Une partie déclarative introduit également les noms et paramètres d'autres sous-programmes imbriqués, paquets, unités de tâches et unités génériques à utiliser dans l'unité de programme.
La séquence d'instructions décrit une séquence d'actions à exécuter. Les instructions sont exécutées successivement (à moins qu'une instruction exit, return ou goto, ou la levée d'une exception, ne provoque la poursuite de l'exécution à partir d'un autre emplacement).
Une instruction d'affectation modifie la valeur d'une variable. Un appel de procédure appelle l'exécution d'une procédure après avoir associé tous les paramètres réels fournis lors de l'appel aux paramètres formels correspondants.
Les instructions Case et If permettent de sélectionner une séquence d'instructions fermée en fonction de la valeur d'une expression ou de la valeur d'une condition.
L'instruction Loop fournit le mécanisme itératif de base du langage de programmation. Une instruction Loop spécifie qu'une séquence d'instructions doit être exécutée de manière répétée selon les instructions d'un schéma d'itération, ou jusqu'à ce qu'une instruction de sortie soit rencontrée.
Un bloc d'instruction comprend une séquence d'instructions précédée de la déclaration d'entités locales utilisées par les instructions.
Certaines instructions ne s'appliquent qu'aux tâches. Une instruction delay retarde l'exécution d'une tâche pendant une durée spécifiée. Une instruction Entry Call est écrite sous la forme d'une instruction Procedure Call ; elle spécifie que la tâche émettant l'appel est prête pour un rendez-vous avec une autre tâche possédant cette entrée. La tâche appelée est prête à accepter l'appel d'entrée lorsque son exécution atteint une instruction Accept correspondante, spécifiant les actions à effectuer ensuite. Une fois le rendez-vous terminé, la tâche appelante et la tâche possédant l'entrée peuvent poursuivre leur exécution en parallèle. Une forme de l'instruction select permet une attente sélective pour l'un des rendez-vous alternatifs. D'autres formes de l'instruction select permettent des appels d'entrée conditionnels ou temporisés.
L'exécution d'une unité de programme peut rencontrer des situations d'erreur dans lesquelles l'exécution normale du programme ne peut pas se poursuivre. Par exemple, un calcul arithmétique peut dépasser la valeur maximale autorisée d'un nombre, ou une tentative d'accès à une composante de tableau peut être effectuée en utilisant une valeur d'index incorrecte. Pour gérer de telles situations d'erreur, les instructions d'une unité de programme peuvent être suivies textuellement par des gestionnaires d'exceptions spécifiant les actions à entreprendre lorsque la situation d'erreur survient. Les exceptions peuvent être déclenchées explicitement par une instruction raise.
Les types de données
Chaque objet du langage de programmation possède un type, caractérisant un ensemble de valeurs et un ensemble d'opérations applicables. Les principales classes de types sont les types scalaires (comprenant les types d'énumération et numériques), les types composites, les types d'accès et les types privés. Un type d'énumération définit un ensemble ordonné de littéraux d'énumération distincts, par exemple une liste d'états ou un alphabet de caractères. Les types d'énumération BOOLEAN et CHARACTER sont prédéfinis.
Les types numériques fournissent un moyen d'effectuer des calculs numériques exacts ou approximatifs. Les calculs exacts utilisent des types entiers, désignant des ensembles d'entiers consécutifs. Les calculs approximatifs utilisent soit des types à virgule fixe, avec des limites absolues sur l'erreur, soit des types à virgule flottante, avec des limites relatives sur l'erreur. Les types numériques INTEGER, FLOAT et DURATION sont prédéfinis.
Les types composites permettent de définir des objets structurés avec des composantes liés. Les types composites du langage de programmation fournissent des tableaux et des enregistrements. Un tableau est un objet avec des composants indexés du même type. Un enregistrement est un objet avec des composants nommés de types éventuellement différents. Le type de tableau STRING est prédéfini.
Un enregistrement peut avoir des composantes spéciaux appelés discriminants. Des structures d'enregistrement alternatives dépendant des valeurs des discriminants peuvent être définies dans un type d'enregistrement.
Les types d'accès permettent la construction de structures de données liées créées par l'évaluation d'allocateurs. Ils permettent à plusieurs variables d'un type d'accès de désigner le même objet, et aux composants d'un objet de désigner le même objet ou d'autres objets. Les éléments d'une telle structure de données liées ainsi que leur relation avec d'autres éléments peuvent être modifiés pendant l'exécution du programme. Les types privés peuvent être définis dans un paquet cachant les détails structurels n'étant pas pertinents de l'extérieur. Seules les propriétés logiquement nécessaires (y compris les discriminants) sont rendues visibles aux utilisateurs de ces types.
Le concept de type est affiné par le concept de sous-type, par lequel un utilisateur peut contraindre l'ensemble des valeurs autorisées d'un type. Les sous-types peuvent être utilisés pour définir des sous-intervalles de types scalaires, des tableaux avec un ensemble limité de valeurs d'index et des enregistrements et des types privés avec des valeurs discriminantes particulières.
Autres fonctionnalités
Les clauses de représentation peuvent être utilisées pour spécifier la correspondance entre les types et les fonctionnalités d'une machine sous-jacente. Par exemple, l'utilisateur peut spécifier que les objets d'un type donné doivent être représentés avec un nombre donné de bits, ou que les composantes d'un enregistrement doivent être représentés à l'aide d'une disposition de stockage donnée. D'autres fonctionnalités permettent l'utilisation contrôlée d'aspects de bas niveau, non portables ou dépendants de l'implémentation, y compris l'insertion directe de code machine. L'entrée-sortie est définie dans le langage de programmation au moyen de paquets de bibliothèque prédéfinis. Des fonctions d'entrée-sortie de valeurs de types définis par l'utilisateur ou prédéfinis sont prévues. Des moyens standards de représentation des valeurs sous forme d'affichage sont également fournis.
Enfin, le langage fournit un puissant moyen de paramétrisation des unités de programme, appelées unités de programme génériques. Les paramètres génériques peuvent être des types et des sous-programmes (ainsi que des objets) et permettent ainsi d'appliquer des algorithmes généraux à tous les types d'une classe donnée.
Méthode de description et notation syntaxique
La forme des unités de programme Ada est décrite au moyen d'une syntaxe indépendante du contexte ainsi que d'exigences dépendantes du contexte exprimées par des règles narratives.
La signification des unités de programme Ada est décrite au moyen de règles narratives définissant à la fois les effets de chaque construction et les règles de composition des constructions. Cette narration utilise des termes techniques dont la définition précise est donnée dans le texte (les références à la section contenant la définition d'un terme technique apparaissent à la fin de chaque section utilisant le terme).
Tous les autres termes sont en langue anglaise et portent leur signification naturelle, telle que définie dans le troisième nouveau dictionnaire international de la langue anglaise de Webster.
La syntaxe indépendante du contexte du langage est décrite à l'aide d'une variante simple de la forme Backus-Naur. En particulier :
- Les mots minuscules, certains contenant des soulignements intégrés, sont utilisés pour désigner des catégories syntaxiques, par exemple :
- adding_operator
Chaque fois que le nom d'une catégorie syntaxique est utilisé en dehors des règles de syntaxe elles-mêmes, les espaces remplacent les soulignements (donc : opérateur d'addition).
Les mots en gras dans la syntaxe ou en blanc dans le code sont utilisés pour désigner des mots réservés, par exemple :
- array
Les crochets entourent les éléments facultatifs. Les deux règles suivantes sont donc équivalentes :
return_statement ::= return [expression]; return_statement ::= return; | return expression; Les accolades encadrent un élément répété. L'élément peut apparaître zéro ou plusieurs fois; les répétitions se produisent de gauche à droite comme avec une règle récursive à gauche équivalente. Ainsi, les deux règles suivantes sont équivalentes :
term factor {multiplying_operator factor} term ::= factor | term multiplying_operator factor - Une barre verticale sépare les éléments alternatifs, sauf si elle apparaît immédiatement après une accolade ouvrante, auquel cas elle se présente
toute seule :
letter_or_digit ::= letter | digit component_association ::= [choice {| choice} =>] expression - Si le nom d'une catégorie syntaxique commence par une partie en italique, il est équivalent au nom de la catégorie sans la partie en italique. La partie en italique est destinée à transmettre des informations sémantiques. Par exemple, type_name et task_name sont tous deux équivalents au nom seul.
Remarque : les règles de syntaxe décrivant les constructions structurées sont présentées sous une forme qui correspond à la mise en paragraphes recommandée. Par exemple, une instruction if est définie comme suit :
if_statement ::= if condition then sequence_of_statements { elsif condition then sequence_of_statements} [ else sequence_of_statements] end if; |
Des lignes différentes sont utilisées pour les parties d'une règle de syntaxe si les parties correspondantes de la construction décrite par la règle sont destinées à être sur des lignes différentes. L'indentation dans la règle est une recommandation pour l'indentation de la partie correspondante de la construction. Il est recommandé que toutes les indentations soient par multiples d'une étape de base d'indentation (le nombre d'espaces pour l'étape de base n'est pas défini). Les emplacements préférés pour les autres sauts de ligne sont après les points-virgules. D'autre part, si une construction complète peut tenir sur une ligne, cela est également autorisé dans la mise en paragraphes recommandée.
Classification des erreurs
La définition du langage de programmation classe les erreurs en plusieurs catégories différentes :
- Erreurs devant être détectées au moment de la compilation par chaque compilateur Ada : Ces erreurs correspondent à toute violation d'une règle donnée autre que les violations correspondant à (b) ou (c) ci-dessous. En particulier, la violation de toute règle qui utilise les termes doit, autorisé, légal ou illégal appartient à cette catégorie. Tout programme contenant une telle erreur n'est pas un programme Ada légal ; d'autre part, le fait qu'un programme soit légal ne signifie pas, en soi, que le programme est exempt d'autres formes d'erreur.
- Erreurs devant être détectées au moment de l'exécution par l'exécution d'un programme Ada : Les situations d'erreur correspondantes sont associées aux noms des exceptions prédéfinies. Chaque compilateur Ada est tenu de générer du code déclenchant l'exception correspondante si une telle situation d'erreur survient pendant l'exécution du programme. Si une exception est certaine d'être déclenchée à chaque exécution d'un programme, les compilateurs sont alors autorisés (mais pas obligés) à signaler ce fait au moment de la compilation.
- Exécution erronée : Les règles du langage spécifient certaines règles auxquelles doivent obéir les programmes Ada, bien qu'il n'y ait aucune obligation pour les compilateurs Ada de fournir une détection au moment de la compilation ou de l'exécution de la violation de ces règles. Les erreurs de cette catégorie sont indiquées par l'utilisation du mot erroné pour qualifier l'exécution des constructions correspondantes. L'effet d'une exécution erronée est imprévisible.
- Dépendances d'ordre incorrectes : Chaque fois que le manuel de référence spécifie que différentes parties d'une construction donnée doivent être exécutées dans un ordre n'étant pas défini par le langage, cela signifie que l'implémentation est autorisée à exécuter ces parties dans n'importe quel ordre donné, en suivant les règles résultant de cet ordre donné, mais pas en parallèle. De plus, la construction est incorrecte si l'exécution de ces parties dans un ordre différent aurait un effet différent. Les compilateurs ne sont pas tenus de fournir une détection au moment de la compilation ou de l'exécution des dépendances d'ordre incorrectes. Ce qui précède est exprimé en termes de processus appelé exécution; il s'applique également aux processus appelés évaluation et élaboration.
Si un compilateur est capable de reconnaître au moment de la compilation qu'une construction est erronée ou contient une dépendance d'ordre incorrecte, alors le compilateur est autorisé à générer, à la place du code généré par ailleurs pour la construction, du code déclenchant l'exception prédéfinie PROGRAM_ERROR. De même, les compilateurs sont autorisés à générer du code vérifiant au moment de l'exécution les constructions erronées, les dépendances d'ordre incorrectes, ou les deux. L'exception prédéfinie PROGRAM_ERROR est levée si une telle vérification échoue.