Maison  >  Article  >  développement back-end  >  Pretty-Printing est une compilation

Pretty-Printing est une compilation

DDD
DDDoriginal
2024-11-01 04:21:02511parcourir

Une plus jolie imprimante de Wadler est une perle fonctionnelle classique. Cependant, que ce soit à cause de la paresse de Haskell ou de la mienne, j'ai eu du mal à réimplémenter ses idées dans d'autres langues (ou même en Haskell, cinq minutes après avoir lu l'article). Heureusement, Lindig a reconnu ce problème et a enflammé les masses dans Strictly Pretty. Pourtant, même cela n’était pas assez stupide pour moi.

Mais après avoir passé un peu plus de temps à bricoler les idées des deux articles, je pense que j'ai enfin compris l'idée.

Aperçu

Comme le titre l'indique, nous considérerons le joli-printing comme un processus de compilation (et d'exécution) de programmes écrits dans un « langage de document » abstrait. Comme la plupart des langages de programmation, ce langage de document, que nous appellerons
Doc : contient des expressions qui peuvent être composées ensemble ; cela permet aux humains de raisonner facilement. Nous compilerons des expressions dans Doc en instructions dans une sorte de langage assembleur (ASM). Les instructions en ASM sont beaucoup plus faciles à transformer en chaînes.

Voici une vue schématique du processus :

Pretty-Printing is Compilation

À titre d'exemple concret, supposons que nous souhaitions imprimer joliment la liste imbriquée :

['onions', ['carrots', 'celery'], 'turnips']

Voici un programme Doc pour ce faire :

group(
    '['
    + nest(
        4,
        br()
        + "'onions'"
        + ','
        + br(' ')
        + group(
            '['
            + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'")
            + br()
            + ']'
        )
        + ','
        + br(' ')
        + "'turnips'"
    )
    + br()
    + ']'
)

Nous rencontrerons le groupe, le nid, etc. sous peu. Pour l'instant, il suffit d'avoir une idée générale du langage du document.

Ce programme est ensuite compilé (avec certains « paramètres architecturaux » définis) dans les instructions ASM :

TEXT '['
LINE 4
TEXT "'onions'"
TEXT ','
LINE 4
TEXT '['
TEXT ''
TEXT "'carrots'"
TEXT ','
TEXT ' '
TEXT "'celery'"
TEXT ''
TEXT ']'
TEXT ','
LINE 4
TEXT "'turnips'"
LINE 0
TEXT ']'

qui sont ensuite interprétés comme une chaîne :

[
    'onions',
    ['carrots', 'celery'],
    'turnips'
]

Une caractéristique importante évoquée ci-dessus est que le compilateur dispose d'un "paramètre architectural" configurable, qui est une largeur de ligne maximale cible. En fonction de la valeur de ce paramètre, le compilateur émettra différentes instructions ASM pour un même programme Doc. Dans l'exemple ci-dessus, nous avons utilisé une largeur cible de 30. Si nous avions utilisé 20 à la place, les instructions d'assemblage émises seraient différentes, tout comme la chaîne résultante :

[
    'onions',
    [
        'carrots',
        'celery'
    ],
    'turnips'
]

Et si on en avait utilisé 60, ce serait :

['onions', ['carrots', 'celery'], 'turnips']

Langage d'assemblage d'imprimante

Le langage assembleur est simple, nous allons donc l'aborder en premier. Nous considérerons les instructions ASM comme contrôlant un périphérique d'impression très simple qui n'est capable de faire que deux choses :

  1. Émettre une chaîne de texte.
  2. Avancer à la ligne suivante et indenter d'un certain montant.

ASM se compose donc de seulement deux instructions :

  1. TEXT , qui émet une chaîne de texte.
  2. LINE , qui fait avancer l'imprimante à la ligne suivante, puis indente par espaces d'indentation.

Les programmes ASM sont interprétés comme des chaînes en exécutant leurs instructions, les unes après les autres. A titre d'exemple, retraçons l'exécution du programme :

['onions', ['carrots', 'celery'], 'turnips']

Nous utiliserons un > pour indiquer l'instruction en cours d'exécution et afficher la sortie actuelle ci-dessous. Le caractère ^ indiquera l'emplacement actuel de la « tête d'impression ». Nous utiliserons également des caractères _ pour indiquer les espaces, car ceux-ci sont autrement difficiles à suivre.

La première instruction TEXT provoque l'émission de la chaîne 'hello' :

group(
    '['
    + nest(
        4,
        br()
        + "'onions'"
        + ','
        + br(' ')
        + group(
            '['
            + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'")
            + br()
            + ']'
        )
        + ','
        + br(' ')
        + "'turnips'"
    )
    + br()
    + ']'
)

LIGNE 2 passe ensuite à la ligne suivante et indente la tête de 2 espaces :

TEXT '['
LINE 4
TEXT "'onions'"
TEXT ','
LINE 4
TEXT '['
TEXT ''
TEXT "'carrots'"
TEXT ','
TEXT ' '
TEXT "'celery'"
TEXT ''
TEXT ']'
TEXT ','
LINE 4
TEXT "'turnips'"
LINE 0
TEXT ']'

Ensuite, TEXT 'indented' provoque l'ajout de 'indented' :

[
    'onions',
    ['carrots', 'celery'],
    'turnips'
]

Suivi de 'monde', en raison du TEXTE 'monde' :

[
    'onions',
    [
        'carrots',
        'celery'
    ],
    'turnips'
]

LINE 0 fait avancer l'imprimante à la ligne suivante (et ne met pas du tout en retrait) :

['onions', ['carrots', 'celery'], 'turnips']

Et enfin, TEXT 'goodbye' émet 'goodbye' :

TEXT 'hello'
LINE 2
TEXT 'indented'
TEXT ' world'
LINE 0
TEXT 'goodbye'

Nous représenterons les instructions ASM comme un « type de somme » :

  • Les instructions TEXTE seront représentées par des chaînes Python.
  • Les instructions LINE seront représentées par des entiers.

C'est-à-dire :

> TEXT 'hello'
  LINE 2
  TEXT 'indented'
  TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
     ^

Interpréter une liste d'AsmInsts dans la chaîne qu'ils représentent est simplement une question de parcourir les instructions et de "faire ce qu'il faut":

  TEXT 'hello'
> LINE 2
  TEXT 'indented'
  TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__
  ^

Pour les instructions TEXTE, l'interprète ajoute le texte au résultat ; pour les instructions LINE, l'interpréteur ajoute une nouvelle ligne (« n ») suivie d'espaces de retrait.

Nous pouvons tester l'interprétation avec les instructions ASM de l'exemple ci-dessus, traduites en Python :

  TEXT 'hello'
  LINE 2
> TEXT 'indented'
  TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__indented
          ^

Le langage Doc (Teaser)

Nous aimons l'ASM car il est facile à interpréter. Mais c'est pénible à utiliser. Cela motive le langage Doc plus convivial. Alors que les programmes ASM sont des séquences d'instructions, les programmes Doc sont des compositions de expressions. Ces expressions sont résumées par la grammaire suivante :

  TEXT 'hello'
  LINE 2
  TEXT 'indented'
> TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__indented world
                ^

Par exemple :

  TEXT 'hello'
  LINE 2
  TEXT 'indented'
  TEXT ' world'
> LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__indented world

^

est une expression Doc, telle quelle :

  TEXT 'hello'
  LINE 2
  TEXT 'indented'
  TEXT ' world'
  LINE 0
> TEXT 'goodbye'

== OUTPUT ==

hello
__indented world
goodbye
       ^

Que représentent-ils ?

  • Un littéral str Python se représente.
  • br() est un saut de ligne possible.
  • nest(indent, doc) crée une sous-expression « imbriquée » qui est visuellement décalée par des espaces de retrait.
  • group(doc) délimite une sous-expression dans laquelle tous les br() sont traités ou non comme des sauts de ligne.
  • combine les expressions Doc.
  • nil agit comme une expression "vide".

Donc, par exemple :

AsmInst = str | int

représente la chaîne :

def interpret(insts: list[AsmInst]) -> str:
    """Interpret the ASM instructions as a string."""
    result = ""
    for inst in insts:
        match inst:
            case text if isinstance(text, str):
                result += inst
            case indent if isinstance(indent, int):
                result += f"\n{' ' * indent}"
    return result

La deuxième expression, plus complexe :

['onions', ['carrots', 'celery'], 'turnips']

peut représenter soit :

group(
    '['
    + nest(
        4,
        br()
        + "'onions'"
        + ','
        + br(' ')
        + group(
            '['
            + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'")
            + br()
            + ']'
        )
        + ','
        + br(' ')
        + "'turnips'"
    )
    + br()
    + ']'
)

ou :

TEXT '['
LINE 4
TEXT "'onions'"
TEXT ','
LINE 4
TEXT '['
TEXT ''
TEXT "'carrots'"
TEXT ','
TEXT ' '
TEXT "'celery'"
TEXT ''
TEXT ']'
TEXT ','
LINE 4
TEXT "'turnips'"
LINE 0
TEXT ']'

en fonction de la valeur du "paramètre architectural" de largeur de ligne maximale cible fournie au compilateur. Ainsi, les expressions br peuvent être traitées comme des sauts de ligne ou du texte normal, auquel cas leur valeur de texte est utilisée (ou '' si aucun argument de texte n'a été fourni).

Nous représenterons les expressions Doc en utilisant des classes strs et Python. En particulier :

[
    'onions',
    ['carrots', 'celery'],
    'turnips'
]

Et DocExpr DocExpr ? Nous représenterons ceux qui utilisent une classe Concat supplémentaire :

[
    'onions',
    [
        'carrots',
        'celery'
    ],
    'turnips'
]

Nous souhaitons prendre en charge l'utilisation pour combiner des expressions, nous devons donc implémenter __add__ et __radd__ sur chacune des classes variantes. L'ajout de deux expressions Doc en utilisant simplement construit un Concat des deux. C'est assez simple de le faire manuellement, par exemple :

['onions', ['carrots', 'celery'], 'turnips']

Mais on peut s'épargner de la saisie en définissant un décorateur pour le faire à notre place :

TEXT 'hello'
LINE 2
TEXT 'indented'
TEXT ' world'
LINE 0
TEXT 'goodbye'

Les classes variantes ressemblent désormais à :

> TEXT 'hello'
  LINE 2
  TEXT 'indented'
  TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
     ^

Notre tâche consiste maintenant à écrire un compilateur qui traduit les expressions du langage Doc en instructions ASM équivalentes, compte tenu d'une certaine largeur de ligne maximale cible :

  TEXT 'hello'
> LINE 2
  TEXT 'indented'
  TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__
  ^

Cependant, il s'avère plus simple de "réduire" d'abord les expressions Doc en expressions dans un langage de Représentation intermédiaire (IR), puis de compiler les expressions IR dans ASM. Introduit ce "pass" supplémentaire rend chaque étape plus claire.

Une représentation intermédiaire

Notre schéma décrivant le processus de jolie impression était donc un peu trop simpliste. Voici l'image complète :

Pretty-Printing is Compilation

Les expressions IR ressemblent aux expressions Doc à bien des égards :

  TEXT 'hello'
  LINE 2
> TEXT 'indented'
  TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__indented
          ^

La principale différence est que nous n'avons plus d'expressions nulles : celles-ci sont converties en listes d'expressions IR dans le processus d'abaissement. En fait, c'est tout que fait la passe de descente :

  TEXT 'hello'
  LINE 2
  TEXT 'indented'
> TEXT ' world'
  LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__indented world
                ^

Nous devons d’abord définir IrExprs.
Cela devrait vous sembler familier :

  TEXT 'hello'
  LINE 2
  TEXT 'indented'
  TEXT ' world'
> LINE 0
  TEXT 'goodbye'

== OUTPUT ==

hello
__indented world

^

Tout ce que Lower fait est de remplacer les instances Nil() par une liste vide ([]) et les instances Concat(car, cdr) en ajoutant les résultats de la réduction des expressions car et cdr. Le même traitement est appliqué aux sous-expressions dans Nest et Group. Ce n'est rien de plus qu'une opération "d'aplatissement" récursive.

  TEXT 'hello'
  LINE 2
  TEXT 'indented'
  TEXT ' world'
  LINE 0
> TEXT 'goodbye'

== OUTPUT ==

hello
__indented world
goodbye
       ^

Test inférieur avec l'un de nos exemples d'expressions Doc ci-dessus :

AsmInst = str | int

ce qui est exactement ce à quoi nous nous attendons.

Le compilateur (enfin)

Maintenant, passons à la dernière étape : compiler. Cette fonction transforme les expressions IR en instructions ASM, en tenant compte de la largeur de ligne maximale cible :

def interpret(insts: list[AsmInst]) -> str:
    """Interpret the ASM instructions as a string."""
    result = ""
    for inst in insts:
        match inst:
            case text if isinstance(text, str):
                result += inst
            case indent if isinstance(indent, int):
                result += f"\n{' ' * indent}"
    return result

Voici l'idée approximative de l'algorithme :

  • Le compilateur conserve certaines informations « d'état » :
    • La position actuelle (horizontale) de la ligne.
    • Le montant actuel de l'indentation.
    • Si les brs doivent être traités ou non comme des sauts de ligne ou rendus « à plat ».
  • Nous parcourons les expressions, émettons des instructions ASM et mettons à jour la position de la ligne de manière appropriée.
['onions', ['carrots', 'celery'], 'turnips']

le processus est l'endroit où la magie opère :

group(
    '['
    + nest(
        4,
        br()
        + "'onions'"
        + ','
        + br(' ')
        + group(
            '['
            + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'")
            + br()
            + ']'
        )
        + ','
        + br(' ')
        + "'turnips'"
    )
    + br()
    + ']'
)

En bref :

  • Pour les expressions textuelles, nous émettons une instruction TEXT et avançons la position (pos) de la longueur du texte.
  • Les expressions br sont gérées en fonction de la valeur de flat :
    • Si flat est True, traitez-les comme du texte.
    • Sinon, émettez une instruction INDENT avec le niveau d'indentation actuel, et réinitialisez la position à cette valeur.
  • Pour les expressions imbriquées, nous traitons toutes les sous-expressions, mais avec le niveau d'indentation actuel augmenté de la valeur d'indentation de l'expression imbriquée.
  • Enfin, pour les expressions de groupe, nous vérifions d'abord si l'ensemble du groupe peut être rendu à plat sans dépasser l'espace restant. Cela détermine la valeur de flat pour toutes les sous-expressions regroupées, qui à son tour décide si les brs sont rendus sous forme de sauts de ligne (ou sous forme de texte).

Comment fonctionnefits_flat ? Il parcourt simplement les instructions du groupe, traite les brs comme du texte et s'arrête quand :

  • Nous n'avons plus d'espace (largeur < 0), auquel cas les sous-expressions groupées ne peuvent pas être rendues à plat.
  • Nous avons traité toutes les sous-expressions, auquel cas le groupe peut être rendu plat.
TEXT '['
LINE 4
TEXT "'onions'"
TEXT ','
LINE 4
TEXT '['
TEXT ''
TEXT "'carrots'"
TEXT ','
TEXT ' '
TEXT "'celery'"
TEXT ''
TEXT ']'
TEXT ','
LINE 4
TEXT "'turnips'"
LINE 0
TEXT ']'

Mettre tout cela ensemble

Nous pouvons enfin assembler les pièces :

[
    'onions',
    ['carrots', 'celery'],
    'turnips'
]

Les seuls éléments restants de l'interface de Pretty-Printer sont les constructeurs d'expressions de documents :

[
    'onions',
    [
        'carrots',
        'celery'
    ],
    'turnips'
]

Essayons l'exemple de l'introduction :

['onions', ['carrots', 'celery'], 'turnips']

Voir la source complète ici.

Comment "programmer" dans Doc

? En construction ?

  • Modèles courants.
  • Comment le frère, le nid et le groupe interagissent.

Cloches et sifflets

? En construction ?

  • Ajout d'un groupe de paramètres de pliage pour forcer le pliage.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn